Skip to content

Commit 1ae9260

Browse files
milispclaude
andcommitted
feat: implement provider state management with Zustand
- Add comprehensive provider store with state persistence - Refactor LlmProviderSelector to use command palette interface - Move formatDate utility to separate file for reusability - Add cmdk dependency for improved command interface - Extract provider configuration to centralized data file - Implement provider-specific API key and base URL management - Update settings dialog to use new store architecture 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f7365db commit 1ae9260

File tree

11 files changed

+517
-207
lines changed

11 files changed

+517
-207
lines changed

tauri-app/bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"axios": "^1.9.0",
3636
"class-variance-authority": "^0.7.1",
3737
"clsx": "^2.1.1",
38+
"cmdk": "^1.1.1",
3839
"i18next": "^24.2.3",
3940
"i18next-browser-languagedetector": "^8.0.4",
4041
"lodash": "^4.17.21",
@@ -453,6 +454,8 @@
453454

454455
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
455456

457+
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
458+
456459
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
457460

458461
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],

tauri-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"axios": "^1.9.0",
4949
"class-variance-authority": "^0.7.1",
5050
"clsx": "^2.1.1",
51+
"cmdk": "^1.1.1",
5152
"i18next": "^24.2.3",
5253
"i18next-browser-languagedetector": "^8.0.4",
5354
"lodash": "^4.17.21",

tauri-app/src/components/ChatPane.tsx

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import { useState } from "react";
2-
import { invoke } from "@tauri-apps/api/core";
3-
import { listen } from "@tauri-apps/api/event";
41
import { useChatStore, useCurrentMessages } from "@/hooks/useChatStore";
52
import { useConversationStore } from "@/hooks/useConversationStore";
63
import { useProvider } from "@/hooks/useProvider";
7-
import { MessageList } from "./chat/MessageList";
4+
import { formatDate } from "@/utils/date";
5+
import { invoke } from "@tauri-apps/api/core";
6+
import { listen } from "@tauri-apps/api/event";
7+
import { FileText, Heart, MessageSquare, PanelLeft, PanelLeftClose, Plus, Search, Settings, Star, Trash2 } from "lucide-react";
8+
import { useState } from "react";
89
import { ChatInput } from "./chat/ChatInput";
9-
import { WelcomeMessage } from "./chat/WelcomeMessage";
10-
import { LlmProviderSelector } from "./chat/LlmProviderSelector";
10+
import { MessageList } from "./chat/MessageList";
1111
import { SettingsDialog } from "./chat/SettingsDialog";
12-
import { NoteList } from "./notes/NoteList";
12+
import { WelcomeMessage } from "./chat/WelcomeMessage";
1313
import { NoteEditor } from "./notes/NoteEditor";
14+
import { NoteList } from "./notes/NoteList";
1415
import { Button } from "./ui/button";
15-
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
1616
import { Input } from "./ui/input";
17-
import { Plus, MessageSquare, Settings, Star, Trash2, Heart, Search, PanelLeftClose, PanelLeft, FileText } from "lucide-react";
17+
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
18+
1819

1920
interface ChatRequest extends Record<string, unknown> {
2021
message: string;
@@ -149,18 +150,6 @@ export function ChatPane() {
149150
toggleFavorite(id);
150151
};
151152

152-
const formatDate = (timestamp: number) => {
153-
const date = new Date(timestamp);
154-
const now = new Date();
155-
const diffTime = now.getTime() - date.getTime();
156-
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
157-
158-
if (diffDays === 0) return "Today";
159-
if (diffDays === 1) return "Yesterday";
160-
if (diffDays < 7) return `${diffDays} days ago`;
161-
return date.toLocaleDateString();
162-
};
163-
164153
return (
165154
<div className="flex h-full bg-white">
166155
{/* Left Sidebar */}
@@ -330,10 +319,6 @@ export function ChatPane() {
330319
{/* Model Selector */}
331320
<div className="p-4 border-b bg-gray-50">
332321
<div className="flex items-center gap-2">
333-
<span className="text-sm font-medium text-gray-700">Model:</span>
334-
<div className="flex-1">
335-
<LlmProviderSelector />
336-
</div>
337322
{!apiKey && (
338323
<span className="text-sm text-red-500">API Key Required</span>
339324
)}
Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,110 @@
1-
import { useProvider } from "@/hooks/useProvider";
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
Command,
4+
CommandGroup,
5+
CommandInput,
6+
CommandItem,
7+
CommandList,
8+
CommandSeparator,
9+
} from "@/components/ui/command";
210
import {
3-
Select,
4-
SelectContent,
5-
SelectItem,
6-
SelectTrigger,
7-
SelectValue,
8-
} from "@/components/ui/select";
11+
Popover,
12+
PopoverContent,
13+
PopoverTrigger,
14+
} from "@/components/ui/popover";
15+
import { useProvider } from "@/hooks/useProvider";
16+
import { Provider } from "@/types/provider";
17+
import { useState } from "react";
918

1019
export function LlmProviderSelector() {
1120
const {
21+
providers,
1222
selectedProvider,
1323
selectedModel,
24+
setSelectedProvider,
1425
setSelectedModel,
15-
providers,
1626
} = useProvider();
27+
const [open, setOpen] = useState(false);
1728

1829
// Find the selected provider object
1930
const selectedProviderObj = providers.find(
2031
(p) => p.value === selectedProvider,
2132
);
2233

34+
// Handle provider selection
35+
const handleProviderSelect = (providerValue: string) => {
36+
setSelectedProvider(providerValue as Provider);
37+
const newProvider = providers.find((p) => p.value === providerValue);
38+
if (newProvider?.models.length) {
39+
setSelectedModel(newProvider.models[0]);
40+
}
41+
};
42+
2343
// Handle model selection
2444
const handleModelSelect = (model: string) => {
2545
setSelectedModel(model);
46+
setOpen(false);
2647
};
2748

2849
return (
29-
<Select value={selectedModel} onValueChange={handleModelSelect}>
30-
<SelectTrigger className="w-full">
31-
<SelectValue placeholder="Choose model" />
32-
</SelectTrigger>
33-
<SelectContent>
34-
{selectedProviderObj?.models.map((model) => (
35-
<SelectItem key={model} value={model}>
36-
{model}
37-
</SelectItem>
38-
))}
39-
</SelectContent>
40-
</Select>
50+
<Popover open={open} onOpenChange={setOpen}>
51+
<PopoverTrigger asChild>
52+
<Button
53+
variant="outline"
54+
className="w-full justify-between"
55+
onClick={() => setOpen(!open)}
56+
>
57+
<span>
58+
{selectedProviderObj
59+
? `${selectedModel}`
60+
: "Choose provider"}
61+
</span>
62+
</Button>
63+
</PopoverTrigger>
64+
<PopoverContent className="p-0 w-80">
65+
<Command>
66+
<CommandInput placeholder="Search provider or model..." />
67+
<CommandList>
68+
<CommandGroup heading="Providers">
69+
{providers.map((provider) => (
70+
<CommandItem
71+
key={provider.value}
72+
value={provider.value}
73+
onSelect={() => handleProviderSelect(provider.value)}
74+
// Highlight selected provider
75+
className={
76+
provider.value === selectedProvider
77+
? "bg-accent text-accent-foreground"
78+
: ""
79+
}
80+
>
81+
{provider.label}
82+
</CommandItem>
83+
))}
84+
</CommandGroup>
85+
<CommandSeparator />
86+
{selectedProviderObj && (
87+
<CommandGroup heading="Models">
88+
{selectedProviderObj.models.map((model) => (
89+
<CommandItem
90+
key={model}
91+
value={model}
92+
onSelect={() => handleModelSelect(model)}
93+
// Highlight selected model
94+
className={
95+
model === selectedModel
96+
? "bg-accent text-accent-foreground"
97+
: ""
98+
}
99+
>
100+
{model}
101+
</CommandItem>
102+
))}
103+
</CommandGroup>
104+
)}
105+
</CommandList>
106+
</Command>
107+
</PopoverContent>
108+
</Popover>
41109
);
42-
}
110+
}

tauri-app/src/components/chat/SettingsDialog.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import { useProvider } from "@/hooks/useProvider";
3+
import { useProviderStore } from "@/stores/providerStore";
34
import {
45
Dialog,
56
DialogContent,
@@ -31,34 +32,37 @@ export function SettingsDialog({ children }: SettingsDialogProps) {
3132
selectedProvider,
3233
setSelectedProvider,
3334
apiKey,
34-
setApiKey,
3535
models,
3636
selectedModel,
3737
setSelectedModel,
3838
baseUrl,
39-
setBaseUrl,
4039
selectedProviderConfig,
4140
} = useProvider();
4241

42+
const { setApiKey: setStoreApiKey, setBaseUrl: setStoreBaseUrl } = useProviderStore();
43+
4344
const [showApiKey, setShowApiKey] = useState(false);
4445
const [tempApiKey, setTempApiKey] = useState(apiKey);
4546
const [tempBaseUrl, setTempBaseUrl] = useState(baseUrl);
4647

48+
// Sync temp values when provider or values change
49+
useEffect(() => {
50+
setTempApiKey(apiKey);
51+
}, [apiKey]);
52+
53+
useEffect(() => {
54+
setTempBaseUrl(baseUrl);
55+
}, [baseUrl]);
56+
4757
const handleSave = () => {
48-
setApiKey(tempApiKey);
49-
setBaseUrl(tempBaseUrl);
58+
setStoreApiKey(selectedProvider, tempApiKey);
59+
setStoreBaseUrl(selectedProvider, tempBaseUrl);
5060
};
5161

5262
const handleProviderChange = (provider: Provider) => {
5363
setSelectedProvider(provider);
54-
// Load the API key for the new provider
55-
const providerApiKey = localStorage.getItem(`${provider}_API_KEY`) || "";
56-
setTempApiKey(providerApiKey);
57-
58-
// Load the base URL for the new provider
59-
const providerConfig = providers.find((p) => p.value === provider);
60-
const storedUrl = localStorage.getItem(`${provider}_BASE_URL`);
61-
setTempBaseUrl(storedUrl || providerConfig?.defaultBaseUrl || "");
64+
// The useProvider hook will automatically update apiKey and baseUrl
65+
// No need to manually load from localStorage anymore
6266
};
6367

6468
return (

0 commit comments

Comments
 (0)