Skip to content

Commit 2451a2d

Browse files
Merge pull request #25 from sidharthmirch/sidharthmirch/settings-providers
Refactor visible models settings by provider
2 parents 80d8adf + b68a92b commit 2451a2d

5 files changed

Lines changed: 489 additions & 244 deletions

File tree

src/core/chorus/api/ProviderVisibilityAPI.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { db } from "../DB";
3-
import { ProviderVisibility, ProviderName } from "../Models";
3+
import { ProviderVisibility, ProviderName, getProviderName } from "../Models";
4+
import { useApiKeys } from "./AppMetadataAPI";
5+
import { useModelConfigs } from "./ModelsAPI";
6+
import { hasApiKey } from "@core/utilities/ProxyUtils";
47

58
const providerVisibilityKeys = {
69
all: () => ["providerVisibility"] as const,
710
list: () => [...providerVisibilityKeys.all(), "list"] as const,
811
};
912

13+
const API_KEY_REQUIRED_HIDDEN_PROVIDERS = new Set<ProviderName>([
14+
"openrouter",
15+
"google",
16+
"openai",
17+
"anthropic",
18+
]);
19+
1020
type ProviderVisibilityDBRow = {
1121
provider_name: string;
1222
model_id: string;
@@ -113,7 +123,29 @@ export function useSetAllProviderModelsVisible() {
113123
*/
114124
export function useProviderVisibilityMap(): Map<string, boolean> | undefined {
115125
const { data } = useProviderVisibleModels();
116-
if (!data) return undefined;
126+
const { data: apiKeys } = useApiKeys();
127+
const { data: allModels } = useModelConfigs();
128+
129+
if (!data && !allModels) return undefined;
130+
131+
const visibilityMap = new Map(
132+
(data ?? []).map((v) => [v.modelId, v.isVisible]),
133+
);
134+
135+
if (!allModels || apiKeys === undefined) {
136+
return visibilityMap;
137+
}
138+
139+
for (const model of allModels) {
140+
const provider = getProviderName(model.modelId);
141+
if (!API_KEY_REQUIRED_HIDDEN_PROVIDERS.has(provider)) {
142+
continue;
143+
}
144+
145+
if (!hasApiKey(provider as keyof typeof apiKeys, apiKeys)) {
146+
visibilityMap.set(model.modelId, false);
147+
}
148+
}
117149

118-
return new Map(data.map((v) => [v.modelId, v.isVisible]));
150+
return visibilityMap;
119151
}

src/core/utilities/ProxyUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export function hasApiKey(
3939
providerKey: keyof ApiKeys,
4040
apiKeys: ApiKeys,
4141
): boolean {
42-
return Boolean(apiKeys[providerKey]);
42+
const key = apiKeys[providerKey];
43+
return typeof key === "string" && key.trim().length > 0;
4344
}
4445

4546
/**

src/ui/components/ManageModelsBox.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,46 @@ import {
6464
} from "./ui/select";
6565

6666
// Helper function to filter models by search terms
67+
const normalizeSearchValue = (value: string): string =>
68+
value.toLowerCase().replace(/[^a-z0-9]/g, "");
69+
70+
const isSubsequenceMatch = (needle: string, haystack: string): boolean => {
71+
if (!needle) return true;
72+
let index = 0;
73+
for (const char of haystack) {
74+
if (char === needle[index]) {
75+
index += 1;
76+
if (index === needle.length) {
77+
return true;
78+
}
79+
}
80+
}
81+
return false;
82+
};
83+
6784
const filterBySearch = (models: ModelConfig[], searchTerms: string[]) => {
6885
if (searchTerms.length === 0) return models;
6986
return models.filter((m) => {
7087
const providerLabel = getProviderLabel(m.modelId);
71-
72-
return searchTerms.every(
73-
(term) =>
74-
m.displayName.toLowerCase().includes(term) ||
75-
providerLabel.toLowerCase().includes(term),
88+
const displayName = m.displayName.toLowerCase();
89+
const providerLabelLower = providerLabel.toLowerCase();
90+
const modelIdLower = m.modelId.toLowerCase();
91+
const normalizedHaystack = normalizeSearchValue(
92+
`${m.displayName} ${providerLabel} ${m.modelId}`,
7693
);
94+
95+
return searchTerms.every((term) => {
96+
const normalizedTerm = normalizeSearchValue(term);
97+
98+
return (
99+
displayName.includes(term) ||
100+
providerLabelLower.includes(term) ||
101+
modelIdLower.includes(term) ||
102+
(normalizedTerm.length > 0 &&
103+
(normalizedHaystack.includes(normalizedTerm) ||
104+
isSubsequenceMatch(normalizedTerm, normalizedHaystack)))
105+
);
106+
});
77107
});
78108
};
79109

src/ui/components/Onboarding.tsx

Lines changed: 114 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,128 @@
1-
import { useEffect, useState, useCallback } from "react";
1+
import { useEffect, useState, useCallback, useMemo } from "react";
22
import { Button } from "./ui/button";
33
import { Input } from "./ui/input";
44
import { Label } from "./ui/label";
55
import { SettingsManager } from "@core/utilities/Settings";
66
import * as AppMetadataAPI from "@core/chorus/api/AppMetadataAPI";
77
import { useQueryClient } from "@tanstack/react-query";
88

9+
type OnboardingProvider = "openrouter" | "google" | "openai" | "anthropic";
10+
11+
const ONBOARDING_API_KEY_FIELDS: Array<{
12+
provider: OnboardingProvider;
13+
label: string;
14+
placeholder: string;
15+
}> = [
16+
{
17+
provider: "openrouter",
18+
label: "OpenRouter API key",
19+
placeholder: "sk-or-v1-...",
20+
},
21+
{
22+
provider: "google",
23+
label: "Google AI API key",
24+
placeholder: "AIza...",
25+
},
26+
{
27+
provider: "openai",
28+
label: "OpenAI API key",
29+
placeholder: "sk-...",
30+
},
31+
{
32+
provider: "anthropic",
33+
label: "Anthropic API key",
34+
placeholder: "sk-ant-...",
35+
},
36+
];
37+
38+
const EMPTY_API_KEY_INPUTS: Record<OnboardingProvider, string> = {
39+
openrouter: "",
40+
google: "",
41+
openai: "",
42+
anthropic: "",
43+
};
44+
945
export default function Onboarding({ onComplete }: { onComplete: () => void }) {
1046
const onboardingStep = AppMetadataAPI.useOnboardingStep();
1147
const setOnboardingStep = AppMetadataAPI.useSetOnboardingStep();
12-
const [openRouterKey, setOpenRouterKey] = useState("");
48+
const [apiKeyInputs, setApiKeyInputs] = useState(EMPTY_API_KEY_INPUTS);
1349
const [isSaving, setIsSaving] = useState(false);
1450
const queryClient = useQueryClient();
1551

52+
const hasAnyApiKey = useMemo(
53+
() =>
54+
Object.values(apiKeyInputs).some(
55+
(apiKey) => apiKey.trim().length > 0,
56+
),
57+
[apiKeyInputs],
58+
);
59+
1660
const handleNextStep = useCallback(() => {
1761
setOnboardingStep.mutate({ step: 1 });
1862
}, [setOnboardingStep]);
1963

2064
const handleSaveAndComplete = useCallback(async () => {
21-
if (openRouterKey.trim()) {
22-
setIsSaving(true);
65+
setIsSaving(true);
66+
try {
2367
const settingsManager = SettingsManager.getInstance();
2468
const currentSettings = await settingsManager.get();
25-
const newApiKeys = {
26-
...currentSettings.apiKeys,
27-
openrouter: openRouterKey.trim(),
28-
};
29-
await settingsManager.set({
69+
70+
const trimmedApiKeys: Partial<Record<OnboardingProvider, string>> =
71+
{};
72+
for (const field of ONBOARDING_API_KEY_FIELDS) {
73+
const value = apiKeyInputs[field.provider].trim();
74+
if (value.length > 0) {
75+
trimmedApiKeys[field.provider] = value;
76+
}
77+
}
78+
79+
const updatedSettings = {
3080
...currentSettings,
31-
apiKeys: newApiKeys,
32-
});
81+
apiKeys: {
82+
...currentSettings.apiKeys,
83+
...trimmedApiKeys,
84+
},
85+
};
86+
87+
await settingsManager.set(updatedSettings);
3388
await queryClient.invalidateQueries({ queryKey: ["apiKeys"] });
89+
} finally {
3490
setIsSaving(false);
3591
}
92+
3693
onComplete();
37-
}, [openRouterKey, queryClient, onComplete]);
94+
}, [apiKeyInputs, queryClient, onComplete]);
95+
96+
const handleApiKeyChange = useCallback(
97+
(provider: OnboardingProvider, value: string) => {
98+
setApiKeyInputs((previous) => ({
99+
...previous,
100+
[provider]: value,
101+
}));
102+
},
103+
[],
104+
);
38105

39-
// Allow pressing Enter to continue quickly
40106
useEffect(() => {
41107
const handleKeyDown = (e: KeyboardEvent) => {
42108
if (e.key === "Enter") {
43-
e.preventDefault();
44109
if (onboardingStep === 0) {
45110
handleNextStep();
46-
} else {
111+
} else if (onboardingStep === 1 && !isSaving) {
47112
void handleSaveAndComplete();
48113
}
49114
}
50115
};
51116

52117
document.addEventListener("keydown", handleKeyDown);
53-
return () => document.removeEventListener("keydown", handleKeyDown);
54-
}, [onboardingStep, handleNextStep, handleSaveAndComplete]);
118+
return () => {
119+
document.removeEventListener("keydown", handleKeyDown);
120+
};
121+
}, [onboardingStep, handleNextStep, handleSaveAndComplete, isSaving]);
55122

56123
if (onboardingStep === 0) {
57124
return (
58-
<div
59-
data-tauri-drag-region
60-
className="fixed inset-0 z-50 flex flex-col items-center justify-center min-h-screen bg-background/95 backdrop-blur-sm px-4"
61-
>
125+
<div className="min-h-screen flex items-center justify-center px-4">
62126
<div className="text-center space-y-6 max-w-3xl w-full">
63127
<div className="space-y-2">
64128
<h1 className="text-2xl font-semibold tracking-tight">
@@ -82,44 +146,38 @@ export default function Onboarding({ onComplete }: { onComplete: () => void }) {
82146
);
83147
}
84148

85-
// Step 2: OpenRouter API key
86149
return (
87-
<div
88-
data-tauri-drag-region
89-
className="fixed inset-0 z-50 flex flex-col items-center justify-center min-h-screen bg-background/95 backdrop-blur-sm px-4"
90-
>
91-
<div className="text-center space-y-6 max-w-md w-full">
92-
<div className="space-y-2">
93-
<h1 className="text-2xl font-semibold tracking-tight">
94-
Add an API Key
95-
</h1>
96-
<p className="text-muted-foreground">
97-
Chorus runs on API keys. We recommend{" "}
98-
<a
99-
href="https://openrouter.ai/keys"
100-
target="_blank"
101-
rel="noopener noreferrer"
102-
className="text-primary underline underline-offset-4"
103-
>
104-
OpenRouter
105-
</a>{" "}
106-
to get started.
150+
<div className="min-h-screen flex items-center justify-center px-4">
151+
<div className="w-full max-w-md space-y-6">
152+
<div className="space-y-2 text-center">
153+
<h2 className="text-xl font-semibold tracking-tight">
154+
Optional API keys
155+
</h2>
156+
<p className="text-sm text-muted-foreground">
157+
Add keys now or skip and configure them later in
158+
Settings.
107159
</p>
108160
</div>
109161

110-
<div className="space-y-2 text-left">
111-
<Label htmlFor="openrouter-key">OpenRouter API Key</Label>
112-
<Input
113-
id="openrouter-key"
114-
type="password"
115-
placeholder="sk-or-..."
116-
value={openRouterKey}
117-
onChange={(e) => setOpenRouterKey(e.target.value)}
118-
autoFocus
119-
/>
120-
<p className="text-xs text-muted-foreground">
121-
You can add more API keys later in Settings.
122-
</p>
162+
<div className="space-y-4">
163+
{ONBOARDING_API_KEY_FIELDS.map((field) => (
164+
<div key={field.provider} className="space-y-2">
165+
<Label htmlFor={`${field.provider}-api-key`}>
166+
{field.label}
167+
</Label>
168+
<Input
169+
id={`${field.provider}-api-key`}
170+
placeholder={field.placeholder}
171+
value={apiKeyInputs[field.provider]}
172+
onChange={(event) =>
173+
handleApiKeyChange(
174+
field.provider,
175+
event.target.value,
176+
)
177+
}
178+
/>
179+
</div>
180+
))}
123181
</div>
124182

125183
<div className="flex flex-col gap-2">
@@ -128,9 +186,7 @@ export default function Onboarding({ onComplete }: { onComplete: () => void }) {
128186
onClick={() => void handleSaveAndComplete()}
129187
disabled={isSaving}
130188
>
131-
{openRouterKey.trim()
132-
? "Save and continue"
133-
: "Skip for now"}{" "}
189+
{hasAnyApiKey ? "Save and continue" : "Skip for now"}{" "}
134190
<span className="text-sm"></span>
135191
</Button>
136192
</div>

0 commit comments

Comments
 (0)