arcie 0.1.4 → 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/agent/index.d.ts +1 -1
- package/dist/build-TWNCPXBU.js +82 -0
- package/dist/build-TWNCPXBU.js.map +1 -0
- package/dist/channels/index.d.ts +1 -1
- package/dist/{chunk-2ZSKZYWK.js → chunk-4WSILP75.js} +37 -164
- package/dist/chunk-4WSILP75.js.map +1 -0
- package/dist/chunk-HVBDOHTL.js +148 -0
- package/dist/chunk-HVBDOHTL.js.map +1 -0
- package/dist/chunk-HWD2Z2CH.js +742 -0
- package/dist/chunk-HWD2Z2CH.js.map +1 -0
- package/dist/chunk-KVSX4MLK.js +139 -0
- package/dist/chunk-KVSX4MLK.js.map +1 -0
- package/dist/chunk-N5R3TEEZ.js +15 -0
- package/dist/chunk-N5R3TEEZ.js.map +1 -0
- package/dist/chunk-SGWQZZ2A.js +54 -0
- package/dist/chunk-SGWQZZ2A.js.map +1 -0
- package/dist/chunk-XOFWQUOO.js +30 -0
- package/dist/chunk-XOFWQUOO.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +156 -231
- package/dist/cli/index.js.map +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/dev-ZZDVOWUZ.js +1967 -0
- package/dist/dev-ZZDVOWUZ.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/{index-b4NcDl3m.d.ts → index-CElLRVTP.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/init-5F7CIVLN.js +822 -0
- package/dist/init-5F7CIVLN.js.map +1 -0
- package/dist/instructions/index.d.ts +1 -1
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +2 -1
- package/dist/scaffold-web-chat-OQL5C2GK.js +7 -0
- package/dist/scaffold-web-chat-OQL5C2GK.js.map +1 -0
- package/dist/schedules/index.d.ts +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/{types-BxOQTzx1.d.ts → types-DwxwdSa2.d.ts} +1 -1
- package/package.json +1 -1
- package/templates/default/.env.local +12 -0
- package/templates/web-chat/.env.local.example +4 -0
- package/templates/web-chat/README.md +51 -0
- package/templates/web-chat/app/api/chat/route.ts +43 -0
- package/templates/web-chat/app/globals.css +65 -0
- package/templates/web-chat/app/layout.tsx +15 -0
- package/templates/web-chat/app/page.tsx +9 -0
- package/templates/web-chat/components/chat.tsx +219 -0
- package/templates/web-chat/components/input-bar.tsx +181 -0
- package/templates/web-chat/components/message.tsx +123 -0
- package/templates/web-chat/components/tool-call.tsx +79 -0
- package/templates/web-chat/components/ui/button.tsx +46 -0
- package/templates/web-chat/components/ui/input.tsx +19 -0
- package/templates/web-chat/components.json +20 -0
- package/templates/web-chat/lib/stream.ts +41 -0
- package/templates/web-chat/lib/types.ts +62 -0
- package/templates/web-chat/lib/utils.ts +6 -0
- package/templates/web-chat/next.config.mjs +6 -0
- package/templates/web-chat/package.json +33 -0
- package/templates/web-chat/postcss.config.mjs +6 -0
- package/templates/web-chat/tailwind.config.ts +61 -0
- package/templates/web-chat/tsconfig.json +23 -0
- package/dist/chunk-2ZSKZYWK.js.map +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ArrowUp, Paperclip, StopCircle, Trash2, X } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
interface InputBarProps {
|
|
8
|
+
onSend(message: string, files?: File[]): void;
|
|
9
|
+
onStop(): void;
|
|
10
|
+
onClear?(): void;
|
|
11
|
+
streaming: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
hasMessages?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function InputBar({
|
|
17
|
+
onSend,
|
|
18
|
+
onStop,
|
|
19
|
+
onClear,
|
|
20
|
+
streaming,
|
|
21
|
+
disabled,
|
|
22
|
+
hasMessages,
|
|
23
|
+
}: InputBarProps) {
|
|
24
|
+
const [value, setValue] = React.useState("");
|
|
25
|
+
const [files, setFiles] = React.useState<File[]>([]);
|
|
26
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
27
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
28
|
+
|
|
29
|
+
const submit = () => {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if ((trimmed.length === 0 && files.length === 0) || streaming || disabled) return;
|
|
32
|
+
onSend(trimmed, files.length > 0 ? files : undefined);
|
|
33
|
+
setValue("");
|
|
34
|
+
setFiles([]);
|
|
35
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
36
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
40
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
submit();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
47
|
+
setValue(event.target.value);
|
|
48
|
+
const el = event.target;
|
|
49
|
+
el.style.height = "auto";
|
|
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
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
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
|
|
71
|
+
className={cn(
|
|
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>
|
|
97
|
+
)}
|
|
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>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { AlertTriangle, Copy, RotateCcw } from "lucide-react";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import type { UiMessage } from "@/lib/types";
|
|
9
|
+
import { ToolCall } from "@/components/tool-call";
|
|
10
|
+
|
|
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) {
|
|
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
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col px-4 items-start">
|
|
35
|
+
<div className="w-full space-y-2">
|
|
36
|
+
{message.errored && (
|
|
37
|
+
<div className="mb-2 flex items-center gap-2 text-xs text-destructive">
|
|
38
|
+
<AlertTriangle className="h-3 w-3" />
|
|
39
|
+
<span>Error</span>
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
{message.reasoning && (
|
|
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>
|
|
49
|
+
</details>
|
|
50
|
+
)}
|
|
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>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{message.toolCalls && message.toolCalls.length > 0 && (
|
|
84
|
+
<div className="flex flex-col gap-1.5">
|
|
85
|
+
{message.toolCalls.map((call) => (
|
|
86
|
+
<ToolCall key={call.callId} call={call} />
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
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
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CheckCircle2, Loader2, XCircle, HelpCircle } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import type { UiToolCall } from "@/lib/types";
|
|
7
|
+
|
|
8
|
+
function summarize(input: unknown, max = 60): string {
|
|
9
|
+
if (input === undefined || input === null) return "";
|
|
10
|
+
if (typeof input !== "object") return String(input).slice(0, max);
|
|
11
|
+
if (Array.isArray(input)) return `[${input.length}]`;
|
|
12
|
+
const entries = Object.entries(input as Record<string, unknown>);
|
|
13
|
+
if (entries.length === 0) return "";
|
|
14
|
+
const parts = entries.map(([key, value]) => {
|
|
15
|
+
if (typeof value === "string") return `${key}="${value.slice(0, 24)}"`;
|
|
16
|
+
if (typeof value === "object") {
|
|
17
|
+
return `${key}=${Array.isArray(value) ? `[${value.length}]` : "{…}"}`;
|
|
18
|
+
}
|
|
19
|
+
return `${key}=${String(value)}`;
|
|
20
|
+
});
|
|
21
|
+
const joined = parts.join(", ");
|
|
22
|
+
return joined.length > max ? `${joined.slice(0, max - 1)}…` : joined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resultText(call: UiToolCall): string | undefined {
|
|
26
|
+
if (call.status === "error") return call.errorMessage;
|
|
27
|
+
if (call.status === "approval") return call.errorMessage ?? "awaiting approval";
|
|
28
|
+
if (call.output === undefined || call.output === null) return undefined;
|
|
29
|
+
if (typeof call.output === "string") {
|
|
30
|
+
return call.output.split("\n").find((line) => line.trim().length > 0) ?? undefined;
|
|
31
|
+
}
|
|
32
|
+
if (typeof call.output !== "object") return String(call.output);
|
|
33
|
+
const record = call.output as Record<string, unknown>;
|
|
34
|
+
for (const key of ["result", "text", "message", "summary", "value", "output"]) {
|
|
35
|
+
const value = record[key];
|
|
36
|
+
if (value !== undefined && typeof value !== "object") return String(value);
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ToolCall({ call }: { call: UiToolCall }) {
|
|
42
|
+
const args = summarize(call.input);
|
|
43
|
+
const result = resultText(call);
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={cn(
|
|
47
|
+
"flex flex-col gap-1 rounded-lg border border-border/30 bg-muted/10 px-3 py-2 text-xs",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<StatusGlyph status={call.status} />
|
|
52
|
+
<span className="font-mono font-medium text-[11px]">{call.name}</span>
|
|
53
|
+
{args.length > 0 && (
|
|
54
|
+
<span className="truncate font-mono text-[10px] text-muted-foreground/70">{args}</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
{result !== undefined && (
|
|
58
|
+
<div className="ml-6 truncate text-[10px] text-muted-foreground/70">
|
|
59
|
+
→ <span className="font-mono">{result}</span>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function StatusGlyph({ status }: { status: UiToolCall["status"] }) {
|
|
67
|
+
switch (status) {
|
|
68
|
+
case "running":
|
|
69
|
+
return <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />;
|
|
70
|
+
case "done":
|
|
71
|
+
return <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />;
|
|
72
|
+
case "error":
|
|
73
|
+
return <XCircle className="h-3.5 w-3.5 text-destructive" />;
|
|
74
|
+
case "approval":
|
|
75
|
+
return <HelpCircle className="h-3.5 w-3.5 text-amber-500" />;
|
|
76
|
+
default:
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
11
|
+
destructive:
|
|
12
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
13
|
+
outline:
|
|
14
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
secondary:
|
|
16
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
17
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
18
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
default: "h-10 px-4 py-2",
|
|
22
|
+
sm: "h-9 rounded-md px-3",
|
|
23
|
+
lg: "h-11 rounded-md px-8",
|
|
24
|
+
icon: "h-10 w-10",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export interface ButtonProps
|
|
32
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
33
|
+
VariantProps<typeof buttonVariants> {}
|
|
34
|
+
|
|
35
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
36
|
+
({ className, variant, size, ...props }, ref) => (
|
|
37
|
+
<button
|
|
38
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
39
|
+
ref={ref}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
Button.displayName = "Button";
|
|
45
|
+
|
|
46
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
|
5
|
+
({ className, type, ...props }, ref) => (
|
|
6
|
+
<input
|
|
7
|
+
type={type}
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
10
|
+
className,
|
|
11
|
+
)}
|
|
12
|
+
ref={ref}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
Input.displayName = "Input";
|
|
18
|
+
|
|
19
|
+
export { Input };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "default",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "tailwind.config.ts",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ArcieStreamEvent } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads an NDJSON stream from `response.body` and yields decoded events.
|
|
5
|
+
* Tolerates partial lines split across chunks and skips malformed rows so
|
|
6
|
+
* one bad event doesn't stop the stream.
|
|
7
|
+
*/
|
|
8
|
+
export async function* readArcieStream(
|
|
9
|
+
response: Response,
|
|
10
|
+
): AsyncIterable<ArcieStreamEvent> {
|
|
11
|
+
if (response.body === null) return;
|
|
12
|
+
const reader = response.body.getReader();
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
let buffer = "";
|
|
15
|
+
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done) break;
|
|
19
|
+
buffer += decoder.decode(value, { stream: true });
|
|
20
|
+
const lines = buffer.split("\n");
|
|
21
|
+
buffer = lines.pop() ?? "";
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (trimmed.length === 0) continue;
|
|
25
|
+
try {
|
|
26
|
+
yield JSON.parse(trimmed) as ArcieStreamEvent;
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignore malformed lines; a partial chunk may still finish next round.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tail = buffer.trim();
|
|
34
|
+
if (tail.length > 0) {
|
|
35
|
+
try {
|
|
36
|
+
yield JSON.parse(tail) as ArcieStreamEvent;
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type ChatRole = "user" | "assistant";
|
|
2
|
+
|
|
3
|
+
export interface UiMessage {
|
|
4
|
+
id: string;
|
|
5
|
+
role: ChatRole;
|
|
6
|
+
content: string;
|
|
7
|
+
toolCalls?: UiToolCall[];
|
|
8
|
+
reasoning?: string;
|
|
9
|
+
streaming?: boolean;
|
|
10
|
+
errored?: boolean;
|
|
11
|
+
latencyMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UiToolCall {
|
|
15
|
+
callId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
input?: unknown;
|
|
18
|
+
output?: unknown;
|
|
19
|
+
status: "running" | "done" | "error" | "approval";
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ArcieStreamEvent =
|
|
24
|
+
| { type: "session.started"; data: { sessionId: string } }
|
|
25
|
+
| { type: "turn.started"; data: { turnId: string } }
|
|
26
|
+
| { type: "message.received"; data: { message: string; turnId: string } }
|
|
27
|
+
| {
|
|
28
|
+
type: "message.appended";
|
|
29
|
+
data: { delta: string; textSoFar: string; turnId: string; stepIndex: number };
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
type: "message.completed";
|
|
33
|
+
data: { text: string | null; turnId: string; stepIndex: number };
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
type: "reasoning.appended";
|
|
37
|
+
data: { delta: string; soFar: string; turnId: string; stepIndex: number };
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
type: "reasoning.completed";
|
|
41
|
+
data: { text: string; turnId: string; stepIndex: number };
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: "tool.started";
|
|
45
|
+
data: { name: string; input: unknown; callId: string; turnId: string };
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: "tool.completed";
|
|
49
|
+
data: {
|
|
50
|
+
name: string;
|
|
51
|
+
output: unknown;
|
|
52
|
+
callId: string;
|
|
53
|
+
status: string;
|
|
54
|
+
error?: { code: string; message: string };
|
|
55
|
+
turnId: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
type: "step.failed" | "turn.failed" | "session.failed";
|
|
60
|
+
data: { code: string; message: string };
|
|
61
|
+
}
|
|
62
|
+
| { type: string; data: unknown };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web-chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev --port 3001",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start --port 3001",
|
|
9
|
+
"lint": "next lint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"class-variance-authority": "^0.7.0",
|
|
13
|
+
"clsx": "^2.1.1",
|
|
14
|
+
"lucide-react": "^0.475.0",
|
|
15
|
+
"next": "^15.1.0",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"react-markdown": "^9.0.1",
|
|
19
|
+
"remark-gfm": "^4.0.0",
|
|
20
|
+
"tailwind-merge": "^2.5.5",
|
|
21
|
+
"tailwindcss-animate": "^1.0.7"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@tailwindcss/typography": "^0.5.15",
|
|
25
|
+
"@types/node": "^22.10.0",
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"autoprefixer": "^10.4.20",
|
|
29
|
+
"postcss": "^8.4.49",
|
|
30
|
+
"tailwindcss": "^3.4.17",
|
|
31
|
+
"typescript": "^5.7.2"
|
|
32
|
+
}
|
|
33
|
+
}
|