@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.
Files changed (68) hide show
  1. package/README.md +322 -0
  2. package/package.json +41 -0
  3. package/src/core/chat-activity.ts +107 -0
  4. package/src/core/chat-commands.ts +173 -0
  5. package/src/core/chat-history.ts +113 -0
  6. package/src/core/chat-reply-suggestions-parse.ts +119 -0
  7. package/src/core/code-highlight.ts +20 -0
  8. package/src/core/create-assistant-store.ts +639 -0
  9. package/src/core/fetch-suggested-prompts.ts +53 -0
  10. package/src/core/index.ts +125 -0
  11. package/src/core/interactive-tools/choices.ts +155 -0
  12. package/src/core/interactive-tools/confirmation.ts +63 -0
  13. package/src/core/interactive-tools/constants.ts +22 -0
  14. package/src/core/interactive-tools/execute.ts +70 -0
  15. package/src/core/interactive-tools/index.ts +41 -0
  16. package/src/core/interactive-tools/suggestions.ts +87 -0
  17. package/src/core/interactive-tools/waiters.ts +55 -0
  18. package/src/core/llm-chat.ts +686 -0
  19. package/src/core/llm-config.ts +101 -0
  20. package/src/core/llm-models.ts +96 -0
  21. package/src/core/llm-provider.ts +99 -0
  22. package/src/core/llm-settings-storage.ts +331 -0
  23. package/src/core/llm-sse.ts +166 -0
  24. package/src/core/llm-types.ts +52 -0
  25. package/src/core/markdown-utils.ts +11 -0
  26. package/src/core/prepare-markdown.ts +38 -0
  27. package/src/core/types.ts +86 -0
  28. package/src/css.d.ts +1 -0
  29. package/src/react/Assistant.tsx +358 -0
  30. package/src/react/components/HighlightedJsonCode.tsx +24 -0
  31. package/src/react/components/MarkdownContent.tsx +98 -0
  32. package/src/react/components/MarkdownEditor.tsx +60 -0
  33. package/src/react/components/MermaidDiagram.tsx +139 -0
  34. package/src/react/components/ModelSelector.tsx +243 -0
  35. package/src/react/components/chat/AssistantErrorCallout.tsx +79 -0
  36. package/src/react/components/chat/ChatActivity.tsx +274 -0
  37. package/src/react/components/chat/ChatComposer.tsx +189 -0
  38. package/src/react/components/chat/ChatEmptyState.tsx +145 -0
  39. package/src/react/components/chat/ChatInteractivePrompt/choices-prompt.tsx +262 -0
  40. package/src/react/components/chat/ChatInteractivePrompt/confirmation-prompt.tsx +97 -0
  41. package/src/react/components/chat/ChatInteractivePrompt/index.tsx +60 -0
  42. package/src/react/components/chat/ChatInteractivePrompt/shell.tsx +60 -0
  43. package/src/react/components/chat/ChatInteractivePrompt/utils.ts +14 -0
  44. package/src/react/components/chat/ChatMessage.tsx +150 -0
  45. package/src/react/components/chat/ChatMessageScroll.tsx +116 -0
  46. package/src/react/components/chat/ChatReplySuggestions.tsx +231 -0
  47. package/src/react/components/chat/ComposerCommandMenu.tsx +69 -0
  48. package/src/react/components/chat/LlmSettingsStrip.tsx +348 -0
  49. package/src/react/components/chat/LlmSetupPrompt.tsx +58 -0
  50. package/src/react/components/chat/LlmUnavailableBanner.tsx +11 -0
  51. package/src/react/components/chat/SuggestedPromptsList.tsx +121 -0
  52. package/src/react/components/chat/SuggestedPromptsStrip.tsx +72 -0
  53. package/src/react/components/chat/SystemPromptField.tsx +107 -0
  54. package/src/react/components/highlighted-code.tsx +107 -0
  55. package/src/react/context.tsx +72 -0
  56. package/src/react/hooks/use-composer-commands.ts +129 -0
  57. package/src/react/hooks/use-suggested-prompts.ts +128 -0
  58. package/src/react/index.ts +39 -0
  59. package/src/react/lib/parse-assistant-error.ts +96 -0
  60. package/src/react/lib/prompt-icons.ts +40 -0
  61. package/src/react/types.ts +83 -0
  62. package/src/react/utils/cn.ts +5 -0
  63. package/src/styles/assistant.css +3009 -0
  64. package/test/buildLlmHistory.test.ts +95 -0
  65. package/test/llm-config.test.ts +72 -0
  66. package/test/llmSettingsStorage.test.ts +121 -0
  67. package/test/parse-assistant-error.test.ts +24 -0
  68. package/tsconfig.json +8 -0
@@ -0,0 +1,262 @@
1
+ import { Check, ListChecks } from "lucide-react";
2
+ import { useEffect, useState } from "react";
3
+ import type {
4
+ ChoiceOption,
5
+ ChoicesToolArgs,
6
+ ChoicesToolResult,
7
+ } from "../../../../core/interactive-tools/index.ts";
8
+ import { useAssistantActions } from "../../../context.tsx";
9
+ import { cn } from "../../../utils/cn.ts";
10
+ import { InteractivePromptShell } from "./shell.tsx";
11
+ import { isCompactTokenLabel, optionKeyLabel } from "./utils.ts";
12
+
13
+ function ChoiceOptionCard({
14
+ option,
15
+ index,
16
+ checked,
17
+ inputType,
18
+ name,
19
+ disabled,
20
+ onSelect,
21
+ }: {
22
+ option: ChoiceOption;
23
+ index: number;
24
+ checked: boolean;
25
+ inputType: "radio" | "checkbox";
26
+ name: string;
27
+ disabled: boolean;
28
+ onSelect: (checked: boolean) => void;
29
+ }) {
30
+ const inputId = `${name}-${option.id}`;
31
+ const keyLabel = optionKeyLabel(index);
32
+ const compactLabel = isCompactTokenLabel(option.label);
33
+
34
+ return (
35
+ <label
36
+ htmlFor={inputId}
37
+ className={cn(
38
+ "assistant-interactive-choice group",
39
+ checked && "assistant-interactive-choice--selected",
40
+ disabled && "assistant-interactive-choice--disabled",
41
+ )}
42
+ >
43
+ <input
44
+ id={inputId}
45
+ type={inputType}
46
+ name={inputType === "radio" ? name : undefined}
47
+ checked={checked}
48
+ disabled={disabled}
49
+ onChange={(event) =>
50
+ onSelect(inputType === "radio" ? true : event.target.checked)
51
+ }
52
+ className="peer sr-only"
53
+ />
54
+
55
+ <span
56
+ aria-hidden
57
+ className={cn(
58
+ "assistant-interactive-choice__check",
59
+ checked && "assistant-interactive-choice__check--on",
60
+ )}
61
+ >
62
+ {checked ? <Check className="size-2.5 stroke-[3]" /> : null}
63
+ </span>
64
+
65
+ <span className="assistant-interactive-choice__content">
66
+ <span
67
+ className={cn(
68
+ "assistant-interactive-choice__label",
69
+ compactLabel && "assistant-interactive-choice__label--mono",
70
+ )}
71
+ >
72
+ {option.label}
73
+ </span>
74
+ {option.description ? (
75
+ <span className="assistant-interactive-choice__description">
76
+ {option.description}
77
+ </span>
78
+ ) : null}
79
+ </span>
80
+
81
+ <kbd className="assistant-interactive-kbd">{keyLabel}</kbd>
82
+ </label>
83
+ );
84
+ }
85
+
86
+ export function ChoicesPrompt({
87
+ args,
88
+ callId,
89
+ disabled,
90
+ }: {
91
+ args: ChoicesToolArgs;
92
+ callId: string;
93
+ disabled: boolean;
94
+ }) {
95
+ const { submitInteractiveToolResult, cancelInteractiveToolResult } =
96
+ useAssistantActions();
97
+ const [selectedIds, setSelectedIds] = useState<string[]>([]);
98
+
99
+ const selectionCount = selectedIds.length;
100
+ const meetsMinimum = selectionCount >= args.minSelections;
101
+ const meetsMaximum =
102
+ args.maxSelections === undefined
103
+ ? true
104
+ : selectionCount <= args.maxSelections;
105
+
106
+ function submitSelection(ids: string[]) {
107
+ const result: ChoicesToolResult = { selected: ids };
108
+ submitInteractiveToolResult(callId, result);
109
+ }
110
+
111
+ function toggleOption(optionId: string, checked: boolean) {
112
+ if (args.allowMultiple) {
113
+ setSelectedIds((current) => {
114
+ if (checked) {
115
+ if (
116
+ args.maxSelections !== undefined &&
117
+ current.length >= args.maxSelections
118
+ ) {
119
+ return current;
120
+ }
121
+ return [...new Set([...current, optionId])];
122
+ }
123
+ return current.filter((id) => id !== optionId);
124
+ });
125
+ return;
126
+ }
127
+
128
+ const next = checked ? [optionId] : [];
129
+ setSelectedIds(next);
130
+ if (checked && !disabled) {
131
+ submitSelection(next);
132
+ }
133
+ }
134
+
135
+ function handleSubmit() {
136
+ if (!meetsMinimum || !meetsMaximum) return;
137
+ submitSelection(selectedIds);
138
+ }
139
+
140
+ useEffect(() => {
141
+ if (disabled) return;
142
+
143
+ function onKeyDown(event: KeyboardEvent) {
144
+ if (event.key === "Escape") {
145
+ event.preventDefault();
146
+ cancelInteractiveToolResult(callId);
147
+ return;
148
+ }
149
+
150
+ if (
151
+ event.key === "Enter" &&
152
+ args.allowMultiple &&
153
+ meetsMinimum &&
154
+ meetsMaximum
155
+ ) {
156
+ event.preventDefault();
157
+ submitInteractiveToolResult(callId, { selected: selectedIds });
158
+ return;
159
+ }
160
+
161
+ if (!/^[1-9]$/.test(event.key)) return;
162
+
163
+ const index = Number(event.key) - 1;
164
+ const option = args.options[index];
165
+ if (!option) return;
166
+
167
+ event.preventDefault();
168
+ if (args.allowMultiple) {
169
+ setSelectedIds((current) => {
170
+ const isSelected = current.includes(option.id);
171
+ if (isSelected) {
172
+ return current.filter((id) => id !== option.id);
173
+ }
174
+ if (
175
+ args.maxSelections !== undefined &&
176
+ current.length >= args.maxSelections
177
+ ) {
178
+ return current;
179
+ }
180
+ return [...new Set([...current, option.id])];
181
+ });
182
+ return;
183
+ }
184
+
185
+ const next = [option.id];
186
+ setSelectedIds(next);
187
+ submitInteractiveToolResult(callId, { selected: next });
188
+ }
189
+
190
+ window.addEventListener("keydown", onKeyDown);
191
+ return () => window.removeEventListener("keydown", onKeyDown);
192
+ }, [
193
+ disabled,
194
+ callId,
195
+ args.options,
196
+ args.allowMultiple,
197
+ args.maxSelections,
198
+ selectedIds,
199
+ meetsMinimum,
200
+ meetsMaximum,
201
+ cancelInteractiveToolResult,
202
+ submitInteractiveToolResult,
203
+ ]);
204
+
205
+ const title = args.allowMultiple ? "Choose options" : "Choose one";
206
+
207
+ return (
208
+ <InteractivePromptShell
209
+ icon={ListChecks}
210
+ title={title}
211
+ keyboardHint={disabled ? undefined : "1–9 · Enter · Esc"}
212
+ actions={
213
+ args.allowMultiple ? (
214
+ <>
215
+ <button
216
+ type="button"
217
+ className="btn btn-secondary btn-compact"
218
+ disabled={disabled}
219
+ onClick={() => cancelInteractiveToolResult(callId)}
220
+ >
221
+ Cancel
222
+ </button>
223
+ <button
224
+ type="button"
225
+ className="btn btn-primary btn-compact"
226
+ disabled={disabled || !meetsMinimum || !meetsMaximum}
227
+ onClick={handleSubmit}
228
+ >
229
+ {args.submitLabel}
230
+ {selectionCount > 0 ? ` (${selectionCount})` : ""}
231
+ </button>
232
+ </>
233
+ ) : (
234
+ <button
235
+ type="button"
236
+ className="btn btn-secondary btn-compact"
237
+ disabled={disabled}
238
+ onClick={() => cancelInteractiveToolResult(callId)}
239
+ >
240
+ Cancel
241
+ </button>
242
+ )
243
+ }
244
+ >
245
+ <p className="assistant-interactive-prompt__message">{args.message}</p>
246
+ <div className="assistant-interactive-choice-list">
247
+ {args.options.map((option, index) => (
248
+ <ChoiceOptionCard
249
+ key={option.id}
250
+ option={option}
251
+ index={index}
252
+ checked={selectedIds.includes(option.id)}
253
+ inputType={args.allowMultiple ? "checkbox" : "radio"}
254
+ name={`chat-choice-${callId}`}
255
+ disabled={disabled}
256
+ onSelect={(checked) => toggleOption(option.id, checked)}
257
+ />
258
+ ))}
259
+ </div>
260
+ </InteractivePromptShell>
261
+ );
262
+ }
@@ -0,0 +1,97 @@
1
+ import { AlertTriangle, ShieldCheck } from "lucide-react";
2
+ import { useEffect } from "react";
3
+ import type { ConfirmationToolArgs } from "../../../../core/interactive-tools/index.ts";
4
+ import { useAssistantActions } from "../../../context.tsx";
5
+ import { cn } from "../../../utils/cn.ts";
6
+ import { InteractivePromptShell } from "./shell.tsx";
7
+ import { isDestructiveAction } from "./utils.ts";
8
+
9
+ export function ConfirmationPrompt({
10
+ args,
11
+ callId,
12
+ disabled,
13
+ }: {
14
+ args: ConfirmationToolArgs;
15
+ callId: string;
16
+ disabled: boolean;
17
+ }) {
18
+ const { submitInteractiveToolResult, cancelInteractiveToolResult } =
19
+ useAssistantActions();
20
+
21
+ const destructive = isDestructiveAction(args.action);
22
+
23
+ function handleConfirm() {
24
+ submitInteractiveToolResult(callId, { confirmed: true });
25
+ }
26
+
27
+ function handleCancel() {
28
+ cancelInteractiveToolResult(callId);
29
+ }
30
+
31
+ useEffect(() => {
32
+ if (disabled) return;
33
+
34
+ function onKeyDown(event: KeyboardEvent) {
35
+ if (event.key === "Enter" && !event.shiftKey) {
36
+ event.preventDefault();
37
+ submitInteractiveToolResult(callId, { confirmed: true });
38
+ }
39
+ if (event.key === "Escape") {
40
+ event.preventDefault();
41
+ cancelInteractiveToolResult(callId);
42
+ }
43
+ }
44
+
45
+ window.addEventListener("keydown", onKeyDown);
46
+ return () => window.removeEventListener("keydown", onKeyDown);
47
+ }, [
48
+ disabled,
49
+ callId,
50
+ submitInteractiveToolResult,
51
+ cancelInteractiveToolResult,
52
+ ]);
53
+
54
+ return (
55
+ <InteractivePromptShell
56
+ icon={destructive ? AlertTriangle : ShieldCheck}
57
+ title={destructive ? "Confirm deletion" : "Confirm"}
58
+ tone={destructive ? "caution" : "default"}
59
+ keyboardHint={disabled ? undefined : "Enter · Esc"}
60
+ actions={
61
+ <>
62
+ <button
63
+ type="button"
64
+ className="btn btn-secondary btn-compact"
65
+ disabled={disabled}
66
+ onClick={handleCancel}
67
+ >
68
+ {args.cancelLabel}
69
+ </button>
70
+ <button
71
+ type="button"
72
+ className={cn(
73
+ "btn btn-compact",
74
+ destructive ? "btn-danger btn-danger--solid" : "btn-primary",
75
+ )}
76
+ disabled={disabled}
77
+ onClick={handleConfirm}
78
+ >
79
+ {args.confirmLabel}
80
+ </button>
81
+ </>
82
+ }
83
+ >
84
+ <p className="assistant-interactive-prompt__message">{args.message}</p>
85
+ {args.action ? (
86
+ <p
87
+ className={cn(
88
+ "assistant-interactive-prompt__action",
89
+ destructive && "assistant-interactive-prompt__action--caution",
90
+ )}
91
+ >
92
+ {args.action}
93
+ </p>
94
+ ) : null}
95
+ </InteractivePromptShell>
96
+ );
97
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ parseChoicesArgs,
3
+ parseConfirmationArgs,
4
+ REQUEST_CHOICES_TOOL,
5
+ REQUEST_CONFIRMATION_TOOL,
6
+ } from "../../../../core/interactive-tools/index.ts";
7
+ import { useAssistant } from "../../../context.tsx";
8
+ import { ChoicesPrompt } from "./choices-prompt.tsx";
9
+ import { ConfirmationPrompt } from "./confirmation-prompt.tsx";
10
+
11
+ export function ChatInteractivePrompt({
12
+ toolName,
13
+ args,
14
+ callId,
15
+ active,
16
+ }: {
17
+ toolName: string;
18
+ args: Record<string, unknown>;
19
+ callId?: string;
20
+ active: boolean;
21
+ }) {
22
+ const chatLoading = useAssistant((state) => state.chatLoading);
23
+ const disabled = !chatLoading;
24
+
25
+ if (!active || !callId) {
26
+ return null;
27
+ }
28
+
29
+ try {
30
+ if (toolName === REQUEST_CONFIRMATION_TOOL) {
31
+ return (
32
+ <ConfirmationPrompt
33
+ args={parseConfirmationArgs(args)}
34
+ callId={callId}
35
+ disabled={disabled}
36
+ />
37
+ );
38
+ }
39
+
40
+ if (toolName === REQUEST_CHOICES_TOOL) {
41
+ return (
42
+ <ChoicesPrompt
43
+ args={parseChoicesArgs(args)}
44
+ callId={callId}
45
+ disabled={disabled}
46
+ />
47
+ );
48
+ }
49
+ } catch (error) {
50
+ return (
51
+ <p className="text-[11px] text-[var(--text-danger)]">
52
+ {error instanceof Error
53
+ ? error.message
54
+ : "Invalid interactive tool arguments"}
55
+ </p>
56
+ );
57
+ }
58
+
59
+ return null;
60
+ }
@@ -0,0 +1,60 @@
1
+ import type { LucideIcon } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+ import { cn } from "../../../utils/cn.ts";
4
+
5
+ type InteractivePromptShellProps = {
6
+ icon: LucideIcon;
7
+ title: string;
8
+ tone?: "default" | "caution";
9
+ children: ReactNode;
10
+ actions?: ReactNode;
11
+ keyboardHint?: string;
12
+ };
13
+
14
+ export function InteractivePromptShell({
15
+ icon: Icon,
16
+ title,
17
+ tone = "default",
18
+ children,
19
+ actions,
20
+ keyboardHint,
21
+ }: InteractivePromptShellProps) {
22
+ return (
23
+ <div
24
+ className={cn(
25
+ "assistant-interactive-prompt",
26
+ tone === "caution" && "assistant-interactive-prompt--caution",
27
+ )}
28
+ >
29
+ <div className="assistant-interactive-prompt__main">
30
+ <span
31
+ className={cn(
32
+ "assistant-interactive-prompt__icon",
33
+ tone === "caution" && "assistant-interactive-prompt__icon--caution",
34
+ )}
35
+ aria-hidden
36
+ >
37
+ <Icon size={13} strokeWidth={2.25} />
38
+ </span>
39
+
40
+ <div className="assistant-interactive-prompt__content min-w-0 flex-1">
41
+ <p className="assistant-interactive-prompt__title">{title}</p>
42
+ {children ? (
43
+ <div className="assistant-interactive-prompt__body">{children}</div>
44
+ ) : null}
45
+ </div>
46
+ </div>
47
+
48
+ {actions ? (
49
+ <div className="assistant-interactive-prompt__bar">
50
+ {keyboardHint ? (
51
+ <p className="assistant-interactive-prompt__hint">{keyboardHint}</p>
52
+ ) : (
53
+ <span aria-hidden />
54
+ )}
55
+ <div className="assistant-interactive-prompt__actions">{actions}</div>
56
+ </div>
57
+ ) : null}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,14 @@
1
+ export function isDestructiveAction(action?: string): boolean {
2
+ if (!action) return false;
3
+ return /\b(delete|remove|disable|destroy|clear|reset|drop|purge|unlink|permanently)\b/i.test(
4
+ action,
5
+ );
6
+ }
7
+
8
+ export function optionKeyLabel(index: number): string {
9
+ return String(index + 1);
10
+ }
11
+
12
+ export function isCompactTokenLabel(label: string): boolean {
13
+ return label.length >= 16 && !/\s/.test(label);
14
+ }
@@ -0,0 +1,150 @@
1
+ import { Bot, User } from "lucide-react";
2
+ import { SUGGEST_REPLIES_TOOL } from "../../../core/interactive-tools/index.ts";
3
+ import { isLlmUnavailableMessage } from "../../../core/llm-config.ts";
4
+ import type { AssistantMessage } from "../../../core/types.ts";
5
+ import { MarkdownContent } from "../MarkdownContent.tsx";
6
+ import { AssistantErrorCallout } from "./AssistantErrorCallout.tsx";
7
+ import { ChatActivity } from "./ChatActivity.tsx";
8
+ import { ChatReplySuggestions } from "./ChatReplySuggestions.tsx";
9
+ import { LlmSetupPrompt } from "./LlmSetupPrompt.tsx";
10
+
11
+ function TypingIndicator() {
12
+ return (
13
+ <output className="assistant-typing" aria-live="polite">
14
+ {[0, 1, 2].map((index) => (
15
+ <span
16
+ key={index}
17
+ className="assistant-typing-dot"
18
+ style={{ animationDelay: `${index * 160}ms` }}
19
+ />
20
+ ))}
21
+ <span className="sr-only">Assistant is thinking</span>
22
+ </output>
23
+ );
24
+ }
25
+
26
+ interface ChatMessageViewProps {
27
+ message: AssistantMessage;
28
+ onSuggestionSelect?: (reply: string) => void;
29
+ onOpenLlmSettings?: () => void;
30
+ onRetryError?: () => void;
31
+ retryErrorLoading?: boolean;
32
+ }
33
+
34
+ export function ChatMessageView({
35
+ message,
36
+ onSuggestionSelect,
37
+ onOpenLlmSettings,
38
+ onRetryError,
39
+ retryErrorLoading,
40
+ }: ChatMessageViewProps) {
41
+ const isUser = message.role === "user";
42
+ const streaming = Boolean(message.streaming);
43
+ const activity = message.activity ?? [];
44
+ const visibleActivity =
45
+ message.replySuggestions && !streaming
46
+ ? activity.filter((step) => step.name !== SUGGEST_REPLIES_TOOL)
47
+ : activity;
48
+ const hasActivity = visibleActivity.length > 0;
49
+ const hasActiveStep = visibleActivity.some(
50
+ (step) => step.status === "active",
51
+ );
52
+ const showTyping =
53
+ !isUser &&
54
+ streaming &&
55
+ !message.content.trim() &&
56
+ !hasActivity &&
57
+ !message.isError;
58
+ const showCard =
59
+ !isUser &&
60
+ (Boolean(message.content.trim()) ||
61
+ showTyping ||
62
+ hasActivity ||
63
+ message.isError ||
64
+ Boolean(message.replySuggestions));
65
+
66
+ if (isUser) {
67
+ return (
68
+ <article className="assistant-message assistant-message--user">
69
+ <div className="assistant-user-row">
70
+ <div className="assistant-user-bubble">
71
+ <p className="assistant-user-bubble__text">{message.content}</p>
72
+ </div>
73
+ <div className="assistant-user-avatar" aria-hidden>
74
+ <User size={14} strokeWidth={2.25} />
75
+ </div>
76
+ </div>
77
+ </article>
78
+ );
79
+ }
80
+
81
+ if (isLlmUnavailableMessage(message) && onOpenLlmSettings) {
82
+ return (
83
+ <article className="assistant-message assistant-message--assistant">
84
+ <div className="assistant-assistant-row">
85
+ <div className="assistant-bot-avatar" aria-hidden>
86
+ <Bot size={14} strokeWidth={2.25} />
87
+ </div>
88
+ <LlmSetupPrompt variant="inline" onConfigure={onOpenLlmSettings} />
89
+ </div>
90
+ </article>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <article className="assistant-message assistant-message--assistant">
96
+ <div className="assistant-assistant-row">
97
+ <div className="assistant-bot-avatar" aria-hidden>
98
+ <Bot size={14} strokeWidth={2.25} />
99
+ </div>
100
+ {showCard ? (
101
+ <div
102
+ className={`assistant-assistant-card${message.isError ? " assistant-assistant-card--error" : ""}`}
103
+ >
104
+ <header className="assistant-assistant-card__meta">
105
+ <span className="assistant-role-label">Assistant</span>
106
+ </header>
107
+
108
+ {message.isError ? (
109
+ <div className="assistant-assistant-card__error">
110
+ <AssistantErrorCallout
111
+ error={message.content}
112
+ context="chat"
113
+ variant="embedded"
114
+ onRetry={onRetryError}
115
+ retryLoading={retryErrorLoading}
116
+ />
117
+ </div>
118
+ ) : message.content.trim() ? (
119
+ <div className="assistant-markdown">
120
+ <MarkdownContent
121
+ content={message.content}
122
+ streaming={streaming}
123
+ />
124
+ {streaming && !hasActiveStep ? (
125
+ <span className="assistant-markdown__cursor" aria-hidden />
126
+ ) : null}
127
+ </div>
128
+ ) : showTyping ? (
129
+ <div className="assistant-assistant-card__typing">
130
+ <TypingIndicator />
131
+ </div>
132
+ ) : null}
133
+
134
+ {hasActivity ? (
135
+ <ChatActivity steps={visibleActivity} streaming={streaming} />
136
+ ) : null}
137
+
138
+ {message.replySuggestions && !streaming && onSuggestionSelect ? (
139
+ <ChatReplySuggestions
140
+ suggestions={message.replySuggestions}
141
+ disabled={streaming}
142
+ onSelect={onSuggestionSelect}
143
+ />
144
+ ) : null}
145
+ </div>
146
+ ) : null}
147
+ </div>
148
+ </article>
149
+ );
150
+ }