@sero-ai/ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-typecheck.log +1 -1
- package/package.json +6 -5
- package/src/components/ai-elements/prompt-input-context.tsx +252 -0
- package/src/components/ai-elements/prompt-input-elements.tsx +371 -0
- package/src/components/ai-elements/prompt-input-textarea.tsx +117 -0
- package/src/components/ai-elements/prompt-input.tsx +59 -990
- package/src/components/ai-elements/voice-selector-accent.tsx +50 -0
- package/src/components/ai-elements/voice-selector.tsx +6 -175
- package/src/components/model-selection/available-model-picker.tsx +231 -0
- package/src/components/model-selection/model-warning-list.tsx +33 -0
- package/src/components/model-selection/thinking-level-picker.tsx +55 -0
- package/src/components/ui/alert-dialog.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/plugin-safety-disclaimer.tsx +25 -0
- package/src/components/ui/search-input.tsx +40 -0
- package/src/index.ts +125 -7
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { Languages } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
export type VoiceSelectorAccentValue =
|
|
6
|
+
| 'american'
|
|
7
|
+
| 'british'
|
|
8
|
+
| 'australian'
|
|
9
|
+
| 'canadian'
|
|
10
|
+
| 'irish'
|
|
11
|
+
| 'scottish'
|
|
12
|
+
| 'indian'
|
|
13
|
+
| 'south-african'
|
|
14
|
+
| 'new-zealand'
|
|
15
|
+
| 'spanish'
|
|
16
|
+
| 'french'
|
|
17
|
+
| 'german'
|
|
18
|
+
| 'italian'
|
|
19
|
+
| 'portuguese'
|
|
20
|
+
| 'brazilian'
|
|
21
|
+
| 'mexican'
|
|
22
|
+
| 'argentinian'
|
|
23
|
+
| 'japanese'
|
|
24
|
+
| 'chinese'
|
|
25
|
+
| 'korean'
|
|
26
|
+
| 'russian'
|
|
27
|
+
| 'arabic'
|
|
28
|
+
| 'dutch'
|
|
29
|
+
| 'swedish'
|
|
30
|
+
| 'norwegian'
|
|
31
|
+
| 'danish'
|
|
32
|
+
| 'finnish'
|
|
33
|
+
| 'polish'
|
|
34
|
+
| 'turkish'
|
|
35
|
+
| 'greek'
|
|
36
|
+
| string;
|
|
37
|
+
|
|
38
|
+
export type VoiceSelectorAccentProps = ComponentProps<'span'> & {
|
|
39
|
+
value?: VoiceSelectorAccentValue;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const VoiceSelectorAccent = ({
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
...props
|
|
46
|
+
}: VoiceSelectorAccentProps) => (
|
|
47
|
+
<span className={cn('text-muted-foreground text-xs', className)} {...props}>
|
|
48
|
+
{children ?? <Languages className="size-4" />}
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import type { ComponentProps, ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
6
|
+
import {
|
|
7
|
+
VoiceSelectorAccent,
|
|
8
|
+
type VoiceSelectorAccentProps,
|
|
9
|
+
} from "./voice-selector-accent";
|
|
6
10
|
import { Button } from "../ui/button";
|
|
7
11
|
import {
|
|
8
12
|
Command,
|
|
@@ -238,181 +242,8 @@ export const VoiceSelectorGender = ({
|
|
|
238
242
|
);
|
|
239
243
|
};
|
|
240
244
|
|
|
241
|
-
export
|
|
242
|
-
|
|
243
|
-
| "american"
|
|
244
|
-
| "british"
|
|
245
|
-
| "australian"
|
|
246
|
-
| "canadian"
|
|
247
|
-
| "irish"
|
|
248
|
-
| "scottish"
|
|
249
|
-
| "indian"
|
|
250
|
-
| "south-african"
|
|
251
|
-
| "new-zealand"
|
|
252
|
-
| "spanish"
|
|
253
|
-
| "french"
|
|
254
|
-
| "german"
|
|
255
|
-
| "italian"
|
|
256
|
-
| "portuguese"
|
|
257
|
-
| "brazilian"
|
|
258
|
-
| "mexican"
|
|
259
|
-
| "argentinian"
|
|
260
|
-
| "japanese"
|
|
261
|
-
| "chinese"
|
|
262
|
-
| "korean"
|
|
263
|
-
| "russian"
|
|
264
|
-
| "arabic"
|
|
265
|
-
| "dutch"
|
|
266
|
-
| "swedish"
|
|
267
|
-
| "norwegian"
|
|
268
|
-
| "danish"
|
|
269
|
-
| "finnish"
|
|
270
|
-
| "polish"
|
|
271
|
-
| "turkish"
|
|
272
|
-
| "greek"
|
|
273
|
-
| string;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
export const VoiceSelectorAccent = ({
|
|
277
|
-
className,
|
|
278
|
-
value,
|
|
279
|
-
children,
|
|
280
|
-
...props
|
|
281
|
-
}: VoiceSelectorAccentProps) => {
|
|
282
|
-
let emoji: string | null = null;
|
|
283
|
-
|
|
284
|
-
switch (value) {
|
|
285
|
-
case "american": {
|
|
286
|
-
emoji = "🇺🇸";
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
case "british": {
|
|
290
|
-
emoji = "🇬🇧";
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
case "australian": {
|
|
294
|
-
emoji = "🇦🇺";
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
case "canadian": {
|
|
298
|
-
emoji = "🇨🇦";
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
case "irish": {
|
|
302
|
-
emoji = "🇮🇪";
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
case "scottish": {
|
|
306
|
-
emoji = "🏴";
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
case "indian": {
|
|
310
|
-
emoji = "🇮🇳";
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
case "south-african": {
|
|
314
|
-
emoji = "🇿🇦";
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
case "new-zealand": {
|
|
318
|
-
emoji = "🇳🇿";
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
case "spanish": {
|
|
322
|
-
emoji = "🇪🇸";
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
case "french": {
|
|
326
|
-
emoji = "🇫🇷";
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
case "german": {
|
|
330
|
-
emoji = "🇩🇪";
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
case "italian": {
|
|
334
|
-
emoji = "🇮🇹";
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
case "portuguese": {
|
|
338
|
-
emoji = "🇵🇹";
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
case "brazilian": {
|
|
342
|
-
emoji = "🇧🇷";
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
case "mexican": {
|
|
346
|
-
emoji = "🇲🇽";
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
case "argentinian": {
|
|
350
|
-
emoji = "🇦🇷";
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
case "japanese": {
|
|
354
|
-
emoji = "🇯🇵";
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
case "chinese": {
|
|
358
|
-
emoji = "🇨🇳";
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
case "korean": {
|
|
362
|
-
emoji = "🇰🇷";
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
case "russian": {
|
|
366
|
-
emoji = "🇷🇺";
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
case "arabic": {
|
|
370
|
-
emoji = "🇸🇦";
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
case "dutch": {
|
|
374
|
-
emoji = "🇳🇱";
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
case "swedish": {
|
|
378
|
-
emoji = "🇸🇪";
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
case "norwegian": {
|
|
382
|
-
emoji = "🇳🇴";
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
case "danish": {
|
|
386
|
-
emoji = "🇩🇰";
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
case "finnish": {
|
|
390
|
-
emoji = "🇫🇮";
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
case "polish": {
|
|
394
|
-
emoji = "🇵🇱";
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
case "turkish": {
|
|
398
|
-
emoji = "🇹🇷";
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
case "greek": {
|
|
402
|
-
emoji = "🇬🇷";
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
default: {
|
|
406
|
-
emoji = null;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return (
|
|
411
|
-
<span className={cn("text-muted-foreground text-xs", className)} {...props}>
|
|
412
|
-
{children ?? emoji}
|
|
413
|
-
</span>
|
|
414
|
-
);
|
|
415
|
-
};
|
|
245
|
+
export { VoiceSelectorAccent };
|
|
246
|
+
export type { VoiceSelectorAccentProps };
|
|
416
247
|
|
|
417
248
|
export type VoiceSelectorAgeProps = ComponentProps<"span">;
|
|
418
249
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Check, ChevronDown, Sparkles, X } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
filterModelGroups,
|
|
5
|
+
findGroup,
|
|
6
|
+
findModel,
|
|
7
|
+
modelKey,
|
|
8
|
+
parseModelKey,
|
|
9
|
+
type SharedAvailableModelGroup,
|
|
10
|
+
type SharedModelInfo,
|
|
11
|
+
} from '@sero-ai/common';
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
13
|
+
import { SearchInput } from '../ui/search-input';
|
|
14
|
+
import { cn } from '../../lib/utils';
|
|
15
|
+
|
|
16
|
+
interface AvailableModelPickerProps<
|
|
17
|
+
TModel extends SharedModelInfo,
|
|
18
|
+
TGroup extends SharedAvailableModelGroup<TModel>,
|
|
19
|
+
> {
|
|
20
|
+
groups: TGroup[];
|
|
21
|
+
value: string;
|
|
22
|
+
onChange: (value: string) => void;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
searchPlaceholder?: string;
|
|
25
|
+
emptyLabel?: string;
|
|
26
|
+
noModelsLabel?: string;
|
|
27
|
+
allowClear?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AvailableModelPicker<
|
|
33
|
+
TModel extends SharedModelInfo,
|
|
34
|
+
TGroup extends SharedAvailableModelGroup<TModel>,
|
|
35
|
+
>({
|
|
36
|
+
groups,
|
|
37
|
+
value,
|
|
38
|
+
onChange,
|
|
39
|
+
placeholder = 'Choose a model',
|
|
40
|
+
searchPlaceholder = 'Search models…',
|
|
41
|
+
emptyLabel = 'No matching models',
|
|
42
|
+
noModelsLabel = 'No models available',
|
|
43
|
+
allowClear = false,
|
|
44
|
+
disabled = false,
|
|
45
|
+
className,
|
|
46
|
+
}: AvailableModelPickerProps<TModel, TGroup>) {
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const [query, setQuery] = useState('');
|
|
49
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
50
|
+
|
|
51
|
+
const selected = useMemo(() => {
|
|
52
|
+
const parsed = parseModelKey(value);
|
|
53
|
+
if (!parsed) {
|
|
54
|
+
return value
|
|
55
|
+
? { group: null, model: null, fallbackLabel: value }
|
|
56
|
+
: { group: null, model: null, fallbackLabel: null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const group = findGroup(groups, parsed.provider, parsed.modelId) ?? null;
|
|
60
|
+
const model = findModel(groups, parsed.provider, parsed.modelId) ?? null;
|
|
61
|
+
return {
|
|
62
|
+
group,
|
|
63
|
+
model,
|
|
64
|
+
fallbackLabel: model ? null : value,
|
|
65
|
+
};
|
|
66
|
+
}, [groups, value]);
|
|
67
|
+
|
|
68
|
+
const filteredGroups = useMemo(
|
|
69
|
+
() => filterModelGroups(groups, query),
|
|
70
|
+
[groups, query],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const totalResults = useMemo(
|
|
74
|
+
() => filteredGroups.reduce((count, group) => count + group.models.length, 0),
|
|
75
|
+
[filteredGroups],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
|
79
|
+
setOpen(nextOpen);
|
|
80
|
+
if (!nextOpen) return;
|
|
81
|
+
setQuery('');
|
|
82
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const handleSelect = useCallback((provider: string, modelId: string) => {
|
|
86
|
+
onChange(modelKey(provider, modelId));
|
|
87
|
+
setOpen(false);
|
|
88
|
+
setQuery('');
|
|
89
|
+
}, [onChange]);
|
|
90
|
+
|
|
91
|
+
const handleClear = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
onChange('');
|
|
94
|
+
setOpen(false);
|
|
95
|
+
setQuery('');
|
|
96
|
+
}, [onChange]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
100
|
+
<PopoverTrigger asChild disabled={disabled}>
|
|
101
|
+
<div
|
|
102
|
+
role="button"
|
|
103
|
+
tabIndex={disabled ? -1 : 0}
|
|
104
|
+
className={cn(
|
|
105
|
+
'flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-left text-sm transition-colors',
|
|
106
|
+
'hover:bg-secondary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
107
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
108
|
+
className,
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{selected.group && selected.model ? (
|
|
112
|
+
<>
|
|
113
|
+
<img
|
|
114
|
+
src={selected.group.logo}
|
|
115
|
+
alt={selected.group.displayName}
|
|
116
|
+
className="size-4 shrink-0 rounded-sm dark:invert"
|
|
117
|
+
/>
|
|
118
|
+
<div className="min-w-0 flex-1">
|
|
119
|
+
<div className="flex items-center gap-1.5">
|
|
120
|
+
<span className="truncate font-medium text-foreground">{selected.model.name}</span>
|
|
121
|
+
{selected.model.reasoning ? (
|
|
122
|
+
<Sparkles className="size-3 shrink-0 text-amber-500/70" />
|
|
123
|
+
) : null}
|
|
124
|
+
</div>
|
|
125
|
+
<div className="truncate text-[11px] text-muted-foreground">
|
|
126
|
+
{selected.group.displayName}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</>
|
|
130
|
+
) : selected.fallbackLabel ? (
|
|
131
|
+
<span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
|
|
132
|
+
{selected.fallbackLabel}
|
|
133
|
+
</span>
|
|
134
|
+
) : (
|
|
135
|
+
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
|
136
|
+
{placeholder}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{allowClear && value ? (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={handleClear}
|
|
144
|
+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
|
145
|
+
title="Clear selection"
|
|
146
|
+
>
|
|
147
|
+
<X className="size-3.5" />
|
|
148
|
+
</button>
|
|
149
|
+
) : null}
|
|
150
|
+
|
|
151
|
+
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
|
|
152
|
+
</div>
|
|
153
|
+
</PopoverTrigger>
|
|
154
|
+
|
|
155
|
+
<PopoverContent
|
|
156
|
+
side="bottom"
|
|
157
|
+
align="start"
|
|
158
|
+
sideOffset={4}
|
|
159
|
+
className="w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-xl border-border/60 bg-background p-0 shadow-xl"
|
|
160
|
+
onWheel={(event) => event.stopPropagation()}
|
|
161
|
+
>
|
|
162
|
+
<SearchInput
|
|
163
|
+
ref={inputRef}
|
|
164
|
+
value={query}
|
|
165
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
166
|
+
placeholder={searchPlaceholder}
|
|
167
|
+
containerClassName="border-b border-border/40"
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
<div className="max-h-[280px] overflow-y-auto py-1">
|
|
171
|
+
{groups.length === 0 ? (
|
|
172
|
+
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
|
|
173
|
+
{noModelsLabel}
|
|
174
|
+
</div>
|
|
175
|
+
) : totalResults === 0 ? (
|
|
176
|
+
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
|
|
177
|
+
{emptyLabel}
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
filteredGroups.map((group, index) => (
|
|
181
|
+
<div key={group.provider}>
|
|
182
|
+
{index > 0 ? <div className="mx-3 border-t border-border/30" /> : null}
|
|
183
|
+
<div className="flex items-center gap-2 px-3 pb-1 pt-2">
|
|
184
|
+
<img
|
|
185
|
+
src={group.logo}
|
|
186
|
+
alt={group.displayName}
|
|
187
|
+
className="size-3.5 shrink-0 rounded-sm dark:invert"
|
|
188
|
+
/>
|
|
189
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
190
|
+
{group.displayName}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="px-1">
|
|
194
|
+
{group.models.map((model) => {
|
|
195
|
+
const nextValue = modelKey(model.provider, model.modelId);
|
|
196
|
+
const isSelected = nextValue === value;
|
|
197
|
+
return (
|
|
198
|
+
<button
|
|
199
|
+
key={nextValue}
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={() => handleSelect(model.provider, model.modelId)}
|
|
202
|
+
className={cn(
|
|
203
|
+
'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left text-xs transition-colors',
|
|
204
|
+
isSelected
|
|
205
|
+
? 'bg-secondary text-foreground'
|
|
206
|
+
: 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground',
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
<div className="flex size-4 shrink-0 items-center justify-center">
|
|
210
|
+
{isSelected ? (
|
|
211
|
+
<Check className="size-3.5 text-emerald-500" />
|
|
212
|
+
) : (
|
|
213
|
+
<div className="size-1.5 rounded-full bg-border" />
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
<span className="min-w-0 flex-1 truncate font-medium">{model.name}</span>
|
|
217
|
+
{model.reasoning ? (
|
|
218
|
+
<Sparkles className="size-3 shrink-0 text-amber-500/60" />
|
|
219
|
+
) : null}
|
|
220
|
+
</button>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
))
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</PopoverContent>
|
|
229
|
+
</Popover>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TriangleAlert } from 'lucide-react';
|
|
2
|
+
import { formatModelValidationWarning, type ModelValidationWarning } from '@sero-ai/common';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
interface ModelWarningListProps {
|
|
6
|
+
warnings: ModelValidationWarning[];
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ModelWarningList({ warnings, className }: ModelWarningListProps) {
|
|
11
|
+
if (warnings.length === 0) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn('space-y-2', className)}>
|
|
15
|
+
{warnings.map((warning, index) => (
|
|
16
|
+
<div
|
|
17
|
+
key={`${warning.code}:${index}`}
|
|
18
|
+
className={cn(
|
|
19
|
+
'rounded-lg border px-3 py-2 text-xs',
|
|
20
|
+
warning.severity === 'info'
|
|
21
|
+
? 'border-primary/20 bg-primary/5 text-primary'
|
|
22
|
+
: 'border-amber-500/20 bg-amber-500/5 text-amber-700 dark:text-amber-300',
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<div className="flex items-start gap-2">
|
|
26
|
+
<TriangleAlert className="mt-0.5 size-3.5 shrink-0" />
|
|
27
|
+
<span>{formatModelValidationWarning(warning)}</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
THINKING_LABELS,
|
|
3
|
+
THINKING_LEVELS,
|
|
4
|
+
type ThinkingLevel,
|
|
5
|
+
} from '@sero-ai/common';
|
|
6
|
+
import { cn } from '../../lib/utils';
|
|
7
|
+
|
|
8
|
+
interface ThinkingLevelPickerProps {
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: (value: ThinkingLevel) => void;
|
|
11
|
+
availableLevels?: readonly string[];
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
showUnsupported?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ThinkingLevelPicker({
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
availableLevels,
|
|
21
|
+
disabled = false,
|
|
22
|
+
showUnsupported = true,
|
|
23
|
+
className,
|
|
24
|
+
}: ThinkingLevelPickerProps) {
|
|
25
|
+
const allowed = new Set<string>(availableLevels ?? THINKING_LEVELS);
|
|
26
|
+
const levels = showUnsupported || !availableLevels
|
|
27
|
+
? THINKING_LEVELS
|
|
28
|
+
: THINKING_LEVELS.filter((level) => allowed.has(level));
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn('grid grid-cols-6 gap-1 rounded-lg border border-border/50 bg-muted/20 p-1', className)}>
|
|
32
|
+
{levels.map((level) => {
|
|
33
|
+
const isActive = value === level;
|
|
34
|
+
const isAvailable = allowed.has(level) || !availableLevels;
|
|
35
|
+
return (
|
|
36
|
+
<button
|
|
37
|
+
key={level}
|
|
38
|
+
type="button"
|
|
39
|
+
disabled={disabled || !isAvailable}
|
|
40
|
+
onClick={() => onChange(level)}
|
|
41
|
+
className={cn(
|
|
42
|
+
'rounded-md px-2 py-1.5 text-[11px] font-medium transition-colors',
|
|
43
|
+
isActive
|
|
44
|
+
? 'bg-background text-foreground shadow-sm'
|
|
45
|
+
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
|
|
46
|
+
(!isAvailable || disabled) && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{THINKING_LABELS[level]}
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -58,7 +58,7 @@ function AlertDialogContent({
|
|
|
58
58
|
data-slot="alert-dialog-content"
|
|
59
59
|
data-size={size}
|
|
60
60
|
className={cn(
|
|
61
|
-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-
|
|
61
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
|
|
62
62
|
className
|
|
63
63
|
)}
|
|
64
64
|
{...props}
|
|
@@ -73,7 +73,7 @@ function CommandInput({
|
|
|
73
73
|
<CommandPrimitive.Input
|
|
74
74
|
data-slot="command-input"
|
|
75
75
|
className={cn(
|
|
76
|
-
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
76
|
+
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent pl-1.5 py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
77
77
|
className
|
|
78
78
|
)}
|
|
79
79
|
{...props}
|
|
@@ -61,7 +61,7 @@ function DialogContent({
|
|
|
61
61
|
<DialogPrimitive.Content
|
|
62
62
|
data-slot="dialog-content"
|
|
63
63
|
className={cn(
|
|
64
|
-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-
|
|
64
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
65
65
|
className
|
|
66
66
|
)}
|
|
67
67
|
{...props}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ShieldAlert } from 'lucide-react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
|
|
4
|
+
export interface PluginSafetyDisclaimerProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function PluginSafetyDisclaimer({ className }: PluginSafetyDisclaimerProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
role="note"
|
|
12
|
+
className={cn(
|
|
13
|
+
'flex shrink-0 items-start gap-2 border-t border-[var(--status-warning-border)] bg-[var(--status-warning-faint)] px-4 py-2.5 text-[11px] leading-5 text-[var(--text-secondary)]',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
<ShieldAlert className="mt-0.5 size-3.5 shrink-0 text-[var(--status-warning)]" />
|
|
18
|
+
<p>
|
|
19
|
+
<span className="font-medium text-[var(--status-warning)]">Heads up:</span>{' '}
|
|
20
|
+
Plugins run with full access to this workspace and can execute code on your device.
|
|
21
|
+
Only install plugins from sources you trust.
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { SearchIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
interface SearchInputProps extends React.ComponentProps<"input"> {
|
|
7
|
+
containerClassName?: string
|
|
8
|
+
endAdornment?: React.ReactNode
|
|
9
|
+
iconClassName?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
|
13
|
+
({ containerClassName, className, endAdornment, iconClassName, ...props }, ref) => {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
data-slot="search-input"
|
|
17
|
+
className={cn("flex items-center gap-2 px-3", containerClassName)}
|
|
18
|
+
>
|
|
19
|
+
<SearchIcon
|
|
20
|
+
data-slot="search-input-icon"
|
|
21
|
+
className={cn("text-muted-foreground size-3.5 shrink-0", iconClassName)}
|
|
22
|
+
/>
|
|
23
|
+
<input
|
|
24
|
+
ref={ref}
|
|
25
|
+
data-slot="search-input-control"
|
|
26
|
+
className={cn(
|
|
27
|
+
"placeholder:text-muted-foreground h-9 w-full min-w-0 bg-transparent pl-1.5 text-xs text-foreground outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
{endAdornment}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
SearchInput.displayName = "SearchInput"
|
|
39
|
+
|
|
40
|
+
export { SearchInput }
|