返回 JoyAI-Echo
PeSelector.tsx
1 import { useEffect, useState } from "react";
2 import { FlaskConical } from "lucide-react";
3
4 import { Button } from "@/components/ui/button";
5 import {
6 DropdownMenu,
7 DropdownMenuContent,
8 DropdownMenuLabel,
9 DropdownMenuRadioGroup,
10 DropdownMenuRadioItem,
11 DropdownMenuSeparator,
12 DropdownMenuTrigger,
13 } from "@/components/ui/dropdown-menu";
14 import { useClient } from "@/providers/ClientProvider";
15 import { listPeSets, type PeSet } from "@/lib/pe-api";
16
17 /** Per-session Prompt Engineering set selector (internal test page). */
18 export function PeSelector({ chatId }: { chatId?: string }) {
19 const { client, token } = useClient();
20 const [sets, setSets] = useState<PeSet[]>([]);
21 const [active, setActive] = useState<string>("");
22 const [enabled, setEnabled] = useState(true);
23
24 useEffect(() => {
25 let cancelled = false;
26 listPeSets(token)
27 .then((res) => {
28 if (cancelled) return;
29 setSets(res.sets);
30 setActive(res.active);
31 setEnabled(res.enabled);
32 })
33 .catch(() => {
34 if (!cancelled) setEnabled(false);
35 });
36 return () => {
37 cancelled = true;
38 };
39 }, [token]);
40
41 useEffect(() => {
42 return client.onPe((evChatId, next) => {
43 // Only reflect changes for the chat this selector is bound to.
44 if (chatId && evChatId === chatId) setActive(next);
45 });
46 }, [client, chatId]);
47
48 if (!enabled || sets.length === 0) return null;
49
50 const selected = sets.find((s) => s.name === active);
51
52 return (
53 <DropdownMenu>
54 <DropdownMenuTrigger asChild>
55 <Button
56 variant="ghost"
57 size="sm"
58 disabled={!chatId}
59 aria-label="Switch prompt kit"
60 className="h-7 gap-1.5 rounded-md px-2 text-[11px] text-muted-foreground hover:bg-accent/35 hover:text-foreground"
61 >
62 <FlaskConical className="h-3.5 w-3.5" />
63 <span className="max-w-[7rem] truncate">
64 {selected?.label ?? active}
65 </span>
66 </Button>
67 </DropdownMenuTrigger>
68 <DropdownMenuContent align="end">
69 <DropdownMenuLabel>Prompt Kit</DropdownMenuLabel>
70 <DropdownMenuSeparator />
71 <DropdownMenuRadioGroup
72 value={active}
73 onValueChange={(value) => {
74 if (value === active || !chatId) return;
75 setActive(value);
76 client.setPe(value, chatId);
77 }}
78 >
79 {sets.map((option) => (
80 <DropdownMenuRadioItem key={option.name} value={option.name}>
81 <span className="flex min-w-0 flex-col gap-0.5">
82 <span>{option.label}</span>
83 {option.description ? (
84 <span className="truncate text-xs text-muted-foreground">
85 {option.description}
86 </span>
87 ) : null}
88 </span>
89 </DropdownMenuRadioItem>
90 ))}
91 </DropdownMenuRadioGroup>
92 </DropdownMenuContent>
93 </DropdownMenu>
94 );
95 }
96
96 lines Plain Text