@silicajs/assistant 0.1.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 +52 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +48 -0
- package/dist/next.js +219 -0
- package/dist/next.js.map +1 -0
- package/dist/server/handler.d.ts +39 -0
- package/dist/server/handler.js +239 -0
- package/dist/server/handler.js.map +1 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.js +43 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/prompt.d.ts +5 -0
- package/dist/server/prompt.js +48 -0
- package/dist/server/prompt.js.map +1 -0
- package/dist/server/provider.d.ts +7 -0
- package/dist/server/provider.js +51 -0
- package/dist/server/provider.js.map +1 -0
- package/dist/server/runtime.d.ts +26 -0
- package/dist/server/runtime.js +111 -0
- package/dist/server/runtime.js.map +1 -0
- package/dist/server/sources.d.ts +29 -0
- package/dist/server/sources.js +73 -0
- package/dist/server/sources.js.map +1 -0
- package/dist/server/tools.d.ts +14 -0
- package/dist/server/tools.js +45 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/server/wikilinks.d.ts +28 -0
- package/dist/server/wikilinks.js +149 -0
- package/dist/server/wikilinks.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/message.d.ts +11 -0
- package/dist/ui/message.js +114 -0
- package/dist/ui/message.js.map +1 -0
- package/dist/ui/panel.d.ts +13 -0
- package/dist/ui/panel.js +201 -0
- package/dist/ui/panel.js.map +1 -0
- package/dist/ui/provider.d.ts +46 -0
- package/dist/ui/provider.js +292 -0
- package/dist/ui/provider.js.map +1 -0
- package/dist/ui/trigger.d.ts +10 -0
- package/dist/ui/trigger.js +91 -0
- package/dist/ui/trigger.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ui/panel.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@silicajs/ui/components/button\";\nimport { Textarea } from \"@silicajs/ui/components/textarea\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@silicajs/ui/components/tooltip\";\nimport { cn } from \"@silicajs/ui/lib/utils\";\nimport {\n ArrowUpIcon,\n SparklesIcon,\n SquareIcon,\n SquarePenIcon,\n XIcon,\n} from \"lucide-react\";\nimport { AssistantMessage } from \"./message.js\";\nimport { useAssistant } from \"./provider.js\";\n\nexport type AssistantPanelProps = {\n className?: string;\n};\n\n/**\n * The assistant chat window: header, conversation, and composer. Fills its\n * container; themes decide where and how to present it (docked sidebar,\n * overlay, …) and typically mount it only while the assistant is open.\n */\nexport function AssistantPanel({ className }: AssistantPanelProps) {\n const assistant = useAssistant();\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const open = assistant?.open ?? false;\n\n const messageCount = assistant?.messages.length ?? 0;\n const lastMessage = assistant?.messages.at(-1);\n React.useEffect(() => {\n const node = scrollRef.current;\n if (node) node.scrollTop = node.scrollHeight;\n }, [\n messageCount,\n lastMessage?.content,\n lastMessage?.state,\n lastMessage?.commands.length,\n ]);\n\n if (!assistant) return null;\n\n return (\n <aside\n aria-label=\"AI assistant\"\n className={cn(\n \"flex h-full min-h-0 w-full min-w-0 flex-col bg-background\",\n className,\n )}\n >\n <header className=\"flex h-12 shrink-0 items-center gap-2 px-3\">\n <SparklesIcon className=\"size-4 text-primary\" />\n <span className=\"flex-1 text-sm font-semibold tracking-tight\">\n Assistant\n </span>\n {assistant.messages.length > 0 ? (\n <Tooltip>\n <TooltipTrigger\n render={\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={assistant.reset}\n >\n <SquarePenIcon />\n <span className=\"sr-only\">New conversation</span>\n </Button>\n }\n />\n <TooltipContent side=\"bottom\">New conversation</TooltipContent>\n </Tooltip>\n ) : null}\n <Tooltip>\n <TooltipTrigger\n render={\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => assistant.setOpen(false)}\n >\n <XIcon />\n <span className=\"sr-only\">Close assistant</span>\n </Button>\n }\n />\n <TooltipContent side=\"bottom\">Close assistant</TooltipContent>\n </Tooltip>\n </header>\n\n <div\n ref={scrollRef}\n className=\"min-w-0 flex-1 overflow-x-hidden overflow-y-auto\"\n >\n {assistant.messages.length === 0 ? (\n <div className=\"flex h-full flex-col items-center justify-center gap-2 px-8 text-center\">\n <SparklesIcon className=\"size-6 text-muted-foreground/60\" />\n <p className=\"text-sm font-medium text-foreground\">Ask anything</p>\n <p className=\"text-xs leading-relaxed text-muted-foreground\">\n Answers come from this site's pages and include links to the\n sources used.\n </p>\n </div>\n ) : (\n <div className=\"flex min-w-0 flex-col gap-5 px-4 py-4\">\n {assistant.messages.map((message) => (\n <AssistantMessage\n key={message.id}\n message={message}\n onRetry={assistant.retry}\n />\n ))}\n </div>\n )}\n </div>\n\n <AssistantComposer\n isStreaming={assistant.isStreaming}\n onAsk={assistant.ask}\n onStop={assistant.stop}\n focusToken={open}\n />\n </aside>\n );\n}\n\nfunction AssistantComposer({\n isStreaming,\n onAsk,\n onStop,\n focusToken,\n}: {\n isStreaming: boolean;\n onAsk: (question: string) => void;\n onStop: () => void;\n focusToken: boolean;\n}) {\n const [draft, setDraft] = React.useState(\"\");\n const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n\n React.useEffect(() => {\n if (focusToken) textareaRef.current?.focus();\n }, [focusToken]);\n\n const submit = () => {\n if (isStreaming || !draft.trim()) return;\n onAsk(draft);\n setDraft(\"\");\n };\n\n return (\n <form\n className=\"shrink-0 px-3 pb-3\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n <div className=\"relative\">\n <Textarea\n ref={textareaRef}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n submit();\n }\n }}\n placeholder=\"Ask a question…\"\n rows={2}\n className=\"max-h-40 min-h-14 resize-none pr-11\"\n />\n {isStreaming ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n className=\"absolute bottom-1.5 right-1.5\"\n onClick={onStop}\n title=\"Stop answering\"\n >\n <SquareIcon />\n <span className=\"sr-only\">Stop answering</span>\n </Button>\n ) : (\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n className=\"absolute bottom-1.5 right-1.5\"\n disabled={!draft.trim()}\n title=\"Send question\"\n >\n <ArrowUpIcon />\n <span className=\"sr-only\">Send question</span>\n </Button>\n )}\n </div>\n </form>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { AssistantCitation, AssistantTranscriptMessage, AssistantStreamEvent } from '../types.js';
|
|
3
|
+
|
|
4
|
+
type AssistantChatMessage = {
|
|
5
|
+
id: string;
|
|
6
|
+
previousMessageId: string | null;
|
|
7
|
+
role: "user" | "assistant";
|
|
8
|
+
content: string;
|
|
9
|
+
signature?: string;
|
|
10
|
+
citations: AssistantCitation[];
|
|
11
|
+
state: "streaming" | "complete" | "error";
|
|
12
|
+
/** Shell commands the assistant ran while searching site pages, in order. */
|
|
13
|
+
commands: string[];
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
type AssistantContextValue = {
|
|
17
|
+
open: boolean;
|
|
18
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
19
|
+
messages: AssistantChatMessage[];
|
|
20
|
+
isStreaming: boolean;
|
|
21
|
+
ask: (question: string) => void;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
retry: () => void;
|
|
24
|
+
reset: () => void;
|
|
25
|
+
};
|
|
26
|
+
declare function useAssistant(): AssistantContextValue | null;
|
|
27
|
+
type AssistantProviderProps = {
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
endpoint?: string;
|
|
30
|
+
};
|
|
31
|
+
declare function buildReplayableHistory(history: AssistantChatMessage[]): {
|
|
32
|
+
messages: AssistantChatMessage[];
|
|
33
|
+
transcript: AssistantTranscriptMessage[];
|
|
34
|
+
};
|
|
35
|
+
declare function AssistantProvider({ children, endpoint, }: AssistantProviderProps): React.JSX.Element;
|
|
36
|
+
declare function streamAnswer(options: {
|
|
37
|
+
endpoint: string;
|
|
38
|
+
transcript: AssistantTranscriptMessage[];
|
|
39
|
+
responseMessageId: string;
|
|
40
|
+
currentSourcePath?: string;
|
|
41
|
+
currentSlug?: string;
|
|
42
|
+
signal: AbortSignal;
|
|
43
|
+
onEvent: (event: AssistantStreamEvent) => void;
|
|
44
|
+
}): Promise<"done" | "aborted">;
|
|
45
|
+
|
|
46
|
+
export { type AssistantChatMessage, type AssistantContextValue, AssistantProvider, type AssistantProviderProps, buildReplayableHistory, streamAnswer, useAssistant };
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { SilicaAssistantProvider } from "@silicajs/components";
|
|
5
|
+
import { useSilicaRouting } from "@silicajs/components/routing";
|
|
6
|
+
const AssistantContext = React.createContext(
|
|
7
|
+
null
|
|
8
|
+
);
|
|
9
|
+
const INVALID_STREAM_MESSAGE = "The assistant returned an invalid stream response.";
|
|
10
|
+
function useAssistant() {
|
|
11
|
+
return React.useContext(AssistantContext);
|
|
12
|
+
}
|
|
13
|
+
function createMessageId() {
|
|
14
|
+
return globalThis.crypto.randomUUID();
|
|
15
|
+
}
|
|
16
|
+
function buildReplayableHistory(history) {
|
|
17
|
+
const messages = [];
|
|
18
|
+
const transcript = [];
|
|
19
|
+
for (let index = 0; index < history.length; index += 2) {
|
|
20
|
+
const user = history[index];
|
|
21
|
+
const assistant = history[index + 1];
|
|
22
|
+
const previousMessageId = transcript.at(-1)?.id ?? null;
|
|
23
|
+
if (!user || user.role !== "user" || user.state !== "complete" || !user.content || user.previousMessageId !== previousMessageId) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
if (!assistant || assistant.role !== "assistant" || assistant.state !== "complete" || !assistant.content || !assistant.signature || assistant.previousMessageId !== user.id) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
messages.push(user, assistant);
|
|
30
|
+
transcript.push(
|
|
31
|
+
{
|
|
32
|
+
id: user.id,
|
|
33
|
+
previousMessageId: user.previousMessageId,
|
|
34
|
+
role: "user",
|
|
35
|
+
content: user.content
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: assistant.id,
|
|
39
|
+
previousMessageId: assistant.previousMessageId,
|
|
40
|
+
role: "assistant",
|
|
41
|
+
content: assistant.content,
|
|
42
|
+
signature: assistant.signature
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return { messages, transcript };
|
|
47
|
+
}
|
|
48
|
+
function AssistantProvider({
|
|
49
|
+
children,
|
|
50
|
+
endpoint = "/api/assistant"
|
|
51
|
+
}) {
|
|
52
|
+
const { currentSlug } = useSilicaRouting();
|
|
53
|
+
const [open, setOpen] = React.useState(false);
|
|
54
|
+
const [messages, setMessages] = React.useState([]);
|
|
55
|
+
const [isStreaming, setIsStreaming] = React.useState(false);
|
|
56
|
+
const messagesRef = React.useRef(messages);
|
|
57
|
+
messagesRef.current = messages;
|
|
58
|
+
const controllerRef = React.useRef(null);
|
|
59
|
+
const updateMessage = React.useCallback(
|
|
60
|
+
(id, update) => {
|
|
61
|
+
setMessages(
|
|
62
|
+
(current) => current.map(
|
|
63
|
+
(message) => message.id === id ? update(message) : message
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
[]
|
|
68
|
+
);
|
|
69
|
+
const sendQuestion = React.useCallback(
|
|
70
|
+
(question, history) => {
|
|
71
|
+
const trimmed = question.trim();
|
|
72
|
+
if (!trimmed) return;
|
|
73
|
+
controllerRef.current?.abort();
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
controllerRef.current = controller;
|
|
76
|
+
const replayableHistory = buildReplayableHistory(history);
|
|
77
|
+
const userId = createMessageId();
|
|
78
|
+
const previousMessageId = replayableHistory.transcript.at(-1)?.id ?? null;
|
|
79
|
+
const transcript = [
|
|
80
|
+
...replayableHistory.transcript,
|
|
81
|
+
{
|
|
82
|
+
id: userId,
|
|
83
|
+
previousMessageId,
|
|
84
|
+
role: "user",
|
|
85
|
+
content: trimmed
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
const answerId = createMessageId();
|
|
89
|
+
setMessages([
|
|
90
|
+
...replayableHistory.messages,
|
|
91
|
+
{
|
|
92
|
+
id: userId,
|
|
93
|
+
previousMessageId,
|
|
94
|
+
role: "user",
|
|
95
|
+
content: trimmed,
|
|
96
|
+
citations: [],
|
|
97
|
+
state: "complete",
|
|
98
|
+
commands: []
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: answerId,
|
|
102
|
+
previousMessageId: userId,
|
|
103
|
+
role: "assistant",
|
|
104
|
+
content: "",
|
|
105
|
+
citations: [],
|
|
106
|
+
state: "streaming",
|
|
107
|
+
commands: []
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
setIsStreaming(true);
|
|
111
|
+
void streamAnswer({
|
|
112
|
+
endpoint,
|
|
113
|
+
transcript,
|
|
114
|
+
responseMessageId: answerId,
|
|
115
|
+
currentSlug,
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
onEvent: (event) => {
|
|
118
|
+
if (event.type === "text-delta") {
|
|
119
|
+
updateMessage(answerId, (message) => ({
|
|
120
|
+
...message,
|
|
121
|
+
content: message.content + event.text
|
|
122
|
+
}));
|
|
123
|
+
} else if (event.type === "tool-status") {
|
|
124
|
+
updateMessage(answerId, (message) => ({
|
|
125
|
+
...message,
|
|
126
|
+
commands: [...message.commands, event.command]
|
|
127
|
+
}));
|
|
128
|
+
} else if (event.type === "citations") {
|
|
129
|
+
updateMessage(answerId, (message) => ({
|
|
130
|
+
...message,
|
|
131
|
+
citations: event.citations
|
|
132
|
+
}));
|
|
133
|
+
} else if (event.type === "message-signature") {
|
|
134
|
+
updateMessage(answerId, (message) => ({
|
|
135
|
+
...message,
|
|
136
|
+
id: event.id,
|
|
137
|
+
previousMessageId: event.previousMessageId,
|
|
138
|
+
signature: event.signature
|
|
139
|
+
}));
|
|
140
|
+
} else if (event.type === "error") {
|
|
141
|
+
updateMessage(answerId, (message) => ({
|
|
142
|
+
...message,
|
|
143
|
+
state: "error",
|
|
144
|
+
error: event.message
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}).then((outcome) => {
|
|
149
|
+
updateMessage(answerId, (message) => {
|
|
150
|
+
if (message.state === "error") return message;
|
|
151
|
+
if (outcome === "aborted") {
|
|
152
|
+
return message.content && message.signature ? { ...message, state: "complete" } : {
|
|
153
|
+
...message,
|
|
154
|
+
state: "error",
|
|
155
|
+
error: "Answer stopped."
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return message.signature ? { ...message, state: "complete" } : {
|
|
159
|
+
...message,
|
|
160
|
+
state: "error",
|
|
161
|
+
error: "The assistant reply could not be verified."
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}).catch((error) => {
|
|
165
|
+
updateMessage(answerId, (message) => ({
|
|
166
|
+
...message,
|
|
167
|
+
state: "error",
|
|
168
|
+
error: error instanceof AssistantRequestError ? error.message : "The assistant is unavailable right now."
|
|
169
|
+
}));
|
|
170
|
+
}).finally(() => {
|
|
171
|
+
if (controllerRef.current === controller) {
|
|
172
|
+
controllerRef.current = null;
|
|
173
|
+
setIsStreaming(false);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
[currentSlug, endpoint, updateMessage]
|
|
178
|
+
);
|
|
179
|
+
const ask = React.useCallback(
|
|
180
|
+
(question) => {
|
|
181
|
+
sendQuestion(question, messagesRef.current);
|
|
182
|
+
},
|
|
183
|
+
[sendQuestion]
|
|
184
|
+
);
|
|
185
|
+
const stop = React.useCallback(() => {
|
|
186
|
+
controllerRef.current?.abort();
|
|
187
|
+
}, []);
|
|
188
|
+
const retry = React.useCallback(() => {
|
|
189
|
+
if (controllerRef.current) return;
|
|
190
|
+
const current = messagesRef.current;
|
|
191
|
+
for (let index = current.length - 1; index >= 0; index -= 1) {
|
|
192
|
+
const message = current[index];
|
|
193
|
+
if (message?.role === "user") {
|
|
194
|
+
sendQuestion(message.content, current.slice(0, index));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}, [sendQuestion]);
|
|
199
|
+
const reset = React.useCallback(() => {
|
|
200
|
+
controllerRef.current?.abort();
|
|
201
|
+
setMessages([]);
|
|
202
|
+
}, []);
|
|
203
|
+
const value = React.useMemo(
|
|
204
|
+
() => ({ open, setOpen, messages, isStreaming, ask, stop, retry, reset }),
|
|
205
|
+
[open, messages, isStreaming, ask, stop, retry, reset]
|
|
206
|
+
);
|
|
207
|
+
const bridge = React.useMemo(
|
|
208
|
+
() => ({
|
|
209
|
+
open,
|
|
210
|
+
openAssistant: (query) => {
|
|
211
|
+
setOpen(true);
|
|
212
|
+
if (query?.trim()) ask(query);
|
|
213
|
+
},
|
|
214
|
+
closeAssistant: () => setOpen(false)
|
|
215
|
+
}),
|
|
216
|
+
[open, ask]
|
|
217
|
+
);
|
|
218
|
+
return /* @__PURE__ */ jsx(AssistantContext.Provider, { value, children: /* @__PURE__ */ jsx(SilicaAssistantProvider, { value: bridge, children }) });
|
|
219
|
+
}
|
|
220
|
+
class AssistantRequestError extends Error {
|
|
221
|
+
}
|
|
222
|
+
async function streamAnswer(options) {
|
|
223
|
+
let response;
|
|
224
|
+
try {
|
|
225
|
+
response = await fetch(options.endpoint, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "content-type": "application/json" },
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
messages: options.transcript,
|
|
230
|
+
responseMessageId: options.responseMessageId,
|
|
231
|
+
currentSourcePath: options.currentSourcePath,
|
|
232
|
+
currentSlug: options.currentSlug
|
|
233
|
+
}),
|
|
234
|
+
signal: options.signal
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (options.signal.aborted) return "aborted";
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
const payload = await response.json().catch(() => void 0);
|
|
242
|
+
throw new AssistantRequestError(
|
|
243
|
+
payload?.error ?? "The assistant is unavailable right now."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (!response.body) {
|
|
247
|
+
throw new AssistantRequestError(
|
|
248
|
+
"The assistant returned an empty response."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const reader = response.body.getReader();
|
|
252
|
+
const decoder = new TextDecoder();
|
|
253
|
+
let buffer = "";
|
|
254
|
+
try {
|
|
255
|
+
for (; ; ) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done) break;
|
|
258
|
+
buffer += decoder.decode(value, { stream: true });
|
|
259
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
260
|
+
while (newlineIndex !== -1) {
|
|
261
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
262
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
263
|
+
if (line) emitStreamEvent(line, options.onEvent);
|
|
264
|
+
newlineIndex = buffer.indexOf("\n");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
buffer += decoder.decode();
|
|
268
|
+
const leftover = buffer.trim();
|
|
269
|
+
if (leftover) emitStreamEvent(leftover, options.onEvent);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (options.signal.aborted) return "aborted";
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
return "done";
|
|
275
|
+
}
|
|
276
|
+
function emitStreamEvent(line, onEvent) {
|
|
277
|
+
let event;
|
|
278
|
+
try {
|
|
279
|
+
event = JSON.parse(line);
|
|
280
|
+
} catch {
|
|
281
|
+
onEvent({ type: "error", message: INVALID_STREAM_MESSAGE });
|
|
282
|
+
throw new AssistantRequestError(INVALID_STREAM_MESSAGE);
|
|
283
|
+
}
|
|
284
|
+
onEvent(event);
|
|
285
|
+
}
|
|
286
|
+
export {
|
|
287
|
+
AssistantProvider,
|
|
288
|
+
buildReplayableHistory,
|
|
289
|
+
streamAnswer,
|
|
290
|
+
useAssistant
|
|
291
|
+
};
|
|
292
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ui/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { SilicaAssistantProvider } from \"@silicajs/components\";\nimport { useSilicaRouting } from \"@silicajs/components/routing\";\nimport type {\n AssistantCitation,\n AssistantStreamEvent,\n AssistantTranscriptMessage,\n} from \"../types.js\";\n\nexport type AssistantChatMessage = {\n id: string;\n previousMessageId: string | null;\n role: \"user\" | \"assistant\";\n content: string;\n signature?: string;\n citations: AssistantCitation[];\n state: \"streaming\" | \"complete\" | \"error\";\n /** Shell commands the assistant ran while searching site pages, in order. */\n commands: string[];\n error?: string;\n};\n\nexport type AssistantContextValue = {\n open: boolean;\n setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n messages: AssistantChatMessage[];\n isStreaming: boolean;\n ask: (question: string) => void;\n stop: () => void;\n retry: () => void;\n reset: () => void;\n};\n\nconst AssistantContext = React.createContext<AssistantContextValue | null>(\n null,\n);\nconst INVALID_STREAM_MESSAGE =\n \"The assistant returned an invalid stream response.\";\n\nexport function useAssistant(): AssistantContextValue | null {\n return React.useContext(AssistantContext);\n}\n\nexport type AssistantProviderProps = {\n children: React.ReactNode;\n endpoint?: string;\n};\n\nfunction createMessageId(): string {\n return globalThis.crypto.randomUUID();\n}\n\nexport function buildReplayableHistory(history: AssistantChatMessage[]): {\n messages: AssistantChatMessage[];\n transcript: AssistantTranscriptMessage[];\n} {\n const messages: AssistantChatMessage[] = [];\n const transcript: AssistantTranscriptMessage[] = [];\n\n for (let index = 0; index < history.length; index += 2) {\n const user = history[index];\n const assistant = history[index + 1];\n const previousMessageId = transcript.at(-1)?.id ?? null;\n\n if (\n !user ||\n user.role !== \"user\" ||\n user.state !== \"complete\" ||\n !user.content ||\n user.previousMessageId !== previousMessageId\n ) {\n break;\n }\n\n if (\n !assistant ||\n assistant.role !== \"assistant\" ||\n assistant.state !== \"complete\" ||\n !assistant.content ||\n !assistant.signature ||\n assistant.previousMessageId !== user.id\n ) {\n break;\n }\n\n messages.push(user, assistant);\n transcript.push(\n {\n id: user.id,\n previousMessageId: user.previousMessageId,\n role: \"user\",\n content: user.content,\n },\n {\n id: assistant.id,\n previousMessageId: assistant.previousMessageId,\n role: \"assistant\",\n content: assistant.content,\n signature: assistant.signature,\n },\n );\n }\n\n return { messages, transcript };\n}\n\nexport function AssistantProvider({\n children,\n endpoint = \"/api/assistant\",\n}: AssistantProviderProps) {\n const { currentSlug } = useSilicaRouting();\n const [open, setOpen] = React.useState(false);\n const [messages, setMessages] = React.useState<AssistantChatMessage[]>([]);\n const [isStreaming, setIsStreaming] = React.useState(false);\n\n const messagesRef = React.useRef(messages);\n messagesRef.current = messages;\n const controllerRef = React.useRef<AbortController | null>(null);\n\n const updateMessage = React.useCallback(\n (\n id: string,\n update: (message: AssistantChatMessage) => AssistantChatMessage,\n ) => {\n setMessages((current) =>\n current.map((message) =>\n message.id === id ? update(message) : message,\n ),\n );\n },\n [],\n );\n\n const sendQuestion = React.useCallback(\n (question: string, history: AssistantChatMessage[]) => {\n const trimmed = question.trim();\n if (!trimmed) return;\n\n controllerRef.current?.abort();\n const controller = new AbortController();\n controllerRef.current = controller;\n\n const replayableHistory = buildReplayableHistory(history);\n const userId = createMessageId();\n const previousMessageId = replayableHistory.transcript.at(-1)?.id ?? null;\n const transcript: AssistantTranscriptMessage[] = [\n ...replayableHistory.transcript,\n {\n id: userId,\n previousMessageId,\n role: \"user\",\n content: trimmed,\n },\n ];\n\n const answerId = createMessageId();\n setMessages([\n ...replayableHistory.messages,\n {\n id: userId,\n previousMessageId,\n role: \"user\",\n content: trimmed,\n citations: [],\n state: \"complete\",\n commands: [],\n },\n {\n id: answerId,\n previousMessageId: userId,\n role: \"assistant\",\n content: \"\",\n citations: [],\n state: \"streaming\",\n commands: [],\n },\n ]);\n setIsStreaming(true);\n\n void streamAnswer({\n endpoint,\n transcript,\n responseMessageId: answerId,\n currentSlug,\n signal: controller.signal,\n onEvent: (event) => {\n if (event.type === \"text-delta\") {\n updateMessage(answerId, (message) => ({\n ...message,\n content: message.content + event.text,\n }));\n } else if (event.type === \"tool-status\") {\n updateMessage(answerId, (message) => ({\n ...message,\n commands: [...message.commands, event.command],\n }));\n } else if (event.type === \"citations\") {\n updateMessage(answerId, (message) => ({\n ...message,\n citations: event.citations,\n }));\n } else if (event.type === \"message-signature\") {\n updateMessage(answerId, (message) => ({\n ...message,\n id: event.id,\n previousMessageId: event.previousMessageId,\n signature: event.signature,\n }));\n } else if (event.type === \"error\") {\n updateMessage(answerId, (message) => ({\n ...message,\n state: \"error\",\n error: event.message,\n }));\n }\n },\n })\n .then((outcome) => {\n updateMessage(answerId, (message) => {\n if (message.state === \"error\") return message;\n if (outcome === \"aborted\") {\n return message.content && message.signature\n ? { ...message, state: \"complete\" }\n : {\n ...message,\n state: \"error\",\n error: \"Answer stopped.\",\n };\n }\n return message.signature\n ? { ...message, state: \"complete\" }\n : {\n ...message,\n state: \"error\",\n error: \"The assistant reply could not be verified.\",\n };\n });\n })\n .catch((error: unknown) => {\n updateMessage(answerId, (message) => ({\n ...message,\n state: \"error\",\n error:\n error instanceof AssistantRequestError\n ? error.message\n : \"The assistant is unavailable right now.\",\n }));\n })\n .finally(() => {\n if (controllerRef.current === controller) {\n controllerRef.current = null;\n setIsStreaming(false);\n }\n });\n },\n [currentSlug, endpoint, updateMessage],\n );\n\n const ask = React.useCallback(\n (question: string) => {\n sendQuestion(question, messagesRef.current);\n },\n [sendQuestion],\n );\n\n const stop = React.useCallback(() => {\n controllerRef.current?.abort();\n }, []);\n\n const retry = React.useCallback(() => {\n if (controllerRef.current) return;\n const current = messagesRef.current;\n for (let index = current.length - 1; index >= 0; index -= 1) {\n const message = current[index];\n if (message?.role === \"user\") {\n sendQuestion(message.content, current.slice(0, index));\n return;\n }\n }\n }, [sendQuestion]);\n\n const reset = React.useCallback(() => {\n controllerRef.current?.abort();\n setMessages([]);\n }, []);\n\n const value = React.useMemo<AssistantContextValue>(\n () => ({ open, setOpen, messages, isStreaming, ask, stop, retry, reset }),\n [open, messages, isStreaming, ask, stop, retry, reset],\n );\n\n const bridge = React.useMemo(\n () => ({\n open,\n openAssistant: (query?: string) => {\n setOpen(true);\n if (query?.trim()) ask(query);\n },\n closeAssistant: () => setOpen(false),\n }),\n [open, ask],\n );\n\n return (\n <AssistantContext.Provider value={value}>\n <SilicaAssistantProvider value={bridge}>\n {children}\n </SilicaAssistantProvider>\n </AssistantContext.Provider>\n );\n}\n\nclass AssistantRequestError extends Error {}\n\nexport async function streamAnswer(options: {\n endpoint: string;\n transcript: AssistantTranscriptMessage[];\n responseMessageId: string;\n currentSourcePath?: string;\n currentSlug?: string;\n signal: AbortSignal;\n onEvent: (event: AssistantStreamEvent) => void;\n}): Promise<\"done\" | \"aborted\"> {\n let response: Response;\n try {\n response = await fetch(options.endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n messages: options.transcript,\n responseMessageId: options.responseMessageId,\n currentSourcePath: options.currentSourcePath,\n currentSlug: options.currentSlug,\n }),\n signal: options.signal,\n });\n } catch (error) {\n if (options.signal.aborted) return \"aborted\";\n throw error;\n }\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => undefined)) as\n | { error?: string }\n | undefined;\n throw new AssistantRequestError(\n payload?.error ?? \"The assistant is unavailable right now.\",\n );\n }\n if (!response.body) {\n throw new AssistantRequestError(\n \"The assistant returned an empty response.\",\n );\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n let newlineIndex = buffer.indexOf(\"\\n\");\n while (newlineIndex !== -1) {\n const line = buffer.slice(0, newlineIndex).trim();\n buffer = buffer.slice(newlineIndex + 1);\n if (line) emitStreamEvent(line, options.onEvent);\n newlineIndex = buffer.indexOf(\"\\n\");\n }\n }\n buffer += decoder.decode();\n const leftover = buffer.trim();\n if (leftover) emitStreamEvent(leftover, options.onEvent);\n } catch (error) {\n if (options.signal.aborted) return \"aborted\";\n throw error;\n }\n return \"done\";\n}\n\nfunction emitStreamEvent(\n line: string,\n onEvent: (event: AssistantStreamEvent) => void,\n): void {\n let event: AssistantStreamEvent;\n try {\n event = JSON.parse(line) as AssistantStreamEvent;\n } catch {\n onEvent({ type: \"error\", message: INVALID_STREAM_MESSAGE });\n throw new AssistantRequestError(INVALID_STREAM_MESSAGE);\n }\n onEvent(event);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type AssistantTriggerProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
iconOnly?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare function AssistantTrigger({ className, label, iconOnly, }: AssistantTriggerProps): React.JSX.Element | null;
|
|
9
|
+
|
|
10
|
+
export { AssistantTrigger, type AssistantTriggerProps };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "@silicajs/ui/components/button";
|
|
5
|
+
import { KeyboardShortcut } from "@silicajs/ui/components/kbd";
|
|
6
|
+
import {
|
|
7
|
+
Tooltip,
|
|
8
|
+
TooltipContent,
|
|
9
|
+
TooltipTrigger
|
|
10
|
+
} from "@silicajs/ui/components/tooltip";
|
|
11
|
+
import { SparklesIcon } from "lucide-react";
|
|
12
|
+
import { useAssistant } from "./provider.js";
|
|
13
|
+
function AssistantTrigger({
|
|
14
|
+
className,
|
|
15
|
+
label = "Ask AI",
|
|
16
|
+
iconOnly = false
|
|
17
|
+
}) {
|
|
18
|
+
const assistant = useAssistant();
|
|
19
|
+
const setOpen = assistant?.setOpen;
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (!setOpen) return;
|
|
22
|
+
const onKeyDown = (event) => {
|
|
23
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "i") {
|
|
24
|
+
if (isEditableTarget(event.target)) return;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
setOpen((open) => !open);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener("keydown", onKeyDown);
|
|
30
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
31
|
+
}, [setOpen]);
|
|
32
|
+
if (!assistant) return null;
|
|
33
|
+
if (iconOnly) {
|
|
34
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
35
|
+
/* @__PURE__ */ jsx(
|
|
36
|
+
TooltipTrigger,
|
|
37
|
+
{
|
|
38
|
+
render: /* @__PURE__ */ jsxs(
|
|
39
|
+
Button,
|
|
40
|
+
{
|
|
41
|
+
type: "button",
|
|
42
|
+
variant: "outline",
|
|
43
|
+
size: "icon-sm",
|
|
44
|
+
onClick: () => assistant.setOpen(true),
|
|
45
|
+
className,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsx(SparklesIcon, { className: "text-muted-foreground" }),
|
|
48
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: label })
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
/* @__PURE__ */ jsxs(TooltipContent, { side: "right", children: [
|
|
55
|
+
/* @__PURE__ */ jsx("span", { children: "Open the assistant panel" }),
|
|
56
|
+
/* @__PURE__ */ jsx(KeyboardShortcut, { keys: "I" })
|
|
57
|
+
] })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
return /* @__PURE__ */ jsxs(
|
|
61
|
+
Button,
|
|
62
|
+
{
|
|
63
|
+
type: "button",
|
|
64
|
+
variant: "outline",
|
|
65
|
+
size: "sm",
|
|
66
|
+
onClick: () => assistant.setOpen(true),
|
|
67
|
+
className,
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
SparklesIcon,
|
|
71
|
+
{
|
|
72
|
+
"data-icon": "inline-start",
|
|
73
|
+
className: "text-muted-foreground"
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 text-left text-muted-foreground", children: label }),
|
|
77
|
+
/* @__PURE__ */ jsx(KeyboardShortcut, { keys: "I", inlineEnd: true })
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function isEditableTarget(target) {
|
|
83
|
+
if (!(target instanceof Element)) return false;
|
|
84
|
+
const editable = target.closest("input, textarea, select, [contenteditable]");
|
|
85
|
+
if (!editable) return false;
|
|
86
|
+
return editable.getAttribute("contenteditable") !== "false";
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
AssistantTrigger
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=trigger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ui/trigger.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@silicajs/ui/components/button\";\nimport { KeyboardShortcut } from \"@silicajs/ui/components/kbd\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@silicajs/ui/components/tooltip\";\nimport { SparklesIcon } from \"lucide-react\";\nimport { useAssistant } from \"./provider.js\";\n\nexport type AssistantTriggerProps = {\n className?: string;\n label?: string;\n iconOnly?: boolean;\n};\n\nexport function AssistantTrigger({\n className,\n label = \"Ask AI\",\n iconOnly = false,\n}: AssistantTriggerProps) {\n const assistant = useAssistant();\n const setOpen = assistant?.setOpen;\n\n React.useEffect(() => {\n if (!setOpen) return;\n const onKeyDown = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === \"i\") {\n if (isEditableTarget(event.target)) return;\n event.preventDefault();\n setOpen((open) => !open);\n }\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [setOpen]);\n\n if (!assistant) return null;\n\n if (iconOnly) {\n return (\n <Tooltip>\n <TooltipTrigger\n render={\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n onClick={() => assistant.setOpen(true)}\n className={className}\n >\n <SparklesIcon className=\"text-muted-foreground\" />\n <span className=\"sr-only\">{label}</span>\n </Button>\n }\n />\n <TooltipContent side=\"right\">\n <span>Open the assistant panel</span>\n <KeyboardShortcut keys=\"I\" />\n </TooltipContent>\n </Tooltip>\n );\n }\n\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => assistant.setOpen(true)}\n className={className}\n >\n <SparklesIcon\n data-icon=\"inline-start\"\n className=\"text-muted-foreground\"\n />\n <span className=\"flex-1 text-left text-muted-foreground\">{label}</span>\n <KeyboardShortcut keys=\"I\" inlineEnd />\n </Button>\n );\n}\n\nfunction isEditableTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n const editable = target.closest(\"input, textarea, select, [contenteditable]\");\n if (!editable) return false;\n return editable.getAttribute(\"contenteditable\") !== \"false\";\n}\n"],"mappings":";AA+CY,SAOE,KAPF;AA7CZ,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAQtB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AACb,GAA0B;AACxB,QAAM,YAAY,aAAa;AAC/B,QAAM,UAAU,WAAW;AAE3B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAS;AACd,UAAM,YAAY,CAAC,UAAyB;AAC1C,WAAK,MAAM,WAAW,MAAM,YAAY,MAAM,IAAI,YAAY,MAAM,KAAK;AACvE,YAAI,iBAAiB,MAAM,MAAM,EAAG;AACpC,cAAM,eAAe;AACrB,gBAAQ,CAAC,SAAS,CAAC,IAAI;AAAA,MACzB;AAAA,IACF;AACA,WAAO,iBAAiB,WAAW,SAAS;AAC5C,WAAO,MAAM,OAAO,oBAAoB,WAAW,SAAS;AAAA,EAC9D,GAAG,CAAC,OAAO,CAAC;AAEZ,MAAI,CAAC,UAAW,QAAO;AAEvB,MAAI,UAAU;AACZ,WACE,qBAAC,WACC;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,QACE;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS,MAAM,UAAU,QAAQ,IAAI;AAAA,cACrC;AAAA,cAEA;AAAA,oCAAC,gBAAa,WAAU,yBAAwB;AAAA,gBAChD,oBAAC,UAAK,WAAU,WAAW,iBAAM;AAAA;AAAA;AAAA,UACnC;AAAA;AAAA,MAEJ;AAAA,MACA,qBAAC,kBAAe,MAAK,SACnB;AAAA,4BAAC,UAAK,sCAAwB;AAAA,QAC9B,oBAAC,oBAAiB,MAAK,KAAI;AAAA,SAC7B;AAAA,OACF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS,MAAM,UAAU,QAAQ,IAAI;AAAA,MACrC;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAU;AAAA;AAAA,QACZ;AAAA,QACA,oBAAC,UAAK,WAAU,0CAA0C,iBAAM;AAAA,QAChE,oBAAC,oBAAiB,MAAK,KAAI,WAAS,MAAC;AAAA;AAAA;AAAA,EACvC;AAEJ;AAEA,SAAS,iBAAiB,QAAqC;AAC7D,MAAI,EAAE,kBAAkB,SAAU,QAAO;AACzC,QAAM,WAAW,OAAO,QAAQ,4CAA4C;AAC5E,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,aAAa,iBAAiB,MAAM;AACtD;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@silicajs/assistant",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Optional AI assistant for Silica knowledge sites — core-ai runtime, constrained markdown tools, and theme-consumable UI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./dist/server/index.d.ts",
|
|
17
|
+
"import": "./dist/server/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./next": {
|
|
20
|
+
"types": "./dist/next.d.ts",
|
|
21
|
+
"import": "./dist/next.js"
|
|
22
|
+
},
|
|
23
|
+
"./ui": {
|
|
24
|
+
"types": "./dist/ui/index.d.ts",
|
|
25
|
+
"import": "./dist/ui/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"lint": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run --passWithNoTests"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@core-ai/core-ai": "^0.11.0",
|
|
40
|
+
"@silicajs/components": "^0.4.0",
|
|
41
|
+
"@silicajs/core": "^0.8.0",
|
|
42
|
+
"@silicajs/ui": "^0.2.0",
|
|
43
|
+
"just-bash": "^3.0.1",
|
|
44
|
+
"lucide-react": "^1.17.0",
|
|
45
|
+
"react-markdown": "^10.1.0",
|
|
46
|
+
"remark-gfm": "^4.0.1",
|
|
47
|
+
"zod": "^4.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "^19.2.0",
|
|
51
|
+
"react-dom": "^19.2.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@silicajs/next": "^0.5.0",
|
|
55
|
+
"@types/react": "^19.2.17",
|
|
56
|
+
"@types/react-dom": "^19.2.3",
|
|
57
|
+
"react": "^19.2.6",
|
|
58
|
+
"react-dom": "^19.2.7",
|
|
59
|
+
"tsup": "^8.5.1"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/agdevhq/silica/tree/main/packages/assistant#readme",
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "git+https://github.com/agdevhq/silica.git",
|
|
65
|
+
"directory": "packages/assistant"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/agdevhq/silica/issues"
|
|
69
|
+
}
|
|
70
|
+
}
|