|
1 | 1 | import React, { useState, useEffect } from "react"; |
2 | | -import { |
3 | | - Card, |
4 | | - Table, |
5 | | - TableHead, |
6 | | - TableRow, |
7 | | - TableHeaderCell, |
8 | | - TableCell, |
9 | | - TableBody, |
10 | | - Title, |
11 | | - Text, |
12 | | - Button, |
13 | | - Icon, |
14 | | - Switch, |
15 | | -} from "@tremor/react"; |
| 2 | +import { Button } from "@/components/ui/button"; |
| 3 | +import { Card, CardContent, CardTitle } from "@/components/ui/card"; |
| 4 | +import { Input } from "@/components/ui/input"; |
| 5 | +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; |
| 6 | +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; |
| 7 | +import { Switch } from "@/components/ui/switch"; |
| 8 | +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; |
16 | 9 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
17 | 10 | import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking"; |
18 | | -import { InputNumber, Select as AntdSelect } from "antd"; |
19 | | -import { TrashIcon } from "@heroicons/react/outline"; |
| 11 | +import { Trash2 } from "lucide-react"; |
20 | 12 | import { StatusBadge } from "@/components/shared/table_cells"; |
21 | 13 |
|
22 | 14 | import RouterSettings from "@/components/router_settings"; |
@@ -44,59 +36,78 @@ export interface generalSettingsItem { |
44 | 36 | field_default_value?: any; |
45 | 37 | } |
46 | 38 |
|
| 39 | +const NUMERIC_INPUT_WIDTH = "w-36"; |
| 40 | + |
| 41 | +const toNumericValue = (raw: string): number | null => (raw === "" ? null : Number(raw)); |
| 42 | + |
47 | 43 | const SettingValueEditor: React.FC<{ |
48 | 44 | setting: generalSettingsItem; |
49 | 45 | onChange: (fieldName: string, newValue: any) => void; |
50 | 46 | }> = ({ setting, onChange }) => { |
51 | 47 | if (setting.field_type === "Integer") { |
52 | 48 | return ( |
53 | | - <InputNumber |
| 49 | + <Input |
| 50 | + type="number" |
54 | 51 | step={1} |
55 | | - value={setting.field_value} |
56 | | - onChange={(newValue) => onChange(setting.field_name, newValue)} |
| 52 | + className={NUMERIC_INPUT_WIDTH} |
| 53 | + value={setting.field_value ?? ""} |
| 54 | + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} |
57 | 55 | /> |
58 | 56 | ); |
59 | 57 | } |
60 | 58 | if (setting.field_type === "Boolean") { |
61 | 59 | return ( |
62 | 60 | <Switch |
63 | 61 | checked={setting.field_value === true || setting.field_value === "true"} |
64 | | - onChange={(checked) => onChange(setting.field_name, checked)} |
| 62 | + onCheckedChange={(checked) => onChange(setting.field_name, checked)} |
65 | 63 | /> |
66 | 64 | ); |
67 | 65 | } |
68 | 66 | if (setting.field_type === "Float") { |
69 | 67 | return ( |
70 | | - <InputNumber |
| 68 | + <Input |
| 69 | + type="number" |
71 | 70 | min={0} |
72 | 71 | max={1} |
73 | 72 | step={0.05} |
74 | | - value={setting.field_value} |
75 | | - onChange={(newValue) => onChange(setting.field_name, newValue)} |
| 73 | + className={NUMERIC_INPUT_WIDTH} |
| 74 | + value={setting.field_value ?? ""} |
| 75 | + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} |
76 | 76 | /> |
77 | 77 | ); |
78 | 78 | } |
79 | 79 | if (setting.field_type === "Dollar") { |
80 | 80 | return ( |
81 | | - <InputNumber |
82 | | - min={0.01} |
83 | | - step={0.25} |
84 | | - prefix="$" |
85 | | - value={setting.field_value} |
86 | | - onChange={(newValue) => onChange(setting.field_name, newValue)} |
87 | | - /> |
| 81 | + <InputGroup className={NUMERIC_INPUT_WIDTH}> |
| 82 | + <InputGroupAddon>$</InputGroupAddon> |
| 83 | + <InputGroupInput |
| 84 | + type="number" |
| 85 | + min={0.01} |
| 86 | + step={0.25} |
| 87 | + value={setting.field_value ?? ""} |
| 88 | + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} |
| 89 | + /> |
| 90 | + </InputGroup> |
88 | 91 | ); |
89 | 92 | } |
90 | 93 | if (setting.field_type === "Select") { |
91 | 94 | return ( |
92 | | - <AntdSelect |
93 | | - allowClear |
94 | | - style={{ minWidth: "8rem" }} |
95 | | - placeholder="Default" |
96 | | - value={setting.field_value || undefined} |
97 | | - options={(setting.field_options ?? []).map((option) => ({ label: option, value: option }))} |
98 | | - onChange={(newValue) => onChange(setting.field_name, newValue ?? "")} |
99 | | - /> |
| 95 | + <Select |
| 96 | + value={setting.field_value || null} |
| 97 | + onValueChange={(newValue) => onChange(setting.field_name, newValue ?? "")} |
| 98 | + > |
| 99 | + <SelectTrigger className="min-w-32"> |
| 100 | + <SelectValue placeholder="Default" /> |
| 101 | + </SelectTrigger> |
| 102 | + <SelectContent> |
| 103 | + <SelectItem value={null}>Default</SelectItem> |
| 104 | + {(setting.field_options ?? []).map((option) => ( |
| 105 | + <SelectItem key={option} value={option}> |
| 106 | + {option} |
| 107 | + </SelectItem> |
| 108 | + ))} |
| 109 | + </SelectContent> |
| 110 | + </Select> |
100 | 111 | ); |
101 | 112 | } |
102 | 113 | return null; |
@@ -131,33 +142,43 @@ export const PromptCachingPanel: React.FC<{ |
131 | 142 |
|
132 | 143 | return ( |
133 | 144 | <Card> |
134 | | - <Title>Prompt Caching</Title> |
| 145 | + <CardContent> |
| 146 | + <CardTitle>Prompt Caching</CardTitle> |
135 | 147 |
|
136 | | - <div className="mt-6 flex items-start justify-between gap-8"> |
137 | | - <div className="max-w-2xl"> |
138 | | - <Text className="font-medium">Automatic Anthropic prompt caching</Text> |
139 | | - <p className="mt-1 text-xs text-gray-500">{enableSetting.field_description}</p> |
140 | | - </div> |
141 | | - <Switch checked={enabled} onChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} /> |
142 | | - </div> |
143 | | - |
144 | | - {ttlSetting && ( |
145 | 148 | <div className="mt-6 flex items-start justify-between gap-8"> |
146 | | - <div className="max-w-2xl"> |
147 | | - <Text className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</Text> |
148 | | - <p className="mt-1 text-xs text-gray-500">{ttlSetting.field_description}</p> |
| 149 | + <div className="min-w-0 max-w-2xl"> |
| 150 | + <p className="font-medium">Automatic Anthropic prompt caching</p> |
| 151 | + <p className="mt-1 break-words text-xs text-gray-500">{enableSetting.field_description}</p> |
149 | 152 | </div> |
150 | | - <AntdSelect |
151 | | - allowClear |
152 | | - disabled={!enabled} |
153 | | - style={{ minWidth: "10rem" }} |
154 | | - placeholder="5m (default)" |
155 | | - value={ttlSetting.field_value || undefined} |
156 | | - options={(ttlSetting.field_options ?? []).map((option) => ({ label: option, value: option }))} |
157 | | - onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")} |
158 | | - /> |
| 153 | + <Switch checked={enabled} onCheckedChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} /> |
159 | 154 | </div> |
160 | | - )} |
| 155 | + |
| 156 | + {ttlSetting && ( |
| 157 | + <div className="mt-6 flex items-start justify-between gap-8"> |
| 158 | + <div className="min-w-0 max-w-2xl"> |
| 159 | + <p className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</p> |
| 160 | + <p className="mt-1 break-words text-xs text-gray-500">{ttlSetting.field_description}</p> |
| 161 | + </div> |
| 162 | + <Select |
| 163 | + disabled={!enabled} |
| 164 | + value={ttlSetting.field_value || null} |
| 165 | + onValueChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")} |
| 166 | + > |
| 167 | + <SelectTrigger className="min-w-40"> |
| 168 | + <SelectValue placeholder="5m (default)" /> |
| 169 | + </SelectTrigger> |
| 170 | + <SelectContent> |
| 171 | + <SelectItem value={null}>5m (default)</SelectItem> |
| 172 | + {(ttlSetting.field_options ?? []).map((option) => ( |
| 173 | + <SelectItem key={option} value={option}> |
| 174 | + {option} |
| 175 | + </SelectItem> |
| 176 | + ))} |
| 177 | + </SelectContent> |
| 178 | + </Select> |
| 179 | + </div> |
| 180 | + )} |
| 181 | + </CardContent> |
161 | 182 | </Card> |
162 | 183 | ); |
163 | 184 | }; |
@@ -254,55 +275,60 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user |
254 | 275 | </TabsContent> |
255 | 276 | <TabsContent value="general" className="px-8 py-6"> |
256 | 277 | <Card> |
257 | | - <Table> |
258 | | - <TableHead> |
259 | | - <TableRow> |
260 | | - <TableHeaderCell>Setting</TableHeaderCell> |
261 | | - <TableHeaderCell>Value</TableHeaderCell> |
262 | | - <TableHeaderCell>Status</TableHeaderCell> |
263 | | - <TableHeaderCell>Action</TableHeaderCell> |
264 | | - </TableRow> |
265 | | - </TableHead> |
266 | | - <TableBody> |
267 | | - {generalSettings |
268 | | - .filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB) |
269 | | - .map((value, index) => ( |
270 | | - <TableRow key={index}> |
271 | | - <TableCell> |
272 | | - <Text>{value.field_name}</Text> |
273 | | - <p |
274 | | - style={{ |
275 | | - fontSize: "0.65rem", |
276 | | - color: "#808080", |
277 | | - fontStyle: "italic", |
278 | | - }} |
279 | | - className="mt-1" |
280 | | - > |
281 | | - {value.field_description} |
282 | | - </p> |
283 | | - </TableCell> |
284 | | - <TableCell> |
285 | | - <SettingValueEditor setting={value} onChange={handleInputChange} /> |
286 | | - </TableCell> |
287 | | - <TableCell> |
288 | | - {value.stored_in_db == true ? ( |
289 | | - <StatusBadge tone="success" label="In DB" /> |
290 | | - ) : value.stored_in_db == false ? ( |
291 | | - <StatusBadge tone="neutral" label="In Config" /> |
292 | | - ) : ( |
293 | | - <StatusBadge tone="neutral" label="Not Set" /> |
294 | | - )} |
295 | | - </TableCell> |
296 | | - <TableCell> |
297 | | - <Button onClick={() => handleUpdateField(value.field_name)}>Update</Button> |
298 | | - <Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name)}> |
299 | | - Reset |
300 | | - </Icon> |
301 | | - </TableCell> |
302 | | - </TableRow> |
303 | | - ))} |
304 | | - </TableBody> |
305 | | - </Table> |
| 278 | + <CardContent> |
| 279 | + <Table> |
| 280 | + <TableHeader> |
| 281 | + <TableRow> |
| 282 | + <TableHead>Setting</TableHead> |
| 283 | + <TableHead>Value</TableHead> |
| 284 | + <TableHead>Status</TableHead> |
| 285 | + <TableHead>Action</TableHead> |
| 286 | + </TableRow> |
| 287 | + </TableHeader> |
| 288 | + <TableBody> |
| 289 | + {generalSettings |
| 290 | + .filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB) |
| 291 | + .map((value, index) => ( |
| 292 | + <TableRow key={index}> |
| 293 | + <TableCell className="whitespace-normal"> |
| 294 | + <p className="break-words">{value.field_name}</p> |
| 295 | + <p |
| 296 | + style={{ |
| 297 | + fontSize: "0.65rem", |
| 298 | + color: "#808080", |
| 299 | + fontStyle: "italic", |
| 300 | + }} |
| 301 | + className="mt-1 break-words" |
| 302 | + > |
| 303 | + {value.field_description} |
| 304 | + </p> |
| 305 | + </TableCell> |
| 306 | + <TableCell> |
| 307 | + <SettingValueEditor setting={value} onChange={handleInputChange} /> |
| 308 | + </TableCell> |
| 309 | + <TableCell> |
| 310 | + {value.stored_in_db == true ? ( |
| 311 | + <StatusBadge tone="success" label="In DB" /> |
| 312 | + ) : value.stored_in_db == false ? ( |
| 313 | + <StatusBadge tone="neutral" label="In Config" /> |
| 314 | + ) : ( |
| 315 | + <StatusBadge tone="neutral" label="Not Set" /> |
| 316 | + )} |
| 317 | + </TableCell> |
| 318 | + <TableCell> |
| 319 | + <Button onClick={() => handleUpdateField(value.field_name)}>Update</Button> |
| 320 | + <span |
| 321 | + onClick={() => handleResetField(value.field_name)} |
| 322 | + className="inline-flex shrink-0 cursor-pointer items-center justify-center px-1.5 py-1.5 text-red-500" |
| 323 | + > |
| 324 | + <Trash2 className="h-5 w-5 shrink-0" /> |
| 325 | + </span> |
| 326 | + </TableCell> |
| 327 | + </TableRow> |
| 328 | + ))} |
| 329 | + </TableBody> |
| 330 | + </Table> |
| 331 | + </CardContent> |
306 | 332 | </Card> |
307 | 333 | </TabsContent> |
308 | 334 | </Tabs> |
|
0 commit comments