Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 31 additions & 35 deletions apps/desktop/src/components/main/body/ai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,42 +60,38 @@ function AIView({ tab }: { tab: Extract<Tab, { type: "ai" }> }) {
[updateAiTabState, tab],
);

const headerAction = (
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => setActiveTab("transcription")}
className={cn([
"gap-1.5 h-7 px-2",
activeTab === "transcription" && "bg-neutral-200",
])}
>
<AudioLinesIcon size={14} />
<span className="text-xs">Transcription</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setActiveTab("intelligence")}
className={cn([
"gap-1.5 h-7 px-2",
activeTab === "intelligence" && "bg-neutral-200",
])}
>
<SparklesIcon size={14} />
<span className="text-xs">Intelligence</span>
</Button>
</div>
);

return (
<div className="flex-1 w-full overflow-y-auto scrollbar-hide p-6">
{activeTab === "transcription" ? (
<STT headerAction={headerAction} />
) : (
<LLM headerAction={headerAction} />
)}
<div className="flex flex-col flex-1 w-full overflow-hidden">
<div className="flex gap-1 px-6 pt-6 pb-2">
<Button
variant="ghost"
size="sm"
onClick={() => setActiveTab("transcription")}
className={cn([
"gap-1.5 h-7 px-2 border border-transparent",
activeTab === "transcription" &&
"bg-neutral-100 border-neutral-200",
])}
>
<AudioLinesIcon size={14} />
<span className="text-xs">Transcription</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setActiveTab("intelligence")}
className={cn([
"gap-1.5 h-7 px-2 border border-transparent",
activeTab === "intelligence" && "bg-neutral-100 border-neutral-200",
])}
>
<SparklesIcon size={14} />
<span className="text-xs">Intelligence</span>
</Button>
</div>
<div className="flex-1 w-full overflow-y-auto scrollbar-hide px-6 pb-6">
{activeTab === "transcription" ? <STT /> : <LLM />}
</div>
</div>
);
}
9 changes: 7 additions & 2 deletions apps/desktop/src/components/main/sidebar/banner/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function Banner({
onDismiss?: () => void;
}) {
return (
<div className="overflow-hidden px-1 py-2">
<div className="overflow-hidden p-1">
<div
className={cn([
"relative group overflow-hidden rounded-lg",
Expand All @@ -27,7 +27,12 @@ export function Banner({
size="icon"
variant="ghost"
aria-label="Dismiss banner"
className="absolute top-1 right-1 opacity-0 group-hover:opacity-10 hover:!opacity-100 transition-all duration-200"
className={cn([
"absolute top-1.5 right-1.5 size-6",
"opacity-0 group-hover:opacity-50 hover:!opacity-100",
"hover:bg-neutral-200",
"transition-all duration-200",
])}
>
<X className="w-3.5 h-3.5" />
</Button>
Expand Down
133 changes: 13 additions & 120 deletions apps/desktop/src/components/settings/ai/llm/configure.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { useForm } from "@tanstack/react-form";
import { useEffect } from "react";

import { type AIProvider, aiProviderSchema } from "@hypr/store";
import {
Accordion,
AccordionContent,
Expand All @@ -12,13 +8,13 @@ import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/utils";

import { useBillingAccess } from "../../../../billing";
import { FormField, StyledStreamdown, useProvider } from "../shared";
import { NonHyprProviderCard, StyledStreamdown } from "../shared";
import { ProviderId, PROVIDERS } from "./shared";

export function ConfigureProviders() {
return (
<div className="flex flex-col gap-3">
<h3 className="text-sm font-semibold">Configure Providers</h3>
<h3 className="text-md font-semibold">Configure Providers</h3>
<Accordion type="single" collapsible className="space-y-3">
<HyprProviderCard
providerId="hyprnote"
Expand All @@ -29,126 +25,20 @@ export function ConfigureProviders() {
/>
{PROVIDERS.filter((provider) => provider.id !== "hyprnote").map(
(provider) => (
<NonHyprProviderCard key={provider.id} config={provider} />
<NonHyprProviderCard
key={provider.id}
config={provider}
providerType="llm"
providers={PROVIDERS}
providerContext={<ProviderContext providerId={provider.id} />}
/>
),
)}
</Accordion>
</div>
);
}

function NonHyprProviderCard({
config,
}: {
config: (typeof PROVIDERS)[number];
}) {
const billing = useBillingAccess();
const [provider, setProvider] = useProvider(config.id);
const locked = config.requiresPro && !billing.isPro;

useEffect(() => {
if (!provider && config.baseUrl && !config.apiKey) {
setProvider({
type: "llm",
base_url: config.baseUrl,
api_key: "",
});
}
}, [provider, config.baseUrl, config.apiKey, setProvider]);

const form = useForm({
onSubmit: ({ value }) => setProvider(value),
defaultValues:
provider ??
({
type: "llm",
base_url: config.baseUrl ?? "",
api_key: "",
} satisfies AIProvider),
listeners: {
onChange: ({ formApi }) => {
queueMicrotask(() => {
const {
form: { errors },
} = formApi.getAllErrors();
if (errors.length > 0) {
console.log(errors);
}

formApi.handleSubmit();
});
},
},
validators: { onChange: aiProviderSchema },
});

return (
<AccordionItem
value={config.id}
className="rounded-xl border-2 border-dashed bg-neutral-50"
disabled={locked}
>
<AccordionTrigger
className={cn([
"capitalize gap-2 px-4",
locked && "cursor-not-allowed opacity-30",
])}
>
<div className="flex items-center gap-2">
{config.icon}
<span>{config.displayName}</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 space-y-6">
<ProviderContext providerId={config.id} />

<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{!config.baseUrl && (
<form.Field name="base_url">
{(field) => (
<FormField field={field} label="Base URL" icon="mdi:web" />
)}
</form.Field>
)}
{config?.apiKey && (
<form.Field name="api_key">
{(field) => (
<FormField
field={field}
label="API Key"
icon="mdi:key"
placeholder="Enter your API key"
type="password"
/>
)}
</form.Field>
)}
{config.baseUrl && (
<details className="space-y-4 pt-2">
<summary className="text-xs cursor-pointer text-neutral-600 hover:text-neutral-900 hover:underline">
Advanced
</summary>
<div className="mt-4">
<form.Field name="base_url">
{(field) => (
<FormField field={field} label="Base URL" icon="mdi:web" />
)}
</form.Field>
</div>
</details>
)}
</form>
</AccordionContent>
</AccordionItem>
);
}

function HyprProviderCard({
providerId,
providerName,
Expand All @@ -164,8 +54,11 @@ function HyprProviderCard({
return (
<AccordionItem
value={providerId}
className="rounded-xl border-2 border-dashed bg-neutral-50"
disabled={locked}
className={cn([
"rounded-xl border-2 bg-neutral-50",
true ? "border-solid border-neutral-300" : "border-dashed",
])}
>
<AccordionTrigger
className={cn([
Expand Down
8 changes: 3 additions & 5 deletions apps/desktop/src/components/settings/ai/llm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { ConfigureProviders } from "./configure";
import { HealthCheckForAvailability } from "./health";
import { SelectProviderAndModel } from "./select";

export function LLM({ headerAction }: { headerAction?: React.ReactNode } = {}) {
export function LLM() {
return (
<div className="space-y-6">
<HealthCheckForAvailability />
<SelectProviderAndModel headerAction={headerAction} />
<div className="space-y-6 mt-4">
<SelectProviderAndModel />
<ConfigureProviders />
</div>
);
Expand Down
18 changes: 11 additions & 7 deletions apps/desktop/src/components/settings/ai/llm/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ import { ModelCombobox } from "../shared/model-combobox";
import { HealthCheckForConnection } from "./health";
import { PROVIDERS } from "./shared";

export function SelectProviderAndModel({
headerAction,
}: { headerAction?: React.ReactNode } = {}) {
export function SelectProviderAndModel() {
const configuredProviders = useConfiguredMapping();

const { current_llm_model, current_llm_provider } = useConfigValues([
Expand Down Expand Up @@ -77,10 +75,7 @@ export function SelectProviderAndModel({

return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<h3 className="text-md font-semibold">Model being used</h3>
{headerAction}
</div>
<h3 className="text-md font-semibold">Model being used</h3>
<div
className={cn([
"flex flex-col gap-4",
Expand Down Expand Up @@ -177,6 +172,15 @@ export function SelectProviderAndModel({
<HealthCheckForConnection />
)}
</div>

{(!current_llm_provider || !current_llm_model) && (
<div className="flex items-center gap-2 pt-2 border-t border-red-200">
<span className="text-sm text-red-600">
<strong className="font-medium">Language model</strong> is needed
to make Hyprnote summarize and chat about your conversations.
</span>
</div>
)}
</div>
</div>
);
Expand Down
Loading
Loading