@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.
Files changed (50) hide show
  1. package/README.md +52 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/next.d.ts +48 -0
  6. package/dist/next.js +219 -0
  7. package/dist/next.js.map +1 -0
  8. package/dist/server/handler.d.ts +39 -0
  9. package/dist/server/handler.js +239 -0
  10. package/dist/server/handler.js.map +1 -0
  11. package/dist/server/index.d.ts +10 -0
  12. package/dist/server/index.js +43 -0
  13. package/dist/server/index.js.map +1 -0
  14. package/dist/server/prompt.d.ts +5 -0
  15. package/dist/server/prompt.js +48 -0
  16. package/dist/server/prompt.js.map +1 -0
  17. package/dist/server/provider.d.ts +7 -0
  18. package/dist/server/provider.js +51 -0
  19. package/dist/server/provider.js.map +1 -0
  20. package/dist/server/runtime.d.ts +26 -0
  21. package/dist/server/runtime.js +111 -0
  22. package/dist/server/runtime.js.map +1 -0
  23. package/dist/server/sources.d.ts +29 -0
  24. package/dist/server/sources.js +73 -0
  25. package/dist/server/sources.js.map +1 -0
  26. package/dist/server/tools.d.ts +14 -0
  27. package/dist/server/tools.js +45 -0
  28. package/dist/server/tools.js.map +1 -0
  29. package/dist/server/wikilinks.d.ts +28 -0
  30. package/dist/server/wikilinks.js +149 -0
  31. package/dist/server/wikilinks.js.map +1 -0
  32. package/dist/types.d.ts +83 -0
  33. package/dist/types.js +1 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/ui/index.d.ts +6 -0
  36. package/dist/ui/index.js +15 -0
  37. package/dist/ui/index.js.map +1 -0
  38. package/dist/ui/message.d.ts +11 -0
  39. package/dist/ui/message.js +114 -0
  40. package/dist/ui/message.js.map +1 -0
  41. package/dist/ui/panel.d.ts +13 -0
  42. package/dist/ui/panel.js +201 -0
  43. package/dist/ui/panel.js.map +1 -0
  44. package/dist/ui/provider.d.ts +46 -0
  45. package/dist/ui/provider.js +292 -0
  46. package/dist/ui/provider.js.map +1 -0
  47. package/dist/ui/trigger.d.ts +10 -0
  48. package/dist/ui/trigger.js +91 -0
  49. package/dist/ui/trigger.js.map +1 -0
  50. package/package.json +70 -0
@@ -0,0 +1,149 @@
1
+ class AssistantWikiLinkFilter {
2
+ constructor(resolve) {
3
+ this.resolve = resolve;
4
+ }
5
+ resolve;
6
+ buffer = "";
7
+ inFence = false;
8
+ inlineCodeTicks = 0;
9
+ lineStart = true;
10
+ async push(chunk) {
11
+ this.buffer += chunk;
12
+ return this.drain(false);
13
+ }
14
+ async flush() {
15
+ return this.drain(true);
16
+ }
17
+ async drain(flush) {
18
+ let output = "";
19
+ let index = 0;
20
+ while (index < this.buffer.length) {
21
+ if (this.buffer[index] === "`") {
22
+ const tickCount = countBackticks(this.buffer, index);
23
+ if (!flush && index + tickCount === this.buffer.length && tickCount < 3) {
24
+ this.buffer = this.buffer.slice(index);
25
+ return output;
26
+ }
27
+ const backticks = this.buffer.slice(index, index + tickCount);
28
+ this.updateCodeState(tickCount);
29
+ output += backticks;
30
+ this.updateLineState(backticks);
31
+ index += tickCount;
32
+ continue;
33
+ }
34
+ const inCode = this.inFence || this.inlineCodeTicks > 0;
35
+ if (!inCode && this.buffer.startsWith("[[", index)) {
36
+ const end = this.buffer.indexOf("]]", index + 2);
37
+ if (end === -1) {
38
+ if (flush) {
39
+ const raw = this.buffer.slice(index);
40
+ output += raw;
41
+ this.updateLineState(raw);
42
+ this.buffer = "";
43
+ } else {
44
+ this.buffer = this.buffer.slice(index);
45
+ }
46
+ return output;
47
+ }
48
+ const rendered = await renderWikiLink(
49
+ this.buffer.slice(index + 2, end),
50
+ this.resolve
51
+ );
52
+ output += rendered;
53
+ this.updateLineState(rendered);
54
+ index = end + 2;
55
+ continue;
56
+ }
57
+ if (!inCode && !flush && this.buffer[index] === "[" && index === this.buffer.length - 1) {
58
+ this.buffer = this.buffer.slice(index);
59
+ return output;
60
+ }
61
+ const char = this.buffer[index];
62
+ output += char;
63
+ this.updateLineState(char);
64
+ index += 1;
65
+ }
66
+ this.buffer = "";
67
+ return output;
68
+ }
69
+ updateCodeState(tickCount) {
70
+ if (this.inFence) {
71
+ if (this.lineStart && tickCount >= 3) this.inFence = false;
72
+ return;
73
+ }
74
+ if (this.inlineCodeTicks > 0) {
75
+ if (tickCount === this.inlineCodeTicks) this.inlineCodeTicks = 0;
76
+ return;
77
+ }
78
+ if (this.lineStart && tickCount >= 3) {
79
+ this.inFence = true;
80
+ return;
81
+ }
82
+ this.inlineCodeTicks = tickCount;
83
+ }
84
+ updateLineState(text) {
85
+ for (const char of text) {
86
+ if (char === "\n") {
87
+ this.lineStart = true;
88
+ } else if (char !== "\r") {
89
+ this.lineStart = false;
90
+ }
91
+ }
92
+ }
93
+ }
94
+ async function resolveAssistantWikiLinks(text, resolve) {
95
+ const filter = new AssistantWikiLinkFilter(resolve);
96
+ return await filter.push(text) + await filter.flush();
97
+ }
98
+ function createAssistantWikiLinkFilter(options) {
99
+ return new AssistantWikiLinkFilter(
100
+ async (target) => options.resolveWikiLink?.(options.currentSourcePath, target)
101
+ );
102
+ }
103
+ async function renderWikiLink(rawValue, resolve) {
104
+ const parsed = parseWikiLink(rawValue);
105
+ if (!parsed) return rawValue;
106
+ const citation = await resolve(parsed.target);
107
+ if (!citation) return escapeMarkdownText(parsed.label);
108
+ return `[${escapeMarkdownLinkLabel(parsed.label)}](${citation.href})`;
109
+ }
110
+ function parseWikiLink(rawValue) {
111
+ const pipeIndex = findUnescapedPipe(rawValue);
112
+ const rawTarget = pipeIndex === -1 ? rawValue : rawValue.slice(0, pipeIndex);
113
+ const rawLabel = pipeIndex === -1 ? rawTarget : rawValue.slice(pipeIndex + 1);
114
+ const target = unescapeWikiText(rawTarget).trim();
115
+ const label = unescapeWikiText(rawLabel).trim() || target;
116
+ if (!target) return void 0;
117
+ return { target, label };
118
+ }
119
+ function findUnescapedPipe(value) {
120
+ for (let index = 0; index < value.length; index += 1) {
121
+ if (value[index] !== "|") continue;
122
+ let slashCount = 0;
123
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) {
124
+ slashCount += 1;
125
+ }
126
+ if (slashCount % 2 === 0) return index;
127
+ }
128
+ return -1;
129
+ }
130
+ function countBackticks(value, start) {
131
+ let count = 0;
132
+ while (value[start + count] === "`") count += 1;
133
+ return count;
134
+ }
135
+ function unescapeWikiText(value) {
136
+ return value.replace(/\\([\\|])/g, "$1");
137
+ }
138
+ function escapeMarkdownLinkLabel(value) {
139
+ return value.replace(/([\\[\]])/g, "\\$1");
140
+ }
141
+ function escapeMarkdownText(value) {
142
+ return value.replace(/([\\[\]])/g, "\\$1");
143
+ }
144
+ export {
145
+ AssistantWikiLinkFilter,
146
+ createAssistantWikiLinkFilter,
147
+ resolveAssistantWikiLinks
148
+ };
149
+ //# sourceMappingURL=wikilinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/wikilinks.ts"],"sourcesContent":["import type { AssistantCitation, AssistantWikiLinkResolver } from \"../types.js\";\n\nexport type WikiLinkResolver = (\n target: string,\n) => AssistantCitation | undefined | Promise<AssistantCitation | undefined>;\n\n/**\n * Converts assistant-visible Obsidian wikilinks into regular markdown links.\n * Unresolved wikilinks degrade to their visible label so internal targets never\n * leak into the answer UI.\n */\nexport class AssistantWikiLinkFilter {\n private buffer = \"\";\n private inFence = false;\n private inlineCodeTicks = 0;\n private lineStart = true;\n\n constructor(private readonly resolve: WikiLinkResolver) {}\n\n async push(chunk: string): Promise<string> {\n this.buffer += chunk;\n return this.drain(false);\n }\n\n async flush(): Promise<string> {\n return this.drain(true);\n }\n\n private async drain(flush: boolean): Promise<string> {\n let output = \"\";\n let index = 0;\n\n while (index < this.buffer.length) {\n if (this.buffer[index] === \"`\") {\n const tickCount = countBackticks(this.buffer, index);\n if (\n !flush &&\n index + tickCount === this.buffer.length &&\n tickCount < 3\n ) {\n this.buffer = this.buffer.slice(index);\n return output;\n }\n\n const backticks = this.buffer.slice(index, index + tickCount);\n this.updateCodeState(tickCount);\n output += backticks;\n this.updateLineState(backticks);\n index += tickCount;\n continue;\n }\n\n const inCode = this.inFence || this.inlineCodeTicks > 0;\n if (!inCode && this.buffer.startsWith(\"[[\", index)) {\n const end = this.buffer.indexOf(\"]]\", index + 2);\n if (end === -1) {\n if (flush) {\n const raw = this.buffer.slice(index);\n output += raw;\n this.updateLineState(raw);\n this.buffer = \"\";\n } else {\n this.buffer = this.buffer.slice(index);\n }\n return output;\n }\n\n const rendered = await renderWikiLink(\n this.buffer.slice(index + 2, end),\n this.resolve,\n );\n output += rendered;\n this.updateLineState(rendered);\n index = end + 2;\n continue;\n }\n\n if (\n !inCode &&\n !flush &&\n this.buffer[index] === \"[\" &&\n index === this.buffer.length - 1\n ) {\n this.buffer = this.buffer.slice(index);\n return output;\n }\n\n const char = this.buffer[index]!;\n output += char;\n this.updateLineState(char);\n index += 1;\n }\n\n this.buffer = \"\";\n return output;\n }\n\n private updateCodeState(tickCount: number): void {\n if (this.inFence) {\n if (this.lineStart && tickCount >= 3) this.inFence = false;\n return;\n }\n\n if (this.inlineCodeTicks > 0) {\n if (tickCount === this.inlineCodeTicks) this.inlineCodeTicks = 0;\n return;\n }\n\n if (this.lineStart && tickCount >= 3) {\n this.inFence = true;\n return;\n }\n\n this.inlineCodeTicks = tickCount;\n }\n\n private updateLineState(text: string): void {\n for (const char of text) {\n if (char === \"\\n\") {\n this.lineStart = true;\n } else if (char !== \"\\r\") {\n this.lineStart = false;\n }\n }\n }\n}\n\nexport async function resolveAssistantWikiLinks(\n text: string,\n resolve: WikiLinkResolver,\n): Promise<string> {\n const filter = new AssistantWikiLinkFilter(resolve);\n return (await filter.push(text)) + (await filter.flush());\n}\n\nexport function createAssistantWikiLinkFilter(options: {\n currentSourcePath: string;\n resolveWikiLink?: AssistantWikiLinkResolver;\n}): AssistantWikiLinkFilter {\n return new AssistantWikiLinkFilter(async (target) =>\n options.resolveWikiLink?.(options.currentSourcePath, target),\n );\n}\n\nasync function renderWikiLink(\n rawValue: string,\n resolve: WikiLinkResolver,\n): Promise<string> {\n const parsed = parseWikiLink(rawValue);\n if (!parsed) return rawValue;\n\n const citation = await resolve(parsed.target);\n if (!citation) return escapeMarkdownText(parsed.label);\n\n return `[${escapeMarkdownLinkLabel(parsed.label)}](${citation.href})`;\n}\n\nfunction parseWikiLink(\n rawValue: string,\n): { target: string; label: string } | undefined {\n const pipeIndex = findUnescapedPipe(rawValue);\n const rawTarget = pipeIndex === -1 ? rawValue : rawValue.slice(0, pipeIndex);\n const rawLabel = pipeIndex === -1 ? rawTarget : rawValue.slice(pipeIndex + 1);\n const target = unescapeWikiText(rawTarget).trim();\n const label = unescapeWikiText(rawLabel).trim() || target;\n if (!target) return undefined;\n return { target, label };\n}\n\nfunction findUnescapedPipe(value: string): number {\n for (let index = 0; index < value.length; index += 1) {\n if (value[index] !== \"|\") continue;\n let slashCount = 0;\n for (\n let cursor = index - 1;\n cursor >= 0 && value[cursor] === \"\\\\\";\n cursor -= 1\n ) {\n slashCount += 1;\n }\n if (slashCount % 2 === 0) return index;\n }\n return -1;\n}\n\nfunction countBackticks(value: string, start: number): number {\n let count = 0;\n while (value[start + count] === \"`\") count += 1;\n return count;\n}\n\nfunction unescapeWikiText(value: string): string {\n return value.replace(/\\\\([\\\\|])/g, \"$1\");\n}\n\nfunction escapeMarkdownLinkLabel(value: string): string {\n return value.replace(/([\\\\[\\]])/g, \"\\\\$1\");\n}\n\nfunction escapeMarkdownText(value: string): string {\n return value.replace(/([\\\\[\\]])/g, \"\\\\$1\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,83 @@
1
+ /** A source page the assistant used to answer a question. */
2
+ type AssistantCitation = {
3
+ slug: string;
4
+ title: string;
5
+ href: string;
6
+ /** Path of the original markdown file relative to the content root. */
7
+ sourcePath: string;
8
+ };
9
+ /** One user turn of the client-held conversation transcript. */
10
+ type AssistantUserTranscriptMessage = {
11
+ id: string;
12
+ previousMessageId: string | null;
13
+ role: "user";
14
+ content: string;
15
+ };
16
+ /** One assistant turn signed by the server for stateless transcript replay. */
17
+ type AssistantSignedTranscriptMessage = {
18
+ id: string;
19
+ previousMessageId: string | null;
20
+ role: "assistant";
21
+ content: string;
22
+ signature: string;
23
+ };
24
+ /** One turn of the client-held conversation transcript. */
25
+ type AssistantTranscriptMessage = AssistantUserTranscriptMessage | AssistantSignedTranscriptMessage;
26
+ /** Request body of `POST /api/assistant`. */
27
+ type AssistantRequest = {
28
+ messages: AssistantTranscriptMessage[];
29
+ responseMessageId: string;
30
+ /** Current page source path relative to the assistant content root. */
31
+ currentSourcePath?: string;
32
+ /** Fallback route slug used when the client cannot provide a source path. */
33
+ currentSlug?: string;
34
+ };
35
+ /**
36
+ * Newline-delimited JSON events streamed back by the assistant route.
37
+ */
38
+ type AssistantStreamEvent = {
39
+ type: "text-delta";
40
+ text: string;
41
+ } | {
42
+ type: "tool-status";
43
+ command: string;
44
+ } | {
45
+ type: "citations";
46
+ citations: AssistantCitation[];
47
+ } | {
48
+ type: "message-signature";
49
+ id: string;
50
+ previousMessageId: string | null;
51
+ signature: string;
52
+ } | {
53
+ type: "done";
54
+ } | {
55
+ type: "error";
56
+ message: string;
57
+ };
58
+ /** Resolves a model-reported source path to a published page citation. */
59
+ type AssistantCitationResolver = (sourcePath: string) => AssistantCitation | undefined | Promise<AssistantCitation | undefined>;
60
+ /** Resolves an Obsidian-style wikilink target from a current source page. */
61
+ type AssistantWikiLinkResolver = (currentSourcePath: string, target: string) => AssistantCitation | undefined | Promise<AssistantCitation | undefined>;
62
+ /** Knowledge-site context the server runtime operates on. */
63
+ type AssistantSiteContext = {
64
+ siteTitle: string;
65
+ siteDescription?: string;
66
+ homePage?: {
67
+ title: string;
68
+ sourcePath: string;
69
+ excerpt: string;
70
+ };
71
+ currentPage?: {
72
+ title: string;
73
+ slug: string;
74
+ sourcePath: string;
75
+ excerpt: string;
76
+ };
77
+ /** Filesystem directory mounted read-only as `/` for shell tools. */
78
+ contentRoot: string;
79
+ resolveCitation: AssistantCitationResolver;
80
+ resolveWikiLink?: AssistantWikiLinkResolver;
81
+ };
82
+
83
+ export type { AssistantCitation, AssistantCitationResolver, AssistantRequest, AssistantSignedTranscriptMessage, AssistantSiteContext, AssistantStreamEvent, AssistantTranscriptMessage, AssistantUserTranscriptMessage, AssistantWikiLinkResolver };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,6 @@
1
+ export { AssistantChatMessage, AssistantContextValue, AssistantProvider, AssistantProviderProps, useAssistant } from './provider.js';
2
+ export { AssistantTrigger, AssistantTriggerProps } from './trigger.js';
3
+ export { AssistantPanel, AssistantPanelProps } from './panel.js';
4
+ export { AssistantMessage, AssistantMessageProps } from './message.js';
5
+ export { AssistantCitation, AssistantStreamEvent, AssistantTranscriptMessage } from '../types.js';
6
+ import 'react';
@@ -0,0 +1,15 @@
1
+ import {
2
+ AssistantProvider,
3
+ useAssistant
4
+ } from "./provider.js";
5
+ import { AssistantTrigger } from "./trigger.js";
6
+ import { AssistantPanel } from "./panel.js";
7
+ import { AssistantMessage } from "./message.js";
8
+ export {
9
+ AssistantMessage,
10
+ AssistantPanel,
11
+ AssistantProvider,
12
+ AssistantTrigger,
13
+ useAssistant
14
+ };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["export {\n AssistantProvider,\n useAssistant,\n type AssistantChatMessage,\n type AssistantContextValue,\n type AssistantProviderProps,\n} from \"./provider.js\";\nexport { AssistantTrigger, type AssistantTriggerProps } from \"./trigger.js\";\nexport { AssistantPanel, type AssistantPanelProps } from \"./panel.js\";\nexport { AssistantMessage, type AssistantMessageProps } from \"./message.js\";\nexport type {\n AssistantCitation,\n AssistantStreamEvent,\n AssistantTranscriptMessage,\n} from \"../types.js\";\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AACP,SAAS,wBAAoD;AAC7D,SAAS,sBAAgD;AACzD,SAAS,wBAAoD;","names":[]}
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ import { AssistantChatMessage } from './provider.js';
3
+ import '../types.js';
4
+
5
+ type AssistantMessageProps = {
6
+ message: AssistantChatMessage;
7
+ onRetry: () => void;
8
+ };
9
+ declare function AssistantMessage({ message, onRetry }: AssistantMessageProps): React.JSX.Element;
10
+
11
+ export { AssistantMessage, type AssistantMessageProps };
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { SilicaLink } from "@silicajs/components";
7
+ import { cn } from "@silicajs/ui/lib/utils";
8
+ import { Button } from "@silicajs/ui/components/button";
9
+ import {
10
+ ChevronDownIcon,
11
+ FileTextIcon,
12
+ LoaderCircleIcon,
13
+ SearchIcon,
14
+ TerminalIcon
15
+ } from "lucide-react";
16
+ function AssistantMessage({ message, onRetry }) {
17
+ if (message.role === "user") {
18
+ return /* @__PURE__ */ jsx("div", { className: "flex min-w-0 justify-end", children: /* @__PURE__ */ jsx("div", { className: "min-w-0 max-w-[85%] rounded-xl rounded-br-sm bg-muted px-3 py-2 text-sm whitespace-pre-wrap break-words text-foreground", children: message.content }) });
19
+ }
20
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 max-w-full flex-col gap-2 overflow-hidden", children: [
21
+ message.commands.length > 0 || message.state === "streaming" && !message.content ? /* @__PURE__ */ jsx(
22
+ AssistantActivity,
23
+ {
24
+ commands: message.commands,
25
+ searching: message.state === "streaming" && !message.content
26
+ }
27
+ ) : null,
28
+ message.content ? /* @__PURE__ */ jsx("div", { className: "prose prose-sm dark:prose-invert min-w-0 max-w-none break-words text-sm", children: /* @__PURE__ */ jsx(
29
+ ReactMarkdown,
30
+ {
31
+ remarkPlugins: [remarkGfm],
32
+ components: { a: MarkdownLink },
33
+ children: message.content
34
+ }
35
+ ) }) : null,
36
+ message.state === "error" ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2", children: [
37
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: message.error ?? "Something went wrong." }),
38
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Retry" })
39
+ ] }) : null,
40
+ message.citations.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: message.citations.map((citation) => /* @__PURE__ */ jsxs(
41
+ SilicaLink,
42
+ {
43
+ href: citation.href,
44
+ className: "inline-flex max-w-full items-center gap-1.5 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground",
45
+ children: [
46
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-3 shrink-0" }),
47
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: citation.title })
48
+ ]
49
+ },
50
+ citation.slug
51
+ )) }) : null
52
+ ] });
53
+ }
54
+ function AssistantActivity({
55
+ commands,
56
+ searching
57
+ }) {
58
+ const [expanded, setExpanded] = React.useState(false);
59
+ if (commands.length === 0) {
60
+ return /* @__PURE__ */ jsx("div", { className: "flex min-w-0 max-w-full flex-col gap-1.5 overflow-hidden text-xs text-muted-foreground", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 self-start", children: [
61
+ /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 shrink-0 animate-spin" }),
62
+ /* @__PURE__ */ jsx("span", { children: "Thinking\u2026" })
63
+ ] }) });
64
+ }
65
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 max-w-full flex-col gap-1.5 overflow-hidden text-xs text-muted-foreground", children: [
66
+ /* @__PURE__ */ jsxs(
67
+ "button",
68
+ {
69
+ type: "button",
70
+ onClick: () => setExpanded((value) => !value),
71
+ "aria-expanded": expanded,
72
+ className: "group flex items-center gap-2 self-start rounded-md text-left transition-colors hover:text-foreground",
73
+ children: [
74
+ searching ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 shrink-0 animate-spin" }) : /* @__PURE__ */ jsx(SearchIcon, { className: "size-3.5 shrink-0" }),
75
+ /* @__PURE__ */ jsx("span", { children: searching ? "Searching the site\u2026" : "Searched the site" }),
76
+ /* @__PURE__ */ jsx(
77
+ ChevronDownIcon,
78
+ {
79
+ className: cn(
80
+ "size-3.5 shrink-0 transition-transform",
81
+ expanded && "rotate-180"
82
+ )
83
+ }
84
+ )
85
+ ]
86
+ }
87
+ ),
88
+ expanded ? /* @__PURE__ */ jsx("ul", { className: "flex min-w-0 max-w-full flex-col gap-1 overflow-hidden pl-[1.375rem]", children: commands.map((command, index) => /* @__PURE__ */ jsxs(
89
+ "li",
90
+ {
91
+ className: "flex min-w-0 max-w-full items-center gap-2 overflow-hidden",
92
+ children: [
93
+ /* @__PURE__ */ jsx(TerminalIcon, { className: "size-3 shrink-0" }),
94
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx("code", { className: "block truncate font-mono", children: command }) })
95
+ ]
96
+ },
97
+ `${index}-${command}`
98
+ )) }) : null
99
+ ] });
100
+ }
101
+ function MarkdownLink({
102
+ href = "",
103
+ children,
104
+ ...props
105
+ }) {
106
+ if (href.startsWith("/")) {
107
+ return /* @__PURE__ */ jsx(SilicaLink, { href, ...props, children });
108
+ }
109
+ return /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noreferrer", ...props, children });
110
+ }
111
+ export {
112
+ AssistantMessage
113
+ };
114
+ //# sourceMappingURL=message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ui/message.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport { SilicaLink } from \"@silicajs/components\";\nimport { cn } from \"@silicajs/ui/lib/utils\";\nimport { Button } from \"@silicajs/ui/components/button\";\nimport {\n ChevronDownIcon,\n FileTextIcon,\n LoaderCircleIcon,\n SearchIcon,\n TerminalIcon,\n} from \"lucide-react\";\nimport type { AssistantChatMessage } from \"./provider.js\";\n\nexport type AssistantMessageProps = {\n message: AssistantChatMessage;\n onRetry: () => void;\n};\n\nexport function AssistantMessage({ message, onRetry }: AssistantMessageProps) {\n if (message.role === \"user\") {\n return (\n <div className=\"flex min-w-0 justify-end\">\n <div className=\"min-w-0 max-w-[85%] rounded-xl rounded-br-sm bg-muted px-3 py-2 text-sm whitespace-pre-wrap break-words text-foreground\">\n {message.content}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex min-w-0 max-w-full flex-col gap-2 overflow-hidden\">\n {message.commands.length > 0 ||\n (message.state === \"streaming\" && !message.content) ? (\n <AssistantActivity\n commands={message.commands}\n searching={message.state === \"streaming\" && !message.content}\n />\n ) : null}\n {message.content ? (\n <div className=\"prose prose-sm dark:prose-invert min-w-0 max-w-none break-words text-sm\">\n <ReactMarkdown\n remarkPlugins={[remarkGfm]}\n components={{ a: MarkdownLink }}\n >\n {message.content}\n </ReactMarkdown>\n </div>\n ) : null}\n {message.state === \"error\" ? (\n <div className=\"flex flex-col items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2\">\n <p className=\"text-sm text-destructive\">\n {message.error ?? \"Something went wrong.\"}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onRetry}>\n Retry\n </Button>\n </div>\n ) : null}\n {message.citations.length > 0 ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {message.citations.map((citation) => (\n <SilicaLink\n key={citation.slug}\n href={citation.href}\n className=\"inline-flex max-w-full items-center gap-1.5 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <FileTextIcon className=\"size-3 shrink-0\" />\n <span className=\"truncate\">{citation.title}</span>\n </SilicaLink>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction AssistantActivity({\n commands,\n searching,\n}: {\n commands: string[];\n /** True only while still gathering pages (before the answer streams). */\n searching: boolean;\n}) {\n const [expanded, setExpanded] = React.useState(false);\n\n // Searching has begun but no command has run yet: a plain progress line.\n // Keep the row markup identical to the toggle below so swapping between the\n // two states does not shift anything vertically.\n if (commands.length === 0) {\n return (\n <div className=\"flex min-w-0 max-w-full flex-col gap-1.5 overflow-hidden text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-2 self-start\">\n <LoaderCircleIcon className=\"size-3.5 shrink-0 animate-spin\" />\n <span>Thinking…</span>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex min-w-0 max-w-full flex-col gap-1.5 overflow-hidden text-xs text-muted-foreground\">\n <button\n type=\"button\"\n onClick={() => setExpanded((value) => !value)}\n aria-expanded={expanded}\n className=\"group flex items-center gap-2 self-start rounded-md text-left transition-colors hover:text-foreground\"\n >\n {searching ? (\n <LoaderCircleIcon className=\"size-3.5 shrink-0 animate-spin\" />\n ) : (\n <SearchIcon className=\"size-3.5 shrink-0\" />\n )}\n <span>{searching ? \"Searching the site…\" : \"Searched the site\"}</span>\n <ChevronDownIcon\n className={cn(\n \"size-3.5 shrink-0 transition-transform\",\n expanded && \"rotate-180\",\n )}\n />\n </button>\n {expanded ? (\n <ul className=\"flex min-w-0 max-w-full flex-col gap-1 overflow-hidden pl-[1.375rem]\">\n {commands.map((command, index) => (\n <li\n key={`${index}-${command}`}\n className=\"flex min-w-0 max-w-full items-center gap-2 overflow-hidden\"\n >\n <TerminalIcon className=\"size-3 shrink-0\" />\n <span className=\"min-w-0 flex-1 overflow-hidden\">\n <code className=\"block truncate font-mono\">{command}</code>\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\nfunction MarkdownLink({\n href = \"\",\n children,\n ...props\n}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {\n if (href.startsWith(\"/\")) {\n return (\n <SilicaLink href={href} {...props}>\n {children}\n </SilicaLink>\n );\n }\n return (\n <a href={href} target=\"_blank\" rel=\"noreferrer\" {...props}>\n {children}\n </a>\n );\n}\n"],"mappings":";AA0BQ,cA2BA,YA3BA;AAxBR,YAAY,WAAW;AACvB,OAAO,mBAAmB;AAC1B,OAAO,eAAe;AACtB,SAAS,kBAAkB;AAC3B,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQA,SAAS,iBAAiB,EAAE,SAAS,QAAQ,GAA0B;AAC5E,MAAI,QAAQ,SAAS,QAAQ;AAC3B,WACE,oBAAC,SAAI,WAAU,4BACb,8BAAC,SAAI,WAAU,2HACZ,kBAAQ,SACX,GACF;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,0DACZ;AAAA,YAAQ,SAAS,SAAS,KAC1B,QAAQ,UAAU,eAAe,CAAC,QAAQ,UACzC;AAAA,MAAC;AAAA;AAAA,QACC,UAAU,QAAQ;AAAA,QAClB,WAAW,QAAQ,UAAU,eAAe,CAAC,QAAQ;AAAA;AAAA,IACvD,IACE;AAAA,IACH,QAAQ,UACP,oBAAC,SAAI,WAAU,2EACb;AAAA,MAAC;AAAA;AAAA,QACC,eAAe,CAAC,SAAS;AAAA,QACzB,YAAY,EAAE,GAAG,aAAa;AAAA,QAE7B,kBAAQ;AAAA;AAAA,IACX,GACF,IACE;AAAA,IACH,QAAQ,UAAU,UACjB,qBAAC,SAAI,WAAU,sGACb;AAAA,0BAAC,OAAE,WAAU,4BACV,kBAAQ,SAAS,yBACpB;AAAA,MACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,SAAS,mBAEpE;AAAA,OACF,IACE;AAAA,IACH,QAAQ,UAAU,SAAS,IAC1B,oBAAC,SAAI,WAAU,0BACZ,kBAAQ,UAAU,IAAI,CAAC,aACtB;AAAA,MAAC;AAAA;AAAA,QAEC,MAAM,SAAS;AAAA,QACf,WAAU;AAAA,QAEV;AAAA,8BAAC,gBAAa,WAAU,mBAAkB;AAAA,UAC1C,oBAAC,UAAK,WAAU,YAAY,mBAAS,OAAM;AAAA;AAAA;AAAA,MALtC,SAAS;AAAA,IAMhB,CACD,GACH,IACE;AAAA,KACN;AAEJ;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAKpD,MAAI,SAAS,WAAW,GAAG;AACzB,WACE,oBAAC,SAAI,WAAU,0FACb,+BAAC,SAAI,WAAU,sCACb;AAAA,0BAAC,oBAAiB,WAAU,kCAAiC;AAAA,MAC7D,oBAAC,UAAK,4BAAS;AAAA,OACjB,GACF;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,0FACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,YAAY,CAAC,UAAU,CAAC,KAAK;AAAA,QAC5C,iBAAe;AAAA,QACf,WAAU;AAAA,QAET;AAAA,sBACC,oBAAC,oBAAiB,WAAU,kCAAiC,IAE7D,oBAAC,cAAW,WAAU,qBAAoB;AAAA,UAE5C,oBAAC,UAAM,sBAAY,6BAAwB,qBAAoB;AAAA,UAC/D;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,YAAY;AAAA,cACd;AAAA;AAAA,UACF;AAAA;AAAA;AAAA,IACF;AAAA,IACC,WACC,oBAAC,QAAG,WAAU,wEACX,mBAAS,IAAI,CAAC,SAAS,UACtB;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAEV;AAAA,8BAAC,gBAAa,WAAU,mBAAkB;AAAA,UAC1C,oBAAC,UAAK,WAAU,kCACd,8BAAC,UAAK,WAAU,4BAA4B,mBAAQ,GACtD;AAAA;AAAA;AAAA,MANK,GAAG,KAAK,IAAI,OAAO;AAAA,IAO1B,CACD,GACH,IACE;AAAA,KACN;AAEJ;AAEA,SAAS,aAAa;AAAA,EACpB,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAkD;AAChD,MAAI,KAAK,WAAW,GAAG,GAAG;AACxB,WACE,oBAAC,cAAW,MAAa,GAAG,OACzB,UACH;AAAA,EAEJ;AACA,SACE,oBAAC,OAAE,MAAY,QAAO,UAAS,KAAI,cAAc,GAAG,OACjD,UACH;AAEJ;","names":[]}
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+
3
+ type AssistantPanelProps = {
4
+ className?: string;
5
+ };
6
+ /**
7
+ * The assistant chat window: header, conversation, and composer. Fills its
8
+ * container; themes decide where and how to present it (docked sidebar,
9
+ * overlay, …) and typically mount it only while the assistant is open.
10
+ */
11
+ declare function AssistantPanel({ className }: AssistantPanelProps): React.JSX.Element | null;
12
+
13
+ export { AssistantPanel, type AssistantPanelProps };
@@ -0,0 +1,201 @@
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 { Textarea } from "@silicajs/ui/components/textarea";
6
+ import {
7
+ Tooltip,
8
+ TooltipContent,
9
+ TooltipTrigger
10
+ } from "@silicajs/ui/components/tooltip";
11
+ import { cn } from "@silicajs/ui/lib/utils";
12
+ import {
13
+ ArrowUpIcon,
14
+ SparklesIcon,
15
+ SquareIcon,
16
+ SquarePenIcon,
17
+ XIcon
18
+ } from "lucide-react";
19
+ import { AssistantMessage } from "./message.js";
20
+ import { useAssistant } from "./provider.js";
21
+ function AssistantPanel({ className }) {
22
+ const assistant = useAssistant();
23
+ const scrollRef = React.useRef(null);
24
+ const open = assistant?.open ?? false;
25
+ const messageCount = assistant?.messages.length ?? 0;
26
+ const lastMessage = assistant?.messages.at(-1);
27
+ React.useEffect(() => {
28
+ const node = scrollRef.current;
29
+ if (node) node.scrollTop = node.scrollHeight;
30
+ }, [
31
+ messageCount,
32
+ lastMessage?.content,
33
+ lastMessage?.state,
34
+ lastMessage?.commands.length
35
+ ]);
36
+ if (!assistant) return null;
37
+ return /* @__PURE__ */ jsxs(
38
+ "aside",
39
+ {
40
+ "aria-label": "AI assistant",
41
+ className: cn(
42
+ "flex h-full min-h-0 w-full min-w-0 flex-col bg-background",
43
+ className
44
+ ),
45
+ children: [
46
+ /* @__PURE__ */ jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 px-3", children: [
47
+ /* @__PURE__ */ jsx(SparklesIcon, { className: "size-4 text-primary" }),
48
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm font-semibold tracking-tight", children: "Assistant" }),
49
+ assistant.messages.length > 0 ? /* @__PURE__ */ jsxs(Tooltip, { children: [
50
+ /* @__PURE__ */ jsx(
51
+ TooltipTrigger,
52
+ {
53
+ render: /* @__PURE__ */ jsxs(
54
+ Button,
55
+ {
56
+ type: "button",
57
+ variant: "ghost",
58
+ size: "icon-sm",
59
+ onClick: assistant.reset,
60
+ children: [
61
+ /* @__PURE__ */ jsx(SquarePenIcon, {}),
62
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "New conversation" })
63
+ ]
64
+ }
65
+ )
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "New conversation" })
69
+ ] }) : null,
70
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
71
+ /* @__PURE__ */ jsx(
72
+ TooltipTrigger,
73
+ {
74
+ render: /* @__PURE__ */ jsxs(
75
+ Button,
76
+ {
77
+ type: "button",
78
+ variant: "ghost",
79
+ size: "icon-sm",
80
+ onClick: () => assistant.setOpen(false),
81
+ children: [
82
+ /* @__PURE__ */ jsx(XIcon, {}),
83
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close assistant" })
84
+ ]
85
+ }
86
+ )
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Close assistant" })
90
+ ] })
91
+ ] }),
92
+ /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ ref: scrollRef,
96
+ className: "min-w-0 flex-1 overflow-x-hidden overflow-y-auto",
97
+ children: assistant.messages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-2 px-8 text-center", children: [
98
+ /* @__PURE__ */ jsx(SparklesIcon, { className: "size-6 text-muted-foreground/60" }),
99
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: "Ask anything" }),
100
+ /* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: "Answers come from this site's pages and include links to the sources used." })
101
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-col gap-5 px-4 py-4", children: assistant.messages.map((message) => /* @__PURE__ */ jsx(
102
+ AssistantMessage,
103
+ {
104
+ message,
105
+ onRetry: assistant.retry
106
+ },
107
+ message.id
108
+ )) })
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsx(
112
+ AssistantComposer,
113
+ {
114
+ isStreaming: assistant.isStreaming,
115
+ onAsk: assistant.ask,
116
+ onStop: assistant.stop,
117
+ focusToken: open
118
+ }
119
+ )
120
+ ]
121
+ }
122
+ );
123
+ }
124
+ function AssistantComposer({
125
+ isStreaming,
126
+ onAsk,
127
+ onStop,
128
+ focusToken
129
+ }) {
130
+ const [draft, setDraft] = React.useState("");
131
+ const textareaRef = React.useRef(null);
132
+ React.useEffect(() => {
133
+ if (focusToken) textareaRef.current?.focus();
134
+ }, [focusToken]);
135
+ const submit = () => {
136
+ if (isStreaming || !draft.trim()) return;
137
+ onAsk(draft);
138
+ setDraft("");
139
+ };
140
+ return /* @__PURE__ */ jsx(
141
+ "form",
142
+ {
143
+ className: "shrink-0 px-3 pb-3",
144
+ onSubmit: (event) => {
145
+ event.preventDefault();
146
+ submit();
147
+ },
148
+ children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
149
+ /* @__PURE__ */ jsx(
150
+ Textarea,
151
+ {
152
+ ref: textareaRef,
153
+ value: draft,
154
+ onChange: (event) => setDraft(event.target.value),
155
+ onKeyDown: (event) => {
156
+ if (event.key === "Enter" && !event.shiftKey) {
157
+ event.preventDefault();
158
+ submit();
159
+ }
160
+ },
161
+ placeholder: "Ask a question\u2026",
162
+ rows: 2,
163
+ className: "max-h-40 min-h-14 resize-none pr-11"
164
+ }
165
+ ),
166
+ isStreaming ? /* @__PURE__ */ jsxs(
167
+ Button,
168
+ {
169
+ type: "button",
170
+ variant: "outline",
171
+ size: "icon-sm",
172
+ className: "absolute bottom-1.5 right-1.5",
173
+ onClick: onStop,
174
+ title: "Stop answering",
175
+ children: [
176
+ /* @__PURE__ */ jsx(SquareIcon, {}),
177
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Stop answering" })
178
+ ]
179
+ }
180
+ ) : /* @__PURE__ */ jsxs(
181
+ Button,
182
+ {
183
+ type: "submit",
184
+ size: "icon-sm",
185
+ className: "absolute bottom-1.5 right-1.5",
186
+ disabled: !draft.trim(),
187
+ title: "Send question",
188
+ children: [
189
+ /* @__PURE__ */ jsx(ArrowUpIcon, {}),
190
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Send question" })
191
+ ]
192
+ }
193
+ )
194
+ ] })
195
+ }
196
+ );
197
+ }
198
+ export {
199
+ AssistantPanel
200
+ };
201
+ //# sourceMappingURL=panel.js.map