Skip to content

Commit b559402

Browse files
committed
feat: basic node (#12)
* refactor: Rename node components to kebab-case, add new UI components (popover, command, spinner), and introduce a node parameters panel. * feat: Extract model selection into a new component and enable inline label editing for text nodes.
1 parent 6163f5c commit b559402

21 files changed

+920
-364
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@xyflow/react": "^12.10.0",
1717
"class-variance-authority": "^0.7.1",
1818
"clsx": "^2.1.1",
19+
"cmdk": "^1.1.1",
1920
"lucide-react": "^0.562.0",
2021
"radix-ui": "^1.4.3",
2122
"react": "^19.2.0",

frontend/package.json.md5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6fe7448857be097109d3aa983c11bf98
1+
b638092a7de1ebaad7aeae60050a509c

frontend/pnpm-lock.yaml

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useEffect, useState } from "react";
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
Popover,
5+
PopoverContent,
6+
PopoverTrigger,
7+
} from "@/components/ui/popover";
8+
import {
9+
Command,
10+
CommandEmpty,
11+
CommandGroup,
12+
CommandInput,
13+
CommandItem,
14+
CommandList,
15+
} from "@/components/ui/command";
16+
import { ListModelProviders } from "../../../wailsjs/go/database/Service";
17+
import { ListModels } from "../../../wailsjs/go/ai/Service";
18+
import { database, ai } from "../../../wailsjs/go/models";
19+
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
20+
import { cn } from "@/lib/utils";
21+
22+
interface ModelSelectorProps {
23+
providerId?: number;
24+
modelId?: string;
25+
onProviderChange: (providerId: number) => void;
26+
onModelChange: (modelId: string) => void;
27+
}
28+
29+
export function ModelSelector({
30+
providerId,
31+
modelId,
32+
onProviderChange,
33+
onModelChange,
34+
}: ModelSelectorProps) {
35+
const [providers, setProviders] = useState<database.ModelProvider[]>([]);
36+
const [models, setModels] = useState<ai.Model[]>([]);
37+
const [loadingProviders, setLoadingProviders] = useState(false);
38+
const [loadingModels, setLoadingModels] = useState(false);
39+
const [open, setOpen] = useState(false);
40+
41+
// Load providers on mount
42+
useEffect(() => {
43+
const load = async () => {
44+
setLoadingProviders(true);
45+
try {
46+
const list = await ListModelProviders();
47+
setProviders(list || []);
48+
} catch (e) {
49+
console.error("Failed to load providers", e);
50+
} finally {
51+
setLoadingProviders(false);
52+
}
53+
};
54+
load();
55+
}, []);
56+
57+
// Load models when provider changes
58+
useEffect(() => {
59+
if (providerId) {
60+
const load = async () => {
61+
setLoadingModels(true);
62+
try {
63+
const list = await ListModels(providerId);
64+
setModels(list || []);
65+
} catch (e) {
66+
console.error("Failed to load models", e);
67+
setModels([]);
68+
} finally {
69+
setLoadingModels(false);
70+
}
71+
};
72+
load();
73+
} else {
74+
setModels([]);
75+
}
76+
}, [providerId]);
77+
78+
const handleProviderSelect = (selectedProviderId: number) => {
79+
if (providerId === selectedProviderId) return;
80+
onProviderChange(selectedProviderId);
81+
};
82+
83+
const handleModelSelect = (selectedModelId: string) => {
84+
onModelChange(selectedModelId);
85+
setOpen(false);
86+
};
87+
88+
const selectedProvider = providers.find((p) => p.id === providerId);
89+
90+
return (
91+
<Popover open={open} onOpenChange={setOpen}>
92+
<PopoverTrigger className="w-60" asChild>
93+
<Button
94+
variant="outline"
95+
role="combobox"
96+
aria-expanded={open}
97+
className="h-8 text-xs"
98+
>
99+
<div className="w-full text-ellipsis justify-start flex items-center gap-2 overflow-hidden">
100+
{selectedProvider && modelId
101+
? `${selectedProvider.name} / ${modelId}`
102+
: "选择模型..."}
103+
</div>
104+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
105+
</Button>
106+
</PopoverTrigger>
107+
<PopoverContent className="w-125 p-0" align="start">
108+
<div className="flex h-75">
109+
{/* Providers Column */}
110+
<div className="w-1/3 border-r overflow-y-auto bg-muted/30 p-1">
111+
<div className="text-xs font-semibold text-muted-foreground px-2 py-1.5 mb-1">
112+
提供商
113+
</div>
114+
{loadingProviders ? (
115+
<div className="flex justify-center p-4">
116+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
117+
</div>
118+
) : (
119+
<div className="space-y-1">
120+
{providers.map((provider) => (
121+
<button
122+
key={provider.id}
123+
onClick={() => handleProviderSelect(provider.id)}
124+
className={cn(
125+
"w-full text-left px-2 py-1.5 rounded-sm text-sm hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between",
126+
providerId === provider.id &&
127+
"bg-accent text-accent-foreground font-medium"
128+
)}
129+
>
130+
<span className="truncate">{provider.name}</span>
131+
{providerId === provider.id && (
132+
<Check className="h-3 w-3" />
133+
)}
134+
</button>
135+
))}
136+
</div>
137+
)}
138+
</div>
139+
140+
{/* Models Column */}
141+
<div className="flex-1 flex flex-col">
142+
<Command className="h-full border-0">
143+
<CommandInput
144+
placeholder="搜索模型..."
145+
className="h-9 border-b"
146+
/>
147+
<CommandList className="max-h-full flex-1 min-h-0">
148+
{loadingModels ? (
149+
<div className="flex justify-center p-8">
150+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
151+
</div>
152+
) : (
153+
<>
154+
<CommandEmpty>未找到模型</CommandEmpty>
155+
<CommandGroup
156+
heading={
157+
selectedProvider
158+
? `${selectedProvider.name} 模型`
159+
: "请先选择提供商"
160+
}
161+
>
162+
{models.map((model) => (
163+
<CommandItem
164+
key={model.id}
165+
value={model.id}
166+
onSelect={() => handleModelSelect(model.id)}
167+
className="text-xs"
168+
>
169+
<Check
170+
className={cn(
171+
"mr-2 h-4 w-4",
172+
modelId === model.id
173+
? "opacity-100"
174+
: "opacity-0"
175+
)}
176+
/>
177+
{model.id}
178+
</CommandItem>
179+
))}
180+
</CommandGroup>
181+
</>
182+
)}
183+
</CommandList>
184+
</Command>
185+
</div>
186+
</div>
187+
</PopoverContent>
188+
</Popover>
189+
);
190+
}

0 commit comments

Comments
 (0)