@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,107 @@
|
|
|
1
|
+
import { Eye, Pencil, RotateCcw } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { MarkdownContent } from "../MarkdownContent.tsx";
|
|
4
|
+
import { MarkdownEditor } from "../MarkdownEditor.tsx";
|
|
5
|
+
|
|
6
|
+
type SystemPromptMode = "edit" | "preview";
|
|
7
|
+
|
|
8
|
+
interface SystemPromptFieldProps {
|
|
9
|
+
value: string;
|
|
10
|
+
defaultPrompt: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
onReset?: () => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SystemPromptField({
|
|
17
|
+
value,
|
|
18
|
+
defaultPrompt,
|
|
19
|
+
onChange,
|
|
20
|
+
onReset,
|
|
21
|
+
disabled = false,
|
|
22
|
+
}: SystemPromptFieldProps) {
|
|
23
|
+
const [mode, setMode] = useState<SystemPromptMode>("preview");
|
|
24
|
+
const usingDefault = !value.trim() || value.trim() === defaultPrompt.trim();
|
|
25
|
+
const previewContent = usingDefault ? defaultPrompt : value;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="assistant-system-prompt">
|
|
29
|
+
<div className="assistant-system-prompt__shell">
|
|
30
|
+
<div className="assistant-system-prompt__bar">
|
|
31
|
+
<div className="assistant-system-prompt__bar-start">
|
|
32
|
+
<span className="assistant-system-prompt__label">
|
|
33
|
+
System prompt
|
|
34
|
+
</span>
|
|
35
|
+
{usingDefault ? (
|
|
36
|
+
<span className="assistant-system-prompt__tag">Default</span>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
className="assistant-system-prompt__modes"
|
|
41
|
+
role="tablist"
|
|
42
|
+
aria-label="System prompt view"
|
|
43
|
+
>
|
|
44
|
+
{!usingDefault && onReset ? (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
className="assistant-system-prompt__mode"
|
|
48
|
+
onClick={onReset}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
aria-label="Reset system prompt"
|
|
51
|
+
title="Reset to default"
|
|
52
|
+
>
|
|
53
|
+
<RotateCcw size={12} aria-hidden />
|
|
54
|
+
</button>
|
|
55
|
+
) : null}
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className={`assistant-system-prompt__mode ${mode === "edit" ? "assistant-system-prompt__mode--active" : ""}`.trim()}
|
|
59
|
+
role="tab"
|
|
60
|
+
aria-selected={mode === "edit"}
|
|
61
|
+
aria-label="Edit"
|
|
62
|
+
title="Edit"
|
|
63
|
+
onClick={() => setMode("edit")}
|
|
64
|
+
>
|
|
65
|
+
<Pencil size={12} aria-hidden />
|
|
66
|
+
</button>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className={`assistant-system-prompt__mode ${mode === "preview" ? "assistant-system-prompt__mode--active" : ""}`.trim()}
|
|
70
|
+
role="tab"
|
|
71
|
+
aria-selected={mode === "preview"}
|
|
72
|
+
aria-label="Preview"
|
|
73
|
+
title="Preview"
|
|
74
|
+
onClick={() => setMode("preview")}
|
|
75
|
+
>
|
|
76
|
+
<Eye size={12} aria-hidden />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{mode === "edit" ? (
|
|
82
|
+
<div role="tabpanel">
|
|
83
|
+
<MarkdownEditor
|
|
84
|
+
className="assistant-system-prompt__editor"
|
|
85
|
+
value={value}
|
|
86
|
+
onChange={onChange}
|
|
87
|
+
placeholder="Optional — replaces app default"
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
aria-label="System prompt editor"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
<div
|
|
94
|
+
className="assistant-system-prompt__doc"
|
|
95
|
+
role="tabpanel"
|
|
96
|
+
aria-label="System prompt preview"
|
|
97
|
+
>
|
|
98
|
+
<MarkdownContent
|
|
99
|
+
content={previewContent}
|
|
100
|
+
className="assistant-system-prompt__md"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentPropsWithoutRef,
|
|
3
|
+
createElement,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useMemo,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { highlightCode } from "../../core/code-highlight.ts";
|
|
8
|
+
import { cn } from "../utils/cn.ts";
|
|
9
|
+
|
|
10
|
+
const HIGHLIGHT_HTML_PATTERN = /<span class="(hljs-[^"]+)">|<\/span>|([^<]+)/g;
|
|
11
|
+
|
|
12
|
+
type HighlightPart =
|
|
13
|
+
| { type: "open"; className: string }
|
|
14
|
+
| { type: "close" }
|
|
15
|
+
| { type: "text"; value: string };
|
|
16
|
+
|
|
17
|
+
function decodeHighlightEntities(text: string): string {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
|
+
.replace(/&/g, "&")
|
|
21
|
+
.replace(/</g, "<")
|
|
22
|
+
.replace(/>/g, ">");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tokenizeHighlightHtml(html: string): HighlightPart[] {
|
|
26
|
+
const parts: HighlightPart[] = [];
|
|
27
|
+
|
|
28
|
+
for (const match of html.matchAll(HIGHLIGHT_HTML_PATTERN)) {
|
|
29
|
+
if (match[1]) {
|
|
30
|
+
parts.push({ type: "open", className: match[1] });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (match[0] === "</span>") {
|
|
35
|
+
parts.push({ type: "close" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (match[2]) {
|
|
40
|
+
parts.push({ type: "text", value: decodeHighlightEntities(match[2]) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return parts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function highlightNodes(
|
|
48
|
+
code: string,
|
|
49
|
+
language: string | null,
|
|
50
|
+
): ReactNode[] {
|
|
51
|
+
const html = highlightCode(code, language);
|
|
52
|
+
const parts = tokenizeHighlightHtml(html);
|
|
53
|
+
const nodes: ReactNode[] = [];
|
|
54
|
+
const stack: { className: string; children: ReactNode[] }[] = [];
|
|
55
|
+
let key = 0;
|
|
56
|
+
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (part.type === "text") {
|
|
59
|
+
const target = stack.at(-1)?.children ?? nodes;
|
|
60
|
+
target.push(part.value);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (part.type === "open") {
|
|
65
|
+
stack.push({ className: part.className, children: [] });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const span = stack.pop();
|
|
70
|
+
if (!span) continue;
|
|
71
|
+
|
|
72
|
+
const element = createElement(
|
|
73
|
+
"span",
|
|
74
|
+
{ key: key++, className: span.className },
|
|
75
|
+
...span.children,
|
|
76
|
+
);
|
|
77
|
+
const target = stack.at(-1)?.children ?? nodes;
|
|
78
|
+
target.push(element);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return nodes;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function HighlightedCodeBlock({
|
|
85
|
+
className,
|
|
86
|
+
code,
|
|
87
|
+
language = "json",
|
|
88
|
+
...props
|
|
89
|
+
}: {
|
|
90
|
+
className?: string;
|
|
91
|
+
code: string;
|
|
92
|
+
language?: string | null;
|
|
93
|
+
} & ComponentPropsWithoutRef<"code">) {
|
|
94
|
+
const children = useMemo(
|
|
95
|
+
() => highlightNodes(code, language),
|
|
96
|
+
[code, language],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<code
|
|
101
|
+
className={cn("hljs font-mono text-[11px] leading-snug", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</code>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { createAssistantStore } from "../core/create-assistant-store.ts";
|
|
10
|
+
import type { AssistantContextValue, AssistantProviderProps } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
const AssistantContext = createContext<AssistantContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
export function AssistantProvider({
|
|
15
|
+
config,
|
|
16
|
+
children,
|
|
17
|
+
}: AssistantProviderProps) {
|
|
18
|
+
const storeRef = useRef<ReturnType<typeof createAssistantStore> | null>(null);
|
|
19
|
+
if (!storeRef.current) {
|
|
20
|
+
storeRef.current = createAssistantStore(config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const value = useMemo(
|
|
24
|
+
(): AssistantContextValue => ({
|
|
25
|
+
store: storeRef.current as ReturnType<typeof createAssistantStore>,
|
|
26
|
+
config,
|
|
27
|
+
}),
|
|
28
|
+
[config],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<AssistantContext.Provider value={value}>
|
|
33
|
+
{children}
|
|
34
|
+
</AssistantContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useAssistantContext(): AssistantContextValue {
|
|
39
|
+
const context = useContext(AssistantContext);
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"useAssistantContext must be used within AssistantProvider",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useAssistant<T>(
|
|
49
|
+
selector: (
|
|
50
|
+
state: ReturnType<ReturnType<typeof createAssistantStore>["getState"]>,
|
|
51
|
+
) => T,
|
|
52
|
+
): T {
|
|
53
|
+
const { store } = useAssistantContext();
|
|
54
|
+
return store(selector);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useAssistantActions() {
|
|
58
|
+
const { store } = useAssistantContext();
|
|
59
|
+
return store.getState();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function AssistantBootstrap({ children }: { children: ReactNode }) {
|
|
63
|
+
const { store, config } = useAssistantContext();
|
|
64
|
+
const autoLoad = config.autoLoadLlmStatus !== false;
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!autoLoad) return;
|
|
68
|
+
void store.getState().loadLlmStatus();
|
|
69
|
+
}, [autoLoad, store]);
|
|
70
|
+
|
|
71
|
+
return children;
|
|
72
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type KeyboardEvent, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
type ChatCommandSuggestion,
|
|
4
|
+
type ChatCommandSurface,
|
|
5
|
+
completionForChatCommand,
|
|
6
|
+
filterChatCommands,
|
|
7
|
+
parseChatCommand,
|
|
8
|
+
shouldShowChatCommandMenu,
|
|
9
|
+
} from "../../core/chat-commands.ts";
|
|
10
|
+
|
|
11
|
+
type UseComposerCommandsOptions = {
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
surface: ChatCommandSurface;
|
|
15
|
+
onExecute: (commandText: string) => void | Promise<void>;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function useComposerCommands({
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
surface,
|
|
23
|
+
onExecute,
|
|
24
|
+
disabled = false,
|
|
25
|
+
}: UseComposerCommandsOptions) {
|
|
26
|
+
const suggestions = useMemo(
|
|
27
|
+
() => filterChatCommands(value, surface),
|
|
28
|
+
[value, surface],
|
|
29
|
+
);
|
|
30
|
+
const menuOpen = !disabled && shouldShowChatCommandMenu(value, surface);
|
|
31
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
32
|
+
|
|
33
|
+
const handleInputChange = (next: string) => {
|
|
34
|
+
setSelectedIndex(0);
|
|
35
|
+
onChange(next);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (selectedIndex >= suggestions.length) {
|
|
40
|
+
setSelectedIndex(0);
|
|
41
|
+
}
|
|
42
|
+
}, [selectedIndex, suggestions.length]);
|
|
43
|
+
|
|
44
|
+
function completeCommand(command: ChatCommandSuggestion) {
|
|
45
|
+
onChange(completionForChatCommand(command));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function tryExecuteCommand(): Promise<boolean> {
|
|
49
|
+
const parsed = parseChatCommand(value);
|
|
50
|
+
if (!parsed) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const command = suggestions.find((entry) => entry.name === parsed.name);
|
|
55
|
+
if (!command) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onChange("");
|
|
60
|
+
await onExecute(command.usage);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): boolean {
|
|
65
|
+
if (menuOpen && suggestions.length > 0) {
|
|
66
|
+
if (event.key === "ArrowDown") {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
setSelectedIndex((current) => (current + 1) % suggestions.length);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (event.key === "ArrowUp") {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
setSelectedIndex(
|
|
75
|
+
(current) => (current - 1 + suggestions.length) % suggestions.length,
|
|
76
|
+
);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (event.key === "Tab") {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
const command = suggestions[selectedIndex] ?? suggestions[0];
|
|
83
|
+
if (command) {
|
|
84
|
+
completeCommand(command);
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
const command = suggestions[selectedIndex] ?? suggestions[0];
|
|
92
|
+
if (!command) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const parsed = parseChatCommand(value);
|
|
97
|
+
if (parsed && suggestions.some((entry) => entry.name === parsed.name)) {
|
|
98
|
+
void tryExecuteCommand();
|
|
99
|
+
} else {
|
|
100
|
+
onChange("");
|
|
101
|
+
void onExecute(command.usage);
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
108
|
+
const parsed = parseChatCommand(value);
|
|
109
|
+
if (parsed) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
void tryExecuteCommand();
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
suggestions,
|
|
121
|
+
menuOpen,
|
|
122
|
+
selectedIndex,
|
|
123
|
+
setSelectedIndex,
|
|
124
|
+
handleInputChange,
|
|
125
|
+
completeCommand,
|
|
126
|
+
tryExecuteCommand,
|
|
127
|
+
handleKeyDown,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type { AssistantToolDefinition } from "../../core/types.ts";
|
|
3
|
+
import { resolvePromptIcon } from "../lib/prompt-icons.ts";
|
|
4
|
+
import type { AssistantSuggestedPromptWithIcon } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
function withIcons(
|
|
7
|
+
prompts: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
}>,
|
|
14
|
+
): AssistantSuggestedPromptWithIcon[] {
|
|
15
|
+
return prompts.map((entry) => ({
|
|
16
|
+
id: entry.id,
|
|
17
|
+
label: entry.label,
|
|
18
|
+
hint: entry.description,
|
|
19
|
+
prompt: entry.prompt,
|
|
20
|
+
icon: resolvePromptIcon(entry.icon),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useSuggestedPrompts(input: {
|
|
25
|
+
staticPrompts?: AssistantSuggestedPromptWithIcon[];
|
|
26
|
+
llmEnabled: boolean;
|
|
27
|
+
model: string | null;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
dynamicSuggestedPrompts?: boolean;
|
|
30
|
+
listTools: () => Promise<AssistantToolDefinition[]>;
|
|
31
|
+
fetchSuggestedPrompts?: (ctx: {
|
|
32
|
+
llmEnabled: boolean;
|
|
33
|
+
model: string | null;
|
|
34
|
+
tools: AssistantToolDefinition[];
|
|
35
|
+
}) => Promise<
|
|
36
|
+
Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
prompt: string;
|
|
41
|
+
icon?: string;
|
|
42
|
+
}>
|
|
43
|
+
>;
|
|
44
|
+
}) {
|
|
45
|
+
const {
|
|
46
|
+
staticPrompts,
|
|
47
|
+
llmEnabled,
|
|
48
|
+
model,
|
|
49
|
+
enabled,
|
|
50
|
+
dynamicSuggestedPrompts,
|
|
51
|
+
listTools,
|
|
52
|
+
fetchSuggestedPrompts,
|
|
53
|
+
} = input;
|
|
54
|
+
|
|
55
|
+
const useDynamic =
|
|
56
|
+
dynamicSuggestedPrompts !== false &&
|
|
57
|
+
llmEnabled &&
|
|
58
|
+
enabled &&
|
|
59
|
+
Boolean(fetchSuggestedPrompts);
|
|
60
|
+
|
|
61
|
+
const [prompts, setPrompts] = useState<AssistantSuggestedPromptWithIcon[]>(
|
|
62
|
+
useDynamic ? [] : (staticPrompts ?? []),
|
|
63
|
+
);
|
|
64
|
+
const [loading, setLoading] = useState(false);
|
|
65
|
+
const [error, setError] = useState<string | null>(null);
|
|
66
|
+
const [hasFetched, setHasFetched] = useState(false);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (useDynamic) {
|
|
70
|
+
setPrompts([]);
|
|
71
|
+
setHasFetched(false);
|
|
72
|
+
setError(null);
|
|
73
|
+
setLoading(false);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setPrompts(staticPrompts ?? []);
|
|
78
|
+
setHasFetched(false);
|
|
79
|
+
setError(null);
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}, [useDynamic, staticPrompts]);
|
|
82
|
+
|
|
83
|
+
const refresh = useCallback(async () => {
|
|
84
|
+
if (!useDynamic || !fetchSuggestedPrompts) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const tools = await listTools();
|
|
93
|
+
const fetched = await fetchSuggestedPrompts({
|
|
94
|
+
llmEnabled,
|
|
95
|
+
model,
|
|
96
|
+
tools,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (fetched.length > 0) {
|
|
100
|
+
setPrompts(withIcons(fetched));
|
|
101
|
+
} else if (staticPrompts?.length) {
|
|
102
|
+
setPrompts(staticPrompts);
|
|
103
|
+
} else {
|
|
104
|
+
setPrompts([]);
|
|
105
|
+
}
|
|
106
|
+
setHasFetched(true);
|
|
107
|
+
} catch (loadError) {
|
|
108
|
+
setError(
|
|
109
|
+
loadError instanceof Error
|
|
110
|
+
? loadError.message
|
|
111
|
+
: "Failed to load suggestions",
|
|
112
|
+
);
|
|
113
|
+
setPrompts(staticPrompts ?? []);
|
|
114
|
+
setHasFetched(true);
|
|
115
|
+
} finally {
|
|
116
|
+
setLoading(false);
|
|
117
|
+
}
|
|
118
|
+
}, [
|
|
119
|
+
useDynamic,
|
|
120
|
+
fetchSuggestedPrompts,
|
|
121
|
+
listTools,
|
|
122
|
+
llmEnabled,
|
|
123
|
+
model,
|
|
124
|
+
staticPrompts,
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
return { prompts, loading, error, useDynamic, refresh, hasFetched };
|
|
128
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
AssistantStoreDependencies,
|
|
4
|
+
AssistantToolDefinition,
|
|
5
|
+
AssistantToolResult,
|
|
6
|
+
} from "../core/types.ts";
|
|
7
|
+
export {
|
|
8
|
+
Assistant,
|
|
9
|
+
AssistantRoot,
|
|
10
|
+
} from "./Assistant.tsx";
|
|
11
|
+
export { ChatActivity } from "./components/chat/ChatActivity.tsx";
|
|
12
|
+
export { ChatComposer } from "./components/chat/ChatComposer.tsx";
|
|
13
|
+
export { ChatEmptyState } from "./components/chat/ChatEmptyState.tsx";
|
|
14
|
+
export { ChatInteractivePrompt } from "./components/chat/ChatInteractivePrompt/index.tsx";
|
|
15
|
+
export { ChatMessageView } from "./components/chat/ChatMessage.tsx";
|
|
16
|
+
export { ChatMessageScroll } from "./components/chat/ChatMessageScroll.tsx";
|
|
17
|
+
export { ChatReplySuggestions } from "./components/chat/ChatReplySuggestions.tsx";
|
|
18
|
+
export { LlmSettingsStrip } from "./components/chat/LlmSettingsStrip.tsx";
|
|
19
|
+
export { MarkdownContent } from "./components/MarkdownContent.tsx";
|
|
20
|
+
export { MermaidDiagram } from "./components/MermaidDiagram.tsx";
|
|
21
|
+
export { ModelSelector } from "./components/ModelSelector.tsx";
|
|
22
|
+
export {
|
|
23
|
+
AssistantBootstrap,
|
|
24
|
+
AssistantProvider,
|
|
25
|
+
useAssistant,
|
|
26
|
+
useAssistantActions,
|
|
27
|
+
useAssistantContext,
|
|
28
|
+
} from "./context.tsx";
|
|
29
|
+
export { useComposerCommands } from "./hooks/use-composer-commands.ts";
|
|
30
|
+
export type {
|
|
31
|
+
AssistantConfig,
|
|
32
|
+
AssistantContextValue,
|
|
33
|
+
AssistantEmptyStateConfig,
|
|
34
|
+
AssistantHeaderConfig,
|
|
35
|
+
AssistantProps,
|
|
36
|
+
AssistantProviderProps,
|
|
37
|
+
AssistantSuggestedPromptWithIcon,
|
|
38
|
+
AssistantUiConfig,
|
|
39
|
+
} from "./types.ts";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type AssistantErrorKind =
|
|
2
|
+
| "network"
|
|
3
|
+
| "auth"
|
|
4
|
+
| "timeout"
|
|
5
|
+
| "rate-limit"
|
|
6
|
+
| "unknown";
|
|
7
|
+
|
|
8
|
+
export type ParsedAssistantError = {
|
|
9
|
+
title: string;
|
|
10
|
+
detail: string;
|
|
11
|
+
hint?: string;
|
|
12
|
+
kind: AssistantErrorKind;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function stripKnownPrefix(message: string): string {
|
|
16
|
+
const chatPrefix = "Chat failed: ";
|
|
17
|
+
if (message.startsWith(chatPrefix)) {
|
|
18
|
+
return message.slice(chatPrefix.length).trim();
|
|
19
|
+
}
|
|
20
|
+
return message.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseAssistantError(
|
|
24
|
+
raw: string,
|
|
25
|
+
context: "chat" | "suggestions" = "chat",
|
|
26
|
+
): ParsedAssistantError {
|
|
27
|
+
const detail = stripKnownPrefix(raw);
|
|
28
|
+
const lower = detail.toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
lower.includes("failed to fetch") ||
|
|
32
|
+
lower.includes("networkerror") ||
|
|
33
|
+
lower.includes("network error") ||
|
|
34
|
+
lower.includes("load failed") ||
|
|
35
|
+
lower.includes("connection refused") ||
|
|
36
|
+
lower.includes("econnrefused")
|
|
37
|
+
) {
|
|
38
|
+
return {
|
|
39
|
+
title: "Connection lost",
|
|
40
|
+
detail,
|
|
41
|
+
hint:
|
|
42
|
+
context === "chat"
|
|
43
|
+
? "Check your network or LLM endpoint, then try again."
|
|
44
|
+
: "Check your connection and LLM settings.",
|
|
45
|
+
kind: "network",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
lower.includes("unauthorized") ||
|
|
51
|
+
lower.includes("401") ||
|
|
52
|
+
lower.includes("invalid api key") ||
|
|
53
|
+
lower.includes("authentication") ||
|
|
54
|
+
lower.includes("permission denied")
|
|
55
|
+
) {
|
|
56
|
+
return {
|
|
57
|
+
title: "Authentication failed",
|
|
58
|
+
detail,
|
|
59
|
+
hint: "Verify your API key or credentials in LLM settings.",
|
|
60
|
+
kind: "auth",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (lower.includes("timeout") || lower.includes("timed out")) {
|
|
65
|
+
return {
|
|
66
|
+
title: "Request timed out",
|
|
67
|
+
detail,
|
|
68
|
+
hint: "The model took too long to respond. Try again in a moment.",
|
|
69
|
+
kind: "timeout",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
lower.includes("rate limit") ||
|
|
75
|
+
lower.includes("429") ||
|
|
76
|
+
lower.includes("too many requests")
|
|
77
|
+
) {
|
|
78
|
+
return {
|
|
79
|
+
title: "Rate limit reached",
|
|
80
|
+
detail,
|
|
81
|
+
hint: "Wait a few seconds before sending another request.",
|
|
82
|
+
kind: "rate-limit",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
title:
|
|
88
|
+
context === "chat" ? "Something went wrong" : "Couldn't load suggestions",
|
|
89
|
+
detail,
|
|
90
|
+
hint:
|
|
91
|
+
context === "chat"
|
|
92
|
+
? "Try again, or review your LLM settings."
|
|
93
|
+
: "Try regenerating, or check your LLM configuration.",
|
|
94
|
+
kind: "unknown",
|
|
95
|
+
};
|
|
96
|
+
}
|