arcie 0.1.5 → 0.1.6
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/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/web-chat/app/globals.css +10 -0
- package/templates/web-chat/app/layout.tsx +1 -1
- package/templates/web-chat/app/page.tsx +1 -1
- package/templates/web-chat/components/chat.tsx +179 -156
- package/templates/web-chat/components/input-bar.tsx +146 -39
- package/templates/web-chat/components/message.tsx +90 -30
- package/templates/web-chat/components/tool-call.tsx +8 -8
- package/templates/web-chat/lib/types.ts +1 -0
- package/templates/web-chat/next.config.mjs +3 -1
- package/templates/web-chat/package.json +1 -0
- package/templates/web-chat/tailwind.config.ts +3 -1
package/dist/cli/index.js
CHANGED
|
@@ -149,9 +149,20 @@ function createCliProgram(logger) {
|
|
|
149
149
|
});
|
|
150
150
|
return program;
|
|
151
151
|
}
|
|
152
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["channels", "init", "build", "dev", "help"]);
|
|
153
|
+
function resolveArgv(argv) {
|
|
154
|
+
if (argv.length === 0) return ["dev"];
|
|
155
|
+
const first = argv[0];
|
|
156
|
+
if (first === "-h" || first === "--help" || first === "-V" || first === "--version") {
|
|
157
|
+
return [...argv];
|
|
158
|
+
}
|
|
159
|
+
if (first.startsWith("-")) return [...argv];
|
|
160
|
+
if (KNOWN_COMMANDS.has(first)) return [...argv];
|
|
161
|
+
return ["init", ...argv];
|
|
162
|
+
}
|
|
152
163
|
async function runCli(argv = process.argv.slice(2), logger = console) {
|
|
153
164
|
const program = createCliProgram(logger);
|
|
154
|
-
const input = argv
|
|
165
|
+
const input = resolveArgv(argv);
|
|
155
166
|
try {
|
|
156
167
|
await program.parseAsync(input, { from: "user" });
|
|
157
168
|
} catch (error) {
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve as resolvePath } from \"node:path\";\nimport { Command, CommanderError, InvalidArgumentError } from \"commander\";\nimport { arcieCliBanner, version } from \"./banner\";\nimport { createCliTheme, renderCliTaggedLine } from \"./ui/output\";\n\ninterface CliLogger {\n error(message: string): void;\n log(message: string): void;\n}\n\nconst DISPLAY_MODES = new Set([\"full\", \"collapsed\", \"auto-collapsed\", \"hidden\"]);\nconst STATS_MODES = new Set([\"tokens\", \"tokensPerSecond\"]);\nconst LOG_MODES = new Set([\"all\", \"stderr\", \"none\"]);\n\ntype TerminalPartDisplayMode = \"full\" | \"collapsed\" | \"auto-collapsed\" | \"hidden\";\ntype AssistantResponseStatsMode = \"tokens\" | \"tokensPerSecond\";\ntype LogDisplayMode = \"all\" | \"stderr\" | \"none\";\n\nfunction parsePortOption(value: string): number {\n if (!/^-?\\d+$/.test(value)) {\n throw new InvalidArgumentError(`Expected a numeric port, received \"${value}\".`);\n }\n const port = Number(value);\n if (port < 0 || port > 65_535) {\n throw new InvalidArgumentError(`Expected a port between 0 and 65535, received \"${value}\".`);\n }\n return port;\n}\n\nfunction parseDisplayMode(value: string): TerminalPartDisplayMode {\n if (!DISPLAY_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...DISPLAY_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as TerminalPartDisplayMode;\n}\n\nfunction parseStatsMode(value: string): AssistantResponseStatsMode {\n if (!STATS_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...STATS_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as AssistantResponseStatsMode;\n}\n\nfunction parseLogsMode(value: string): LogDisplayMode {\n if (!LOG_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...LOG_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as LogDisplayMode;\n}\n\nfunction parseContextSizeOption(value: string): number {\n const size = Number(value);\n if (!Number.isFinite(size) || size <= 0) {\n throw new InvalidArgumentError(`Expected a positive number, received \"${value}\".`);\n }\n return size;\n}\n\nfunction shouldPrintCliBootBanner(actionCommand: Command): boolean {\n const name = actionCommand.name();\n return name === \"info\" || name === \"dev\" || name === \"init\";\n}\n\nfunction createCliProgram(logger: CliLogger): Command {\n const program = new Command();\n const theme = createCliTheme();\n\n program\n .name(\"arcie\")\n .description(\"arcie — the electronic line, build agents at the speed of light\")\n .version(version())\n .showHelpAfterError()\n .exitOverride()\n .hook(\"preAction\", (_program, actionCommand) => {\n if (shouldPrintCliBootBanner(actionCommand)) {\n logger.log(arcieCliBanner());\n }\n })\n .configureOutput({\n writeErr: (message) => logger.error(message.trimEnd()),\n writeOut: (message) => logger.log(message.trimEnd()),\n });\n\n const channels = program\n .command(\"channels\")\n .description(\"Manage channels for the current agent (web UI, HTTP, etc.).\");\n\n channels\n .command(\"add <kind>\")\n .description(\"Scaffold a channel (currently supports: web).\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \".\")\n .action(async (kind: string, options: { agentDir: string }) => {\n if (kind !== \"web\") {\n logger.error(`unknown channel kind: ${kind}. supported: web`);\n process.exit(1);\n }\n const agentDir = resolvePath(process.cwd(), options.agentDir);\n const { scaffoldWebChat } = await import(\"./scaffold-web-chat\");\n try {\n const result = scaffoldWebChat(agentDir);\n if (result.alreadyExisted) {\n logger.error(`channels/web already exists at ${result.targetPath}`);\n process.exit(1);\n }\n logger.log(\n renderCliTaggedLine(theme, {\n message: `scaffolded ${result.targetPath}`,\n tag: \"channels\",\n tone: \"success\",\n }),\n );\n logger.log(\n renderCliTaggedLine(theme, {\n message: \"cd into it, then: npm install && cp .env.local.example .env.local && npm run dev\",\n tag: \"next\",\n tone: \"info\",\n }),\n );\n } catch (err) {\n logger.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\n program\n .command(\"init [target]\")\n .description(\"Create a new arcie agent, or add one to an existing project directory.\")\n .option(\"--template <name>\", \"Template to use (default, agent-only)\", \"default\")\n .option(\"-y, --yes\", \"Accepted for compatibility; has no effect\")\n .action(async (target: string | undefined, options: { template: string; yes?: boolean }) => {\n if (options.yes) logger.error(\"warning: --yes has no effect for arcie init.\");\n const { initCommand } = await import(\"./init\");\n await initCommand(target, { template: options.template });\n });\n\n program\n .command(\"build\")\n .description(\"Compile the agent for production.\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--out-dir <path>\", \"Output directory\", \".arcie\")\n .action(async (options: { agentDir: string; outDir: string }) => {\n const { buildCommand } = await import(\"./build\");\n await buildCommand(options);\n logger.log(\n renderCliTaggedLine(theme, {\n message: `output at ${options.outDir}`,\n tag: \"build\",\n tone: \"success\",\n }),\n );\n });\n\n program\n .command(\"dev\")\n .description(\"Start the arcie development server or attach an interactive UI.\")\n .option(\"-p, --port <port>\", \"Port to listen on\", parsePortOption, 3000)\n .option(\"--host <host>\", \"Host interface to bind\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--input <text>\", \"Pre-fill the prompt input, or start onboarding with /model\")\n .option(\"--no-ui\", \"Start the server without an interactive UI\")\n .option(\"--name <name>\", \"Title shown in the terminal UI (defaults to the app folder name)\")\n .option(\n \"--tools <mode>\",\n \"How tool calls render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--reasoning <mode>\",\n \"How reasoning renders: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--subagents <mode>\",\n \"How subagent sections render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--assistant-response-stats <mode>\",\n \"Assistant header statistic: tokens | tokensPerSecond\",\n parseStatsMode,\n )\n .option(\n \"--context-size <tokens>\",\n \"Model context window size, shown as a usage percentage\",\n parseContextSizeOption,\n )\n .option(\"--logs <mode>\", \"Which logs to show: all | stderr | none\", parseLogsMode)\n .action(async (options) => {\n const { devCommand } = await import(\"./dev\");\n await devCommand({\n port: String(options.port ?? 3000),\n agentDir: options.agentDir,\n input: options.ui === false ? false : Boolean(options.input) || options.ui !== false,\n });\n });\n\n return program;\n}\n\nexport async function runCli(\n argv: string[] = process.argv.slice(2),\n logger: CliLogger = console,\n): Promise<void> {\n const program = createCliProgram(logger);\n const input = argv.length === 0 ? [\"dev\"] : argv;\n\n try {\n await program.parseAsync(input, { from: \"user\" });\n } catch (error) {\n if (error instanceof CommanderError) {\n if (error.exitCode === 0) return;\n throw new Error(error.message);\n }\n throw error;\n }\n}\n\nrunCli().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve as resolvePath } from \"node:path\";\nimport { Command, CommanderError, InvalidArgumentError } from \"commander\";\nimport { arcieCliBanner, version } from \"./banner\";\nimport { createCliTheme, renderCliTaggedLine } from \"./ui/output\";\n\ninterface CliLogger {\n error(message: string): void;\n log(message: string): void;\n}\n\nconst DISPLAY_MODES = new Set([\"full\", \"collapsed\", \"auto-collapsed\", \"hidden\"]);\nconst STATS_MODES = new Set([\"tokens\", \"tokensPerSecond\"]);\nconst LOG_MODES = new Set([\"all\", \"stderr\", \"none\"]);\n\ntype TerminalPartDisplayMode = \"full\" | \"collapsed\" | \"auto-collapsed\" | \"hidden\";\ntype AssistantResponseStatsMode = \"tokens\" | \"tokensPerSecond\";\ntype LogDisplayMode = \"all\" | \"stderr\" | \"none\";\n\nfunction parsePortOption(value: string): number {\n if (!/^-?\\d+$/.test(value)) {\n throw new InvalidArgumentError(`Expected a numeric port, received \"${value}\".`);\n }\n const port = Number(value);\n if (port < 0 || port > 65_535) {\n throw new InvalidArgumentError(`Expected a port between 0 and 65535, received \"${value}\".`);\n }\n return port;\n}\n\nfunction parseDisplayMode(value: string): TerminalPartDisplayMode {\n if (!DISPLAY_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...DISPLAY_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as TerminalPartDisplayMode;\n}\n\nfunction parseStatsMode(value: string): AssistantResponseStatsMode {\n if (!STATS_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...STATS_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as AssistantResponseStatsMode;\n}\n\nfunction parseLogsMode(value: string): LogDisplayMode {\n if (!LOG_MODES.has(value)) {\n throw new InvalidArgumentError(\n `Expected one of ${[...LOG_MODES].join(\", \")}, received \"${value}\".`,\n );\n }\n return value as LogDisplayMode;\n}\n\nfunction parseContextSizeOption(value: string): number {\n const size = Number(value);\n if (!Number.isFinite(size) || size <= 0) {\n throw new InvalidArgumentError(`Expected a positive number, received \"${value}\".`);\n }\n return size;\n}\n\nfunction shouldPrintCliBootBanner(actionCommand: Command): boolean {\n const name = actionCommand.name();\n return name === \"info\" || name === \"dev\" || name === \"init\";\n}\n\nfunction createCliProgram(logger: CliLogger): Command {\n const program = new Command();\n const theme = createCliTheme();\n\n program\n .name(\"arcie\")\n .description(\"arcie — the electronic line, build agents at the speed of light\")\n .version(version())\n .showHelpAfterError()\n .exitOverride()\n .hook(\"preAction\", (_program, actionCommand) => {\n if (shouldPrintCliBootBanner(actionCommand)) {\n logger.log(arcieCliBanner());\n }\n })\n .configureOutput({\n writeErr: (message) => logger.error(message.trimEnd()),\n writeOut: (message) => logger.log(message.trimEnd()),\n });\n\n const channels = program\n .command(\"channels\")\n .description(\"Manage channels for the current agent (web UI, HTTP, etc.).\");\n\n channels\n .command(\"add <kind>\")\n .description(\"Scaffold a channel (currently supports: web).\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \".\")\n .action(async (kind: string, options: { agentDir: string }) => {\n if (kind !== \"web\") {\n logger.error(`unknown channel kind: ${kind}. supported: web`);\n process.exit(1);\n }\n const agentDir = resolvePath(process.cwd(), options.agentDir);\n const { scaffoldWebChat } = await import(\"./scaffold-web-chat\");\n try {\n const result = scaffoldWebChat(agentDir);\n if (result.alreadyExisted) {\n logger.error(`channels/web already exists at ${result.targetPath}`);\n process.exit(1);\n }\n logger.log(\n renderCliTaggedLine(theme, {\n message: `scaffolded ${result.targetPath}`,\n tag: \"channels\",\n tone: \"success\",\n }),\n );\n logger.log(\n renderCliTaggedLine(theme, {\n message: \"cd into it, then: npm install && cp .env.local.example .env.local && npm run dev\",\n tag: \"next\",\n tone: \"info\",\n }),\n );\n } catch (err) {\n logger.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n });\n\n program\n .command(\"init [target]\")\n .description(\"Create a new arcie agent, or add one to an existing project directory.\")\n .option(\"--template <name>\", \"Template to use (default, agent-only)\", \"default\")\n .option(\"-y, --yes\", \"Accepted for compatibility; has no effect\")\n .action(async (target: string | undefined, options: { template: string; yes?: boolean }) => {\n if (options.yes) logger.error(\"warning: --yes has no effect for arcie init.\");\n const { initCommand } = await import(\"./init\");\n await initCommand(target, { template: options.template });\n });\n\n program\n .command(\"build\")\n .description(\"Compile the agent for production.\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--out-dir <path>\", \"Output directory\", \".arcie\")\n .action(async (options: { agentDir: string; outDir: string }) => {\n const { buildCommand } = await import(\"./build\");\n await buildCommand(options);\n logger.log(\n renderCliTaggedLine(theme, {\n message: `output at ${options.outDir}`,\n tag: \"build\",\n tone: \"success\",\n }),\n );\n });\n\n program\n .command(\"dev\")\n .description(\"Start the arcie development server or attach an interactive UI.\")\n .option(\"-p, --port <port>\", \"Port to listen on\", parsePortOption, 3000)\n .option(\"--host <host>\", \"Host interface to bind\")\n .option(\"--agent-dir <path>\", \"Path to agent directory\", \"agent\")\n .option(\"--input <text>\", \"Pre-fill the prompt input, or start onboarding with /model\")\n .option(\"--no-ui\", \"Start the server without an interactive UI\")\n .option(\"--name <name>\", \"Title shown in the terminal UI (defaults to the app folder name)\")\n .option(\n \"--tools <mode>\",\n \"How tool calls render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--reasoning <mode>\",\n \"How reasoning renders: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--subagents <mode>\",\n \"How subagent sections render: full | collapsed | auto-collapsed | hidden\",\n parseDisplayMode,\n )\n .option(\n \"--assistant-response-stats <mode>\",\n \"Assistant header statistic: tokens | tokensPerSecond\",\n parseStatsMode,\n )\n .option(\n \"--context-size <tokens>\",\n \"Model context window size, shown as a usage percentage\",\n parseContextSizeOption,\n )\n .option(\"--logs <mode>\", \"Which logs to show: all | stderr | none\", parseLogsMode)\n .action(async (options) => {\n const { devCommand } = await import(\"./dev\");\n await devCommand({\n port: String(options.port ?? 3000),\n agentDir: options.agentDir,\n input: options.ui === false ? false : Boolean(options.input) || options.ui !== false,\n });\n });\n\n return program;\n}\n\nconst KNOWN_COMMANDS = new Set([\"channels\", \"init\", \"build\", \"dev\", \"help\"]);\n\nfunction resolveArgv(argv: readonly string[]): string[] {\n if (argv.length === 0) return [\"dev\"];\n const first = argv[0]!;\n if (first === \"-h\" || first === \"--help\" || first === \"-V\" || first === \"--version\") {\n return [...argv];\n }\n if (first.startsWith(\"-\")) return [...argv];\n if (KNOWN_COMMANDS.has(first)) return [...argv];\n // Bare positional like `arcie my-agent` — treat as `arcie init my-agent`.\n return [\"init\", ...argv];\n}\n\nexport async function runCli(\n argv: string[] = process.argv.slice(2),\n logger: CliLogger = console,\n): Promise<void> {\n const program = createCliProgram(logger);\n const input = resolveArgv(argv);\n\n try {\n await program.parseAsync(input, { from: \"user\" });\n } catch (error) {\n if (error instanceof CommanderError) {\n if (error.exitCode === 0) return;\n throw new Error(error.message);\n }\n throw error;\n }\n}\n\nrunCli().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -49,7 +49,17 @@
|
|
|
49
49
|
* {
|
|
50
50
|
@apply border-border;
|
|
51
51
|
}
|
|
52
|
+
html,
|
|
52
53
|
body {
|
|
53
54
|
@apply bg-background text-foreground antialiased;
|
|
55
|
+
min-height: 100%;
|
|
56
|
+
}
|
|
57
|
+
/* Hide Next.js dev-mode injected UI (SegmentExplorer, portals, toasts). */
|
|
58
|
+
nextjs-portal,
|
|
59
|
+
[data-nextjs-toast-wrapper],
|
|
60
|
+
[data-nextjs-dialog-overlay],
|
|
61
|
+
[data-nextjs-scroll-focus-boundary],
|
|
62
|
+
[data-nextjs-devtools-panel] {
|
|
63
|
+
display: none !important;
|
|
54
64
|
}
|
|
55
65
|
}
|
|
@@ -8,7 +8,7 @@ export const metadata: Metadata = {
|
|
|
8
8
|
|
|
9
9
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
10
|
return (
|
|
11
|
-
<html lang="en" className="h-full">
|
|
11
|
+
<html lang="en" className="h-full dark" suppressHydrationWarning>
|
|
12
12
|
<body className="h-full">{children}</body>
|
|
13
13
|
</html>
|
|
14
14
|
);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Zap } from "lucide-react";
|
|
5
4
|
import { InputBar } from "@/components/input-bar";
|
|
6
5
|
import { Message } from "@/components/message";
|
|
7
6
|
import { readArcieStream } from "@/lib/stream";
|
|
8
7
|
import type { UiMessage, UiToolCall } from "@/lib/types";
|
|
9
|
-
import { cn } from "@/lib/utils";
|
|
10
8
|
|
|
11
9
|
function newId(prefix: string): string {
|
|
12
10
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -16,10 +14,12 @@ export function Chat() {
|
|
|
16
14
|
const [messages, setMessages] = React.useState<UiMessage[]>([]);
|
|
17
15
|
const [streaming, setStreaming] = React.useState(false);
|
|
18
16
|
const abortRef = React.useRef<AbortController | undefined>(undefined);
|
|
19
|
-
const
|
|
17
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
20
18
|
|
|
21
19
|
React.useEffect(() => {
|
|
22
|
-
|
|
20
|
+
const el = containerRef.current;
|
|
21
|
+
if (el === null) return;
|
|
22
|
+
el.scrollTop = el.scrollHeight;
|
|
23
23
|
}, [messages]);
|
|
24
24
|
|
|
25
25
|
const stop = () => {
|
|
@@ -27,170 +27,193 @@ export function Chat() {
|
|
|
27
27
|
setStreaming(false);
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
id: assistantId,
|
|
35
|
-
role: "assistant",
|
|
36
|
-
content: "",
|
|
37
|
-
streaming: true,
|
|
38
|
-
toolCalls: [],
|
|
39
|
-
};
|
|
40
|
-
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
|
41
|
-
setStreaming(true);
|
|
42
|
-
|
|
43
|
-
const controller = new AbortController();
|
|
44
|
-
abortRef.current = controller;
|
|
45
|
-
|
|
46
|
-
const updateAssistant = (patch: (prev: UiMessage) => UiMessage) => {
|
|
47
|
-
setMessages((prev) =>
|
|
48
|
-
prev.map((message) => (message.id === assistantId ? patch(message) : message)),
|
|
49
|
-
);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const response = await fetch("/api/chat", {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: { "Content-Type": "application/json" },
|
|
56
|
-
body: JSON.stringify({ message: text }),
|
|
57
|
-
signal: controller.signal,
|
|
58
|
-
});
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
const errorBody = await response.text();
|
|
61
|
-
updateAssistant((m) => ({
|
|
62
|
-
...m,
|
|
63
|
-
content: errorBody || `Server error (${response.status})`,
|
|
64
|
-
streaming: false,
|
|
65
|
-
errored: true,
|
|
66
|
-
}));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
30
|
+
const clearAll = () => {
|
|
31
|
+
if (streaming) stop();
|
|
32
|
+
setMessages([]);
|
|
33
|
+
};
|
|
69
34
|
|
|
70
|
-
|
|
35
|
+
const copy = (text: string) => {
|
|
36
|
+
void navigator.clipboard.writeText(text);
|
|
37
|
+
};
|
|
71
38
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
39
|
+
const send = React.useCallback(
|
|
40
|
+
async (text: string, historyOverride?: UiMessage[]) => {
|
|
41
|
+
const base = historyOverride ?? messages;
|
|
42
|
+
const userMessage: UiMessage = { id: newId("u"), role: "user", content: text };
|
|
43
|
+
const assistantId = newId("a");
|
|
44
|
+
const assistantMessage: UiMessage = {
|
|
45
|
+
id: assistantId,
|
|
46
|
+
role: "assistant",
|
|
47
|
+
content: "",
|
|
48
|
+
streaming: true,
|
|
49
|
+
toolCalls: [],
|
|
50
|
+
};
|
|
51
|
+
const isRegeneration = historyOverride !== undefined;
|
|
52
|
+
setMessages((prev) => (isRegeneration ? [...base, assistantMessage] : [...prev, userMessage, assistantMessage]));
|
|
53
|
+
setStreaming(true);
|
|
54
|
+
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
abortRef.current = controller;
|
|
57
|
+
const startedAt = Date.now();
|
|
58
|
+
|
|
59
|
+
const patchAssistant = (patch: (prev: UiMessage) => UiMessage) => {
|
|
60
|
+
setMessages((prev) =>
|
|
61
|
+
prev.map((message) => (message.id === assistantId ? patch(message) : message)),
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch("/api/chat", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({ message: text }),
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const body = await response.text();
|
|
74
|
+
patchAssistant((m) => ({
|
|
75
|
+
...m,
|
|
76
|
+
content: body || `Server error (${response.status})`,
|
|
77
|
+
streaming: false,
|
|
78
|
+
errored: true,
|
|
79
|
+
}));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const toolIndex = new Map<string, number>();
|
|
84
|
+
|
|
85
|
+
for await (const event of readArcieStream(response)) {
|
|
86
|
+
switch (event.type) {
|
|
87
|
+
case "message.appended": {
|
|
88
|
+
const delta = (event.data as { delta?: string }).delta ?? "";
|
|
89
|
+
patchAssistant((m) => ({ ...m, content: `${m.content}${delta}` }));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "message.completed": {
|
|
93
|
+
const finished = (event.data as { text?: string | null }).text;
|
|
94
|
+
patchAssistant((m) => ({
|
|
95
|
+
...m,
|
|
96
|
+
content: typeof finished === "string" && finished.length > 0 ? finished : m.content,
|
|
97
|
+
}));
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "reasoning.appended": {
|
|
101
|
+
const delta = (event.data as { delta?: string }).delta ?? "";
|
|
102
|
+
patchAssistant((m) => ({ ...m, reasoning: `${m.reasoning ?? ""}${delta}` }));
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "tool.started": {
|
|
106
|
+
const data = event.data as { name: string; callId: string; input: unknown };
|
|
107
|
+
const call: UiToolCall = {
|
|
108
|
+
callId: data.callId,
|
|
109
|
+
name: data.name,
|
|
110
|
+
input: data.input,
|
|
111
|
+
status: "running",
|
|
130
112
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
113
|
+
patchAssistant((m) => {
|
|
114
|
+
const toolCalls = m.toolCalls ?? [];
|
|
115
|
+
toolIndex.set(data.callId, toolCalls.length);
|
|
116
|
+
return { ...m, toolCalls: [...toolCalls, call] };
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "tool.completed": {
|
|
121
|
+
const data = event.data as {
|
|
122
|
+
callId: string;
|
|
123
|
+
output: unknown;
|
|
124
|
+
status: string;
|
|
125
|
+
error?: { code: string; message: string };
|
|
126
|
+
};
|
|
127
|
+
patchAssistant((m) => {
|
|
128
|
+
const toolCalls = [...(m.toolCalls ?? [])];
|
|
129
|
+
const idx = toolIndex.get(data.callId);
|
|
130
|
+
if (idx === undefined || toolCalls[idx] === undefined) return m;
|
|
131
|
+
const previous = toolCalls[idx];
|
|
132
|
+
const isApproval =
|
|
133
|
+
data.status === "pending" && data.error?.code === "needs_approval";
|
|
134
|
+
toolCalls[idx] = {
|
|
135
|
+
...previous,
|
|
136
|
+
status: isApproval
|
|
137
|
+
? "approval"
|
|
138
|
+
: data.status === "completed"
|
|
139
|
+
? "done"
|
|
140
|
+
: "error",
|
|
141
|
+
output: data.output,
|
|
142
|
+
errorMessage: data.error?.message,
|
|
143
|
+
};
|
|
144
|
+
return { ...m, toolCalls };
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "step.failed":
|
|
149
|
+
case "turn.failed":
|
|
150
|
+
case "session.failed": {
|
|
151
|
+
const data = event.data as { code?: string; message?: string };
|
|
152
|
+
patchAssistant((m) => ({
|
|
153
|
+
...m,
|
|
154
|
+
content: data.message ?? "Something went wrong.",
|
|
155
|
+
streaming: false,
|
|
156
|
+
errored: true,
|
|
157
|
+
}));
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
146
160
|
}
|
|
147
161
|
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!controller.signal.aborted) {
|
|
164
|
+
patchAssistant((m) => ({
|
|
165
|
+
...m,
|
|
166
|
+
content: error instanceof Error ? error.message : String(error),
|
|
167
|
+
streaming: false,
|
|
168
|
+
errored: true,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
} finally {
|
|
172
|
+
const latencyMs = Date.now() - startedAt;
|
|
173
|
+
patchAssistant((m) => ({ ...m, streaming: false, latencyMs }));
|
|
174
|
+
setStreaming(false);
|
|
175
|
+
abortRef.current = undefined;
|
|
148
176
|
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
} finally {
|
|
161
|
-
updateAssistant((m) => ({ ...m, streaming: false }));
|
|
162
|
-
setStreaming(false);
|
|
163
|
-
abortRef.current = undefined;
|
|
164
|
-
}
|
|
177
|
+
},
|
|
178
|
+
[messages],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const regenerate = () => {
|
|
182
|
+
const lastUserIndex = messages.map((m) => m.role).lastIndexOf("user");
|
|
183
|
+
if (lastUserIndex === -1) return;
|
|
184
|
+
const historyBefore = messages.slice(0, lastUserIndex);
|
|
185
|
+
const lastUser = messages[lastUserIndex]!;
|
|
186
|
+
setMessages([...historyBefore, lastUser]);
|
|
187
|
+
void send(lastUser.content, [...historyBefore, lastUser]);
|
|
165
188
|
};
|
|
166
189
|
|
|
167
190
|
return (
|
|
168
|
-
<div className="
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
191
|
+
<div className="fixed inset-0 flex flex-col overflow-hidden bg-background text-foreground">
|
|
192
|
+
<div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
193
|
+
<div className="mx-auto flex min-h-full w-full max-w-3xl flex-col justify-start gap-6 pt-8 pb-4">
|
|
194
|
+
{messages.map((message, index) => {
|
|
195
|
+
const isLast =
|
|
196
|
+
message.role === "assistant" && index === messages.length - 1 && !streaming;
|
|
197
|
+
return (
|
|
198
|
+
<Message
|
|
199
|
+
key={message.id}
|
|
200
|
+
message={message}
|
|
201
|
+
isLast={isLast}
|
|
202
|
+
onCopy={copy}
|
|
203
|
+
onRegenerate={regenerate}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
})}
|
|
181
207
|
</div>
|
|
182
208
|
</div>
|
|
183
|
-
<InputBar onSend={send} onStop={stop} streaming={streaming} />
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
209
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
<InputBar
|
|
211
|
+
onSend={(text) => void send(text)}
|
|
212
|
+
onStop={stop}
|
|
213
|
+
onClear={clearAll}
|
|
214
|
+
streaming={streaming}
|
|
215
|
+
hasMessages={messages.length > 0}
|
|
216
|
+
/>
|
|
194
217
|
</div>
|
|
195
218
|
);
|
|
196
219
|
}
|
|
@@ -1,26 +1,39 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { ArrowUp, Paperclip, StopCircle, Trash2, X } from "lucide-react";
|
|
6
5
|
import { cn } from "@/lib/utils";
|
|
7
6
|
|
|
8
7
|
interface InputBarProps {
|
|
9
|
-
onSend(message: string): void;
|
|
8
|
+
onSend(message: string, files?: File[]): void;
|
|
10
9
|
onStop(): void;
|
|
10
|
+
onClear?(): void;
|
|
11
11
|
streaming: boolean;
|
|
12
12
|
disabled?: boolean;
|
|
13
|
+
hasMessages?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function InputBar({
|
|
16
|
+
export function InputBar({
|
|
17
|
+
onSend,
|
|
18
|
+
onStop,
|
|
19
|
+
onClear,
|
|
20
|
+
streaming,
|
|
21
|
+
disabled,
|
|
22
|
+
hasMessages,
|
|
23
|
+
}: InputBarProps) {
|
|
16
24
|
const [value, setValue] = React.useState("");
|
|
25
|
+
const [files, setFiles] = React.useState<File[]>([]);
|
|
17
26
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
27
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
18
28
|
|
|
19
29
|
const submit = () => {
|
|
20
30
|
const trimmed = value.trim();
|
|
21
|
-
if (trimmed.length === 0 || streaming || disabled) return;
|
|
22
|
-
onSend(trimmed);
|
|
31
|
+
if ((trimmed.length === 0 && files.length === 0) || streaming || disabled) return;
|
|
32
|
+
onSend(trimmed, files.length > 0 ? files : undefined);
|
|
23
33
|
setValue("");
|
|
34
|
+
setFiles([]);
|
|
35
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
36
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
24
37
|
};
|
|
25
38
|
|
|
26
39
|
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -30,44 +43,138 @@ export function InputBar({ onSend, onStop, streaming, disabled }: InputBarProps)
|
|
|
30
43
|
}
|
|
31
44
|
};
|
|
32
45
|
|
|
33
|
-
React.
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
47
|
+
setValue(event.target.value);
|
|
48
|
+
const el = event.target;
|
|
36
49
|
el.style.height = "auto";
|
|
37
|
-
el.style.height = `${Math.min(el.scrollHeight,
|
|
38
|
-
}
|
|
50
|
+
el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
54
|
+
const next = event.target.files;
|
|
55
|
+
if (next === null) return;
|
|
56
|
+
setFiles((prev) => [...prev, ...Array.from(next)]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const removeFile = (index: number) => {
|
|
60
|
+
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const openFilePicker = () => {
|
|
64
|
+
fileInputRef.current?.click();
|
|
65
|
+
};
|
|
39
66
|
|
|
40
67
|
return (
|
|
41
|
-
<div className="
|
|
42
|
-
<div className="mx-auto
|
|
43
|
-
<
|
|
44
|
-
ref={textareaRef}
|
|
45
|
-
value={value}
|
|
46
|
-
onChange={(event) => setValue(event.target.value)}
|
|
47
|
-
onKeyDown={onKeyDown}
|
|
48
|
-
rows={1}
|
|
49
|
-
placeholder={streaming ? "…" : "Send a message"}
|
|
50
|
-
disabled={disabled}
|
|
68
|
+
<div className="shrink-0 bg-transparent px-4 pt-4 pb-6">
|
|
69
|
+
<div className="mx-auto w-full max-w-3xl">
|
|
70
|
+
<div
|
|
51
71
|
className={cn(
|
|
52
|
-
"flex
|
|
53
|
-
"
|
|
54
|
-
"
|
|
72
|
+
"relative flex flex-col rounded-2xl border border-border/40 bg-muted/10 backdrop-blur-md p-3 transition-all",
|
|
73
|
+
"hover:border-border/60 hover:bg-muted/20",
|
|
74
|
+
"focus-within:border-primary/45 focus-within:ring-1 focus-within:ring-primary/20",
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{files.length > 0 && (
|
|
78
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
79
|
+
{files.map((file, index) => (
|
|
80
|
+
<div
|
|
81
|
+
key={`${file.name}-${index}`}
|
|
82
|
+
className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-card/60 text-[11px] font-medium text-foreground border border-border/30"
|
|
83
|
+
>
|
|
84
|
+
<Paperclip className="h-3 w-3 text-muted-foreground/70" />
|
|
85
|
+
<span className="max-w-[160px] truncate">{file.name}</span>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => removeFile(index)}
|
|
89
|
+
className="text-muted-foreground/50 hover:text-foreground transition-colors"
|
|
90
|
+
title="Remove attachment"
|
|
91
|
+
>
|
|
92
|
+
<X className="h-3 w-3" />
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
55
97
|
)}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
|
|
99
|
+
<textarea
|
|
100
|
+
ref={textareaRef}
|
|
101
|
+
value={value}
|
|
102
|
+
onChange={onChange}
|
|
103
|
+
onKeyDown={onKeyDown}
|
|
104
|
+
rows={2}
|
|
105
|
+
placeholder={streaming ? "…" : "Ask a question..."}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
className={cn(
|
|
108
|
+
"max-h-40 min-h-[48px] w-full resize-none bg-transparent py-1.5 text-xs",
|
|
109
|
+
"placeholder:text-muted-foreground/50 focus:outline-none leading-relaxed",
|
|
110
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<div className="flex items-center justify-between pt-2.5 mt-2 select-none">
|
|
115
|
+
<div className="flex items-center gap-1.5">
|
|
116
|
+
<input
|
|
117
|
+
ref={fileInputRef}
|
|
118
|
+
type="file"
|
|
119
|
+
multiple
|
|
120
|
+
onChange={onFileChange}
|
|
121
|
+
className="hidden"
|
|
122
|
+
/>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={openFilePicker}
|
|
126
|
+
disabled={streaming}
|
|
127
|
+
className={cn(
|
|
128
|
+
"h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 transition-colors",
|
|
129
|
+
"hover:bg-muted/30 hover:text-foreground",
|
|
130
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
131
|
+
)}
|
|
132
|
+
aria-label="Attach files"
|
|
133
|
+
title="Attach files"
|
|
134
|
+
>
|
|
135
|
+
<Paperclip className="h-4 w-4" />
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
140
|
+
{hasMessages && onClear && (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={onClear}
|
|
144
|
+
className="h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 hover:bg-muted/30 hover:text-foreground transition-colors"
|
|
145
|
+
aria-label="New chat"
|
|
146
|
+
title="New chat"
|
|
147
|
+
>
|
|
148
|
+
<Trash2 className="h-4 w-4" />
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
151
|
+
{streaming ? (
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={onStop}
|
|
155
|
+
className="h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background hover:bg-foreground/90 transition-colors"
|
|
156
|
+
aria-label="Stop generation"
|
|
157
|
+
>
|
|
158
|
+
<StopCircle className="h-3.5 w-3.5 fill-current" />
|
|
159
|
+
</button>
|
|
160
|
+
) : (
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={submit}
|
|
164
|
+
disabled={(value.trim().length === 0 && files.length === 0) || disabled}
|
|
165
|
+
className={cn(
|
|
166
|
+
"h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background transition-colors",
|
|
167
|
+
"hover:bg-foreground/90",
|
|
168
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
169
|
+
)}
|
|
170
|
+
aria-label="Send message"
|
|
171
|
+
>
|
|
172
|
+
<ArrowUp className="h-3.5 w-3.5" />
|
|
173
|
+
</button>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
71
178
|
</div>
|
|
72
179
|
</div>
|
|
73
180
|
);
|
|
@@ -3,58 +3,118 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import ReactMarkdown from "react-markdown";
|
|
5
5
|
import remarkGfm from "remark-gfm";
|
|
6
|
-
import { AlertTriangle } from "lucide-react";
|
|
6
|
+
import { AlertTriangle, Copy, RotateCcw } from "lucide-react";
|
|
7
7
|
import { cn } from "@/lib/utils";
|
|
8
8
|
import type { UiMessage } from "@/lib/types";
|
|
9
9
|
import { ToolCall } from "@/components/tool-call";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface MessageProps {
|
|
12
|
+
message: UiMessage;
|
|
13
|
+
isLast?: boolean;
|
|
14
|
+
onCopy?(text: string): void;
|
|
15
|
+
onRegenerate?(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Message({ message, isLast, onCopy, onRegenerate }: MessageProps) {
|
|
12
19
|
const isUser = message.role === "user";
|
|
20
|
+
|
|
21
|
+
if (isUser) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col px-4 items-end">
|
|
24
|
+
<div className="max-w-[85%] rounded-2xl rounded-tr-sm bg-primary px-3.5 py-2 text-primary-foreground shadow-sm">
|
|
25
|
+
<p className="text-xs font-medium leading-relaxed whitespace-pre-wrap">
|
|
26
|
+
{message.content}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
return (
|
|
14
|
-
<div className=
|
|
15
|
-
<div
|
|
16
|
-
className={cn(
|
|
17
|
-
"max-w-[85%] rounded-lg px-4 py-2.5 text-sm animate-fade-in",
|
|
18
|
-
isUser
|
|
19
|
-
? "bg-primary text-primary-foreground"
|
|
20
|
-
: "bg-muted text-foreground",
|
|
21
|
-
message.errored && "border border-destructive/40",
|
|
22
|
-
)}
|
|
23
|
-
>
|
|
34
|
+
<div className="flex flex-col px-4 items-start">
|
|
35
|
+
<div className="w-full space-y-2">
|
|
24
36
|
{message.errored && (
|
|
25
37
|
<div className="mb-2 flex items-center gap-2 text-xs text-destructive">
|
|
26
38
|
<AlertTriangle className="h-3 w-3" />
|
|
27
39
|
<span>Error</span>
|
|
28
40
|
</div>
|
|
29
41
|
)}
|
|
42
|
+
|
|
30
43
|
{message.reasoning && (
|
|
31
|
-
<details className="
|
|
32
|
-
<summary className="cursor-pointer select-none">thinking</summary>
|
|
33
|
-
<div className="mt-1 whitespace-pre-wrap italic">
|
|
44
|
+
<details className="text-[11px] text-muted-foreground/70 border border-border/20 rounded-lg px-2.5 py-1.5 bg-muted/10">
|
|
45
|
+
<summary className="cursor-pointer select-none font-medium">thinking</summary>
|
|
46
|
+
<div className="mt-1.5 whitespace-pre-wrap italic leading-relaxed">
|
|
47
|
+
{message.reasoning}
|
|
48
|
+
</div>
|
|
34
49
|
</details>
|
|
35
50
|
)}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
|
|
52
|
+
{message.streaming && message.content.length === 0 && !message.toolCalls?.length ? (
|
|
53
|
+
<span className="inline-flex items-center gap-[3px]">
|
|
54
|
+
<span
|
|
55
|
+
className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
|
|
56
|
+
style={{ animationDelay: "0ms" }}
|
|
57
|
+
/>
|
|
58
|
+
<span
|
|
59
|
+
className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
|
|
60
|
+
style={{ animationDelay: "150ms" }}
|
|
61
|
+
/>
|
|
62
|
+
<span
|
|
63
|
+
className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
|
|
64
|
+
style={{ animationDelay: "300ms" }}
|
|
65
|
+
/>
|
|
66
|
+
</span>
|
|
67
|
+
) : (
|
|
68
|
+
message.content.length > 0 && (
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"prose prose-sm prose-zinc max-w-none dark:prose-invert",
|
|
72
|
+
"prose-p:my-2 prose-p:leading-relaxed prose-p:text-xs",
|
|
73
|
+
"prose-pre:my-2 prose-pre:text-xs",
|
|
74
|
+
"prose-code:text-xs prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none",
|
|
75
|
+
message.errored && "text-destructive",
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
|
|
44
79
|
</div>
|
|
45
|
-
)
|
|
80
|
+
)
|
|
81
|
+
)}
|
|
82
|
+
|
|
46
83
|
{message.toolCalls && message.toolCalls.length > 0 && (
|
|
47
|
-
<div className="
|
|
84
|
+
<div className="flex flex-col gap-1.5">
|
|
48
85
|
{message.toolCalls.map((call) => (
|
|
49
86
|
<ToolCall key={call.callId} call={call} />
|
|
50
87
|
))}
|
|
51
88
|
</div>
|
|
52
89
|
)}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
90
|
+
|
|
91
|
+
{!message.streaming && message.content.length > 0 && (
|
|
92
|
+
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
|
93
|
+
{message.latencyMs !== undefined && (
|
|
94
|
+
<span className="h-5 rounded border border-border/30 bg-muted/5 px-1.5 font-mono text-[9px] text-muted-foreground/70">
|
|
95
|
+
{message.latencyMs}ms
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
{onCopy && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => onCopy(message.content)}
|
|
102
|
+
className="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-muted/30 transition-colors"
|
|
103
|
+
title="Copy to clipboard"
|
|
104
|
+
>
|
|
105
|
+
<Copy className="h-3 w-3" />
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
{isLast && onRegenerate && (
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={onRegenerate}
|
|
112
|
+
className="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-muted/30 transition-colors"
|
|
113
|
+
title="Regenerate response"
|
|
114
|
+
>
|
|
115
|
+
<RotateCcw className="h-3 w-3" />
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
58
118
|
</div>
|
|
59
119
|
)}
|
|
60
120
|
</div>
|
|
@@ -44,18 +44,18 @@ export function ToolCall({ call }: { call: UiToolCall }) {
|
|
|
44
44
|
return (
|
|
45
45
|
<div
|
|
46
46
|
className={cn(
|
|
47
|
-
"flex flex-col gap-1 rounded-
|
|
47
|
+
"flex flex-col gap-1 rounded-lg border border-border/30 bg-muted/10 px-3 py-2 text-xs",
|
|
48
48
|
)}
|
|
49
49
|
>
|
|
50
50
|
<div className="flex items-center gap-2">
|
|
51
51
|
<StatusGlyph status={call.status} />
|
|
52
|
-
<span className="font-mono font-medium">{call.name}</span>
|
|
52
|
+
<span className="font-mono font-medium text-[11px]">{call.name}</span>
|
|
53
53
|
{args.length > 0 && (
|
|
54
|
-
<span className="truncate font-mono text-
|
|
54
|
+
<span className="truncate font-mono text-[10px] text-muted-foreground/70">{args}</span>
|
|
55
55
|
)}
|
|
56
56
|
</div>
|
|
57
57
|
{result !== undefined && (
|
|
58
|
-
<div className="ml-6 truncate text-
|
|
58
|
+
<div className="ml-6 truncate text-[10px] text-muted-foreground/70">
|
|
59
59
|
→ <span className="font-mono">{result}</span>
|
|
60
60
|
</div>
|
|
61
61
|
)}
|
|
@@ -66,13 +66,13 @@ export function ToolCall({ call }: { call: UiToolCall }) {
|
|
|
66
66
|
function StatusGlyph({ status }: { status: UiToolCall["status"] }) {
|
|
67
67
|
switch (status) {
|
|
68
68
|
case "running":
|
|
69
|
-
return <Loader2 className="h-
|
|
69
|
+
return <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />;
|
|
70
70
|
case "done":
|
|
71
|
-
return <CheckCircle2 className="h-
|
|
71
|
+
return <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />;
|
|
72
72
|
case "error":
|
|
73
|
-
return <XCircle className="h-
|
|
73
|
+
return <XCircle className="h-3.5 w-3.5 text-destructive" />;
|
|
74
74
|
case "approval":
|
|
75
|
-
return <HelpCircle className="h-
|
|
75
|
+
return <HelpCircle className="h-3.5 w-3.5 text-amber-500" />;
|
|
76
76
|
default:
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Config } from "tailwindcss";
|
|
2
|
+
import tailwindcssAnimate from "tailwindcss-animate";
|
|
3
|
+
import tailwindcssTypography from "@tailwindcss/typography";
|
|
2
4
|
|
|
3
5
|
const config: Config = {
|
|
4
6
|
darkMode: ["class"],
|
|
@@ -53,7 +55,7 @@ const config: Config = {
|
|
|
53
55
|
},
|
|
54
56
|
},
|
|
55
57
|
},
|
|
56
|
-
plugins: [
|
|
58
|
+
plugins: [tailwindcssAnimate, tailwindcssTypography],
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
export default config;
|