@vvedantb/ui 0.17.1 → 0.18.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/README.md +251 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- package/tokens/src/css/markdown.css +318 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuTrigger,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuSub,
|
|
9
|
+
DropdownMenuSubTrigger,
|
|
10
|
+
DropdownMenuSubContent,
|
|
11
|
+
DropdownMenuRadioGroup,
|
|
12
|
+
DropdownMenuRadioItem,
|
|
13
|
+
} from "../ui/dropdown-menu";
|
|
14
|
+
import { useCn, useUiConfig } from "../ui/ui-config";
|
|
15
|
+
import { IconDots } from "@tabler/icons-react";
|
|
16
|
+
|
|
17
|
+
/** One model in `PromptInputSettings`, grouped by `provider`. */
|
|
18
|
+
export interface PromptInputSettingsOption<TModel extends string = string> {
|
|
19
|
+
id: TModel;
|
|
20
|
+
provider: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `UiConfig.PromptInputSettings`. `providerLabel` names a provider group
|
|
26
|
+
* (default: the provider id); `providerIcon` renders the icon before a
|
|
27
|
+
* provider and before "Model" (default: none). `classes.trigger` replaces the
|
|
28
|
+
* trigger button's classes.
|
|
29
|
+
*/
|
|
30
|
+
export interface PromptInputSettingsConfig {
|
|
31
|
+
providerLabel?: (provider: string) => string;
|
|
32
|
+
providerIcon?: (provider: string) => ReactNode;
|
|
33
|
+
classes?: {
|
|
34
|
+
trigger?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getProviderOptions<TModel extends string>(
|
|
39
|
+
options: ReadonlyArray<PromptInputSettingsOption<TModel>>,
|
|
40
|
+
providerLabel: (provider: string) => string,
|
|
41
|
+
): ReadonlyArray<{ id: string; label: string }> {
|
|
42
|
+
const providers: Array<{ id: string; label: string }> = [];
|
|
43
|
+
for (const option of options) {
|
|
44
|
+
if (providers.some((provider) => provider.id === option.provider)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
providers.push({
|
|
48
|
+
id: option.provider,
|
|
49
|
+
label: providerLabel(option.provider),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return providers;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PromptInputSettingsProps<TModel extends string = string> {
|
|
56
|
+
model: TModel;
|
|
57
|
+
onModelChange: (model: TModel) => void;
|
|
58
|
+
options: ReadonlyArray<PromptInputSettingsOption<TModel>>;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
icon?: ReactNode;
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Overflow menu for the prompt footer with a "Model" submenu: one radio list,
|
|
66
|
+
* or one submenu per provider when the options span several providers.
|
|
67
|
+
*/
|
|
68
|
+
export function PromptInputSettings<TModel extends string>({
|
|
69
|
+
model,
|
|
70
|
+
onModelChange,
|
|
71
|
+
options,
|
|
72
|
+
disabled,
|
|
73
|
+
icon,
|
|
74
|
+
className,
|
|
75
|
+
}: PromptInputSettingsProps<TModel>) {
|
|
76
|
+
const config = useUiConfig("PromptInputSettings");
|
|
77
|
+
const merge = useCn();
|
|
78
|
+
const providerLabel = config?.providerLabel ?? ((provider: string) => provider);
|
|
79
|
+
const providerIcon = config?.providerIcon;
|
|
80
|
+
const selectedModel =
|
|
81
|
+
options.find((entry) => entry.id === model) ?? options[0] ?? null;
|
|
82
|
+
const providerOptions = getProviderOptions(options, providerLabel);
|
|
83
|
+
const handleValueChange = (nextValue: string) => {
|
|
84
|
+
const option = options.find((entry) => entry.id === nextValue);
|
|
85
|
+
if (option) {
|
|
86
|
+
onModelChange(option.id);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<DropdownMenu modal={false}>
|
|
92
|
+
<DropdownMenuTrigger asChild>
|
|
93
|
+
<button
|
|
94
|
+
className={merge(
|
|
95
|
+
config?.classes?.trigger ??
|
|
96
|
+
"rounded-lg p-1.5 text-muted-foreground transition-[color,background-color,scale] duration-150 ease-out hover:bg-surface-secondary hover:text-foreground active:scale-[0.92] disabled:opacity-50",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
>
|
|
101
|
+
{icon ?? <IconDots size={16} />}
|
|
102
|
+
</button>
|
|
103
|
+
</DropdownMenuTrigger>
|
|
104
|
+
<DropdownMenuContent align="start">
|
|
105
|
+
<DropdownMenuSub>
|
|
106
|
+
<DropdownMenuSubTrigger>
|
|
107
|
+
{providerIcon?.(selectedModel?.provider ?? "")}
|
|
108
|
+
Model
|
|
109
|
+
</DropdownMenuSubTrigger>
|
|
110
|
+
<DropdownMenuSubContent>
|
|
111
|
+
{providerOptions.map((provider) => {
|
|
112
|
+
const models = options.filter(
|
|
113
|
+
(option) => option.provider === provider.id,
|
|
114
|
+
);
|
|
115
|
+
const radios = (
|
|
116
|
+
<DropdownMenuRadioGroup
|
|
117
|
+
key={provider.id}
|
|
118
|
+
value={selectedModel?.id}
|
|
119
|
+
onValueChange={handleValueChange}
|
|
120
|
+
>
|
|
121
|
+
{models.map((option) => (
|
|
122
|
+
<DropdownMenuRadioItem key={option.id} value={option.id}>
|
|
123
|
+
{option.label}
|
|
124
|
+
</DropdownMenuRadioItem>
|
|
125
|
+
))}
|
|
126
|
+
</DropdownMenuRadioGroup>
|
|
127
|
+
);
|
|
128
|
+
if (providerOptions.length === 1) return radios;
|
|
129
|
+
return (
|
|
130
|
+
<DropdownMenuSub key={provider.id}>
|
|
131
|
+
<DropdownMenuSubTrigger>
|
|
132
|
+
{providerIcon?.(provider.id)}
|
|
133
|
+
{provider.label}
|
|
134
|
+
</DropdownMenuSubTrigger>
|
|
135
|
+
<DropdownMenuSubContent>{radios}</DropdownMenuSubContent>
|
|
136
|
+
</DropdownMenuSub>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</DropdownMenuSubContent>
|
|
140
|
+
</DropdownMenuSub>
|
|
141
|
+
</DropdownMenuContent>
|
|
142
|
+
</DropdownMenu>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { IconMicrophone, IconPlayerStop } from "@tabler/icons-react";
|
|
5
|
+
import { PromptInputButton } from "./prompt-input";
|
|
6
|
+
|
|
7
|
+
// Web Speech API dictation for the prompt input: `useSpeechRecognition` streams
|
|
8
|
+
// the transcript (prefix + finals + interim) to a callback, and
|
|
9
|
+
// `PromptInputSpeech` is a microphone button that writes it into the form's
|
|
10
|
+
// `textarea[name="message"]`.
|
|
11
|
+
|
|
12
|
+
interface SpeechRecognitionResult {
|
|
13
|
+
readonly isFinal: boolean;
|
|
14
|
+
readonly length: number;
|
|
15
|
+
[index: number]: { transcript: string; confidence: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SpeechRecognitionResultList {
|
|
19
|
+
readonly length: number;
|
|
20
|
+
[index: number]: SpeechRecognitionResult;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SpeechRecognitionEvent extends Event {
|
|
24
|
+
readonly resultIndex: number;
|
|
25
|
+
readonly results: SpeechRecognitionResultList;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SpeechRecognitionInstance extends EventTarget {
|
|
29
|
+
continuous: boolean;
|
|
30
|
+
interimResults: boolean;
|
|
31
|
+
lang: string;
|
|
32
|
+
onresult: ((event: SpeechRecognitionEvent) => void) | null;
|
|
33
|
+
onerror: ((event: Event) => void) | null;
|
|
34
|
+
onend: (() => void) | null;
|
|
35
|
+
start(): void;
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
|
|
40
|
+
|
|
41
|
+
// Read the constructors through a local type: a package must not augment the
|
|
42
|
+
// global Window. Apps often declare their own `Window.SpeechRecognition` with
|
|
43
|
+
// other event types, so `window` is cast to this shape rather than assigned
|
|
44
|
+
// (an assignment fails to typecheck in those apps; see
|
|
45
|
+
// consumer/window-speech-global.d.ts).
|
|
46
|
+
type SpeechRecognitionWindow = {
|
|
47
|
+
SpeechRecognition?: SpeechRecognitionConstructor;
|
|
48
|
+
webkitSpeechRecognition?: SpeechRecognitionConstructor;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The browser's `SpeechRecognition` constructor, or `null` without one. */
|
|
52
|
+
export function getSpeechRecognition(): SpeechRecognitionConstructor | null {
|
|
53
|
+
if (typeof window === "undefined") return null;
|
|
54
|
+
const speechWindow = window as unknown as SpeechRecognitionWindow;
|
|
55
|
+
return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Continuous dictation. `toggle(prefix)` starts listening (or stops); each
|
|
60
|
+
* result calls `onUpdate` with `prefix` plus the transcript so far.
|
|
61
|
+
*/
|
|
62
|
+
export function useSpeechRecognition(onUpdate: (fullText: string) => void) {
|
|
63
|
+
const [isListening, setIsListening] = useState(false);
|
|
64
|
+
const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
|
|
65
|
+
const onUpdateRef = useRef(onUpdate);
|
|
66
|
+
onUpdateRef.current = onUpdate;
|
|
67
|
+
const prefixRef = useRef("");
|
|
68
|
+
|
|
69
|
+
const stop = () => {
|
|
70
|
+
recognitionRef.current?.stop();
|
|
71
|
+
recognitionRef.current = null;
|
|
72
|
+
setIsListening(false);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const start = (prefix: string) => {
|
|
76
|
+
const SR = getSpeechRecognition();
|
|
77
|
+
if (!SR) return;
|
|
78
|
+
|
|
79
|
+
const separator = prefix && !prefix.endsWith(" ") ? " " : "";
|
|
80
|
+
prefixRef.current = prefix + separator;
|
|
81
|
+
|
|
82
|
+
const recognition = new SR();
|
|
83
|
+
recognition.continuous = true;
|
|
84
|
+
recognition.interimResults = true;
|
|
85
|
+
recognition.lang = "en-US";
|
|
86
|
+
|
|
87
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
88
|
+
let finals = "";
|
|
89
|
+
let interim = "";
|
|
90
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
91
|
+
const result = event.results[i];
|
|
92
|
+
if (!result) continue;
|
|
93
|
+
const text = result[0]?.transcript ?? "";
|
|
94
|
+
if (result.isFinal) {
|
|
95
|
+
finals += text;
|
|
96
|
+
} else {
|
|
97
|
+
interim += text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
onUpdateRef.current(prefixRef.current + finals + interim);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
recognition.onerror = () => {
|
|
104
|
+
stop();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
recognition.onend = () => {
|
|
108
|
+
setIsListening(false);
|
|
109
|
+
recognitionRef.current = null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
recognitionRef.current = recognition;
|
|
113
|
+
recognition.start();
|
|
114
|
+
setIsListening(true);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const toggle = (prefix: string) => {
|
|
118
|
+
if (isListening) {
|
|
119
|
+
stop();
|
|
120
|
+
} else {
|
|
121
|
+
start(prefix);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Stop listening on unmount.
|
|
126
|
+
useEffect(
|
|
127
|
+
() => () => {
|
|
128
|
+
recognitionRef.current?.stop();
|
|
129
|
+
recognitionRef.current = null;
|
|
130
|
+
},
|
|
131
|
+
[],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return { isListening, toggle };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface PromptInputSpeechProps {
|
|
138
|
+
disabled?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Microphone button for the prompt input. Renders nothing without the Web Speech API. */
|
|
142
|
+
export function PromptInputSpeech({ disabled }: PromptInputSpeechProps) {
|
|
143
|
+
if (!getSpeechRecognition()) return null;
|
|
144
|
+
|
|
145
|
+
return <SpeechButton disabled={disabled} />;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
|
|
149
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
150
|
+
HTMLTextAreaElement.prototype,
|
|
151
|
+
"value",
|
|
152
|
+
)?.set;
|
|
153
|
+
if (!setter) return;
|
|
154
|
+
setter.call(textarea, value);
|
|
155
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function SpeechButton({ disabled }: PromptInputSpeechProps) {
|
|
159
|
+
const { isListening, toggle } = useSpeechRecognition((fullText) => {
|
|
160
|
+
const textarea = document.querySelector<HTMLTextAreaElement>(
|
|
161
|
+
'textarea[name="message"]',
|
|
162
|
+
);
|
|
163
|
+
if (!textarea) return;
|
|
164
|
+
setTextareaValue(textarea, fullText);
|
|
165
|
+
textarea.focus();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const handleToggle = () => {
|
|
169
|
+
const textarea = document.querySelector<HTMLTextAreaElement>(
|
|
170
|
+
'textarea[name="message"]',
|
|
171
|
+
);
|
|
172
|
+
toggle(textarea?.value ?? "");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<PromptInputButton
|
|
177
|
+
tooltip={isListening ? "Stop recording" : "Voice input"}
|
|
178
|
+
onClick={handleToggle}
|
|
179
|
+
disabled={disabled}
|
|
180
|
+
className={isListening ? "text-destructive" : ""}
|
|
181
|
+
>
|
|
182
|
+
{isListening ? (
|
|
183
|
+
<IconPlayerStop className="size-4" />
|
|
184
|
+
) : (
|
|
185
|
+
<IconMicrophone className="size-4" />
|
|
186
|
+
)}
|
|
187
|
+
</PromptInputButton>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useOptionalPromptInputController } from "./prompt-input";
|
|
4
|
+
import { useCn, useUiConfig } from "../ui/ui-config";
|
|
5
|
+
|
|
6
|
+
const ULTRATHINK_PROMPT_PREFIX = "Ultrathink:\n";
|
|
7
|
+
|
|
8
|
+
export function isUltrathinkPrompt(text: string | null | undefined): boolean {
|
|
9
|
+
return typeof text === "string" && /\bultrathink\b/i.test(text);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function applyUltrathinkPrefix(text: string): string {
|
|
13
|
+
const trimmed = text.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return ULTRATHINK_PROMPT_PREFIX;
|
|
16
|
+
}
|
|
17
|
+
if (trimmed.startsWith("Ultrathink:")) {
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
return `${ULTRATHINK_PROMPT_PREFIX}${trimmed}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `UiConfig.TraitsPanel`: classes that replace the row `label`, the `empty`
|
|
25
|
+
* text, the option button `option` base and its `optionSelected` /
|
|
26
|
+
* `optionIdle` states, and the ultrathink `note`.
|
|
27
|
+
*/
|
|
28
|
+
export interface TraitsPanelConfig {
|
|
29
|
+
classes?: {
|
|
30
|
+
label?: string;
|
|
31
|
+
empty?: string;
|
|
32
|
+
option?: string;
|
|
33
|
+
optionSelected?: string;
|
|
34
|
+
optionIdle?: string;
|
|
35
|
+
note?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TraitsReasoningConfig {
|
|
40
|
+
levels: ReadonlyArray<string>;
|
|
41
|
+
default: string;
|
|
42
|
+
ultrathink?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TraitsMenuConfig {
|
|
46
|
+
reasoning?: TraitsReasoningConfig;
|
|
47
|
+
thinkingToggle?: boolean;
|
|
48
|
+
contextWindow1m?: boolean;
|
|
49
|
+
contextWindowDefaultLabel?: string;
|
|
50
|
+
fastMode?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TraitsMenuProps {
|
|
54
|
+
config: TraitsMenuConfig;
|
|
55
|
+
effortLevel: string | undefined;
|
|
56
|
+
thinkingEnabled: boolean;
|
|
57
|
+
use1mContext: boolean;
|
|
58
|
+
fastMode: boolean;
|
|
59
|
+
getLevelLabel: (level: string) => string;
|
|
60
|
+
onEffortLevelChange: (level: string) => void;
|
|
61
|
+
onThinkingEnabledChange: (enabled: boolean) => void;
|
|
62
|
+
onUse1mContextChange: (use1m: boolean) => void;
|
|
63
|
+
onFastModeChange: (enabled: boolean) => void;
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface TraitOption {
|
|
69
|
+
value: string;
|
|
70
|
+
label: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function TraitSegment({
|
|
74
|
+
label,
|
|
75
|
+
value,
|
|
76
|
+
options,
|
|
77
|
+
onChange,
|
|
78
|
+
disabled,
|
|
79
|
+
}: {
|
|
80
|
+
label: string;
|
|
81
|
+
value: string;
|
|
82
|
+
options: ReadonlyArray<TraitOption>;
|
|
83
|
+
onChange: (value: string) => void;
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
}) {
|
|
86
|
+
const classes = useUiConfig("TraitsPanel")?.classes;
|
|
87
|
+
const merge = useCn();
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex items-start gap-2.5">
|
|
90
|
+
<span
|
|
91
|
+
className={
|
|
92
|
+
classes?.label ??
|
|
93
|
+
"w-[4.75rem] shrink-0 pt-1 text-[0.6875rem] leading-4 font-medium text-muted-foreground"
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
{label}
|
|
97
|
+
</span>
|
|
98
|
+
{options.length === 0 ? (
|
|
99
|
+
<span
|
|
100
|
+
className={
|
|
101
|
+
classes?.empty ?? "flex h-6 items-center text-[0.6875rem] leading-4 text-muted-foreground/70"
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
No options available
|
|
105
|
+
</span>
|
|
106
|
+
) : (
|
|
107
|
+
<div
|
|
108
|
+
className="flex min-w-0 flex-1 flex-wrap gap-0.5"
|
|
109
|
+
role="radiogroup"
|
|
110
|
+
aria-label={label}
|
|
111
|
+
>
|
|
112
|
+
{options.map((option) => {
|
|
113
|
+
const selected = option.value === value;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
key={option.value}
|
|
117
|
+
type="button"
|
|
118
|
+
role="radio"
|
|
119
|
+
aria-checked={selected}
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
122
|
+
onClick={() => onChange(option.value)}
|
|
123
|
+
className={merge(
|
|
124
|
+
classes?.option ??
|
|
125
|
+
"h-6 rounded-md px-2 text-[0.6875rem] leading-4 font-medium transition-[color,background-color,scale] duration-150 ease-out active:scale-[0.96]",
|
|
126
|
+
selected
|
|
127
|
+
? (classes?.optionSelected ?? "bg-background text-foreground")
|
|
128
|
+
: (classes?.optionIdle ??
|
|
129
|
+
"text-muted-foreground hover:bg-surface-secondary/80 hover:text-foreground"),
|
|
130
|
+
disabled && "pointer-events-none opacity-50",
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
{option.label}
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compact trait controls for embedding in the model picker. Every section
|
|
145
|
+
* stays mounted so switching models cannot collapse the header; unsupported
|
|
146
|
+
* traits show a muted empty state.
|
|
147
|
+
*/
|
|
148
|
+
export function TraitsPanel({
|
|
149
|
+
config,
|
|
150
|
+
effortLevel,
|
|
151
|
+
thinkingEnabled,
|
|
152
|
+
use1mContext,
|
|
153
|
+
fastMode,
|
|
154
|
+
getLevelLabel,
|
|
155
|
+
onEffortLevelChange,
|
|
156
|
+
onThinkingEnabledChange,
|
|
157
|
+
onUse1mContextChange,
|
|
158
|
+
onFastModeChange,
|
|
159
|
+
disabled,
|
|
160
|
+
className,
|
|
161
|
+
}: TraitsMenuProps) {
|
|
162
|
+
const controller = useOptionalPromptInputController();
|
|
163
|
+
const classes = useUiConfig("TraitsPanel")?.classes;
|
|
164
|
+
const merge = useCn();
|
|
165
|
+
const prompt = controller?.textInput.value ?? "";
|
|
166
|
+
|
|
167
|
+
const ultrathinkAvailable =
|
|
168
|
+
Boolean(config.reasoning?.ultrathink) && controller !== null;
|
|
169
|
+
const ultrathinkPromptControlled =
|
|
170
|
+
ultrathinkAvailable && isUltrathinkPrompt(prompt);
|
|
171
|
+
const ultrathinkInBodyText =
|
|
172
|
+
ultrathinkPromptControlled &&
|
|
173
|
+
isUltrathinkPrompt(prompt.replace(/^Ultrathink:\s*/i, ""));
|
|
174
|
+
|
|
175
|
+
const resolvedEffort = effortLevel ?? config.reasoning?.default ?? "";
|
|
176
|
+
const contextDefaultLabel = config.contextWindowDefaultLabel ?? "200K";
|
|
177
|
+
|
|
178
|
+
const handleEffortChange = (value: string) => {
|
|
179
|
+
if (!value || !config.reasoning) return;
|
|
180
|
+
|
|
181
|
+
if (value === "ultrathink") {
|
|
182
|
+
if (!controller) return;
|
|
183
|
+
const nextPrompt =
|
|
184
|
+
prompt.trim().length === 0
|
|
185
|
+
? ULTRATHINK_PROMPT_PREFIX
|
|
186
|
+
: applyUltrathinkPrefix(prompt);
|
|
187
|
+
controller.textInput.setInput(nextPrompt);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (ultrathinkInBodyText) return;
|
|
192
|
+
|
|
193
|
+
if (ultrathinkPromptControlled && controller) {
|
|
194
|
+
controller.textInput.setInput(prompt.replace(/^Ultrathink:\s*/i, ""));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onEffortLevelChange(value);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const effortRadioValue = ultrathinkPromptControlled
|
|
201
|
+
? "ultrathink"
|
|
202
|
+
: resolvedEffort;
|
|
203
|
+
|
|
204
|
+
const reasoningOptions: TraitOption[] = config.reasoning
|
|
205
|
+
? [
|
|
206
|
+
...config.reasoning.levels.map((level) => ({
|
|
207
|
+
value: level,
|
|
208
|
+
label: getLevelLabel(level),
|
|
209
|
+
})),
|
|
210
|
+
...(ultrathinkAvailable
|
|
211
|
+
? [{ value: "ultrathink", label: "Ultrathink" }]
|
|
212
|
+
: []),
|
|
213
|
+
]
|
|
214
|
+
: [];
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className={merge("flex flex-col gap-1.5", className)}>
|
|
218
|
+
<div className="flex min-h-6 flex-col gap-1">
|
|
219
|
+
<TraitSegment
|
|
220
|
+
label="Reasoning"
|
|
221
|
+
value={effortRadioValue}
|
|
222
|
+
options={reasoningOptions}
|
|
223
|
+
onChange={handleEffortChange}
|
|
224
|
+
disabled={disabled || ultrathinkInBodyText}
|
|
225
|
+
/>
|
|
226
|
+
{ultrathinkInBodyText ? (
|
|
227
|
+
<p
|
|
228
|
+
className={
|
|
229
|
+
classes?.note ?? "pl-[5.375rem] text-[0.6875rem] leading-snug text-muted-foreground/80"
|
|
230
|
+
}
|
|
231
|
+
>
|
|
232
|
+
Your prompt contains "ultrathink" in the text. Remove it
|
|
233
|
+
to change this option.
|
|
234
|
+
</p>
|
|
235
|
+
) : null}
|
|
236
|
+
</div>
|
|
237
|
+
<TraitSegment
|
|
238
|
+
label="Speed"
|
|
239
|
+
value={fastMode ? "fast" : "standard"}
|
|
240
|
+
options={
|
|
241
|
+
config.fastMode
|
|
242
|
+
? [
|
|
243
|
+
{ value: "standard", label: "Standard" },
|
|
244
|
+
{ value: "fast", label: "Fast" },
|
|
245
|
+
]
|
|
246
|
+
: []
|
|
247
|
+
}
|
|
248
|
+
onChange={(value) => onFastModeChange(value === "fast")}
|
|
249
|
+
disabled={disabled}
|
|
250
|
+
/>
|
|
251
|
+
<TraitSegment
|
|
252
|
+
label="Context"
|
|
253
|
+
value={use1mContext ? "1m" : "standard"}
|
|
254
|
+
options={
|
|
255
|
+
config.contextWindow1m
|
|
256
|
+
? [
|
|
257
|
+
{ value: "standard", label: contextDefaultLabel },
|
|
258
|
+
{ value: "1m", label: "1M" },
|
|
259
|
+
]
|
|
260
|
+
: []
|
|
261
|
+
}
|
|
262
|
+
onChange={(value) => onUse1mContextChange(value === "1m")}
|
|
263
|
+
disabled={disabled}
|
|
264
|
+
/>
|
|
265
|
+
<TraitSegment
|
|
266
|
+
label="Thinking"
|
|
267
|
+
value={thinkingEnabled ? "on" : "off"}
|
|
268
|
+
options={
|
|
269
|
+
config.thinkingToggle
|
|
270
|
+
? [
|
|
271
|
+
{ value: "on", label: "On" },
|
|
272
|
+
{ value: "off", label: "Off" },
|
|
273
|
+
]
|
|
274
|
+
: []
|
|
275
|
+
}
|
|
276
|
+
onChange={(value) => onThinkingEnabledChange(value === "on")}
|
|
277
|
+
disabled={disabled}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|