@4djs/assistant 0.0.0 → 0.0.1

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 (188) hide show
  1. package/dist/core/chat-activity.d.ts +19 -0
  2. package/dist/core/chat-activity.d.ts.map +1 -0
  3. package/dist/core/chat-commands.d.ts +33 -0
  4. package/dist/core/chat-commands.d.ts.map +1 -0
  5. package/dist/core/chat-history.d.ts +14 -0
  6. package/dist/core/chat-history.d.ts.map +1 -0
  7. package/dist/core/chat-reply-suggestions-parse.d.ts +20 -0
  8. package/dist/core/chat-reply-suggestions-parse.d.ts.map +1 -0
  9. package/dist/core/code-highlight.d.ts +3 -0
  10. package/dist/core/code-highlight.d.ts.map +1 -0
  11. package/dist/core/create-assistant-store.d.ts +33 -0
  12. package/dist/core/create-assistant-store.d.ts.map +1 -0
  13. package/dist/core/fetch-suggested-prompts.d.ts +11 -0
  14. package/dist/core/fetch-suggested-prompts.d.ts.map +1 -0
  15. package/dist/core/index.d.ts +19 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +2876 -0
  18. package/dist/core/interactive-tools/choices.d.ts +22 -0
  19. package/dist/core/interactive-tools/choices.d.ts.map +1 -0
  20. package/dist/core/interactive-tools/confirmation.d.ts +15 -0
  21. package/dist/core/interactive-tools/confirmation.d.ts.map +1 -0
  22. package/dist/core/interactive-tools/constants.d.ts +6 -0
  23. package/dist/core/interactive-tools/constants.d.ts.map +1 -0
  24. package/dist/core/interactive-tools/execute.d.ts +11 -0
  25. package/dist/core/interactive-tools/execute.d.ts.map +1 -0
  26. package/dist/core/interactive-tools/index.d.ts +7 -0
  27. package/dist/core/interactive-tools/index.d.ts.map +1 -0
  28. package/dist/core/interactive-tools/suggestions.d.ts +13 -0
  29. package/dist/core/interactive-tools/suggestions.d.ts.map +1 -0
  30. package/dist/core/interactive-tools/waiters.d.ts +4 -0
  31. package/dist/core/interactive-tools/waiters.d.ts.map +1 -0
  32. package/dist/core/llm-chat.d.ts +96 -0
  33. package/dist/core/llm-chat.d.ts.map +1 -0
  34. package/dist/core/llm-config.d.ts +24 -0
  35. package/dist/core/llm-config.d.ts.map +1 -0
  36. package/dist/core/llm-models.d.ts +14 -0
  37. package/dist/core/llm-models.d.ts.map +1 -0
  38. package/dist/core/llm-provider.d.ts +13 -0
  39. package/dist/core/llm-provider.d.ts.map +1 -0
  40. package/dist/core/llm-settings-storage.d.ts +47 -0
  41. package/dist/core/llm-settings-storage.d.ts.map +1 -0
  42. package/dist/core/llm-sse.d.ts +13 -0
  43. package/dist/core/llm-sse.d.ts.map +1 -0
  44. package/dist/core/llm-types.d.ts +49 -0
  45. package/dist/core/llm-types.d.ts.map +1 -0
  46. package/dist/core/markdown-utils.d.ts +3 -0
  47. package/dist/core/markdown-utils.d.ts.map +1 -0
  48. package/dist/core/prepare-markdown.d.ts +7 -0
  49. package/dist/core/prepare-markdown.d.ts.map +1 -0
  50. package/dist/core/types.d.ts +74 -0
  51. package/dist/core/types.d.ts.map +1 -0
  52. package/dist/index.css +1195 -0
  53. package/dist/index.js +184948 -0
  54. package/dist/react/Assistant.d.ts +10 -0
  55. package/dist/react/Assistant.d.ts.map +1 -0
  56. package/dist/react/components/HighlightedJsonCode.d.ts +6 -0
  57. package/dist/react/components/HighlightedJsonCode.d.ts.map +1 -0
  58. package/dist/react/components/MarkdownContent.d.ts +10 -0
  59. package/dist/react/components/MarkdownContent.d.ts.map +1 -0
  60. package/dist/react/components/MarkdownEditor.d.ts +11 -0
  61. package/dist/react/components/MarkdownEditor.d.ts.map +1 -0
  62. package/dist/react/components/MermaidDiagram.d.ts +8 -0
  63. package/dist/react/components/MermaidDiagram.d.ts.map +1 -0
  64. package/dist/react/components/ModelSelector.d.ts +8 -0
  65. package/dist/react/components/ModelSelector.d.ts.map +1 -0
  66. package/dist/react/components/chat/AssistantErrorCallout.d.ts +11 -0
  67. package/dist/react/components/chat/AssistantErrorCallout.d.ts.map +1 -0
  68. package/dist/react/components/chat/ChatActivity.d.ts +8 -0
  69. package/dist/react/components/chat/ChatActivity.d.ts.map +1 -0
  70. package/dist/react/components/chat/ChatComposer.d.ts +36 -0
  71. package/dist/react/components/chat/ChatComposer.d.ts.map +1 -0
  72. package/dist/react/components/chat/ChatEmptyState.d.ts +10 -0
  73. package/dist/react/components/chat/ChatEmptyState.d.ts.map +1 -0
  74. package/dist/react/components/chat/ChatInteractivePrompt/choices-prompt.d.ts +7 -0
  75. package/dist/react/components/chat/ChatInteractivePrompt/choices-prompt.d.ts.map +1 -0
  76. package/dist/react/components/chat/ChatInteractivePrompt/confirmation-prompt.d.ts +7 -0
  77. package/dist/react/components/chat/ChatInteractivePrompt/confirmation-prompt.d.ts.map +1 -0
  78. package/dist/react/components/chat/ChatInteractivePrompt/index.d.ts +7 -0
  79. package/dist/react/components/chat/ChatInteractivePrompt/index.d.ts.map +1 -0
  80. package/dist/react/components/chat/ChatInteractivePrompt/shell.d.ts +13 -0
  81. package/dist/react/components/chat/ChatInteractivePrompt/shell.d.ts.map +1 -0
  82. package/dist/react/components/chat/ChatInteractivePrompt/utils.d.ts +4 -0
  83. package/dist/react/components/chat/ChatInteractivePrompt/utils.d.ts.map +1 -0
  84. package/dist/react/components/chat/ChatMessage.d.ts +11 -0
  85. package/dist/react/components/chat/ChatMessage.d.ts.map +1 -0
  86. package/dist/react/components/chat/ChatMessageScroll.d.ts +8 -0
  87. package/dist/react/components/chat/ChatMessageScroll.d.ts.map +1 -0
  88. package/dist/react/components/chat/ChatReplySuggestions.d.ts +9 -0
  89. package/dist/react/components/chat/ChatReplySuggestions.d.ts.map +1 -0
  90. package/dist/react/components/chat/ComposerCommandMenu.d.ts +10 -0
  91. package/dist/react/components/chat/ComposerCommandMenu.d.ts.map +1 -0
  92. package/dist/react/components/chat/LlmSettingsStrip.d.ts +7 -0
  93. package/dist/react/components/chat/LlmSettingsStrip.d.ts.map +1 -0
  94. package/dist/react/components/chat/LlmSetupPrompt.d.ts +7 -0
  95. package/dist/react/components/chat/LlmSetupPrompt.d.ts.map +1 -0
  96. package/dist/react/components/chat/LlmUnavailableBanner.d.ts +6 -0
  97. package/dist/react/components/chat/LlmUnavailableBanner.d.ts.map +1 -0
  98. package/dist/react/components/chat/SuggestedPromptsList.d.ts +14 -0
  99. package/dist/react/components/chat/SuggestedPromptsList.d.ts.map +1 -0
  100. package/dist/react/components/chat/SuggestedPromptsStrip.d.ts +11 -0
  101. package/dist/react/components/chat/SuggestedPromptsStrip.d.ts.map +1 -0
  102. package/dist/react/components/chat/SystemPromptField.d.ts +10 -0
  103. package/dist/react/components/chat/SystemPromptField.d.ts.map +1 -0
  104. package/dist/react/components/highlighted-code.d.ts +8 -0
  105. package/dist/react/components/highlighted-code.d.ts.map +1 -0
  106. package/dist/react/context.d.ts +11 -0
  107. package/dist/react/context.d.ts.map +1 -0
  108. package/dist/react/hooks/use-composer-commands.d.ts +21 -0
  109. package/dist/react/hooks/use-composer-commands.d.ts.map +1 -0
  110. package/dist/react/hooks/use-suggested-prompts.d.ts +29 -0
  111. package/dist/react/hooks/use-suggested-prompts.d.ts.map +1 -0
  112. package/dist/react/index.d.ts +17 -0
  113. package/dist/react/index.d.ts.map +1 -0
  114. package/dist/react/lib/parse-assistant-error.d.ts +9 -0
  115. package/dist/react/lib/parse-assistant-error.d.ts.map +1 -0
  116. package/dist/react/lib/prompt-icons.d.ts +5 -0
  117. package/dist/react/lib/prompt-icons.d.ts.map +1 -0
  118. package/dist/react/types.d.ts +69 -0
  119. package/dist/react/types.d.ts.map +1 -0
  120. package/dist/react/utils/cn.d.ts +2 -0
  121. package/dist/react/utils/cn.d.ts.map +1 -0
  122. package/package.json +16 -5
  123. package/src/core/chat-activity.ts +0 -107
  124. package/src/core/chat-commands.ts +0 -173
  125. package/src/core/chat-history.ts +0 -113
  126. package/src/core/chat-reply-suggestions-parse.ts +0 -119
  127. package/src/core/code-highlight.ts +0 -20
  128. package/src/core/create-assistant-store.ts +0 -639
  129. package/src/core/fetch-suggested-prompts.ts +0 -53
  130. package/src/core/index.ts +0 -125
  131. package/src/core/interactive-tools/choices.ts +0 -155
  132. package/src/core/interactive-tools/confirmation.ts +0 -63
  133. package/src/core/interactive-tools/constants.ts +0 -22
  134. package/src/core/interactive-tools/execute.ts +0 -70
  135. package/src/core/interactive-tools/index.ts +0 -41
  136. package/src/core/interactive-tools/suggestions.ts +0 -87
  137. package/src/core/interactive-tools/waiters.ts +0 -55
  138. package/src/core/llm-chat.ts +0 -686
  139. package/src/core/llm-config.ts +0 -101
  140. package/src/core/llm-models.ts +0 -96
  141. package/src/core/llm-provider.ts +0 -99
  142. package/src/core/llm-settings-storage.ts +0 -331
  143. package/src/core/llm-sse.ts +0 -166
  144. package/src/core/llm-types.ts +0 -52
  145. package/src/core/markdown-utils.ts +0 -11
  146. package/src/core/prepare-markdown.ts +0 -38
  147. package/src/core/types.ts +0 -86
  148. package/src/css.d.ts +0 -1
  149. package/src/react/Assistant.tsx +0 -358
  150. package/src/react/components/HighlightedJsonCode.tsx +0 -24
  151. package/src/react/components/MarkdownContent.tsx +0 -98
  152. package/src/react/components/MarkdownEditor.tsx +0 -60
  153. package/src/react/components/MermaidDiagram.tsx +0 -139
  154. package/src/react/components/ModelSelector.tsx +0 -243
  155. package/src/react/components/chat/AssistantErrorCallout.tsx +0 -79
  156. package/src/react/components/chat/ChatActivity.tsx +0 -274
  157. package/src/react/components/chat/ChatComposer.tsx +0 -189
  158. package/src/react/components/chat/ChatEmptyState.tsx +0 -145
  159. package/src/react/components/chat/ChatInteractivePrompt/choices-prompt.tsx +0 -262
  160. package/src/react/components/chat/ChatInteractivePrompt/confirmation-prompt.tsx +0 -97
  161. package/src/react/components/chat/ChatInteractivePrompt/index.tsx +0 -60
  162. package/src/react/components/chat/ChatInteractivePrompt/shell.tsx +0 -60
  163. package/src/react/components/chat/ChatInteractivePrompt/utils.ts +0 -14
  164. package/src/react/components/chat/ChatMessage.tsx +0 -150
  165. package/src/react/components/chat/ChatMessageScroll.tsx +0 -116
  166. package/src/react/components/chat/ChatReplySuggestions.tsx +0 -231
  167. package/src/react/components/chat/ComposerCommandMenu.tsx +0 -69
  168. package/src/react/components/chat/LlmSettingsStrip.tsx +0 -348
  169. package/src/react/components/chat/LlmSetupPrompt.tsx +0 -58
  170. package/src/react/components/chat/LlmUnavailableBanner.tsx +0 -11
  171. package/src/react/components/chat/SuggestedPromptsList.tsx +0 -121
  172. package/src/react/components/chat/SuggestedPromptsStrip.tsx +0 -72
  173. package/src/react/components/chat/SystemPromptField.tsx +0 -107
  174. package/src/react/components/highlighted-code.tsx +0 -107
  175. package/src/react/context.tsx +0 -72
  176. package/src/react/hooks/use-composer-commands.ts +0 -129
  177. package/src/react/hooks/use-suggested-prompts.ts +0 -128
  178. package/src/react/index.ts +0 -39
  179. package/src/react/lib/parse-assistant-error.ts +0 -96
  180. package/src/react/lib/prompt-icons.ts +0 -40
  181. package/src/react/types.ts +0 -83
  182. package/src/react/utils/cn.ts +0 -5
  183. package/test/buildLlmHistory.test.ts +0 -95
  184. package/test/llm-config.test.ts +0 -72
  185. package/test/llmSettingsStorage.test.ts +0 -121
  186. package/test/parse-assistant-error.test.ts +0 -24
  187. package/tsconfig.json +0 -8
  188. /package/{src/styles/assistant.css → dist/styles.css} +0 -0
@@ -1,24 +0,0 @@
1
- import type { ComponentPropsWithoutRef } from "react";
2
- import { formatJsonIfLarge } from "../../core/chat-activity.ts";
3
- import { cn } from "../utils/cn.ts";
4
- import { HighlightedCodeBlock } from "./highlighted-code.tsx";
5
-
6
- export function HighlightedJsonCode({
7
- code,
8
- className,
9
- ...props
10
- }: {
11
- code: string;
12
- className?: string;
13
- } & ComponentPropsWithoutRef<"code">) {
14
- const formatted = formatJsonIfLarge(code);
15
-
16
- return (
17
- <HighlightedCodeBlock
18
- className={cn("language-json", className)}
19
- code={formatted}
20
- language="json"
21
- {...props}
22
- />
23
- );
24
- }
@@ -1,98 +0,0 @@
1
- import { useMemo } from "react";
2
- import type { Components } from "react-markdown";
3
- import ReactMarkdown from "react-markdown";
4
- import rehypeKatex from "rehype-katex";
5
- import remarkGfm from "remark-gfm";
6
- import remarkMath from "remark-math";
7
- import { childrenToText } from "../../core/markdown-utils.ts";
8
- import { prepareMarkdown } from "../../core/prepare-markdown.ts";
9
- import { cn } from "../utils/cn.ts";
10
- import { HighlightedCodeBlock } from "./highlighted-code.tsx";
11
- import { MermaidDiagram } from "./MermaidDiagram.tsx";
12
-
13
- import "katex/dist/katex.min.css";
14
-
15
- interface MarkdownContentProps {
16
- content: string;
17
- className?: string;
18
- invert?: boolean;
19
- streaming?: boolean;
20
- }
21
-
22
- function blockLanguage(className?: string): string | null {
23
- const match = /language-([\w-]+)/.exec(className ?? "");
24
- return match?.[1] ?? null;
25
- }
26
-
27
- function createMarkdownComponents(streaming: boolean): Components {
28
- return {
29
- a: ({ href, children }) => (
30
- <a href={href} target="_blank" rel="noopener noreferrer">
31
- {children}
32
- </a>
33
- ),
34
- pre: ({ children }) => <pre className="markdown-pre">{children}</pre>,
35
- code: ({ className, children, ...props }) => {
36
- const raw = childrenToText(children).replace(/\n$/, "");
37
- const language = blockLanguage(className);
38
- const isBlock = Boolean(language) || raw.includes("\n");
39
-
40
- if (language === "mermaid") {
41
- return <MermaidDiagram chart={raw} streaming={streaming} />;
42
- }
43
-
44
- if (isBlock) {
45
- return (
46
- <HighlightedCodeBlock
47
- className={className}
48
- code={raw}
49
- language={language}
50
- {...props}
51
- />
52
- );
53
- }
54
-
55
- return (
56
- <code className="markdown-inline-code" {...props}>
57
- {children}
58
- </code>
59
- );
60
- },
61
- };
62
- }
63
-
64
- export function MarkdownContent({
65
- content,
66
- className = "",
67
- invert = false,
68
- streaming = false,
69
- }: MarkdownContentProps) {
70
- const prepared = useMemo(() => prepareMarkdown(content), [content]);
71
- const components = useMemo(
72
- () => createMarkdownComponents(streaming),
73
- [streaming],
74
- );
75
-
76
- return (
77
- <div
78
- className={cn(
79
- "markdown-body",
80
- invert && "markdown-body--invert",
81
- className,
82
- )}
83
- >
84
- <ReactMarkdown
85
- remarkPlugins={[
86
- remarkGfm,
87
- [remarkMath, { singleDollarTextMath: true }],
88
- ]}
89
- rehypePlugins={[
90
- [rehypeKatex, { strict: "ignore", throwOnError: false }],
91
- ]}
92
- components={components}
93
- >
94
- {prepared}
95
- </ReactMarkdown>
96
- </div>
97
- );
98
- }
@@ -1,60 +0,0 @@
1
- import { useCallback, useMemo, useRef } from "react";
2
- import { highlightNodes } from "./highlighted-code.tsx";
3
-
4
- interface MarkdownEditorProps {
5
- value: string;
6
- onChange: (value: string) => void;
7
- placeholder?: string;
8
- disabled?: boolean;
9
- className?: string;
10
- "aria-label"?: string;
11
- }
12
-
13
- export function MarkdownEditor({
14
- value,
15
- onChange,
16
- placeholder,
17
- disabled = false,
18
- className = "",
19
- "aria-label": ariaLabel,
20
- }: MarkdownEditorProps) {
21
- const textareaRef = useRef<HTMLTextAreaElement>(null);
22
- const highlightRef = useRef<HTMLPreElement>(null);
23
-
24
- const highlighted = useMemo(() => {
25
- if (!value) return null;
26
- const source = value.endsWith("\n") ? `${value}\n` : value;
27
- return highlightNodes(source, "markdown");
28
- }, [value]);
29
-
30
- const syncScroll = useCallback(() => {
31
- const textarea = textareaRef.current;
32
- const highlight = highlightRef.current;
33
- if (!textarea || !highlight) return;
34
- highlight.scrollTop = textarea.scrollTop;
35
- highlight.scrollLeft = textarea.scrollLeft;
36
- }, []);
37
-
38
- return (
39
- <div className={`assistant-markdown-editor ${className}`.trim()}>
40
- <pre
41
- ref={highlightRef}
42
- className="assistant-markdown-editor__highlight"
43
- aria-hidden
44
- >
45
- <code className="hljs language-markdown">{highlighted}</code>
46
- </pre>
47
- <textarea
48
- ref={textareaRef}
49
- className="assistant-markdown-editor__input"
50
- value={value}
51
- onChange={(event) => onChange(event.target.value)}
52
- onScroll={syncScroll}
53
- placeholder={placeholder}
54
- spellCheck={false}
55
- disabled={disabled}
56
- aria-label={ariaLabel}
57
- />
58
- </div>
59
- );
60
- }
@@ -1,139 +0,0 @@
1
- import mermaid from "mermaid";
2
- import { useEffect, useId, useRef, useState } from "react";
3
- import { cn } from "../utils/cn.ts";
4
- import { HighlightedCodeBlock } from "./highlighted-code.tsx";
5
-
6
- const MERMAID_FONT = "ui-sans-serif, system-ui, -apple-system, sans-serif";
7
-
8
- function getMermaidTheme(): "dark" | "default" {
9
- if (typeof document === "undefined") return "dark";
10
- return document.documentElement.dataset.theme === "light"
11
- ? "default"
12
- : "dark";
13
- }
14
-
15
- function buildMermaidConfig(theme: "dark" | "default") {
16
- return {
17
- startOnLoad: false,
18
- theme,
19
- securityLevel: "strict" as const,
20
- fontFamily: MERMAID_FONT,
21
- htmlLabels: false,
22
- useMaxWidth: false,
23
- class: {
24
- useMaxWidth: false,
25
- },
26
- flowchart: {
27
- useMaxWidth: false,
28
- },
29
- sequence: {
30
- useMaxWidth: false,
31
- },
32
- };
33
- }
34
-
35
- function normalizeMermaidSvg(svg: string): string {
36
- return svg.replace(/<svg\b([^>]*)>/, (_match, attrs: string) => {
37
- let next = attrs.replace(/\swidth="100%"/, "");
38
- next = next.replace(/\sstyle="([^"]*)"/, (_styleMatch, style: string) => {
39
- const cleaned = style
40
- .replace(/max-width:\s*[^;]+;?/gi, "")
41
- .replace(/;\s*;/g, ";")
42
- .trim();
43
- return cleaned ? ` style="${cleaned}"` : "";
44
- });
45
- return `<svg${next}>`;
46
- });
47
- }
48
-
49
- function MermaidSourceCode({ chart }: { chart: string }) {
50
- return (
51
- <HighlightedCodeBlock
52
- className="language-mermaid"
53
- code={chart.trim()}
54
- language="mermaid"
55
- />
56
- );
57
- }
58
-
59
- interface MermaidDiagramProps {
60
- chart: string;
61
- streaming?: boolean;
62
- className?: string;
63
- }
64
-
65
- export function MermaidDiagram({
66
- chart,
67
- streaming = false,
68
- className,
69
- }: MermaidDiagramProps) {
70
- const reactId = useId();
71
- const svgHostRef = useRef<HTMLDivElement>(null);
72
- const [svg, setSvg] = useState<string | null>(null);
73
- const [theme, setTheme] = useState(getMermaidTheme);
74
- const source = chart.trim();
75
-
76
- useEffect(() => {
77
- const root = document.documentElement;
78
- const observer = new MutationObserver(() => {
79
- setTheme(getMermaidTheme());
80
- });
81
- observer.observe(root, {
82
- attributes: true,
83
- attributeFilter: ["data-theme"],
84
- });
85
- return () => observer.disconnect();
86
- }, []);
87
-
88
- useEffect(() => {
89
- if (streaming || !source) {
90
- setSvg(null);
91
- return;
92
- }
93
-
94
- let cancelled = false;
95
- setSvg(null);
96
-
97
- const renderId = `mermaid-${reactId.replace(/:/g, "")}-${Math.random().toString(36).slice(2, 8)}`;
98
-
99
- async function render() {
100
- try {
101
- mermaid.initialize(buildMermaidConfig(theme));
102
- const { svg: rendered } = await mermaid.render(renderId, source);
103
- if (!cancelled) {
104
- setSvg(normalizeMermaidSvg(rendered));
105
- }
106
- } catch {
107
- if (!cancelled) {
108
- setSvg(null);
109
- }
110
- }
111
- }
112
-
113
- void render();
114
- return () => {
115
- cancelled = true;
116
- };
117
- }, [source, reactId, theme, streaming]);
118
-
119
- useEffect(() => {
120
- const host = svgHostRef.current;
121
- if (!host) return;
122
-
123
- if (svg) {
124
- host.innerHTML = svg;
125
- } else {
126
- host.textContent = "";
127
- }
128
-
129
- return () => {
130
- host.textContent = "";
131
- };
132
- }, [svg]);
133
-
134
- if (streaming || !svg) {
135
- return <MermaidSourceCode chart={chart} />;
136
- }
137
-
138
- return <div ref={svgHostRef} className={cn("markdown-mermaid", className)} />;
139
- }
@@ -1,243 +0,0 @@
1
- import {
2
- type KeyboardEvent,
3
- useEffect,
4
- useId,
5
- useMemo,
6
- useRef,
7
- useState,
8
- } from "react";
9
- import { useAssistant, useAssistantActions } from "../context.tsx";
10
-
11
- function filterModels(models: string[], query: string): string[] {
12
- const needle = query.trim().toLowerCase();
13
- if (!needle) return models;
14
- return models.filter((model) => model.toLowerCase().includes(needle));
15
- }
16
-
17
- interface ModelSelectorProps {
18
- disabled?: boolean;
19
- variant?: "header" | "footer";
20
- dropUp?: boolean;
21
- }
22
-
23
- export function ModelSelector({
24
- disabled,
25
- variant = "header",
26
- dropUp = false,
27
- }: ModelSelectorProps) {
28
- const llmModels = useAssistant((s) => s.llmModels);
29
- const llmModelsLoading = useAssistant((s) => s.llmModelsLoading);
30
- const selectedModel = useAssistant((s) => s.selectedModel);
31
- const { setSelectedModel } = useAssistantActions();
32
-
33
- const listboxId = useId();
34
- const rootRef = useRef<HTMLDivElement>(null);
35
- const inputRef = useRef<HTMLInputElement>(null);
36
- const skipBlurCommitRef = useRef(false);
37
-
38
- const [query, setQuery] = useState(selectedModel ?? "");
39
- const [open, setOpen] = useState(false);
40
- const [activeIndex, setActiveIndex] = useState(0);
41
-
42
- const suggestions = useMemo(() => {
43
- const filtered = filterModels(llmModels, query);
44
- const trimmed = query.trim();
45
- if (
46
- trimmed &&
47
- !llmModels.some((model) => model.toLowerCase() === trimmed.toLowerCase())
48
- ) {
49
- return [trimmed, ...filtered.filter((model) => model !== trimmed)];
50
- }
51
- return filtered;
52
- }, [llmModels, query]);
53
-
54
- useEffect(() => {
55
- setQuery(selectedModel ?? "");
56
- }, [selectedModel]);
57
-
58
- useEffect(() => {
59
- if (!open) return;
60
- const onPointerDown = (event: MouseEvent) => {
61
- if (!rootRef.current?.contains(event.target as Node)) {
62
- setOpen(false);
63
- }
64
- };
65
- document.addEventListener("pointerdown", onPointerDown);
66
- return () => document.removeEventListener("pointerdown", onPointerDown);
67
- }, [open]);
68
-
69
- function commitModel(value: string) {
70
- const trimmed = value.trim();
71
- if (!trimmed) {
72
- setQuery(selectedModel ?? "");
73
- return;
74
- }
75
- setSelectedModel(trimmed);
76
- setQuery(trimmed);
77
- setOpen(false);
78
- }
79
-
80
- function selectSuggestion(index: number) {
81
- const model = suggestions[index];
82
- if (!model) return;
83
- skipBlurCommitRef.current = true;
84
- commitModel(model);
85
- inputRef.current?.blur();
86
- }
87
-
88
- function resolveEnterSelection(): string | null {
89
- const trimmed = query.trim();
90
- if (!trimmed) return null;
91
-
92
- if (open && suggestions.length > 0) {
93
- return suggestions[activeIndex] ?? trimmed;
94
- }
95
-
96
- const exact = llmModels.find(
97
- (model) => model.toLowerCase() === trimmed.toLowerCase(),
98
- );
99
- if (exact) return exact;
100
-
101
- if (suggestions.length > 0) {
102
- return suggestions[0] ?? trimmed;
103
- }
104
-
105
- return trimmed;
106
- }
107
-
108
- function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
109
- if (event.key === "Enter") {
110
- event.preventDefault();
111
- const model = resolveEnterSelection();
112
- if (model) {
113
- skipBlurCommitRef.current = true;
114
- commitModel(model);
115
- inputRef.current?.blur();
116
- }
117
- return;
118
- }
119
-
120
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
121
- event.preventDefault();
122
- if (!open) {
123
- setOpen(true);
124
- return;
125
- }
126
- setActiveIndex((index) => {
127
- if (!suggestions.length) return 0;
128
- return event.key === "ArrowDown"
129
- ? (index + 1) % suggestions.length
130
- : (index - 1 + suggestions.length) % suggestions.length;
131
- });
132
- return;
133
- }
134
-
135
- if (!open) return;
136
-
137
- switch (event.key) {
138
- case "Escape":
139
- event.preventDefault();
140
- setQuery(selectedModel ?? "");
141
- setOpen(false);
142
- break;
143
- case "Tab":
144
- setOpen(false);
145
- break;
146
- }
147
- }
148
-
149
- const showList = open && suggestions.length > 0;
150
-
151
- return (
152
- <div
153
- ref={rootRef}
154
- className={`model-selector ${variant === "footer" ? "model-selector--footer" : ""}`}
155
- >
156
- <label className="model-selector__label">
157
- {variant === "header" ? (
158
- <span className="model-selector__label-text">Model</span>
159
- ) : null}
160
- <div className="model-selector__field">
161
- <input
162
- ref={inputRef}
163
- type="text"
164
- className="assistant-input model-selector__input"
165
- value={query}
166
- onChange={(event) => {
167
- setQuery(event.target.value);
168
- setActiveIndex(0);
169
- setOpen(true);
170
- }}
171
- onFocus={() => setOpen(true)}
172
- onBlur={() => {
173
- window.setTimeout(() => {
174
- if (skipBlurCommitRef.current) {
175
- skipBlurCommitRef.current = false;
176
- return;
177
- }
178
- if (!rootRef.current?.contains(document.activeElement)) {
179
- commitModel(query);
180
- }
181
- }, 0);
182
- }}
183
- onKeyDown={onKeyDown}
184
- disabled={disabled || llmModelsLoading}
185
- role="combobox"
186
- aria-expanded={showList}
187
- aria-controls={showList ? listboxId : undefined}
188
- aria-autocomplete="list"
189
- aria-activedescendant={
190
- showList ? `${listboxId}-option-${activeIndex}` : undefined
191
- }
192
- aria-label="LLM model"
193
- aria-busy={llmModelsLoading}
194
- placeholder={
195
- llmModelsLoading ? "Loading models…" : "Search models…"
196
- }
197
- autoComplete="off"
198
- spellCheck={false}
199
- />
200
- {showList && (
201
- <div
202
- id={listboxId}
203
- role="listbox"
204
- className={`model-selector__list ${dropUp ? "model-selector__list--drop-up" : ""}`}
205
- aria-label="Model suggestions"
206
- >
207
- {suggestions.map((model, index) => {
208
- const isCustom =
209
- model === query.trim() &&
210
- !llmModels.some(
211
- (entry) => entry.toLowerCase() === model.toLowerCase(),
212
- );
213
-
214
- return (
215
- <button
216
- key={isCustom ? `custom:${model}` : model}
217
- type="button"
218
- id={`${listboxId}-option-${index}`}
219
- role="option"
220
- aria-selected={index === activeIndex}
221
- className={`model-selector__option ${index === activeIndex ? "model-selector__option--active" : ""}`}
222
- onMouseDown={(event) => event.preventDefault()}
223
- onClick={() => selectSuggestion(index)}
224
- onMouseEnter={() => setActiveIndex(index)}
225
- >
226
- <span className="model-selector__option-label">
227
- {model}
228
- </span>
229
- {isCustom && (
230
- <span className="model-selector__option-hint">
231
- Custom
232
- </span>
233
- )}
234
- </button>
235
- );
236
- })}
237
- </div>
238
- )}
239
- </div>
240
- </label>
241
- </div>
242
- );
243
- }
@@ -1,79 +0,0 @@
1
- import {
2
- CircleAlert,
3
- Clock3,
4
- Gauge,
5
- KeyRound,
6
- RefreshCw,
7
- WifiOff,
8
- } from "lucide-react";
9
- import type { ComponentType } from "react";
10
- import {
11
- type AssistantErrorKind,
12
- parseAssistantError,
13
- } from "../../lib/parse-assistant-error.ts";
14
-
15
- const ERROR_ICONS: Record<
16
- AssistantErrorKind,
17
- ComponentType<{ size?: number; strokeWidth?: number }>
18
- > = {
19
- network: WifiOff,
20
- auth: KeyRound,
21
- timeout: Clock3,
22
- "rate-limit": Gauge,
23
- unknown: CircleAlert,
24
- };
25
-
26
- interface AssistantErrorCalloutProps {
27
- error: string;
28
- context?: "chat" | "suggestions";
29
- variant?: "embedded" | "panel";
30
- onRetry?: () => void;
31
- retryLabel?: string;
32
- retryLoading?: boolean;
33
- }
34
-
35
- export function AssistantErrorCallout({
36
- error,
37
- context = "chat",
38
- variant = "embedded",
39
- onRetry,
40
- retryLabel = "Try again",
41
- retryLoading = false,
42
- }: AssistantErrorCalloutProps) {
43
- const parsed = parseAssistantError(error, context);
44
- const Icon = ERROR_ICONS[parsed.kind];
45
-
46
- return (
47
- <div
48
- className={`assistant-error-callout assistant-error-callout--${variant}`}
49
- role="alert"
50
- >
51
- <span className="assistant-error-callout__badge" aria-hidden>
52
- <Icon size={12} strokeWidth={2.25} />
53
- </span>
54
- <div className="assistant-error-callout__body">
55
- <p className="assistant-error-callout__title">{parsed.title}</p>
56
- <p className="assistant-error-callout__detail">{parsed.detail}</p>
57
- {parsed.hint ? (
58
- <p className="assistant-error-callout__hint">{parsed.hint}</p>
59
- ) : null}
60
- </div>
61
- {onRetry ? (
62
- <button
63
- type="button"
64
- className="assistant-error-callout__chip"
65
- onClick={onRetry}
66
- disabled={retryLoading}
67
- >
68
- <RefreshCw
69
- size={12}
70
- strokeWidth={2.25}
71
- className={retryLoading ? "assistant-icon-spin" : undefined}
72
- aria-hidden
73
- />
74
- {retryLabel}
75
- </button>
76
- ) : null}
77
- </div>
78
- );
79
- }