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
99 changes: 54 additions & 45 deletions apps/desktop/src/components/settings/ai/llm/configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,23 @@ export function ConfigureProviders() {
{PROVIDERS.map((provider) => (
<ProviderCard
key={provider.id}
icon={provider.icon}
providerId={provider.id}
providerName={provider.displayName}
providerConfig={provider}
config={provider}
/>
))}
</Accordion>
</div>
);
}

function ProviderCard({
providerId,
providerName,
icon,
providerConfig,
}: {
providerId: ProviderId;
providerName: string;
icon: React.ReactNode;
providerConfig: typeof PROVIDERS[number];
}) {
const [provider, setProvider] = useProvider(providerId);
function ProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
const [provider, setProvider] = useProvider(config.id);

const form = useForm({
onSubmit: ({ value }) => setProvider(value),
defaultValues: provider
?? ({
type: "llm",
base_url: "",
base_url: config.baseUrl ?? "",
api_key: "",
} satisfies internal.AIProvider),
listeners: {
Expand All @@ -65,51 +52,71 @@ function ProviderCard({

return (
<AccordionItem
value={providerId}
value={config.id}
className={cn(["rounded-lg border-2 border-dashed bg-neutral-50"])}
>
<AccordionTrigger className={cn(["capitalize gap-2 px-4"])}>
<div className="flex items-center gap-2">
{icon}
<span>{providerName}</span>
{providerConfig.badge && (
{config.icon}
<span>{config.displayName}</span>
{config.badge && (
<span className="text-xs text-neutral-500 font-light border border-neutral-300 rounded-full px-2">
{providerConfig.badge}
{config.badge}
</span>
)}
</div>
</AccordionTrigger>
<AccordionContent className="px-4">
<ProviderContext providerId={providerId} />
<ProviderContext providerId={config.id} />
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<form.Field name="base_url" defaultValue={providerConfig.baseUrl.value}>
{(field) => (
<FormField
field={field}
hidden={providerConfig.baseUrl.immutable}
label="Base URL"
icon="mdi:web"
/>
)}
</form.Field>
<form.Field name="api_key">
{(field) => (
<FormField
field={field}
hidden={!providerConfig?.apiKey}
label="API Key"
icon="mdi:key"
placeholder="Enter your API key"
type="password"
/>
)}
</form.Field>
{!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>
Expand All @@ -119,6 +126,8 @@ function ProviderCard({
function ProviderContext({ providerId }: { providerId: ProviderId }) {
const content = providerId === "hyprnote"
? "Hyprnote is great"
: providerId === "custom"
? "We only support **OpenAI compatible** endpoints for now."
: "";

if (!content) {
Expand Down
21 changes: 15 additions & 6 deletions apps/desktop/src/components/settings/ai/llm/shared.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Icon } from "@iconify-icon/react";
import { Anthropic, LmStudio, Ollama, OpenAI, OpenRouter } from "@lobehub/icons";

export type ProviderId = typeof PROVIDERS[number]["id"];
Expand All @@ -9,46 +10,54 @@ export const PROVIDERS = [
badge: "Recommended",
icon: <img src="/assets/icon.png" alt="Hyprnote" className="size-5" />,
apiKey: false,
baseUrl: { value: "https://api.hyprnote.com/v1", immutable: true },
baseUrl: "https://api.hyprnote.com/v1",
},
{
id: "openai",
displayName: "OpenAI",
badge: null,
icon: <OpenAI size={16} />,
apiKey: true,
baseUrl: { value: "https://api.openai.com/v1", immutable: true },
baseUrl: "https://api.openai.com/v1",
},
{
id: "anthropic",
displayName: "Anthropic",
badge: null,
icon: <Anthropic size={16} />,
apiKey: true,
baseUrl: { value: "https://api.anthropic.com/v1", immutable: true },
baseUrl: "https://api.anthropic.com/v1",
},
{
id: "openrouter",
displayName: "OpenRouter",
badge: null,
icon: <OpenRouter size={16} />,
apiKey: true,
baseUrl: { value: "https://openrouter.ai/api/v1", immutable: true },
baseUrl: "https://openrouter.ai/api/v1",
},
{
id: "ollama",
displayName: "Ollama",
badge: null,
icon: <Ollama size={16} />,
apiKey: false,
baseUrl: { value: "http://localhost:11434", immutable: false },
baseUrl: "http://localhost:11434",
},
{
id: "lmstudio",
displayName: "LM Studio",
badge: null,
icon: <LmStudio size={16} />,
apiKey: false,
baseUrl: { value: "http://localhost:8000", immutable: false },
baseUrl: "http://localhost:8000",
},
{
id: "custom",
displayName: "Custom",
badge: null,
icon: <Icon icon="mingcute:random-fill" />,
apiKey: true,
baseUrl: undefined,
},
] as const;
14 changes: 8 additions & 6 deletions apps/desktop/src/components/settings/ai/shared/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,25 @@ export function FormField({
icon,
placeholder,
type,
hidden,
}: {
field: AnyFieldApi;
label: string;
icon: string;
placeholder?: string;
type?: string;
hidden?: boolean;
}) {
const { meta: { errors, isTouched, isDirty } } = field.state;
const hasError = isDirty && isTouched && errors && errors.length > 0;
const { meta: { errors, isTouched } } = field.state;
const hasError = isTouched && errors && errors.length > 0;
const errorMessage = hasError
? (typeof errors[0] === "string" ? errors[0] : (errors[0] as any)?.message || "Invalid value")
? (typeof errors[0] === "string"
? errors[0]
: "message" in errors[0]
? errors[0].message
: JSON.stringify(errors[0]))
: null;

return (
<div className="space-y-2" hidden={hidden}>
<div className="space-y-2">
<label className="block text-xs font-medium">{label}</label>
<InputGroup className="bg-white">
<InputGroupAddon align="inline-start">
Expand Down
49 changes: 35 additions & 14 deletions apps/desktop/src/components/settings/ai/stt/configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function NonHyprProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
defaultValues: provider
?? ({
type: "stt",
base_url: "",
base_url: config.baseUrl ?? "",
api_key: "",
} satisfies internal.AIProvider),
listeners: {
Expand Down Expand Up @@ -96,16 +96,17 @@ function NonHyprProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
e.stopPropagation();
}}
>
<form.Field name="base_url" defaultValue={config.baseUrl.value}>
{(field) => (
<FormField
field={field}
hidden={config.baseUrl.immutable}
label="Base URL"
icon="mdi:web"
/>
)}
</form.Field>
{!config.baseUrl && (
<form.Field name="base_url">
{(field) => (
<FormField
field={field}
label="Base URL"
icon="mdi:web"
/>
)}
</form.Field>
)}
<form.Field name="api_key">
{(field) => (
<FormField
Expand All @@ -117,6 +118,24 @@ function NonHyprProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
/>
)}
</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>
Expand Down Expand Up @@ -370,9 +389,11 @@ function ProviderContext({ providerId }: { providerId: ProviderId }) {
? "Hyprnote curates list of on-device models and also cloud models with high-availability and performance."
: providerId === "deepgram"
? `Use [Deepgram](https://deepgram.com) for transcriptions. \
You can choose to use the [EU Endpoint](https://developers.deepgram.com/reference/custom-endpoints#eu-endpoints) if you prefer.`
: providerId === "deepgram-custom"
? `If you're using a [Dedicated endpoint](https://developers.deepgram.com/reference/custom-endpoints#deepgram-dedicated-endpoints), or other Deepgram-compatible endpoint, you can configure it here.`
If you want to use a [Dedicated](https://developers.deepgram.com/reference/custom-endpoints#deepgram-dedicated-endpoints)
or [EU](https://developers.deepgram.com/reference/custom-endpoints#eu-endpoints) endpoint,
you can do that in the **advanced** section.`
: providerId === "custom"
? `We only support **Deepgram compatible** endpoints for now.`
: "";

if (!content.trim()) {
Expand Down
18 changes: 9 additions & 9 deletions apps/desktop/src/components/settings/ai/stt/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,33 @@ export const PROVIDERS = [
id: "hyprnote",
displayName: "Hyprnote",
icon: <img src="/assets/icon.png" alt="Hyprnote" className="size-5" />,
baseUrl: { value: "https://api.hyprnote.com/v1", immutable: true },
baseUrl: "https://api.hyprnote.com/v1",
models: ["am-parakeet-v2", "am-parakeet-v3"] satisfies SupportedSttModel[],
},
{
disabled: false,
id: "deepgram",
displayName: "Deepgram",
icon: <Icon icon="simple-icons:deepgram" className="size-4" />,
baseUrl: { value: "https://api.deepgram.com/v1", immutable: true },
baseUrl: "https://api.deepgram.com/v1",
models: ["nova-3", "nova-3-general", "nova-3-medical"],
},
{
disabled: false,
id: "deepgram-custom",
displayName: "Deepgram (Custom)",
id: "custom",
displayName: "Custom",
badge: null,
icon: <Icon icon="simple-icons:deepgram" className="size-4" />,
baseUrl: { value: "https://api.openai.com/v1", immutable: false },
models: ["nova-3", "nova-3-general", "nova-3-medical"],
icon: <Icon icon="mingcute:random-fill" />,
baseUrl: undefined,
models: [],
},
{
disabled: true,
id: "groq",
displayName: "Groq",
badge: null,
icon: <Groq size={16} />,
baseUrl: { value: "https://api.groq.com/v1", immutable: false },
baseUrl: "https://api.groq.com/v1",
models: ["whisper-large-v3-turbo", "whisper-large-v3"],
},
{
Expand All @@ -62,7 +62,7 @@ export const PROVIDERS = [
displayName: "Fireworks",
badge: null,
icon: <Fireworks size={16} />,
baseUrl: { value: "https://api.firework.ai/v1", immutable: false },
baseUrl: "https://api.firework.ai/v1",
models: ["whisper-large-v3-turbo", "whisper-large-v3"],
},
] as const;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/hooks/useLLMConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const useLLMConnection = (): {
const providerId = current_llm_provider as ProviderId;
const providerDefinition = PROVIDERS.find((provider) => provider.id === providerId);

const baseUrl = providerConfig?.base_url?.trim() || providerDefinition?.baseUrl.value || "";
const baseUrl = providerConfig?.base_url?.trim() || providerDefinition?.baseUrl || "";
const apiKey = providerConfig?.api_key?.trim() || "";

if (!baseUrl) {
Expand Down
Loading
Loading