camox 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/createBlock.js +235 -198
- package/dist/features/agent-chat/agent-chat-labels.js +200 -0
- package/dist/features/agent-chat/components/AgentChatSheet.js +207 -0
- package/dist/features/agent-chat/components/AgentChatThread.js +594 -0
- package/dist/features/agent-chat/components/AgentToolCallCard.js +141 -0
- package/dist/features/preview/CamoxPreview.js +60 -13
- package/dist/features/preview/components/CreatePageModal.js +257 -121
- package/dist/features/preview/components/PageNicknameField.js +3 -3
- package/dist/features/preview/components/PreviewPanel.js +29 -6
- package/dist/features/preview/components/PreviewSideSheet.js +20 -16
- package/dist/features/preview/previewStore.js +49 -8
- package/dist/features/provider/components/CommandPalette.js +6 -4
- package/dist/features/studio/components/Navbar.js +14 -7
- package/dist/features/studio/useTheme.js +14 -6
- package/dist/features/vite/vite.js +1 -0
- package/dist/lib/auth.js +18 -12
- package/dist/studio.css +1 -1
- package/package.json +6 -4
- package/dist/features/preview/components/AgentChatSheet.js +0 -101
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { previewStore } from "../../preview/previewStore.js";
|
|
2
|
+
import { getAuthCookieHeader } from "../../../lib/auth.js";
|
|
3
|
+
import { getApiClient, getApiUrl, getEnvironmentName } from "../../../lib/api-client.js";
|
|
4
|
+
import { cn } from "../../../lib/utils.js";
|
|
5
|
+
import { buildAgentChatToolLabelContext, getToolCallLabel } from "../agent-chat-labels.js";
|
|
6
|
+
import { AgentToolCallCard } from "./AgentToolCallCard.js";
|
|
7
|
+
import { c } from "react/compiler-runtime";
|
|
8
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { Button } from "@camox/ui/button";
|
|
12
|
+
import { ArrowUp, Brain, Loader2, Square } from "lucide-react";
|
|
13
|
+
import { Alert, AlertDescription, AlertTitle } from "@camox/ui/alert";
|
|
14
|
+
import { Textarea } from "@camox/ui/textarea";
|
|
15
|
+
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react";
|
|
16
|
+
import { Streamdown } from "streamdown";
|
|
17
|
+
|
|
18
|
+
//#region src/features/agent-chat/components/AgentChatThread.tsx
|
|
19
|
+
function createTextMessage(id, role, content) {
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
role,
|
|
23
|
+
parts: [{
|
|
24
|
+
type: "text",
|
|
25
|
+
content
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createPageScaffoldMessages({ nickname, fullPath }) {
|
|
30
|
+
return [createTextMessage(`page-scaffold-user:${fullPath}`, "user", `I just created an empty _${nickname}_ page at \`${fullPath}\`.`), createTextMessage(`page-scaffold-assistant:${fullPath}`, "assistant", "I can create a draft of the page structure and content.<br/>**What should this page be about?**")];
|
|
31
|
+
}
|
|
32
|
+
function getTextPartContent(part) {
|
|
33
|
+
if (part.type !== "text") return null;
|
|
34
|
+
return part.content;
|
|
35
|
+
}
|
|
36
|
+
function hasVisibleAssistantOutput(message) {
|
|
37
|
+
return message.parts.some((part) => {
|
|
38
|
+
if (part.type === "text") return part.content.trim().length > 0;
|
|
39
|
+
if (part.type === "thinking") return true;
|
|
40
|
+
if (part.type === "tool-call") return true;
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isAssistantResponseOutputPart(part) {
|
|
45
|
+
if (part.type === "text") return part.content.trim().length > 0;
|
|
46
|
+
if (part.type === "tool-call") return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function getAssistantResponseOutputKey(message) {
|
|
50
|
+
const outputParts = message.parts.flatMap((part) => {
|
|
51
|
+
if (part.type === "text" && isAssistantResponseOutputPart(part)) return [`text:${part.content.length}`];
|
|
52
|
+
if (part.type === "tool-call") return [`tool:${part.id}:${part.state}`];
|
|
53
|
+
return [];
|
|
54
|
+
});
|
|
55
|
+
return outputParts.length > 0 ? outputParts.join("|") : null;
|
|
56
|
+
}
|
|
57
|
+
function hasLaterAssistantResponseOutput(parts, partIndex) {
|
|
58
|
+
return parts.slice(partIndex + 1).some(isAssistantResponseOutputPart);
|
|
59
|
+
}
|
|
60
|
+
function hasThinkingPart(message) {
|
|
61
|
+
return message.parts.some((part) => part.type === "thinking");
|
|
62
|
+
}
|
|
63
|
+
function getThinkingPartDurationKey(message, part, partIndex) {
|
|
64
|
+
const stepId = part.stepId;
|
|
65
|
+
return `${message.id}:${typeof stepId === "string" ? stepId : partIndex}`;
|
|
66
|
+
}
|
|
67
|
+
function formatThinkingDuration(seconds) {
|
|
68
|
+
return Math.max(1, Math.round(seconds)).toString();
|
|
69
|
+
}
|
|
70
|
+
function hasRequiresApprovalMetadata(part) {
|
|
71
|
+
return part.metadata?.risk === "requiresApproval";
|
|
72
|
+
}
|
|
73
|
+
function findToolResult(parts, toolCallId) {
|
|
74
|
+
return parts.find((part) => part.type === "tool-result" && part.toolCallId === toolCallId);
|
|
75
|
+
}
|
|
76
|
+
const markdownComponents = {
|
|
77
|
+
p: ({ children }) => /* @__PURE__ */ jsx("p", {
|
|
78
|
+
className: "leading-relaxed",
|
|
79
|
+
children
|
|
80
|
+
}),
|
|
81
|
+
a: ({ children, ...props }) => /* @__PURE__ */ jsx("a", {
|
|
82
|
+
...props,
|
|
83
|
+
className: "wrap-break-words underline underline-offset-2",
|
|
84
|
+
children
|
|
85
|
+
}),
|
|
86
|
+
ul: ({ children }) => /* @__PURE__ */ jsx("ul", {
|
|
87
|
+
className: "my-2 list-disc space-y-1 pl-5",
|
|
88
|
+
children
|
|
89
|
+
}),
|
|
90
|
+
ol: ({ children }) => /* @__PURE__ */ jsx("ol", {
|
|
91
|
+
className: "my-2 list-decimal space-y-1 pl-5",
|
|
92
|
+
children
|
|
93
|
+
}),
|
|
94
|
+
blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", {
|
|
95
|
+
className: "my-2 border-l-2 border-current/30 pl-3",
|
|
96
|
+
children
|
|
97
|
+
}),
|
|
98
|
+
code: ({ children }) => /* @__PURE__ */ jsx("code", {
|
|
99
|
+
className: "bg-background/50 rounded px-1 py-0.5 font-mono text-[0.9em]",
|
|
100
|
+
children
|
|
101
|
+
}),
|
|
102
|
+
pre: ({ children }) => /* @__PURE__ */ jsx("pre", {
|
|
103
|
+
className: "bg-background/50 my-2 max-w-full overflow-x-auto rounded-md p-3 text-xs",
|
|
104
|
+
children
|
|
105
|
+
})
|
|
106
|
+
};
|
|
107
|
+
const MessageBubble = (t0) => {
|
|
108
|
+
const $ = c(34);
|
|
109
|
+
if ($[0] !== "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82") {
|
|
110
|
+
for (let $i = 0; $i < 34; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
|
|
111
|
+
$[0] = "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82";
|
|
112
|
+
}
|
|
113
|
+
const { message, source, isThinkingActive, activeThinkingDurationKeys, thinkingDurations, toolLabelContext, onApprovalResponse } = t0;
|
|
114
|
+
const isUser = message.role === "user";
|
|
115
|
+
const fallbackThinkingDurationSeconds = thinkingDurations[message.id];
|
|
116
|
+
let t1;
|
|
117
|
+
if ($[1] !== fallbackThinkingDurationSeconds || $[2] !== isThinkingActive || $[3] !== isUser || $[4] !== message) {
|
|
118
|
+
t1 = !isUser && !hasThinkingPart(message) && (isThinkingActive || fallbackThinkingDurationSeconds !== void 0);
|
|
119
|
+
$[1] = fallbackThinkingDurationSeconds;
|
|
120
|
+
$[2] = isThinkingActive;
|
|
121
|
+
$[3] = isUser;
|
|
122
|
+
$[4] = message;
|
|
123
|
+
$[5] = t1;
|
|
124
|
+
} else t1 = $[5];
|
|
125
|
+
const shouldShowMessageThinkingIndicator = t1;
|
|
126
|
+
const t2 = isUser && "justify-end";
|
|
127
|
+
let t3;
|
|
128
|
+
if ($[6] !== t2) {
|
|
129
|
+
t3 = cn("flex gap-3", t2);
|
|
130
|
+
$[6] = t2;
|
|
131
|
+
$[7] = t3;
|
|
132
|
+
} else t3 = $[7];
|
|
133
|
+
const t4 = isUser ? "max-w-[85%]" : "max-w-full";
|
|
134
|
+
let t5;
|
|
135
|
+
if ($[8] !== t4) {
|
|
136
|
+
t5 = cn("space-y-1", t4);
|
|
137
|
+
$[8] = t4;
|
|
138
|
+
$[9] = t5;
|
|
139
|
+
} else t5 = $[9];
|
|
140
|
+
const t6 = isUser ? "bg-primary text-primary-foreground rounded-lg px-3 py-2" : "text-foreground space-y-3";
|
|
141
|
+
let t7;
|
|
142
|
+
if ($[10] !== t6) {
|
|
143
|
+
t7 = cn("text-sm", t6);
|
|
144
|
+
$[10] = t6;
|
|
145
|
+
$[11] = t7;
|
|
146
|
+
} else t7 = $[11];
|
|
147
|
+
let t8;
|
|
148
|
+
if ($[12] !== fallbackThinkingDurationSeconds || $[13] !== shouldShowMessageThinkingIndicator) {
|
|
149
|
+
t8 = shouldShowMessageThinkingIndicator && /* @__PURE__ */ jsx(AgentThinkingIndicator, { durationSeconds: fallbackThinkingDurationSeconds });
|
|
150
|
+
$[12] = fallbackThinkingDurationSeconds;
|
|
151
|
+
$[13] = shouldShowMessageThinkingIndicator;
|
|
152
|
+
$[14] = t8;
|
|
153
|
+
} else t8 = $[14];
|
|
154
|
+
let t9;
|
|
155
|
+
if ($[15] !== activeThinkingDurationKeys || $[16] !== fallbackThinkingDurationSeconds || $[17] !== isUser || $[18] !== message || $[19] !== onApprovalResponse || $[20] !== source || $[21] !== thinkingDurations || $[22] !== toolLabelContext) {
|
|
156
|
+
t9 = message.parts.map((part, index) => {
|
|
157
|
+
const text = getTextPartContent(part);
|
|
158
|
+
if (text != null) {
|
|
159
|
+
if (text.trim().length === 0) return null;
|
|
160
|
+
return /* @__PURE__ */ jsx(Streamdown, {
|
|
161
|
+
className: cn("wrap-break-words space-y-2", !isUser && "pl-6"),
|
|
162
|
+
components: markdownComponents,
|
|
163
|
+
controls: false,
|
|
164
|
+
isAnimating: !isUser,
|
|
165
|
+
children: text
|
|
166
|
+
}, index);
|
|
167
|
+
}
|
|
168
|
+
if (part.type === "thinking") {
|
|
169
|
+
const durationKey = getThinkingPartDurationKey(message, part, index);
|
|
170
|
+
const durationSeconds = thinkingDurations[durationKey] ?? (hasLaterAssistantResponseOutput(message.parts, index) ? fallbackThinkingDurationSeconds : void 0);
|
|
171
|
+
if (durationSeconds !== void 0 || activeThinkingDurationKeys.has(durationKey)) return /* @__PURE__ */ jsx(AgentThinkingIndicator, { durationSeconds }, durationKey);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (part.type === "tool-call") return /* @__PURE__ */ jsx(AgentToolCallCard, {
|
|
175
|
+
part,
|
|
176
|
+
label: getToolCallLabel(part, toolLabelContext),
|
|
177
|
+
result: findToolResult(message.parts, part.id),
|
|
178
|
+
requiresApprovalFallback: source === "draft" && hasRequiresApprovalMetadata(part),
|
|
179
|
+
onApprovalResponse: (approved) => onApprovalResponse(part, approved)
|
|
180
|
+
}, part.id);
|
|
181
|
+
if (part.type === "tool-result") return null;
|
|
182
|
+
return null;
|
|
183
|
+
});
|
|
184
|
+
$[15] = activeThinkingDurationKeys;
|
|
185
|
+
$[16] = fallbackThinkingDurationSeconds;
|
|
186
|
+
$[17] = isUser;
|
|
187
|
+
$[18] = message;
|
|
188
|
+
$[19] = onApprovalResponse;
|
|
189
|
+
$[20] = source;
|
|
190
|
+
$[21] = thinkingDurations;
|
|
191
|
+
$[22] = toolLabelContext;
|
|
192
|
+
$[23] = t9;
|
|
193
|
+
} else t9 = $[23];
|
|
194
|
+
let t10;
|
|
195
|
+
if ($[24] !== t7 || $[25] !== t8 || $[26] !== t9) {
|
|
196
|
+
t10 = /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
className: t7,
|
|
198
|
+
children: [t8, t9]
|
|
199
|
+
});
|
|
200
|
+
$[24] = t7;
|
|
201
|
+
$[25] = t8;
|
|
202
|
+
$[26] = t9;
|
|
203
|
+
$[27] = t10;
|
|
204
|
+
} else t10 = $[27];
|
|
205
|
+
let t11;
|
|
206
|
+
if ($[28] !== t10 || $[29] !== t5) {
|
|
207
|
+
t11 = /* @__PURE__ */ jsx("div", {
|
|
208
|
+
className: t5,
|
|
209
|
+
children: t10
|
|
210
|
+
});
|
|
211
|
+
$[28] = t10;
|
|
212
|
+
$[29] = t5;
|
|
213
|
+
$[30] = t11;
|
|
214
|
+
} else t11 = $[30];
|
|
215
|
+
let t12;
|
|
216
|
+
if ($[31] !== t11 || $[32] !== t3) {
|
|
217
|
+
t12 = /* @__PURE__ */ jsx("div", {
|
|
218
|
+
className: t3,
|
|
219
|
+
children: t11
|
|
220
|
+
});
|
|
221
|
+
$[31] = t11;
|
|
222
|
+
$[32] = t3;
|
|
223
|
+
$[33] = t12;
|
|
224
|
+
} else t12 = $[33];
|
|
225
|
+
return t12;
|
|
226
|
+
};
|
|
227
|
+
const AgentThinkingIndicator = (t0) => {
|
|
228
|
+
const $ = c(7);
|
|
229
|
+
if ($[0] !== "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82") {
|
|
230
|
+
for (let $i = 0; $i < 7; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
|
|
231
|
+
$[0] = "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82";
|
|
232
|
+
}
|
|
233
|
+
const { durationSeconds } = t0;
|
|
234
|
+
if (durationSeconds !== void 0) {
|
|
235
|
+
let t1;
|
|
236
|
+
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
|
237
|
+
t1 = /* @__PURE__ */ jsx(Brain, { className: "size-3 justify-self-center" });
|
|
238
|
+
$[1] = t1;
|
|
239
|
+
} else t1 = $[1];
|
|
240
|
+
let t2;
|
|
241
|
+
if ($[2] !== durationSeconds) {
|
|
242
|
+
t2 = formatThinkingDuration(durationSeconds);
|
|
243
|
+
$[2] = durationSeconds;
|
|
244
|
+
$[3] = t2;
|
|
245
|
+
} else t2 = $[3];
|
|
246
|
+
let t3;
|
|
247
|
+
if ($[4] !== t2) {
|
|
248
|
+
t3 = /* @__PURE__ */ jsxs("div", {
|
|
249
|
+
className: "text-muted-foreground grid grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 text-sm",
|
|
250
|
+
children: [t1, /* @__PURE__ */ jsxs("span", { children: [
|
|
251
|
+
"Thought for ",
|
|
252
|
+
t2,
|
|
253
|
+
"s"
|
|
254
|
+
] })]
|
|
255
|
+
});
|
|
256
|
+
$[4] = t2;
|
|
257
|
+
$[5] = t3;
|
|
258
|
+
} else t3 = $[5];
|
|
259
|
+
return t3;
|
|
260
|
+
}
|
|
261
|
+
let t1;
|
|
262
|
+
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
|
263
|
+
t1 = /* @__PURE__ */ jsxs("div", {
|
|
264
|
+
className: "text-muted-foreground grid grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 text-sm",
|
|
265
|
+
children: [/* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin justify-self-center" }), /* @__PURE__ */ jsx("span", { children: "Thinking..." })]
|
|
266
|
+
});
|
|
267
|
+
$[6] = t1;
|
|
268
|
+
} else t1 = $[6];
|
|
269
|
+
return t1;
|
|
270
|
+
};
|
|
271
|
+
const AgentChatThread = ({ projectId, currentPath, source, disabled, focusKey = 0, pageScaffoldContext }) => {
|
|
272
|
+
const [input, setInput] = React.useState("");
|
|
273
|
+
const inputRef = React.useRef(null);
|
|
274
|
+
const messagesScrollRef = React.useRef(null);
|
|
275
|
+
const thinkingStartedAtRef = React.useRef(null);
|
|
276
|
+
const thinkingSegmentStartsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
277
|
+
const toolCallContextByIdRef = React.useRef(/* @__PURE__ */ new Map());
|
|
278
|
+
const [thinkingDurations, setThinkingDurations] = React.useState({});
|
|
279
|
+
const [activeThinkingDurationKeys, setActiveThinkingDurationKeys] = React.useState(() => /* @__PURE__ */ new Set());
|
|
280
|
+
const navigate = useNavigate();
|
|
281
|
+
const initialMessages = React.useMemo(() => {
|
|
282
|
+
if (!pageScaffoldContext) return [];
|
|
283
|
+
return createPageScaffoldMessages(pageScaffoldContext);
|
|
284
|
+
}, [pageScaffoldContext]);
|
|
285
|
+
const connection = React.useMemo(() => fetchServerSentEvents(`${getApiUrl()}/agent/chat`, () => {
|
|
286
|
+
const headers = {
|
|
287
|
+
"Better-Auth-Cookie": getAuthCookieHeader(),
|
|
288
|
+
"x-camox-client": "studio"
|
|
289
|
+
};
|
|
290
|
+
const environmentName = getEnvironmentName();
|
|
291
|
+
if (environmentName) headers["x-environment-name"] = environmentName;
|
|
292
|
+
if (__CAMOX_TELEMETRY_DISABLED__) headers["x-camox-telemetry-disabled"] = "1";
|
|
293
|
+
return {
|
|
294
|
+
headers,
|
|
295
|
+
credentials: "omit",
|
|
296
|
+
body: {
|
|
297
|
+
projectId,
|
|
298
|
+
currentPath,
|
|
299
|
+
source
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}), [
|
|
303
|
+
currentPath,
|
|
304
|
+
projectId,
|
|
305
|
+
source
|
|
306
|
+
]);
|
|
307
|
+
const handleCustomEvent = React.useCallback((eventType, data) => {
|
|
308
|
+
if (eventType !== "agent-chat-ui-action") return;
|
|
309
|
+
if (!data || typeof data !== "object") return;
|
|
310
|
+
const action = data.action;
|
|
311
|
+
if (action === "navigate") {
|
|
312
|
+
const fullPath = data.fullPath;
|
|
313
|
+
if (typeof fullPath !== "string" || !fullPath.startsWith("/")) return;
|
|
314
|
+
if (fullPath === currentPath) return;
|
|
315
|
+
navigate({ to: fullPath });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (action !== "focusBlock") return;
|
|
319
|
+
const blockId = data.blockId;
|
|
320
|
+
if (typeof blockId !== "number") return;
|
|
321
|
+
previewStore.send({
|
|
322
|
+
type: "focusAgentBlock",
|
|
323
|
+
blockId
|
|
324
|
+
});
|
|
325
|
+
}, [currentPath, navigate]);
|
|
326
|
+
const { messages, sendMessage, isLoading, error, stop, addToolApprovalResponse, addToolResult } = useChat({
|
|
327
|
+
connection,
|
|
328
|
+
body: {
|
|
329
|
+
projectId,
|
|
330
|
+
currentPath,
|
|
331
|
+
source
|
|
332
|
+
},
|
|
333
|
+
initialMessages,
|
|
334
|
+
onCustomEvent: handleCustomEvent
|
|
335
|
+
});
|
|
336
|
+
const lastMessage = messages[messages.length - 1];
|
|
337
|
+
const lastAssistantMessage = lastMessage?.role === "assistant" ? lastMessage : [...messages].reverse().find((message) => message.role === "assistant");
|
|
338
|
+
const activeThinkingMessageId = isLoading && thinkingStartedAtRef.current !== null && lastMessage?.role === "assistant" ? lastMessage.id : void 0;
|
|
339
|
+
const lastMessageResponseOutputKey = lastMessage?.role === "assistant" ? getAssistantResponseOutputKey(lastMessage) : null;
|
|
340
|
+
const shouldShowThinkingFallback = isLoading && thinkingStartedAtRef.current !== null && !error && (!lastMessage || lastMessage.role === "user" || lastMessage.role === "assistant" && !hasVisibleAssistantOutput(lastMessage));
|
|
341
|
+
const toolCallContextById = React.useMemo(() => {
|
|
342
|
+
for (const message_0 of messages) for (const part of message_0.parts) {
|
|
343
|
+
if (part.type !== "tool-call") continue;
|
|
344
|
+
if (toolCallContextByIdRef.current.has(part.id)) continue;
|
|
345
|
+
toolCallContextByIdRef.current.set(part.id, {
|
|
346
|
+
currentPath,
|
|
347
|
+
source
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return new Map(toolCallContextByIdRef.current);
|
|
351
|
+
}, [
|
|
352
|
+
currentPath,
|
|
353
|
+
messages,
|
|
354
|
+
source
|
|
355
|
+
]);
|
|
356
|
+
const toolLabelContext = React.useMemo(() => buildAgentChatToolLabelContext({
|
|
357
|
+
messages,
|
|
358
|
+
currentPath,
|
|
359
|
+
source,
|
|
360
|
+
toolCallContextById
|
|
361
|
+
}), [
|
|
362
|
+
currentPath,
|
|
363
|
+
messages,
|
|
364
|
+
source,
|
|
365
|
+
toolCallContextById
|
|
366
|
+
]);
|
|
367
|
+
React.useEffect(() => {
|
|
368
|
+
const now = Date.now();
|
|
369
|
+
const activeDurationKeys = /* @__PURE__ */ new Set();
|
|
370
|
+
setThinkingDurations((durations) => {
|
|
371
|
+
let nextDurations = durations;
|
|
372
|
+
for (const message_1 of messages) {
|
|
373
|
+
if (message_1.role !== "assistant") continue;
|
|
374
|
+
message_1.parts.forEach((part_0, index) => {
|
|
375
|
+
if (part_0.type !== "thinking") return;
|
|
376
|
+
const durationKey = getThinkingPartDurationKey(message_1, part_0, index);
|
|
377
|
+
if (durations[durationKey] !== void 0) return;
|
|
378
|
+
const startedAt = thinkingSegmentStartsRef.current.get(durationKey) ?? now;
|
|
379
|
+
thinkingSegmentStartsRef.current.set(durationKey, startedAt);
|
|
380
|
+
if (!hasLaterAssistantResponseOutput(message_1.parts, index)) {
|
|
381
|
+
activeDurationKeys.add(durationKey);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (nextDurations === durations) nextDurations = { ...durations };
|
|
385
|
+
nextDurations[durationKey] = Math.max(0, (now - startedAt) / 1e3);
|
|
386
|
+
thinkingSegmentStartsRef.current.delete(durationKey);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return nextDurations;
|
|
390
|
+
});
|
|
391
|
+
setActiveThinkingDurationKeys(activeDurationKeys);
|
|
392
|
+
}, [messages]);
|
|
393
|
+
React.useEffect(() => {
|
|
394
|
+
if (!isLoading) return;
|
|
395
|
+
if (thinkingStartedAtRef.current !== null) return;
|
|
396
|
+
thinkingStartedAtRef.current = Date.now();
|
|
397
|
+
}, [isLoading]);
|
|
398
|
+
React.useEffect(() => {
|
|
399
|
+
const startedAt_0 = thinkingStartedAtRef.current;
|
|
400
|
+
if (startedAt_0 === null) return;
|
|
401
|
+
if (lastMessage?.role !== "assistant") return;
|
|
402
|
+
if (!lastMessageResponseOutputKey) return;
|
|
403
|
+
thinkingStartedAtRef.current = null;
|
|
404
|
+
const durationSeconds = Math.max(0, (Date.now() - startedAt_0) / 1e3);
|
|
405
|
+
setThinkingDurations((durations_0) => ({
|
|
406
|
+
...durations_0,
|
|
407
|
+
[lastMessage.id]: durations_0[lastMessage.id] ?? durationSeconds
|
|
408
|
+
}));
|
|
409
|
+
}, [
|
|
410
|
+
lastMessage?.id,
|
|
411
|
+
lastMessage?.role,
|
|
412
|
+
lastMessageResponseOutputKey
|
|
413
|
+
]);
|
|
414
|
+
React.useEffect(() => {
|
|
415
|
+
if (isLoading) return;
|
|
416
|
+
const startedAt_1 = thinkingStartedAtRef.current;
|
|
417
|
+
if (startedAt_1 === null) return;
|
|
418
|
+
thinkingStartedAtRef.current = null;
|
|
419
|
+
if (!lastAssistantMessage) return;
|
|
420
|
+
const durationSeconds_0 = Math.max(0, (Date.now() - startedAt_1) / 1e3);
|
|
421
|
+
setThinkingDurations((durations_1) => ({
|
|
422
|
+
...durations_1,
|
|
423
|
+
[lastAssistantMessage.id]: durationSeconds_0
|
|
424
|
+
}));
|
|
425
|
+
}, [isLoading, lastAssistantMessage]);
|
|
426
|
+
React.useEffect(() => {
|
|
427
|
+
if (focusKey === 0) return;
|
|
428
|
+
const frame = requestAnimationFrame(() => {
|
|
429
|
+
inputRef.current?.focus();
|
|
430
|
+
});
|
|
431
|
+
return () => cancelAnimationFrame(frame);
|
|
432
|
+
}, [focusKey]);
|
|
433
|
+
React.useEffect(() => {
|
|
434
|
+
const scrollContainer = messagesScrollRef.current;
|
|
435
|
+
if (!scrollContainer) return;
|
|
436
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
437
|
+
}, [messages]);
|
|
438
|
+
const handleApprovalResponse = async (part_1, approved) => {
|
|
439
|
+
const errorText = "User declined tool execution";
|
|
440
|
+
const refocusInput = () => requestAnimationFrame(() => inputRef.current?.focus());
|
|
441
|
+
if (!approved) {
|
|
442
|
+
if (part_1.approval) await addToolApprovalResponse({
|
|
443
|
+
id: part_1.approval.id,
|
|
444
|
+
approved: false
|
|
445
|
+
});
|
|
446
|
+
await addToolResult({
|
|
447
|
+
toolCallId: part_1.id,
|
|
448
|
+
tool: part_1.name,
|
|
449
|
+
output: { error: errorText },
|
|
450
|
+
state: "output-error",
|
|
451
|
+
errorText
|
|
452
|
+
});
|
|
453
|
+
refocusInput();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (source !== "draft") {
|
|
457
|
+
await addToolResult({
|
|
458
|
+
toolCallId: part_1.id,
|
|
459
|
+
tool: part_1.name,
|
|
460
|
+
output: { error: "This tool cannot be approved from the current source." },
|
|
461
|
+
state: "output-error",
|
|
462
|
+
errorText: "This tool cannot be approved from the current source."
|
|
463
|
+
});
|
|
464
|
+
refocusInput();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
let args = {};
|
|
468
|
+
try {
|
|
469
|
+
args = part_1.input ?? (part_1.arguments ? JSON.parse(part_1.arguments) : {});
|
|
470
|
+
} catch {
|
|
471
|
+
await addToolResult({
|
|
472
|
+
toolCallId: part_1.id,
|
|
473
|
+
tool: part_1.name,
|
|
474
|
+
output: { error: "Could not parse tool arguments." },
|
|
475
|
+
state: "output-error",
|
|
476
|
+
errorText: "Could not parse tool arguments."
|
|
477
|
+
});
|
|
478
|
+
refocusInput();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const response = await getApiClient().agent.callTool({
|
|
482
|
+
projectId,
|
|
483
|
+
name: part_1.name,
|
|
484
|
+
arguments: args
|
|
485
|
+
});
|
|
486
|
+
if (response.ok) {
|
|
487
|
+
if (part_1.approval) await addToolApprovalResponse({
|
|
488
|
+
id: part_1.approval.id,
|
|
489
|
+
approved: true
|
|
490
|
+
});
|
|
491
|
+
await addToolResult({
|
|
492
|
+
toolCallId: part_1.id,
|
|
493
|
+
tool: part_1.name,
|
|
494
|
+
output: response.result
|
|
495
|
+
});
|
|
496
|
+
refocusInput();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await addToolResult({
|
|
500
|
+
toolCallId: part_1.id,
|
|
501
|
+
tool: part_1.name,
|
|
502
|
+
output: response.error,
|
|
503
|
+
state: "output-error",
|
|
504
|
+
errorText: response.error.message
|
|
505
|
+
});
|
|
506
|
+
refocusInput();
|
|
507
|
+
};
|
|
508
|
+
const handleSubmit = (event) => {
|
|
509
|
+
event.preventDefault();
|
|
510
|
+
const message_2 = input.trim();
|
|
511
|
+
if (!message_2 || isLoading || disabled) return;
|
|
512
|
+
setInput("");
|
|
513
|
+
sendMessage(message_2);
|
|
514
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
515
|
+
};
|
|
516
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
517
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
518
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
519
|
+
ref: messagesScrollRef,
|
|
520
|
+
className: "min-h-0 flex-1 overflow-y-auto border-t p-4",
|
|
521
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
522
|
+
className: "flex min-h-full flex-col justify-end",
|
|
523
|
+
children: [
|
|
524
|
+
messages.map((message_3, index_0) => {
|
|
525
|
+
const previousMessage = messages[index_0 - 1];
|
|
526
|
+
const isConsecutiveAssistantMessage = message_3.role === "assistant" && previousMessage?.role === "assistant";
|
|
527
|
+
return /* @__PURE__ */ jsx("div", {
|
|
528
|
+
className: cn(index_0 > 0 && "mt-4", isConsecutiveAssistantMessage && "mt-3"),
|
|
529
|
+
children: /* @__PURE__ */ jsx(MessageBubble, {
|
|
530
|
+
message: message_3,
|
|
531
|
+
source,
|
|
532
|
+
isThinkingActive: activeThinkingMessageId === message_3.id,
|
|
533
|
+
activeThinkingDurationKeys,
|
|
534
|
+
thinkingDurations,
|
|
535
|
+
toolLabelContext,
|
|
536
|
+
onApprovalResponse: (part_2, approved_0) => void handleApprovalResponse(part_2, approved_0)
|
|
537
|
+
})
|
|
538
|
+
}, message_3.id);
|
|
539
|
+
}),
|
|
540
|
+
shouldShowThinkingFallback && /* @__PURE__ */ jsx("div", {
|
|
541
|
+
className: cn(messages.length > 0 && "mt-3"),
|
|
542
|
+
children: /* @__PURE__ */ jsx(AgentThinkingIndicator, {})
|
|
543
|
+
}),
|
|
544
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
545
|
+
className: cn((messages.length > 0 || shouldShowThinkingFallback) && "mt-4"),
|
|
546
|
+
children: /* @__PURE__ */ jsxs(Alert, {
|
|
547
|
+
variant: "destructive",
|
|
548
|
+
children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Agent Chat failed" }), /* @__PURE__ */ jsx(AlertDescription, { children: error.message })]
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
]
|
|
552
|
+
})
|
|
553
|
+
}), /* @__PURE__ */ jsx("form", {
|
|
554
|
+
onSubmit: handleSubmit,
|
|
555
|
+
className: "border-border border-t p-4",
|
|
556
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
557
|
+
className: "border-input focus-within:border-ring focus-within:ring-ring/50 flex items-center gap-2 rounded-2xl border px-3 transition-colors focus-within:ring-[3px]",
|
|
558
|
+
children: [/* @__PURE__ */ jsx(Textarea, {
|
|
559
|
+
ref: inputRef,
|
|
560
|
+
value: input,
|
|
561
|
+
onChange: (event_0) => setInput(event_0.target.value),
|
|
562
|
+
onKeyDown: (event_1) => {
|
|
563
|
+
if (event_1.key === "Enter" && !event_1.shiftKey) {
|
|
564
|
+
event_1.preventDefault();
|
|
565
|
+
event_1.currentTarget.form?.requestSubmit();
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
disabled,
|
|
569
|
+
placeholder: disabled ? "Switch to draft to chat" : "Ask for changes or inspect this page…",
|
|
570
|
+
className: "max-h-32 min-h-10 resize-none border-0 bg-inherit! px-0 py-4 leading-6 shadow-none focus-visible:ring-0"
|
|
571
|
+
}), isLoading ? /* @__PURE__ */ jsx(Button, {
|
|
572
|
+
type: "button",
|
|
573
|
+
size: "icon",
|
|
574
|
+
className: "shrink-0",
|
|
575
|
+
"aria-label": "Stop response",
|
|
576
|
+
onClick: stop,
|
|
577
|
+
children: /* @__PURE__ */ jsx(Square, { className: "size-4 fill-current" })
|
|
578
|
+
}) : /* @__PURE__ */ jsxs(Button, {
|
|
579
|
+
type: "submit",
|
|
580
|
+
disabled: disabled || !input.trim(),
|
|
581
|
+
size: "icon",
|
|
582
|
+
className: "shrink-0",
|
|
583
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
584
|
+
className: "sr-only",
|
|
585
|
+
children: "Send message"
|
|
586
|
+
}), /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" })]
|
|
587
|
+
})]
|
|
588
|
+
})
|
|
589
|
+
})]
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
//#endregion
|
|
594
|
+
export { AgentChatThread };
|