arcie 0.1.5 → 0.1.7
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/_tsup-dts-rollup.d.ts +2243 -0
- package/dist/agent/index.d.ts +1 -6
- package/dist/auth/index.d.ts +4 -11
- package/dist/channels/index.d.ts +3 -8
- package/dist/cli/index.d.ts +1 -8
- package/dist/cli/index.js +14 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/context/index.d.ts +9 -14
- package/dist/{dev-ZZDVOWUZ.js → dev-A6KGOSNL.js} +86 -62
- package/dist/dev-A6KGOSNL.js.map +1 -0
- package/dist/discover/index.d.ts +6 -41
- package/dist/hooks/index.d.ts +1 -6
- package/dist/index.d.ts +113 -297
- package/dist/{init-5F7CIVLN.js → init-DNVKDUHX.js} +3 -3
- package/dist/instructions/index.d.ts +2 -7
- package/dist/protocol/events.d.ts +44 -200
- package/dist/runner/index.d.ts +4 -4
- package/dist/schedules/index.d.ts +1 -6
- package/dist/skills/index.d.ts +2 -7
- package/dist/tools/index.d.ts +3 -13
- package/package.json +2 -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/dev-ZZDVOWUZ.js.map +0 -1
- package/dist/index-CElLRVTP.d.ts +0 -136
- package/dist/types-DwxwdSa2.d.ts +0 -151
- /package/dist/{init-5F7CIVLN.js.map → init-DNVKDUHX.js.map} +0 -0
|
@@ -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
|
);
|