@4djs/assistant 0.0.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 +322 -0
- package/package.json +41 -0
- package/src/core/chat-activity.ts +107 -0
- package/src/core/chat-commands.ts +173 -0
- package/src/core/chat-history.ts +113 -0
- package/src/core/chat-reply-suggestions-parse.ts +119 -0
- package/src/core/code-highlight.ts +20 -0
- package/src/core/create-assistant-store.ts +639 -0
- package/src/core/fetch-suggested-prompts.ts +53 -0
- package/src/core/index.ts +125 -0
- package/src/core/interactive-tools/choices.ts +155 -0
- package/src/core/interactive-tools/confirmation.ts +63 -0
- package/src/core/interactive-tools/constants.ts +22 -0
- package/src/core/interactive-tools/execute.ts +70 -0
- package/src/core/interactive-tools/index.ts +41 -0
- package/src/core/interactive-tools/suggestions.ts +87 -0
- package/src/core/interactive-tools/waiters.ts +55 -0
- package/src/core/llm-chat.ts +686 -0
- package/src/core/llm-config.ts +101 -0
- package/src/core/llm-models.ts +96 -0
- package/src/core/llm-provider.ts +99 -0
- package/src/core/llm-settings-storage.ts +331 -0
- package/src/core/llm-sse.ts +166 -0
- package/src/core/llm-types.ts +52 -0
- package/src/core/markdown-utils.ts +11 -0
- package/src/core/prepare-markdown.ts +38 -0
- package/src/core/types.ts +86 -0
- package/src/css.d.ts +1 -0
- package/src/react/Assistant.tsx +358 -0
- package/src/react/components/HighlightedJsonCode.tsx +24 -0
- package/src/react/components/MarkdownContent.tsx +98 -0
- package/src/react/components/MarkdownEditor.tsx +60 -0
- package/src/react/components/MermaidDiagram.tsx +139 -0
- package/src/react/components/ModelSelector.tsx +243 -0
- package/src/react/components/chat/AssistantErrorCallout.tsx +79 -0
- package/src/react/components/chat/ChatActivity.tsx +274 -0
- package/src/react/components/chat/ChatComposer.tsx +189 -0
- package/src/react/components/chat/ChatEmptyState.tsx +145 -0
- package/src/react/components/chat/ChatInteractivePrompt/choices-prompt.tsx +262 -0
- package/src/react/components/chat/ChatInteractivePrompt/confirmation-prompt.tsx +97 -0
- package/src/react/components/chat/ChatInteractivePrompt/index.tsx +60 -0
- package/src/react/components/chat/ChatInteractivePrompt/shell.tsx +60 -0
- package/src/react/components/chat/ChatInteractivePrompt/utils.ts +14 -0
- package/src/react/components/chat/ChatMessage.tsx +150 -0
- package/src/react/components/chat/ChatMessageScroll.tsx +116 -0
- package/src/react/components/chat/ChatReplySuggestions.tsx +231 -0
- package/src/react/components/chat/ComposerCommandMenu.tsx +69 -0
- package/src/react/components/chat/LlmSettingsStrip.tsx +348 -0
- package/src/react/components/chat/LlmSetupPrompt.tsx +58 -0
- package/src/react/components/chat/LlmUnavailableBanner.tsx +11 -0
- package/src/react/components/chat/SuggestedPromptsList.tsx +121 -0
- package/src/react/components/chat/SuggestedPromptsStrip.tsx +72 -0
- package/src/react/components/chat/SystemPromptField.tsx +107 -0
- package/src/react/components/highlighted-code.tsx +107 -0
- package/src/react/context.tsx +72 -0
- package/src/react/hooks/use-composer-commands.ts +129 -0
- package/src/react/hooks/use-suggested-prompts.ts +128 -0
- package/src/react/index.ts +39 -0
- package/src/react/lib/parse-assistant-error.ts +96 -0
- package/src/react/lib/prompt-icons.ts +40 -0
- package/src/react/types.ts +83 -0
- package/src/react/utils/cn.ts +5 -0
- package/src/styles/assistant.css +3009 -0
- package/test/buildLlmHistory.test.ts +95 -0
- package/test/llm-config.test.ts +72 -0
- package/test/llmSettingsStorage.test.ts +121 -0
- package/test/parse-assistant-error.test.ts +24 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Check, ChevronRight, Loader2, Wrench } from "lucide-react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type ChatActivityStep,
|
|
5
|
+
chatActivityStepLabel,
|
|
6
|
+
formatActivityJsonValue,
|
|
7
|
+
isEmptyActivityJson,
|
|
8
|
+
stepHasDetails,
|
|
9
|
+
} from "../../../core/chat-activity.ts";
|
|
10
|
+
import { isInteractiveChatTool } from "../../../core/interactive-tools/index.ts";
|
|
11
|
+
import { HighlightedJsonCode } from "../HighlightedJsonCode.tsx";
|
|
12
|
+
import { ChatInteractivePrompt } from "./ChatInteractivePrompt/index.tsx";
|
|
13
|
+
|
|
14
|
+
interface ChatActivityProps {
|
|
15
|
+
steps: ChatActivityStep[];
|
|
16
|
+
streaming?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const JSON_SCROLL_LINE_THRESHOLD = 10;
|
|
20
|
+
|
|
21
|
+
function ActivityJsonCode({ value }: { value: string }) {
|
|
22
|
+
const lineCount = value.split("\n").length;
|
|
23
|
+
const needsScroll = lineCount > JSON_SCROLL_LINE_THRESHOLD;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<pre
|
|
27
|
+
className="font-mono text-[11px] leading-snug wrap-break-word whitespace-pre-wrap"
|
|
28
|
+
style={needsScroll ? { maxHeight: "8rem", overflowY: "auto" } : undefined}
|
|
29
|
+
>
|
|
30
|
+
<HighlightedJsonCode code={value} className="text-[11px] leading-snug" />
|
|
31
|
+
</pre>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function JsonBlock({ label, value }: { label: string; value: string }) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="min-w-0">
|
|
38
|
+
<p className="assistant-activity-step__json-label">{label}</p>
|
|
39
|
+
<div className="assistant-activity-step__json-block">
|
|
40
|
+
<ActivityJsonCode value={value} />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function StepDetailsBody({ step }: { step: ChatActivityStep }) {
|
|
47
|
+
const isInteractive =
|
|
48
|
+
step.kind === "tool" && isInteractiveChatTool(step.name);
|
|
49
|
+
const isWaiting = isInteractive && step.status === "active";
|
|
50
|
+
|
|
51
|
+
if (isInteractive) {
|
|
52
|
+
const formattedResult = step.error
|
|
53
|
+
? step.error
|
|
54
|
+
: formatActivityJsonValue(step.result);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={
|
|
59
|
+
isWaiting ? "assistant-activity-step-details--interactive" : undefined
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
<ChatInteractivePrompt
|
|
63
|
+
toolName={step.name}
|
|
64
|
+
args={step.args}
|
|
65
|
+
callId={step.callId}
|
|
66
|
+
active={step.status === "active"}
|
|
67
|
+
/>
|
|
68
|
+
{formattedResult && step.status !== "active" ? (
|
|
69
|
+
<JsonBlock
|
|
70
|
+
label={step.status === "error" ? "Error" : "Result"}
|
|
71
|
+
value={formattedResult}
|
|
72
|
+
/>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const formattedArgs = formatActivityJsonValue(step.args);
|
|
79
|
+
const formattedResult = step.error
|
|
80
|
+
? step.error
|
|
81
|
+
: formatActivityJsonValue(step.result);
|
|
82
|
+
const showArgs = formattedArgs && !isEmptyActivityJson(step.args);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
{showArgs ? <JsonBlock label="Arguments" value={formattedArgs} /> : null}
|
|
87
|
+
{step.status === "active" && !isInteractiveChatTool(step.name) ? (
|
|
88
|
+
<p className="text-[11px] text-[var(--text-chat-tertiary)]">Running…</p>
|
|
89
|
+
) : formattedResult ? (
|
|
90
|
+
<JsonBlock
|
|
91
|
+
label={step.status === "error" ? "Error" : "Result"}
|
|
92
|
+
value={formattedResult}
|
|
93
|
+
/>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function StepStatusIcon({ step }: { step: ChatActivityStep }) {
|
|
100
|
+
if (step.status === "active") {
|
|
101
|
+
return <Loader2 size={12} className="assistant-icon-spin" aria-hidden />;
|
|
102
|
+
}
|
|
103
|
+
if (step.status === "error") {
|
|
104
|
+
return <Wrench size={12} aria-hidden />;
|
|
105
|
+
}
|
|
106
|
+
return <Check size={12} strokeWidth={2.5} aria-hidden />;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stepIconClasses(step: ChatActivityStep): string {
|
|
110
|
+
if (step.status === "active") {
|
|
111
|
+
return "assistant-trace-icon assistant-trace-icon--active";
|
|
112
|
+
}
|
|
113
|
+
if (step.status === "error") {
|
|
114
|
+
return "assistant-trace-icon assistant-trace-icon--error";
|
|
115
|
+
}
|
|
116
|
+
return "assistant-trace-icon";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ActivityStepRow({
|
|
120
|
+
step,
|
|
121
|
+
expanded,
|
|
122
|
+
onToggle,
|
|
123
|
+
isLast,
|
|
124
|
+
}: {
|
|
125
|
+
step: ChatActivityStep;
|
|
126
|
+
expanded: boolean;
|
|
127
|
+
onToggle: () => void;
|
|
128
|
+
isLast: boolean;
|
|
129
|
+
}) {
|
|
130
|
+
const isExpandable =
|
|
131
|
+
stepHasDetails(step) || step.kind === "tool" || step.status === "active";
|
|
132
|
+
const isWaitingInteractive =
|
|
133
|
+
step.kind === "tool" &&
|
|
134
|
+
isInteractiveChatTool(step.name) &&
|
|
135
|
+
step.status === "active";
|
|
136
|
+
const label = chatActivityStepLabel(step);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<li
|
|
140
|
+
className={
|
|
141
|
+
isWaitingInteractive
|
|
142
|
+
? "assistant-activity-step assistant-activity-step--waiting"
|
|
143
|
+
: "assistant-activity-step"
|
|
144
|
+
}
|
|
145
|
+
>
|
|
146
|
+
<div className="assistant-activity-step__rail">
|
|
147
|
+
<span aria-hidden className={stepIconClasses(step)}>
|
|
148
|
+
<StepStatusIcon step={step} />
|
|
149
|
+
</span>
|
|
150
|
+
{!isLast ? (
|
|
151
|
+
<span aria-hidden className="assistant-activity-step__connector" />
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="assistant-activity-step__body">
|
|
156
|
+
{isExpandable ? (
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className={`assistant-activity-step__header${step.status === "active" ? " assistant-activity-step__header--active" : ""}`}
|
|
160
|
+
aria-expanded={expanded}
|
|
161
|
+
onClick={onToggle}
|
|
162
|
+
>
|
|
163
|
+
<ChevronRight
|
|
164
|
+
size={14}
|
|
165
|
+
className={`assistant-activity-step__chevron${expanded ? " assistant-activity-step__chevron--open" : ""}`}
|
|
166
|
+
aria-hidden
|
|
167
|
+
/>
|
|
168
|
+
<span className="assistant-activity-step__label">
|
|
169
|
+
<Wrench
|
|
170
|
+
size={12}
|
|
171
|
+
className="assistant-activity-step__tool-icon"
|
|
172
|
+
aria-hidden
|
|
173
|
+
/>
|
|
174
|
+
<span className="assistant-activity-step__label-text">
|
|
175
|
+
{label}
|
|
176
|
+
</span>
|
|
177
|
+
</span>
|
|
178
|
+
</button>
|
|
179
|
+
) : (
|
|
180
|
+
<div className="assistant-activity-step__header assistant-activity-step__header--static">
|
|
181
|
+
<span className="assistant-activity-step__label">
|
|
182
|
+
<span className="assistant-activity-step__label-text">
|
|
183
|
+
{label}
|
|
184
|
+
</span>
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{expanded && isExpandable ? (
|
|
190
|
+
<div
|
|
191
|
+
className={`assistant-activity-step__details${isWaitingInteractive ? " assistant-activity-step__details--interactive" : ""}`}
|
|
192
|
+
>
|
|
193
|
+
<StepDetailsBody step={step} />
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
</div>
|
|
197
|
+
</li>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function useActivityExpansion(steps: ChatActivityStep[]) {
|
|
202
|
+
const [expandedStepId, setExpandedStepId] = useState<string | null>(null);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const activeStep = steps.find((step) => step.status === "active");
|
|
206
|
+
if (!activeStep) return;
|
|
207
|
+
setExpandedStepId((current) =>
|
|
208
|
+
current === activeStep.id ? current : activeStep.id,
|
|
209
|
+
);
|
|
210
|
+
}, [steps]);
|
|
211
|
+
|
|
212
|
+
function toggleStep(stepId: string) {
|
|
213
|
+
setExpandedStepId((current) => (current === stepId ? null : stepId));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { expandedStepId, toggleStep };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function ChatActivity({ steps, streaming }: ChatActivityProps) {
|
|
220
|
+
const [panelCollapsed, setPanelCollapsed] = useState(false);
|
|
221
|
+
const { expandedStepId, toggleStep } = useActivityExpansion(steps);
|
|
222
|
+
|
|
223
|
+
const hasActiveStep = steps.some((step) => step.status === "active");
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (streaming || hasActiveStep) {
|
|
227
|
+
setPanelCollapsed(false);
|
|
228
|
+
}
|
|
229
|
+
}, [streaming, hasActiveStep]);
|
|
230
|
+
|
|
231
|
+
if (steps.length === 0) return null;
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div
|
|
235
|
+
className="assistant-activity assistant-activity--embedded"
|
|
236
|
+
role="status"
|
|
237
|
+
aria-live={streaming || hasActiveStep ? "polite" : "off"}
|
|
238
|
+
>
|
|
239
|
+
<div className="assistant-activity__header">
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
className="assistant-activity__toggle"
|
|
243
|
+
aria-expanded={!panelCollapsed}
|
|
244
|
+
onClick={() => setPanelCollapsed((value) => !value)}
|
|
245
|
+
>
|
|
246
|
+
<ChevronRight
|
|
247
|
+
size={14}
|
|
248
|
+
className={`assistant-activity__toggle-chevron${panelCollapsed ? "" : " assistant-activity__toggle-chevron--open"}`}
|
|
249
|
+
aria-hidden
|
|
250
|
+
/>
|
|
251
|
+
<span className="assistant-activity__title">Trace</span>
|
|
252
|
+
<span className="assistant-activity__count">{steps.length}</span>
|
|
253
|
+
</button>
|
|
254
|
+
{hasActiveStep ? (
|
|
255
|
+
<span className="assistant-activity__live">Live</span>
|
|
256
|
+
) : null}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{!panelCollapsed ? (
|
|
260
|
+
<ol className="assistant-activity__steps">
|
|
261
|
+
{steps.map((step, index) => (
|
|
262
|
+
<ActivityStepRow
|
|
263
|
+
key={step.id}
|
|
264
|
+
step={step}
|
|
265
|
+
expanded={expandedStepId === step.id}
|
|
266
|
+
onToggle={() => toggleStep(step.id)}
|
|
267
|
+
isLast={index === steps.length - 1}
|
|
268
|
+
/>
|
|
269
|
+
))}
|
|
270
|
+
</ol>
|
|
271
|
+
) : null}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Loader2,
|
|
3
|
+
SendHorizontal,
|
|
4
|
+
Settings2,
|
|
5
|
+
Sparkles,
|
|
6
|
+
Square,
|
|
7
|
+
Trash2,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
import type { KeyboardEvent, RefObject } from "react";
|
|
10
|
+
import type { ChatCommandSuggestion } from "../../../core/chat-commands.ts";
|
|
11
|
+
import { listChatCommands } from "../../../core/chat-commands.ts";
|
|
12
|
+
import { ModelSelector } from "../ModelSelector.tsx";
|
|
13
|
+
import { ComposerCommandMenu } from "./ComposerCommandMenu.tsx";
|
|
14
|
+
|
|
15
|
+
interface ChatComposerToolbar {
|
|
16
|
+
showLlmSettings?: boolean;
|
|
17
|
+
onOpenLlmSettings?: () => void;
|
|
18
|
+
showGenerateSuggestions?: boolean;
|
|
19
|
+
onGenerateSuggestions?: () => void;
|
|
20
|
+
suggestionsLoading?: boolean;
|
|
21
|
+
showClear?: boolean;
|
|
22
|
+
onClear?: () => void;
|
|
23
|
+
clearDisabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ChatComposerProps {
|
|
27
|
+
input: string;
|
|
28
|
+
onInputChange: (value: string) => void;
|
|
29
|
+
onSubmit: () => void;
|
|
30
|
+
onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
31
|
+
streaming: boolean;
|
|
32
|
+
onStop: () => void;
|
|
33
|
+
textareaRef?: RefObject<HTMLTextAreaElement | null>;
|
|
34
|
+
llmEnabled: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** When true, only blocks message input and send — toolbar actions stay available. */
|
|
37
|
+
inputDisabled?: boolean;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
toolbar?: ChatComposerToolbar;
|
|
40
|
+
commandMenu?: {
|
|
41
|
+
open: boolean;
|
|
42
|
+
commands: ChatCommandSuggestion[];
|
|
43
|
+
selectedIndex: number;
|
|
44
|
+
onSelect: (command: ChatCommandSuggestion) => void;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ChatComposer({
|
|
49
|
+
input,
|
|
50
|
+
onInputChange,
|
|
51
|
+
onSubmit,
|
|
52
|
+
onKeyDown,
|
|
53
|
+
streaming,
|
|
54
|
+
onStop,
|
|
55
|
+
textareaRef,
|
|
56
|
+
llmEnabled,
|
|
57
|
+
disabled,
|
|
58
|
+
inputDisabled,
|
|
59
|
+
placeholder = "Ask the assistant…",
|
|
60
|
+
toolbar,
|
|
61
|
+
commandMenu,
|
|
62
|
+
}: ChatComposerProps) {
|
|
63
|
+
const commandHint = listChatCommands("assistant")
|
|
64
|
+
.map((command) => `/${command.name}`)
|
|
65
|
+
.join(" · ");
|
|
66
|
+
|
|
67
|
+
const blockInput = inputDisabled ?? disabled ?? false;
|
|
68
|
+
|
|
69
|
+
const showToolbar =
|
|
70
|
+
toolbar?.showLlmSettings ||
|
|
71
|
+
toolbar?.showGenerateSuggestions ||
|
|
72
|
+
toolbar?.showClear ||
|
|
73
|
+
llmEnabled;
|
|
74
|
+
const showLlmSettings = toolbar?.showLlmSettings === true;
|
|
75
|
+
const showGenerateSuggestions = toolbar?.showGenerateSuggestions === true;
|
|
76
|
+
const showClear = toolbar?.showClear === true;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<div className="assistant-composer">
|
|
81
|
+
{commandMenu?.open ? (
|
|
82
|
+
<ComposerCommandMenu
|
|
83
|
+
commands={commandMenu.commands}
|
|
84
|
+
selectedIndex={commandMenu.selectedIndex}
|
|
85
|
+
onSelect={commandMenu.onSelect}
|
|
86
|
+
/>
|
|
87
|
+
) : null}
|
|
88
|
+
<div className="assistant-composer__input-row">
|
|
89
|
+
<textarea
|
|
90
|
+
ref={textareaRef}
|
|
91
|
+
className="assistant-composer__textarea"
|
|
92
|
+
value={input}
|
|
93
|
+
onChange={(event) => onInputChange(event.target.value)}
|
|
94
|
+
onKeyDown={onKeyDown}
|
|
95
|
+
placeholder={placeholder}
|
|
96
|
+
rows={2}
|
|
97
|
+
disabled={blockInput || streaming}
|
|
98
|
+
aria-label="Chat message"
|
|
99
|
+
/>
|
|
100
|
+
{streaming ? (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className="assistant-btn assistant-btn--secondary assistant-composer__stop"
|
|
104
|
+
aria-label="Stop"
|
|
105
|
+
onClick={onStop}
|
|
106
|
+
>
|
|
107
|
+
<Square size={14} className="fill-current" aria-hidden />
|
|
108
|
+
</button>
|
|
109
|
+
) : (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className="assistant-btn assistant-btn--primary assistant-composer__send"
|
|
113
|
+
aria-label="Send"
|
|
114
|
+
disabled={blockInput || !input.trim()}
|
|
115
|
+
onClick={onSubmit}
|
|
116
|
+
>
|
|
117
|
+
<SendHorizontal size={16} aria-hidden />
|
|
118
|
+
</button>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
{showToolbar ? (
|
|
122
|
+
<div className="assistant-composer__footer">
|
|
123
|
+
{llmEnabled ? (
|
|
124
|
+
<ModelSelector
|
|
125
|
+
disabled={blockInput || streaming}
|
|
126
|
+
variant="footer"
|
|
127
|
+
dropUp
|
|
128
|
+
/>
|
|
129
|
+
) : null}
|
|
130
|
+
{showLlmSettings || showGenerateSuggestions || showClear ? (
|
|
131
|
+
<div className="assistant-composer__tools">
|
|
132
|
+
{showLlmSettings ? (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className="assistant-btn assistant-btn--ghost assistant-composer__tool"
|
|
136
|
+
onClick={toolbar?.onOpenLlmSettings}
|
|
137
|
+
disabled={streaming}
|
|
138
|
+
aria-label="LLM settings"
|
|
139
|
+
title="LLM settings"
|
|
140
|
+
>
|
|
141
|
+
<Settings2 size={16} aria-hidden />
|
|
142
|
+
</button>
|
|
143
|
+
) : null}
|
|
144
|
+
{showGenerateSuggestions ? (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
className="assistant-btn assistant-btn--ghost assistant-composer__tool"
|
|
148
|
+
onClick={toolbar?.onGenerateSuggestions}
|
|
149
|
+
disabled={
|
|
150
|
+
blockInput || streaming || toolbar?.suggestionsLoading
|
|
151
|
+
}
|
|
152
|
+
aria-label="Generate suggestions"
|
|
153
|
+
title="Generate suggestions"
|
|
154
|
+
>
|
|
155
|
+
{toolbar?.suggestionsLoading ? (
|
|
156
|
+
<Loader2
|
|
157
|
+
size={16}
|
|
158
|
+
className="assistant-composer__tool-spinner"
|
|
159
|
+
aria-hidden
|
|
160
|
+
/>
|
|
161
|
+
) : (
|
|
162
|
+
<Sparkles size={16} aria-hidden />
|
|
163
|
+
)}
|
|
164
|
+
</button>
|
|
165
|
+
) : null}
|
|
166
|
+
{showClear ? (
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className="assistant-btn assistant-btn--ghost assistant-composer__tool"
|
|
170
|
+
onClick={toolbar?.onClear}
|
|
171
|
+
disabled={streaming || toolbar?.clearDisabled}
|
|
172
|
+
aria-label="Clear conversation"
|
|
173
|
+
title="Clear conversation"
|
|
174
|
+
>
|
|
175
|
+
<Trash2 size={16} aria-hidden />
|
|
176
|
+
</button>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
</div>
|
|
183
|
+
<p className="assistant-composer__hint">
|
|
184
|
+
Enter to send · Shift+Enter for new line
|
|
185
|
+
{commandHint ? <> · {commandHint}</> : null}
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Database,
|
|
3
|
+
Loader2,
|
|
4
|
+
RefreshCw,
|
|
5
|
+
Search,
|
|
6
|
+
Sparkles,
|
|
7
|
+
Zap,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
import type { useSuggestedPrompts } from "../../hooks/use-suggested-prompts.ts";
|
|
10
|
+
import type { AssistantEmptyStateConfig } from "../../types.ts";
|
|
11
|
+
import { SuggestedPromptsList } from "./SuggestedPromptsList.tsx";
|
|
12
|
+
|
|
13
|
+
const CAPABILITIES = [
|
|
14
|
+
{ icon: Database, label: "Catalog" },
|
|
15
|
+
{ icon: Search, label: "Query" },
|
|
16
|
+
{ icon: Zap, label: "Mutate" },
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
interface ChatEmptyStateProps {
|
|
20
|
+
config: AssistantEmptyStateConfig;
|
|
21
|
+
suggestions: ReturnType<typeof useSuggestedPrompts>;
|
|
22
|
+
onSelect: (prompt: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ChatEmptyState({
|
|
26
|
+
config,
|
|
27
|
+
suggestions,
|
|
28
|
+
onSelect,
|
|
29
|
+
}: ChatEmptyStateProps) {
|
|
30
|
+
const { prompts, loading, error, useDynamic, refresh, hasFetched } =
|
|
31
|
+
suggestions;
|
|
32
|
+
|
|
33
|
+
function handleFetch() {
|
|
34
|
+
void refresh();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const showStaticPrompts = !useDynamic && prompts.length > 0;
|
|
38
|
+
const showDynamicFetch = useDynamic && !hasFetched && !loading;
|
|
39
|
+
const showDynamicResults = useDynamic && (hasFetched || loading);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="assistant-empty-state">
|
|
43
|
+
<div className="assistant-empty-state__card">
|
|
44
|
+
<div className="assistant-empty-state__card-glow" aria-hidden />
|
|
45
|
+
<div className="assistant-empty-state__card-inner">
|
|
46
|
+
<p className="assistant-empty-state__eyebrow">AI Assistant</p>
|
|
47
|
+
|
|
48
|
+
<div className="assistant-empty-state__icon-ring" aria-hidden>
|
|
49
|
+
<Sparkles size={22} strokeWidth={2} />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<h3 className="assistant-empty-state__title">{config.title}</h3>
|
|
53
|
+
<p className="assistant-empty-state__description">
|
|
54
|
+
{config.description}
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<div className="assistant-empty-state__capabilities">
|
|
58
|
+
{CAPABILITIES.map(({ icon: Icon, label }) => (
|
|
59
|
+
<span key={label} className="assistant-empty-state__capability">
|
|
60
|
+
<Icon size={12} strokeWidth={2} aria-hidden />
|
|
61
|
+
{label}
|
|
62
|
+
</span>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{showDynamicFetch ? (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className="assistant-empty-state__cta"
|
|
70
|
+
onClick={handleFetch}
|
|
71
|
+
>
|
|
72
|
+
<span className="assistant-empty-state__cta-icon" aria-hidden>
|
|
73
|
+
<Sparkles size={18} strokeWidth={2} />
|
|
74
|
+
</span>
|
|
75
|
+
<span className="assistant-empty-state__cta-text">
|
|
76
|
+
<span className="assistant-empty-state__cta-label">
|
|
77
|
+
Generate suggestions
|
|
78
|
+
</span>
|
|
79
|
+
<span className="assistant-empty-state__cta-hint">
|
|
80
|
+
Tailored prompts based on your live catalog
|
|
81
|
+
</span>
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
84
|
+
) : null}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{showDynamicResults ? (
|
|
89
|
+
<section
|
|
90
|
+
className="assistant-empty-state__section"
|
|
91
|
+
aria-busy={loading || undefined}
|
|
92
|
+
>
|
|
93
|
+
<header className="assistant-empty-state__section-header">
|
|
94
|
+
<h4 className="assistant-empty-state__section-title">
|
|
95
|
+
{loading ? (
|
|
96
|
+
<>
|
|
97
|
+
<Loader2
|
|
98
|
+
size={14}
|
|
99
|
+
className="assistant-empty-state__section-spinner"
|
|
100
|
+
aria-hidden
|
|
101
|
+
/>
|
|
102
|
+
Generating suggestions
|
|
103
|
+
</>
|
|
104
|
+
) : (
|
|
105
|
+
<>
|
|
106
|
+
<Sparkles size={14} aria-hidden />
|
|
107
|
+
Suggested for you
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</h4>
|
|
111
|
+
{hasFetched && !loading ? (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className="assistant-btn assistant-btn--ghost assistant-empty-state__section-refresh"
|
|
115
|
+
onClick={handleFetch}
|
|
116
|
+
>
|
|
117
|
+
<RefreshCw size={14} aria-hidden />
|
|
118
|
+
Regenerate
|
|
119
|
+
</button>
|
|
120
|
+
) : null}
|
|
121
|
+
</header>
|
|
122
|
+
<SuggestedPromptsList
|
|
123
|
+
prompts={prompts}
|
|
124
|
+
loading={loading}
|
|
125
|
+
error={error}
|
|
126
|
+
onRetry={handleFetch}
|
|
127
|
+
retryLoading={loading}
|
|
128
|
+
onSelect={onSelect}
|
|
129
|
+
/>
|
|
130
|
+
</section>
|
|
131
|
+
) : null}
|
|
132
|
+
|
|
133
|
+
{showStaticPrompts ? (
|
|
134
|
+
<section className="assistant-empty-state__section">
|
|
135
|
+
<header className="assistant-empty-state__section-header">
|
|
136
|
+
<h4 className="assistant-empty-state__section-title">
|
|
137
|
+
Quick starts
|
|
138
|
+
</h4>
|
|
139
|
+
</header>
|
|
140
|
+
<SuggestedPromptsList prompts={prompts} onSelect={onSelect} />
|
|
141
|
+
</section>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|