lemma-sdk 0.2.10 → 0.2.11
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/README.md +54 -0
- package/dist/react/components/AssistantExperience.d.ts +79 -0
- package/dist/react/components/AssistantExperience.js +1032 -0
- package/dist/react/components/assistant-types.d.ts +72 -0
- package/dist/react/components/assistant-types.js +1 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAssistantController.d.ts +83 -0
- package/dist/react/useAssistantController.js +1089 -0
- package/dist/react/useAssistantRuntime.d.ts +3 -1
- package/dist/react/useAssistantRuntime.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { AvailableModels } from "../../types.js";
|
|
4
|
+
function cx(...values) {
|
|
5
|
+
return values.filter(Boolean).join(" ");
|
|
6
|
+
}
|
|
7
|
+
function asArray(value) {
|
|
8
|
+
return Array.isArray(value) ? value : [];
|
|
9
|
+
}
|
|
10
|
+
function asRecord(value) {
|
|
11
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: {};
|
|
14
|
+
}
|
|
15
|
+
function asString(value) {
|
|
16
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
function truncateLabel(value, max = 72) {
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
if (trimmed.length <= max)
|
|
21
|
+
return trimmed;
|
|
22
|
+
return `${trimmed.slice(0, max - 1)}…`;
|
|
23
|
+
}
|
|
24
|
+
function fileNameFromPath(path) {
|
|
25
|
+
const normalized = path.replace(/\\/g, "/");
|
|
26
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
27
|
+
return parts[parts.length - 1] || normalized;
|
|
28
|
+
}
|
|
29
|
+
function toolInvocationKey(tool) {
|
|
30
|
+
return `${tool.toolCallId}:${tool.state}`;
|
|
31
|
+
}
|
|
32
|
+
export function dedupToolInvocations(message) {
|
|
33
|
+
const invocations = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
(message.parts || []).forEach((part) => {
|
|
36
|
+
if (part.type !== "tool")
|
|
37
|
+
return;
|
|
38
|
+
const key = toolInvocationKey(part.toolInvocation);
|
|
39
|
+
if (seen.has(key))
|
|
40
|
+
return;
|
|
41
|
+
seen.add(key);
|
|
42
|
+
invocations.push(part.toolInvocation);
|
|
43
|
+
});
|
|
44
|
+
(message.toolInvocations || []).forEach((invocation) => {
|
|
45
|
+
const key = toolInvocationKey(invocation);
|
|
46
|
+
if (seen.has(key))
|
|
47
|
+
return;
|
|
48
|
+
seen.add(key);
|
|
49
|
+
invocations.push(invocation);
|
|
50
|
+
});
|
|
51
|
+
return invocations;
|
|
52
|
+
}
|
|
53
|
+
function normalizePlanStatus(rawStatus) {
|
|
54
|
+
const status = typeof rawStatus === "string" ? rawStatus.trim().toLowerCase() : "";
|
|
55
|
+
if (status === "completed" || status === "complete" || status === "done")
|
|
56
|
+
return "completed";
|
|
57
|
+
if (status === "in_progress" || status === "in-progress" || status === "running" || status === "active")
|
|
58
|
+
return "in_progress";
|
|
59
|
+
return "pending";
|
|
60
|
+
}
|
|
61
|
+
function parsePlanSteps(value) {
|
|
62
|
+
const entries = asArray(value);
|
|
63
|
+
return entries
|
|
64
|
+
.map((entry, index) => {
|
|
65
|
+
const obj = asRecord(entry);
|
|
66
|
+
const step = asString(obj.step) || asString(obj.title) || `Step ${index + 1}`;
|
|
67
|
+
if (!step)
|
|
68
|
+
return null;
|
|
69
|
+
return {
|
|
70
|
+
step,
|
|
71
|
+
status: normalizePlanStatus(obj.status),
|
|
72
|
+
};
|
|
73
|
+
})
|
|
74
|
+
.filter((entry) => entry !== null);
|
|
75
|
+
}
|
|
76
|
+
export function latestPlanSummary(messages) {
|
|
77
|
+
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
|
78
|
+
const invocations = dedupToolInvocations(messages[messageIndex]);
|
|
79
|
+
for (let invocationIndex = invocations.length - 1; invocationIndex >= 0; invocationIndex -= 1) {
|
|
80
|
+
const invocation = invocations[invocationIndex];
|
|
81
|
+
if (invocation.toolName.toLowerCase() !== "update_plan")
|
|
82
|
+
continue;
|
|
83
|
+
const argsObj = asRecord(invocation.args);
|
|
84
|
+
let steps = parsePlanSteps(argsObj.plan);
|
|
85
|
+
if (steps.length === 0) {
|
|
86
|
+
const resultObj = asRecord(invocation.result);
|
|
87
|
+
const outputObj = asRecord(resultObj.output);
|
|
88
|
+
steps = parsePlanSteps(outputObj.plan ?? resultObj.plan);
|
|
89
|
+
}
|
|
90
|
+
if (steps.length === 0)
|
|
91
|
+
continue;
|
|
92
|
+
const completedCount = steps.filter((step) => step.status === "completed").length;
|
|
93
|
+
const inProgressCount = steps.filter((step) => step.status === "in_progress").length;
|
|
94
|
+
const activeStep = steps.find((step) => step.status === "in_progress")?.step;
|
|
95
|
+
const running = invocation.state !== "result" || inProgressCount > 0;
|
|
96
|
+
return {
|
|
97
|
+
steps,
|
|
98
|
+
completedCount,
|
|
99
|
+
inProgressCount,
|
|
100
|
+
running,
|
|
101
|
+
activeStep,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function normalizeQuestionType(value) {
|
|
108
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
109
|
+
if (normalized === "multi_select")
|
|
110
|
+
return "multi_select";
|
|
111
|
+
if (normalized === "rank_priorities")
|
|
112
|
+
return "rank_priorities";
|
|
113
|
+
return "single_select";
|
|
114
|
+
}
|
|
115
|
+
function parseAskUserInputQuestions(value) {
|
|
116
|
+
return asArray(value)
|
|
117
|
+
.map((entry) => {
|
|
118
|
+
const obj = asRecord(entry);
|
|
119
|
+
const question = asString(obj.question);
|
|
120
|
+
const options = asArray(obj.options)
|
|
121
|
+
.map((option) => (typeof option === "string" ? option.trim() : ""))
|
|
122
|
+
.filter((option) => option.length > 0)
|
|
123
|
+
.slice(0, 4);
|
|
124
|
+
if (!question || options.length < 2) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
question,
|
|
129
|
+
options,
|
|
130
|
+
type: normalizeQuestionType(obj.type),
|
|
131
|
+
};
|
|
132
|
+
})
|
|
133
|
+
.filter((entry) => entry !== null)
|
|
134
|
+
.slice(0, 3);
|
|
135
|
+
}
|
|
136
|
+
function extractAskUserInputQuestionsFromInvocation(invocation) {
|
|
137
|
+
if (invocation.toolName.toLowerCase() !== "ask_user_input")
|
|
138
|
+
return [];
|
|
139
|
+
const args = asRecord(invocation.args);
|
|
140
|
+
const result = asRecord(invocation.result);
|
|
141
|
+
const output = asRecord(result.output);
|
|
142
|
+
const fromArgs = parseAskUserInputQuestions(args.questions);
|
|
143
|
+
if (fromArgs.length > 0)
|
|
144
|
+
return fromArgs;
|
|
145
|
+
const fromResult = parseAskUserInputQuestions(result.questions);
|
|
146
|
+
if (fromResult.length > 0)
|
|
147
|
+
return fromResult;
|
|
148
|
+
return parseAskUserInputQuestions(output.questions);
|
|
149
|
+
}
|
|
150
|
+
export function findPendingAskUserInput(messages) {
|
|
151
|
+
let latestUserMessageIndex = -1;
|
|
152
|
+
messages.forEach((message, index) => {
|
|
153
|
+
if (message.role === "user") {
|
|
154
|
+
latestUserMessageIndex = index;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
let pending;
|
|
158
|
+
messages.forEach((message, messageIndex) => {
|
|
159
|
+
if (message.role !== "assistant")
|
|
160
|
+
return;
|
|
161
|
+
const invocations = dedupToolInvocations(message);
|
|
162
|
+
invocations.forEach((invocation) => {
|
|
163
|
+
const questions = extractAskUserInputQuestionsFromInvocation(invocation);
|
|
164
|
+
if (questions.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
pending = {
|
|
167
|
+
toolCallId: invocation.toolCallId || `${message.id}-ask-user-input`,
|
|
168
|
+
messageIndex,
|
|
169
|
+
questions,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
if (!pending || latestUserMessageIndex > pending.messageIndex) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return pending;
|
|
177
|
+
}
|
|
178
|
+
export function formatAskUserInputAnswers(questions, answers) {
|
|
179
|
+
const rows = [];
|
|
180
|
+
questions.forEach((question, index) => {
|
|
181
|
+
const answer = answers[index] || [];
|
|
182
|
+
if (answer.length === 0)
|
|
183
|
+
return;
|
|
184
|
+
const answerText = question.type === "rank_priorities"
|
|
185
|
+
? answer.join(" > ")
|
|
186
|
+
: answer.join(", ");
|
|
187
|
+
rows.push(`Q: ${question.question}`);
|
|
188
|
+
rows.push(`A: ${answerText}`);
|
|
189
|
+
rows.push("");
|
|
190
|
+
});
|
|
191
|
+
return rows.join("\n").trim();
|
|
192
|
+
}
|
|
193
|
+
function normalizeFilepaths(value) {
|
|
194
|
+
return asArray(value)
|
|
195
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
196
|
+
.filter((path) => path.length > 0);
|
|
197
|
+
}
|
|
198
|
+
export function extractPresentFilePathsFromInvocation(invocation) {
|
|
199
|
+
if (invocation.toolName.toLowerCase() !== "present_files")
|
|
200
|
+
return [];
|
|
201
|
+
const args = asRecord(invocation.args);
|
|
202
|
+
const result = asRecord(invocation.result);
|
|
203
|
+
const output = asRecord(result.output);
|
|
204
|
+
const fromArgs = [
|
|
205
|
+
normalizeFilepaths(args.filepaths),
|
|
206
|
+
normalizeFilepaths(args.file_paths),
|
|
207
|
+
normalizeFilepaths(args.paths),
|
|
208
|
+
].find((entries) => entries.length > 0) || [];
|
|
209
|
+
if (fromArgs.length > 0)
|
|
210
|
+
return fromArgs;
|
|
211
|
+
return [
|
|
212
|
+
normalizeFilepaths(result.filepaths),
|
|
213
|
+
normalizeFilepaths(result.file_paths),
|
|
214
|
+
normalizeFilepaths(output.filepaths),
|
|
215
|
+
normalizeFilepaths(output.file_paths),
|
|
216
|
+
].find((entries) => entries.length > 0) || [];
|
|
217
|
+
}
|
|
218
|
+
function formatCommandPreview(cmd) {
|
|
219
|
+
const compact = cmd.replace(/\s+/g, " ").trim();
|
|
220
|
+
return truncateLabel(compact, 64);
|
|
221
|
+
}
|
|
222
|
+
function primaryToolArgs(args) {
|
|
223
|
+
const request = asRecord(args.request);
|
|
224
|
+
if (Object.keys(request).length > 0)
|
|
225
|
+
return request;
|
|
226
|
+
const waitConfig = asRecord(args.wait_config);
|
|
227
|
+
if (Object.keys(waitConfig).length > 0)
|
|
228
|
+
return waitConfig;
|
|
229
|
+
return args;
|
|
230
|
+
}
|
|
231
|
+
function toolArg(args, key) {
|
|
232
|
+
const direct = args[key];
|
|
233
|
+
if (typeof direct !== "undefined")
|
|
234
|
+
return direct;
|
|
235
|
+
return primaryToolArgs(args)[key];
|
|
236
|
+
}
|
|
237
|
+
function formatToolDisplayName(toolName) {
|
|
238
|
+
return toolName
|
|
239
|
+
.replace(/_/g, " ")
|
|
240
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
241
|
+
}
|
|
242
|
+
function commentLabelFromArgs(args) {
|
|
243
|
+
const comment = asString(toolArg(args, "comment"));
|
|
244
|
+
return comment ? truncateLabel(comment, 72) : null;
|
|
245
|
+
}
|
|
246
|
+
function formatActiveToolSummary(toolName, args) {
|
|
247
|
+
const lowerName = toolName.toLowerCase();
|
|
248
|
+
if (lowerName === "exec_command") {
|
|
249
|
+
const comment = commentLabelFromArgs(args);
|
|
250
|
+
if (comment)
|
|
251
|
+
return `Running ${comment}`;
|
|
252
|
+
const cmd = asString(toolArg(args, "cmd"));
|
|
253
|
+
return cmd ? `Running ${formatCommandPreview(cmd)}` : "Running command";
|
|
254
|
+
}
|
|
255
|
+
if (lowerName === "ask_user_input") {
|
|
256
|
+
const questions = parseAskUserInputQuestions(toolArg(args, "questions"));
|
|
257
|
+
return questions.length > 0
|
|
258
|
+
? `Waiting for user input (${questions.length} question${questions.length === 1 ? "" : "s"})`
|
|
259
|
+
: "Waiting for user input";
|
|
260
|
+
}
|
|
261
|
+
if (lowerName === "present_files") {
|
|
262
|
+
const filepaths = extractPresentFilePathsFromInvocation({
|
|
263
|
+
toolCallId: "",
|
|
264
|
+
toolName,
|
|
265
|
+
args,
|
|
266
|
+
state: "call",
|
|
267
|
+
});
|
|
268
|
+
return filepaths.length > 0
|
|
269
|
+
? `Presenting ${filepaths.length} file${filepaths.length === 1 ? "" : "s"}`
|
|
270
|
+
: "Presenting files";
|
|
271
|
+
}
|
|
272
|
+
if (lowerName === "update_plan") {
|
|
273
|
+
const plan = asArray(toolArg(args, "plan"));
|
|
274
|
+
return `Updating plan (${plan.length} step${plan.length === 1 ? "" : "s"})`;
|
|
275
|
+
}
|
|
276
|
+
return `Running ${formatToolDisplayName(toolName)}`;
|
|
277
|
+
}
|
|
278
|
+
function formatToolResultSummary(toolName, args, result) {
|
|
279
|
+
const lowerName = toolName.toLowerCase();
|
|
280
|
+
if (lowerName === "present_files") {
|
|
281
|
+
const filepaths = extractPresentFilePathsFromInvocation({
|
|
282
|
+
toolCallId: "",
|
|
283
|
+
toolName,
|
|
284
|
+
args,
|
|
285
|
+
state: "result",
|
|
286
|
+
result,
|
|
287
|
+
});
|
|
288
|
+
if (filepaths.length > 0) {
|
|
289
|
+
return `Presented ${filepaths.length} file${filepaths.length === 1 ? "" : "s"}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (lowerName === "update_plan") {
|
|
293
|
+
const plan = asArray(toolArg(args, "plan"));
|
|
294
|
+
const completed = plan.filter((step) => asRecord(step).status === "completed").length;
|
|
295
|
+
if (plan.length > 0) {
|
|
296
|
+
return `${completed}/${plan.length} complete`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const rawMessage = asString(result.message);
|
|
300
|
+
if (rawMessage)
|
|
301
|
+
return truncateLabel(rawMessage, 90);
|
|
302
|
+
if (typeof result.error === "string" && result.error.trim()) {
|
|
303
|
+
return truncateLabel(result.error.trim(), 90);
|
|
304
|
+
}
|
|
305
|
+
if (typeof result.resourceType === "string" && typeof result.resourceId === "string") {
|
|
306
|
+
return `Created ${result.resourceType}`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function hasMeaningfulTextPart(message) {
|
|
311
|
+
return (message.parts || []).some((part) => part.type === "text" && part.text.trim().length > 0);
|
|
312
|
+
}
|
|
313
|
+
function isShowWidgetToolName(toolName) {
|
|
314
|
+
const normalized = toolName.trim().toLowerCase();
|
|
315
|
+
return normalized === "visualize:show_widget"
|
|
316
|
+
|| normalized === "visualize.show_widget"
|
|
317
|
+
|| normalized === "show_widget"
|
|
318
|
+
|| normalized === "render_widget";
|
|
319
|
+
}
|
|
320
|
+
function isCollapsibleAssistantMessage(message) {
|
|
321
|
+
if (message.role !== "assistant")
|
|
322
|
+
return false;
|
|
323
|
+
const hasTools = (message.toolInvocations?.length || 0) > 0 || (message.parts || []).some((part) => part.type === "tool");
|
|
324
|
+
const hasReasoning = (message.parts || []).some((part) => part.type === "reasoning" && part.text.trim().length > 0);
|
|
325
|
+
if (!hasTools && !hasReasoning)
|
|
326
|
+
return false;
|
|
327
|
+
return !hasMeaningfulTextPart(message) && (!message.content || message.content.trim().length === 0);
|
|
328
|
+
}
|
|
329
|
+
export function buildDisplayMessageRows(messages) {
|
|
330
|
+
const rows = [];
|
|
331
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
332
|
+
const message = messages[i];
|
|
333
|
+
if (!isCollapsibleAssistantMessage(message)) {
|
|
334
|
+
rows.push({
|
|
335
|
+
id: message.id,
|
|
336
|
+
message,
|
|
337
|
+
sourceIndexes: [i],
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const cluster = [message];
|
|
342
|
+
const sourceIndexes = [i];
|
|
343
|
+
let j = i + 1;
|
|
344
|
+
while (j < messages.length && isCollapsibleAssistantMessage(messages[j])) {
|
|
345
|
+
cluster.push(messages[j]);
|
|
346
|
+
sourceIndexes.push(j);
|
|
347
|
+
j += 1;
|
|
348
|
+
}
|
|
349
|
+
if (cluster.length === 1) {
|
|
350
|
+
rows.push({
|
|
351
|
+
id: message.id,
|
|
352
|
+
message,
|
|
353
|
+
sourceIndexes,
|
|
354
|
+
});
|
|
355
|
+
i = j - 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const mergedParts = [];
|
|
359
|
+
const mergedToolInvocations = [];
|
|
360
|
+
cluster.forEach((entry) => {
|
|
361
|
+
if (entry.parts?.length) {
|
|
362
|
+
mergedParts.push(...entry.parts);
|
|
363
|
+
}
|
|
364
|
+
if (entry.toolInvocations?.length) {
|
|
365
|
+
mergedToolInvocations.push(...entry.toolInvocations);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
rows.push({
|
|
369
|
+
id: `tool-cluster-${cluster[0].id}`,
|
|
370
|
+
message: {
|
|
371
|
+
id: `tool-cluster-${cluster[0].id}`,
|
|
372
|
+
role: "assistant",
|
|
373
|
+
content: "",
|
|
374
|
+
parts: mergedParts,
|
|
375
|
+
toolInvocations: mergedToolInvocations,
|
|
376
|
+
createdAt: cluster[cluster.length - 1]?.createdAt ?? cluster[0]?.createdAt,
|
|
377
|
+
},
|
|
378
|
+
sourceIndexes,
|
|
379
|
+
});
|
|
380
|
+
i = j - 1;
|
|
381
|
+
}
|
|
382
|
+
return rows;
|
|
383
|
+
}
|
|
384
|
+
export function getActiveToolBanner(messages) {
|
|
385
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
386
|
+
const message = messages[i];
|
|
387
|
+
if (message.role !== "assistant")
|
|
388
|
+
continue;
|
|
389
|
+
const activeInvocations = dedupToolInvocations(message).filter((invocation) => invocation.state !== "result");
|
|
390
|
+
if (activeInvocations.length === 0)
|
|
391
|
+
continue;
|
|
392
|
+
const currentInvocation = activeInvocations[activeInvocations.length - 1];
|
|
393
|
+
return {
|
|
394
|
+
summary: formatActiveToolSummary(currentInvocation.toolName, currentInvocation.args),
|
|
395
|
+
activeCount: activeInvocations.length,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function extractShowWidgetPayloadFromRecord(value) {
|
|
401
|
+
const record = asRecord(value);
|
|
402
|
+
const widgetCode = asString(record.widget_code);
|
|
403
|
+
if (!widgetCode) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
const loadingMessages = asArray(record.loading_messages)
|
|
407
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
408
|
+
.filter((entry) => entry.length > 0)
|
|
409
|
+
.slice(0, 4);
|
|
410
|
+
return {
|
|
411
|
+
title: asString(record.title),
|
|
412
|
+
widgetCode,
|
|
413
|
+
loadingMessages,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function extractShowWidgetPayload(args, result) {
|
|
417
|
+
const fromArgs = extractShowWidgetPayloadFromRecord(args);
|
|
418
|
+
if (fromArgs)
|
|
419
|
+
return fromArgs;
|
|
420
|
+
const resultObject = asRecord(result);
|
|
421
|
+
const outputObject = asRecord(resultObject.output);
|
|
422
|
+
const dataObject = asRecord(resultObject.data);
|
|
423
|
+
return extractShowWidgetPayloadFromRecord(outputObject)
|
|
424
|
+
|| extractShowWidgetPayloadFromRecord(dataObject)
|
|
425
|
+
|| extractShowWidgetPayloadFromRecord(resultObject);
|
|
426
|
+
}
|
|
427
|
+
function escapeHtml(value) {
|
|
428
|
+
return value
|
|
429
|
+
.replace(/&/g, "&")
|
|
430
|
+
.replace(/</g, "<")
|
|
431
|
+
.replace(/>/g, ">")
|
|
432
|
+
.replace(/"/g, """)
|
|
433
|
+
.replace(/'/g, "'");
|
|
434
|
+
}
|
|
435
|
+
function buildWidgetIframeDocument(toolCallId, payload) {
|
|
436
|
+
const widgetCode = payload.widgetCode.trim();
|
|
437
|
+
const isSvg = /^<svg[\s>]/i.test(widgetCode);
|
|
438
|
+
const safeToolCallId = JSON.stringify(toolCallId);
|
|
439
|
+
const safeTitle = escapeHtml(payload.title || "Widget");
|
|
440
|
+
return `<!doctype html>
|
|
441
|
+
<html>
|
|
442
|
+
<head>
|
|
443
|
+
<meta charset="utf-8" />
|
|
444
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
445
|
+
<style>
|
|
446
|
+
html, body { margin: 0; padding: 0; width: 100%; background: transparent; color: #0f172a; }
|
|
447
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
448
|
+
.widget-svg-root { width: 100%; display: flex; justify-content: center; align-items: flex-start; overflow: visible; }
|
|
449
|
+
.widget-svg-root > svg { width: 100%; max-width: 100%; height: auto; }
|
|
450
|
+
</style>
|
|
451
|
+
<script>
|
|
452
|
+
(function () {
|
|
453
|
+
var toolCallId = ${safeToolCallId};
|
|
454
|
+
function computeHeight() {
|
|
455
|
+
var doc = document.documentElement;
|
|
456
|
+
var body = document.body;
|
|
457
|
+
return Math.max(
|
|
458
|
+
doc ? doc.scrollHeight : 0,
|
|
459
|
+
body ? body.scrollHeight : 0,
|
|
460
|
+
120
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function reportHeight() {
|
|
464
|
+
parent.postMessage({ type: 'lemma-widget-height', height: Math.max(120, Math.ceil(computeHeight())), toolCallId: toolCallId }, '*');
|
|
465
|
+
}
|
|
466
|
+
window.sendPrompt = function (text) {
|
|
467
|
+
var message = typeof text === 'string' ? text.trim() : '';
|
|
468
|
+
if (!message) return;
|
|
469
|
+
parent.postMessage({ type: 'lemma-widget-send-prompt', text: message, toolCallId: toolCallId }, '*');
|
|
470
|
+
};
|
|
471
|
+
window.addEventListener('load', function () {
|
|
472
|
+
reportHeight();
|
|
473
|
+
setTimeout(reportHeight, 50);
|
|
474
|
+
});
|
|
475
|
+
window.addEventListener('resize', reportHeight);
|
|
476
|
+
if (typeof MutationObserver !== 'undefined' && document.documentElement) {
|
|
477
|
+
var observer = new MutationObserver(reportHeight);
|
|
478
|
+
observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
479
|
+
}
|
|
480
|
+
})();
|
|
481
|
+
</script>
|
|
482
|
+
</head>
|
|
483
|
+
<body aria-label="${safeTitle}">
|
|
484
|
+
${isSvg ? `<div class="widget-svg-root">${widgetCode}</div>` : widgetCode}
|
|
485
|
+
</body>
|
|
486
|
+
</html>`;
|
|
487
|
+
}
|
|
488
|
+
function useControllableDraft(controlledValue, onChange) {
|
|
489
|
+
const [uncontrolledValue, setUncontrolledValue] = useState("");
|
|
490
|
+
const isControlled = typeof controlledValue === "string";
|
|
491
|
+
const setValue = useCallback((nextValue) => {
|
|
492
|
+
if (!isControlled) {
|
|
493
|
+
setUncontrolledValue(nextValue);
|
|
494
|
+
}
|
|
495
|
+
onChange?.(nextValue);
|
|
496
|
+
}, [isControlled, onChange]);
|
|
497
|
+
return [isControlled ? controlledValue : uncontrolledValue, setValue];
|
|
498
|
+
}
|
|
499
|
+
function defaultConversationLabel({ conversation }) {
|
|
500
|
+
return conversation.title || "Untitled conversation";
|
|
501
|
+
}
|
|
502
|
+
function defaultMessageContent({ message }) {
|
|
503
|
+
return _jsx("div", { className: "whitespace-pre-wrap", children: message.content });
|
|
504
|
+
}
|
|
505
|
+
function defaultPresentedFile({ filepath }) {
|
|
506
|
+
return (_jsxs("div", { className: "rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_78%,_transparent)] bg-[linear-gradient(180deg,color-mix(in_srgb,var(--bg-surface)_96%,transparent),color-mix(in_srgb,var(--bg-canvas)_76%,transparent))] px-3 py-2.5", children: [_jsx("div", { className: "text-[14px] font-medium text-[var(--text-primary)]", children: fileNameFromPath(filepath) }), _jsx("div", { className: "mt-1 text-[12px] text-[var(--text-tertiary)]", children: filepath })] }));
|
|
507
|
+
}
|
|
508
|
+
function defaultPendingFile({ file, remove }) {
|
|
509
|
+
return (_jsxs("span", { className: "inline-flex max-w-full items-center gap-1.5 rounded-full bg-[var(--bg-subtle)] px-2 py-1 text-[11px] text-[var(--text-secondary)]", children: [_jsx("span", { className: "truncate max-w-[180px]", children: file.name }), _jsx("button", { type: "button", onClick: remove, className: "inline-flex h-4 w-4 items-center justify-center rounded-full hover:bg-[var(--bg-canvas)]", title: "Remove file", children: "\u00D7" })] }));
|
|
510
|
+
}
|
|
511
|
+
export function PlanSummaryStrip({ plan, onHide }) {
|
|
512
|
+
const [showAll, setShowAll] = useState(false);
|
|
513
|
+
const visibleSteps = showAll ? plan.steps : plan.steps.slice(0, 5);
|
|
514
|
+
const hiddenCount = Math.max(0, plan.steps.length - visibleSteps.length);
|
|
515
|
+
return (_jsxs("div", { className: "rounded-xl border border-[color:color-mix(in_srgb,_var(--brand-primary)_24%,_var(--border-subtle))] bg-[color:color-mix(in_srgb,_var(--brand-glow)_18%,_var(--bg-surface))] px-3 py-2.5 shadow-[var(--shadow-sm)]", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "text-[12px] font-semibold text-[var(--text-primary)]", children: "Task plan" }), _jsxs("span", { className: "text-[11px] text-[var(--text-tertiary)]", children: [plan.completedCount, "/", plan.steps.length, " complete"] }), plan.inProgressCount > 0 ? (_jsxs("span", { className: "rounded-full bg-[color:color-mix(in_srgb,_var(--brand-primary)_16%,_transparent)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--brand-primary)]", children: [plan.inProgressCount, " active"] })) : null] }), _jsx("button", { type: "button", onClick: onHide, className: "text-[11px] font-medium text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors", children: "Hide" })] }), plan.activeStep ? (_jsxs("div", { className: "mt-1.5 truncate text-[11px] text-[var(--text-secondary)]", title: plan.activeStep, children: [plan.running ? "Running:" : "Current:", " ", plan.activeStep] })) : null, _jsxs("div", { className: "mt-2 space-y-1", children: [visibleSteps.map((step, index) => (_jsxs("div", { className: "flex items-start gap-2 text-[11px]", children: [_jsx("span", { className: cx("mt-1 inline-block h-2 w-2 shrink-0 rounded-full", step.status === "completed" && "bg-[var(--state-success)]", step.status === "in_progress" && "bg-[var(--brand-primary)]", step.status === "pending" && "bg-[var(--border-default)]") }), _jsx("span", { className: cx("leading-5", step.status === "completed" && "text-[var(--text-tertiary)] line-through", step.status === "in_progress" && "text-[var(--brand-primary)] font-medium", step.status === "pending" && "text-[var(--text-secondary)]"), children: step.step })] }, `${step.step}-${index}`))), plan.steps.length > 5 ? (_jsxs("div", { className: "flex items-center gap-2 pt-0.5", children: [_jsx("button", { type: "button", onClick: () => setShowAll((prev) => !prev), className: "text-[10px] font-medium text-[var(--brand-primary)] hover:text-[var(--text-primary)] transition-colors", children: showAll ? "Show less" : `See all ${plan.steps.length} steps` }), !showAll && hiddenCount > 0 ? (_jsxs("span", { className: "text-[10px] text-[var(--text-tertiary)]", children: ["+", hiddenCount, " more"] })) : null] })) : null] })] }));
|
|
516
|
+
}
|
|
517
|
+
export function ThinkingIndicator() {
|
|
518
|
+
const [show, setShow] = useState(false);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
const timer = setTimeout(() => setShow(true), 350);
|
|
521
|
+
return () => clearTimeout(timer);
|
|
522
|
+
}, []);
|
|
523
|
+
if (!show)
|
|
524
|
+
return null;
|
|
525
|
+
return (_jsx("div", { className: "px-1 animate-in fade-in duration-300", children: _jsxs("div", { className: "inline-flex items-center gap-2.5 text-[12px] leading-5 text-[var(--text-tertiary)]", children: [_jsx("span", { className: "inline-flex h-2 w-2 rounded-full bg-[var(--brand-accent)]" }), _jsx("span", { className: "font-semibold text-transparent bg-clip-text bg-[linear-gradient(110deg,var(--text-secondary),35%,var(--brand-accent),50%,var(--text-secondary),65%)] bg-[length:250%_100%] animate-pulse", children: "Thinking..." })] }) }));
|
|
526
|
+
}
|
|
527
|
+
export function EmptyState({ onSendMessage }) {
|
|
528
|
+
const suggestions = [
|
|
529
|
+
{ text: "Create an agent that summarizes documents", icon: "🤖" },
|
|
530
|
+
{ text: "Add a table for tracking leads", icon: "📊" },
|
|
531
|
+
{ text: "Create a full React desk page for an executive dashboard", icon: "🧩" },
|
|
532
|
+
{ text: "Create a flow to process emails", icon: "⚡" },
|
|
533
|
+
];
|
|
534
|
+
return (_jsxs("div", { className: "text-center py-5 px-2", children: [_jsxs("div", { className: "flex flex-col items-center justify-center mb-6", children: [_jsx("div", { className: "mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--brand-primary),var(--brand-secondary))] shadow-[var(--shadow-xs)]", children: _jsx("span", { className: "text-[var(--text-on-brand)] text-lg", children: "\u2728" }) }), _jsx("h4", { className: "font-semibold text-[var(--text-primary)] text-[15px]", children: "What can I help you build?" }), _jsx("p", { className: "text-[13px] text-[var(--text-tertiary)] mt-1.5 max-w-sm leading-relaxed", children: "I can create agents, set up data stores, build pages, and automate workflows for your pod." })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2.5 max-w-[500px] mx-auto", children: suggestions.map((suggestion, index) => (_jsxs("button", { onClick: () => onSendMessage(suggestion.text), className: "text-left px-3 py-2.5 rounded-lg border border-[var(--border-default)] bg-[var(--bg-surface)] text-xs text-[var(--text-secondary)] hover:border-[color:color-mix(in_srgb,_var(--brand-accent)_52%,_var(--border-subtle))] hover:bg-[color:color-mix(in_srgb,_var(--brand-glow)_72%,_var(--bg-surface))] hover:text-[var(--text-primary)] transition-all duration-200 flex items-center gap-2.5 group", children: [_jsx("span", { className: "text-base opacity-70 group-hover:opacity-100 transition-opacity", children: suggestion.icon }), _jsx("span", { className: "flex-1 leading-snug", children: suggestion.text }), _jsx("span", { className: "text-[var(--text-tertiary)] group-hover:text-[var(--state-warning)] group-hover:translate-x-0.5 transition-all opacity-0 group-hover:opacity-100", children: "\u203A" })] }, `${suggestion.text}-${index}`))) })] }));
|
|
535
|
+
}
|
|
536
|
+
function ReasoningPartCard({ text, isStreaming, durationMs, }) {
|
|
537
|
+
return (_jsxs("details", { className: "group", open: isStreaming, children: [_jsxs("summary", { className: "list-none cursor-pointer inline-flex items-center gap-1.5 text-[12px] leading-5 text-[var(--text-tertiary)]", children: [_jsx("span", { className: "transition-transform group-open:rotate-90", children: "\u203A" }), _jsx("span", { className: cx("font-semibold", isStreaming && "text-transparent bg-clip-text bg-[linear-gradient(110deg,var(--text-secondary),35%,var(--brand-accent),50%,var(--text-secondary),65%)] bg-[length:250%_100%] animate-pulse"), children: isStreaming ? "Thinking" : `Thought${durationMs ? ` · ${Math.max(1, Math.round(durationMs / 1000))}s` : ""}` })] }), _jsx("div", { className: "mt-1 pl-4 border-l border-[var(--border-default)]", children: _jsx("pre", { className: "text-[11px] leading-5 text-[var(--text-tertiary)] whitespace-pre-wrap font-mono", children: text }) })] }));
|
|
538
|
+
}
|
|
539
|
+
function PresentFilesCard({ filepaths, conversationId, renderPresentedFile, }) {
|
|
540
|
+
const fakeMessage = {
|
|
541
|
+
id: "present-files",
|
|
542
|
+
role: "assistant",
|
|
543
|
+
content: "",
|
|
544
|
+
};
|
|
545
|
+
const fakeInvocation = {
|
|
546
|
+
toolCallId: "present-files",
|
|
547
|
+
toolName: "present_files",
|
|
548
|
+
args: { filepaths },
|
|
549
|
+
state: "result",
|
|
550
|
+
};
|
|
551
|
+
return (_jsx("div", { className: "pt-1 space-y-2", children: filepaths.map((filepath) => (_jsx("div", { children: (renderPresentedFile || defaultPresentedFile)({
|
|
552
|
+
filepath,
|
|
553
|
+
activeConversationId: conversationId ?? null,
|
|
554
|
+
invocation: fakeInvocation,
|
|
555
|
+
message: fakeMessage,
|
|
556
|
+
}) }, `present-file-${filepath}`))) }));
|
|
557
|
+
}
|
|
558
|
+
function ToolDetailsPanel({ toolName, args, state, result, onNavigateResource, renderToolInvocation, message, activeConversationId, }) {
|
|
559
|
+
const resultData = result || {};
|
|
560
|
+
const canNavigate = state === "result"
|
|
561
|
+
&& resultData.success !== false
|
|
562
|
+
&& typeof resultData.resourceType === "string"
|
|
563
|
+
&& typeof resultData.resourceId === "string";
|
|
564
|
+
if (renderToolInvocation) {
|
|
565
|
+
return (_jsx("div", { className: "pl-4 border-l border-[var(--border-default)]", children: renderToolInvocation({
|
|
566
|
+
invocation: {
|
|
567
|
+
toolCallId: "detail-tool",
|
|
568
|
+
toolName,
|
|
569
|
+
args,
|
|
570
|
+
state: state === "result" ? "result" : "call",
|
|
571
|
+
...(result ? { result } : {}),
|
|
572
|
+
},
|
|
573
|
+
message,
|
|
574
|
+
activeConversationId,
|
|
575
|
+
}) }));
|
|
576
|
+
}
|
|
577
|
+
return (_jsxs("div", { className: "pl-4 border-l border-[var(--border-default)] space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: formatToolDisplayName(toolName) }), canNavigate && onNavigateResource ? (_jsx("button", { type: "button", onClick: () => onNavigateResource(resultData.resourceType, resultData.resourceId, resultData), className: "inline-flex items-center gap-1 text-[10px] font-medium text-[var(--state-success)] hover:text-[var(--state-success)] transition-colors", children: "Open \u203A" })) : null] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium uppercase tracking-[0.1em] text-[var(--text-tertiary)] mb-1", children: "Input" }), _jsx("div", { className: "p-2 rounded bg-[color:color-mix(in_srgb,_var(--bg-canvas)_70%,_transparent)] font-mono text-[11px] max-h-24 overflow-auto", children: _jsx("pre", { className: "text-[var(--text-secondary)] whitespace-pre-wrap", children: JSON.stringify(args, null, 2) }) })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium uppercase tracking-[0.1em] text-[var(--text-tertiary)] mb-1", children: "Output" }), _jsx("div", { className: "p-2 rounded bg-[color:color-mix(in_srgb,_var(--bg-canvas)_70%,_transparent)] font-mono text-[11px] max-h-24 overflow-auto", children: _jsx("pre", { className: "text-[var(--text-secondary)] whitespace-pre-wrap", children: Object.keys(resultData).length > 0 ? JSON.stringify(resultData, null, 2) : "No output yet" }) })] })] })] }));
|
|
578
|
+
}
|
|
579
|
+
function InlineToolCall({ invocation, isSelected, onClick, }) {
|
|
580
|
+
const resultData = (invocation.result || {});
|
|
581
|
+
const isExecuting = invocation.state !== "result";
|
|
582
|
+
const isComplete = invocation.state === "result" && resultData.success !== false;
|
|
583
|
+
const isFailed = invocation.state === "result" && resultData.success === false;
|
|
584
|
+
const summary = isExecuting
|
|
585
|
+
? formatActiveToolSummary(invocation.toolName, invocation.args)
|
|
586
|
+
: isFailed
|
|
587
|
+
? (typeof resultData.error === "string" ? resultData.error : "Tool failed")
|
|
588
|
+
: (formatToolResultSummary(invocation.toolName, invocation.args, resultData) || "Completed");
|
|
589
|
+
return (_jsxs("button", { type: "button", onClick: onClick, className: cx("w-full text-left inline-flex items-center gap-1.5 text-[12px] leading-5 transition-colors hover:text-[var(--text-primary)]", isExecuting && "text-[var(--state-info)]", isComplete && "text-[var(--state-success)]", isFailed && "text-[var(--state-error)]", !isExecuting && !isComplete && !isFailed && "text-[var(--text-secondary)]"), children: [_jsx("span", { className: "font-medium whitespace-nowrap", children: formatToolDisplayName(invocation.toolName) }), _jsx("span", { className: "text-current/80 truncate", children: summary }), _jsx("span", { className: "ml-auto transition-transform", children: isSelected ? "⌄" : "›" })] }));
|
|
590
|
+
}
|
|
591
|
+
function ToolActivityRollup({ detailParts, onNavigateResource, renderToolInvocation, message, activeConversationId, }) {
|
|
592
|
+
const [activeToolCallId, setActiveToolCallId] = useState(null);
|
|
593
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
594
|
+
const toolParts = detailParts.filter((part) => part.type === "tool");
|
|
595
|
+
const reasoningParts = detailParts.filter((part) => part.type === "reasoning");
|
|
596
|
+
const activeInvocation = [...toolParts]
|
|
597
|
+
.reverse()
|
|
598
|
+
.find((part) => part.toolInvocation.state !== "result")
|
|
599
|
+
?.toolInvocation;
|
|
600
|
+
const failedCount = toolParts.filter((part) => (part.toolInvocation.state === "result" && part.toolInvocation.result?.success === false)).length;
|
|
601
|
+
const isWorking = !!activeInvocation || reasoningParts.some((part) => part.state === "streaming");
|
|
602
|
+
const summary = activeInvocation
|
|
603
|
+
? formatActiveToolSummary(activeInvocation.toolName, activeInvocation.args)
|
|
604
|
+
: `Worked across ${toolParts.length} tool${toolParts.length === 1 ? "" : "s"}${failedCount > 0 ? ` · ${failedCount} failed` : ""}`;
|
|
605
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsxs("button", { type: "button", onClick: () => setIsExpanded((prev) => !prev), className: "inline-flex items-center gap-1.5 text-[12px] leading-5 text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors", children: [_jsx("span", { className: cx("transition-transform", isExpanded && "rotate-90"), children: "\u203A" }), isWorking ? _jsx("span", { className: "inline-flex h-2 w-2 rounded-full bg-[var(--brand-accent)]" }) : null, _jsx("span", { className: cx("text-[var(--text-secondary)]", isWorking && "font-medium"), children: summary })] }), isExpanded ? (_jsx("div", { className: "pl-4 border-l border-[var(--border-default)] space-y-1.5", children: detailParts.map((part) => {
|
|
606
|
+
if (part.type === "reasoning") {
|
|
607
|
+
return (_jsxs("div", { className: "rounded-md bg-[var(--bg-canvas)] px-2.5 py-2", children: [_jsx("div", { className: "mb-1 text-[10px] font-medium uppercase tracking-[0.1em] text-[var(--text-tertiary)]", children: part.state === "streaming" ? "Thinking" : "Thought" }), _jsx("pre", { className: "text-[11px] leading-5 text-[var(--text-secondary)] whitespace-pre-wrap font-mono max-h-40 overflow-auto", children: part.text })] }, `thinking-${part.id}`));
|
|
608
|
+
}
|
|
609
|
+
const invocation = part.toolInvocation;
|
|
610
|
+
const isSelected = activeToolCallId === invocation.toolCallId;
|
|
611
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx(InlineToolCall, { invocation: invocation, isSelected: isSelected, onClick: () => setActiveToolCallId((prev) => (prev === invocation.toolCallId ? null : invocation.toolCallId)) }), isSelected ? (_jsx(ToolDetailsPanel, { toolName: invocation.toolName, args: invocation.args, state: invocation.state, result: invocation.result, onNavigateResource: onNavigateResource, renderToolInvocation: renderToolInvocation, message: message, activeConversationId: activeConversationId })) : null] }, part.id));
|
|
612
|
+
}) })) : null] }));
|
|
613
|
+
}
|
|
614
|
+
function ShowWidgetToolCard({ invocation, onSendPrompt, }) {
|
|
615
|
+
const resultData = (invocation.result || {});
|
|
616
|
+
const payload = extractShowWidgetPayload(invocation.args, resultData);
|
|
617
|
+
const displayName = payload?.title || formatToolDisplayName(invocation.toolName);
|
|
618
|
+
const hasResultData = Object.keys(resultData).length > 0;
|
|
619
|
+
const isExecuting = invocation.state !== "result" && !hasResultData;
|
|
620
|
+
const isFailed = resultData.success === false || (!isExecuting && typeof resultData.error === "string" && resultData.error.length > 0);
|
|
621
|
+
const iframeRef = useRef(null);
|
|
622
|
+
const [height, setHeight] = useState(220);
|
|
623
|
+
const iframeDocument = useMemo(() => (payload ? buildWidgetIframeDocument(invocation.toolCallId, payload) : ""), [invocation.toolCallId, payload]);
|
|
624
|
+
useEffect(() => {
|
|
625
|
+
const handleMessage = (event) => {
|
|
626
|
+
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow)
|
|
627
|
+
return;
|
|
628
|
+
const data = asRecord(event.data);
|
|
629
|
+
const messageType = asString(data.type);
|
|
630
|
+
if (!messageType)
|
|
631
|
+
return;
|
|
632
|
+
if (messageType === "lemma-widget-send-prompt") {
|
|
633
|
+
const text = asString(data.text);
|
|
634
|
+
if (!text)
|
|
635
|
+
return;
|
|
636
|
+
void onSendPrompt(text);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (messageType === "lemma-widget-height") {
|
|
640
|
+
const rawHeight = typeof data.height === "number" ? data.height : Number(data.height);
|
|
641
|
+
if (!Number.isFinite(rawHeight))
|
|
642
|
+
return;
|
|
643
|
+
setHeight(Math.max(120, Math.min(2400, Math.round(rawHeight))));
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
window.addEventListener("message", handleMessage);
|
|
647
|
+
return () => {
|
|
648
|
+
window.removeEventListener("message", handleMessage);
|
|
649
|
+
};
|
|
650
|
+
}, [onSendPrompt]);
|
|
651
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "text-[11px] font-semibold text-[var(--state-info)]", children: displayName }), _jsx("span", { className: cx("inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium", isExecuting && "bg-[color:color-mix(in_srgb,_var(--state-info)_16%,_transparent)] text-[var(--state-info)]", isFailed && "bg-[color:color-mix(in_srgb,_var(--state-error)_12%,_transparent)] text-[var(--state-error)]", !isExecuting && !isFailed && "bg-[color:color-mix(in_srgb,_var(--state-success)_12%,_transparent)] text-[var(--state-success)]"), children: isExecuting ? "Rendering" : isFailed ? "Failed" : "Ready" })] }), isFailed ? (_jsx("p", { className: "text-[11px] text-[var(--state-error)]", children: typeof resultData.error === "string" && resultData.error.length > 0
|
|
652
|
+
? resultData.error
|
|
653
|
+
: "Failed to render widget." })) : null, !isFailed && payload ? (_jsx("iframe", { ref: iframeRef, title: displayName, srcDoc: iframeDocument, sandbox: "allow-scripts allow-forms allow-popups allow-downloads", height: height, className: "w-full border-0 bg-transparent rounded-xl" })) : null, !isFailed && !payload ? (_jsx("p", { className: "text-[11px] text-[var(--text-secondary)]", children: "Widget output is missing `widget_code`." })) : null] }));
|
|
654
|
+
}
|
|
655
|
+
export function MessageGroup({ message, conversationId, onNavigateResource, onWidgetSendPrompt, isStreaming, showAssistantHeader, renderMessageContent, renderPresentedFile, renderToolInvocation, }) {
|
|
656
|
+
const orderedParts = message.parts && message.parts.length > 0
|
|
657
|
+
? message.parts
|
|
658
|
+
: [
|
|
659
|
+
...(message.content?.trim()
|
|
660
|
+
? [{ id: `${message.id}-fallback-text`, type: "text", text: message.content }]
|
|
661
|
+
: []),
|
|
662
|
+
...((message.toolInvocations || []).map((tool, index) => ({
|
|
663
|
+
id: `${tool.toolCallId || message.id}-fallback-tool-${index}`,
|
|
664
|
+
type: "tool",
|
|
665
|
+
toolInvocation: tool,
|
|
666
|
+
}))),
|
|
667
|
+
];
|
|
668
|
+
const toolParts = orderedParts.filter((part) => part.type === "tool");
|
|
669
|
+
const groupedToolParts = toolParts.filter((part) => !isShowWidgetToolName(part.toolInvocation.toolName));
|
|
670
|
+
const reasoningParts = orderedParts.filter((part) => part.type === "reasoning");
|
|
671
|
+
const presentableFilepaths = Array.from(new Set(groupedToolParts.flatMap((part) => extractPresentFilePathsFromInvocation(part.toolInvocation))));
|
|
672
|
+
const rollupOrderedParts = orderedParts.filter((part) => (part.type === "reasoning" || (part.type === "tool" && !isShowWidgetToolName(part.toolInvocation.toolName))));
|
|
673
|
+
const blocks = [];
|
|
674
|
+
orderedParts.forEach((part) => {
|
|
675
|
+
if (part.type === "tool") {
|
|
676
|
+
if (isShowWidgetToolName(part.toolInvocation.toolName)) {
|
|
677
|
+
blocks.push({
|
|
678
|
+
id: `${part.id}-widget`,
|
|
679
|
+
kind: "widget",
|
|
680
|
+
toolPart: part,
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const lastBlock = blocks[blocks.length - 1];
|
|
685
|
+
if (lastBlock?.kind === "tools") {
|
|
686
|
+
lastBlock.toolParts.push(part);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
blocks.push({
|
|
690
|
+
id: `${part.id}-tools`,
|
|
691
|
+
kind: "tools",
|
|
692
|
+
toolParts: [part],
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
blocks.push({
|
|
698
|
+
id: part.id,
|
|
699
|
+
kind: "content",
|
|
700
|
+
part,
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
const nonToolParts = orderedParts.filter((part) => part.type !== "tool");
|
|
704
|
+
const firstToolsBlockId = blocks.find((block) => block.kind === "tools")?.id;
|
|
705
|
+
const hasTextParts = orderedParts.some((part) => part.type === "text" && part.text.trim().length > 0);
|
|
706
|
+
const foldReasoningIntoToolRollup = groupedToolParts.length > 0 && reasoningParts.length > 0 && !hasTextParts;
|
|
707
|
+
const lastTextPartId = [...nonToolParts]
|
|
708
|
+
.reverse()
|
|
709
|
+
.find((part) => part.type === "text" && part.text.trim().length > 0)
|
|
710
|
+
?.id;
|
|
711
|
+
if (message.role === "user") {
|
|
712
|
+
return (_jsx("div", { className: "flex justify-end", children: _jsx("div", { className: "max-w-[72ch] rounded-xl bg-[var(--brand-primary)] text-[var(--text-on-brand)] px-3.5 py-2.5", children: renderMessageContent({
|
|
713
|
+
message: {
|
|
714
|
+
...message,
|
|
715
|
+
content: message.content,
|
|
716
|
+
parts: undefined,
|
|
717
|
+
toolInvocations: undefined,
|
|
718
|
+
},
|
|
719
|
+
}) }) }));
|
|
720
|
+
}
|
|
721
|
+
return (_jsxs("div", { className: "px-1 py-0.5 space-y-1.5 max-w-[78ch]", children: [showAssistantHeader ? (_jsxs("div", { className: "inline-flex items-center gap-1.5 text-[11px] text-[var(--text-tertiary)]", children: [_jsx("span", { className: "inline-block h-1.5 w-1.5 rounded-full bg-[color:color-mix(in_srgb,_var(--brand-primary)_40%,_transparent)]" }), "Lemma"] })) : null, _jsxs("div", { className: "space-y-2", children: [blocks.map((block) => {
|
|
722
|
+
if (block.kind === "tools") {
|
|
723
|
+
if (foldReasoningIntoToolRollup && block.id !== firstToolsBlockId) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
return (_jsx(ToolActivityRollup, { detailParts: foldReasoningIntoToolRollup && block.id === firstToolsBlockId
|
|
727
|
+
? rollupOrderedParts
|
|
728
|
+
: block.toolParts, onNavigateResource: onNavigateResource, renderToolInvocation: renderToolInvocation, message: message, activeConversationId: conversationId ?? null }, block.id));
|
|
729
|
+
}
|
|
730
|
+
if (block.kind === "widget") {
|
|
731
|
+
return (_jsx(ShowWidgetToolCard, { invocation: block.toolPart.toolInvocation, onSendPrompt: onWidgetSendPrompt }, block.id));
|
|
732
|
+
}
|
|
733
|
+
const part = block.part;
|
|
734
|
+
if (part.type === "text") {
|
|
735
|
+
const trimmedText = part.text.trim();
|
|
736
|
+
if (trimmedText.length === 0) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
return (_jsx("div", { className: "text-[13px] text-[var(--text-secondary)] leading-6", children: renderMessageContent({
|
|
740
|
+
message: {
|
|
741
|
+
...message,
|
|
742
|
+
content: trimmedText + (isStreaming && part.id === lastTextPartId ? " ▍" : ""),
|
|
743
|
+
parts: undefined,
|
|
744
|
+
toolInvocations: undefined,
|
|
745
|
+
},
|
|
746
|
+
}) }, part.id));
|
|
747
|
+
}
|
|
748
|
+
if (part.type === "reasoning") {
|
|
749
|
+
if (foldReasoningIntoToolRollup) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
return (_jsx(ReasoningPartCard, { text: part.text, isStreaming: part.state === "streaming", durationMs: part.durationMs }, part.id));
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}), presentableFilepaths.length > 0 ? (_jsx(PresentFilesCard, { filepaths: presentableFilepaths, conversationId: conversationId, renderPresentedFile: renderPresentedFile })) : null] })] }));
|
|
756
|
+
}
|
|
757
|
+
export function AssistantExperienceView({ controller, title = "Lemma Assistant", subtitle = "Ask across your workspace and organization.", placeholder = "Message Lemma Assistant", emptyState, draft: controlledDraft, onDraftChange, showConversationList = false, onNavigateResource, renderConversationLabel = defaultConversationLabel, renderMessageContent = defaultMessageContent, renderPresentedFile, renderPendingFile = defaultPendingFile, renderToolInvocation, }) {
|
|
758
|
+
const [draft, setDraft] = useControllableDraft(controlledDraft, onDraftChange);
|
|
759
|
+
const [isPlanHidden, setIsPlanHidden] = useState(false);
|
|
760
|
+
const [dismissedAskToolCallIds, setDismissedAskToolCallIds] = useState([]);
|
|
761
|
+
const [askOverlayState, setAskOverlayState] = useState(null);
|
|
762
|
+
const [isUpdatingModel, setIsUpdatingModel] = useState(false);
|
|
763
|
+
const messagesContainerRef = useRef(null);
|
|
764
|
+
const inputRef = useRef(null);
|
|
765
|
+
const fileInputRef = useRef(null);
|
|
766
|
+
const isPinnedToBottomRef = useRef(true);
|
|
767
|
+
const loadingOlderFromScrollRef = useRef(false);
|
|
768
|
+
const isConversationBusy = controller.isLoading || controller.isActiveConversationRunning;
|
|
769
|
+
const availableModels = useMemo(() => Object.values(AvailableModels), []);
|
|
770
|
+
const resizeComposer = useCallback(() => {
|
|
771
|
+
const textarea = inputRef.current;
|
|
772
|
+
if (!textarea)
|
|
773
|
+
return;
|
|
774
|
+
const minHeight = 48;
|
|
775
|
+
const maxHeight = 220;
|
|
776
|
+
textarea.style.height = "0px";
|
|
777
|
+
const nextHeight = Math.min(maxHeight, Math.max(minHeight, textarea.scrollHeight));
|
|
778
|
+
textarea.style.height = `${nextHeight}px`;
|
|
779
|
+
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
780
|
+
}, []);
|
|
781
|
+
const scrollToLatest = useCallback((behavior = "auto") => {
|
|
782
|
+
const el = messagesContainerRef.current;
|
|
783
|
+
if (!el)
|
|
784
|
+
return;
|
|
785
|
+
el.scrollTo({
|
|
786
|
+
top: el.scrollHeight,
|
|
787
|
+
behavior,
|
|
788
|
+
});
|
|
789
|
+
isPinnedToBottomRef.current = true;
|
|
790
|
+
}, []);
|
|
791
|
+
const updatePinnedState = useCallback(() => {
|
|
792
|
+
const el = messagesContainerRef.current;
|
|
793
|
+
if (!el)
|
|
794
|
+
return;
|
|
795
|
+
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
796
|
+
isPinnedToBottomRef.current = distanceFromBottom <= 112;
|
|
797
|
+
if (el.scrollTop > 48)
|
|
798
|
+
return;
|
|
799
|
+
if (!controller.hasOlderMessages || controller.isLoadingMessages || controller.isLoadingOlderMessages || loadingOlderFromScrollRef.current)
|
|
800
|
+
return;
|
|
801
|
+
const previousScrollTop = el.scrollTop;
|
|
802
|
+
const previousScrollHeight = el.scrollHeight;
|
|
803
|
+
loadingOlderFromScrollRef.current = true;
|
|
804
|
+
void controller.loadOlderMessages()
|
|
805
|
+
.then((didLoad) => {
|
|
806
|
+
if (!didLoad)
|
|
807
|
+
return;
|
|
808
|
+
requestAnimationFrame(() => {
|
|
809
|
+
const nextEl = messagesContainerRef.current;
|
|
810
|
+
if (!nextEl)
|
|
811
|
+
return;
|
|
812
|
+
nextEl.scrollTop = previousScrollTop + (nextEl.scrollHeight - previousScrollHeight);
|
|
813
|
+
});
|
|
814
|
+
})
|
|
815
|
+
.finally(() => {
|
|
816
|
+
loadingOlderFromScrollRef.current = false;
|
|
817
|
+
});
|
|
818
|
+
}, [
|
|
819
|
+
controller,
|
|
820
|
+
]);
|
|
821
|
+
useEffect(() => {
|
|
822
|
+
const el = messagesContainerRef.current;
|
|
823
|
+
if (!el)
|
|
824
|
+
return;
|
|
825
|
+
if (isPinnedToBottomRef.current) {
|
|
826
|
+
requestAnimationFrame(() => {
|
|
827
|
+
scrollToLatest(isConversationBusy ? "auto" : "smooth");
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}, [controller.messages, isConversationBusy, scrollToLatest]);
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
resizeComposer();
|
|
833
|
+
}, [draft, resizeComposer]);
|
|
834
|
+
const displayMessageRows = useMemo(() => buildDisplayMessageRows(controller.messages), [controller.messages]);
|
|
835
|
+
const activeToolBanner = useMemo(() => getActiveToolBanner(controller.messages), [controller.messages]);
|
|
836
|
+
const planSummary = useMemo(() => latestPlanSummary(controller.messages), [controller.messages]);
|
|
837
|
+
const pendingAskUserInput = useMemo(() => {
|
|
838
|
+
const pending = findPendingAskUserInput(controller.messages);
|
|
839
|
+
if (!pending)
|
|
840
|
+
return null;
|
|
841
|
+
if (dismissedAskToolCallIds.includes(pending.toolCallId))
|
|
842
|
+
return null;
|
|
843
|
+
return pending;
|
|
844
|
+
}, [controller.messages, dismissedAskToolCallIds]);
|
|
845
|
+
const effectiveAskOverlayState = useMemo(() => {
|
|
846
|
+
if (!pendingAskUserInput)
|
|
847
|
+
return null;
|
|
848
|
+
if (askOverlayState && askOverlayState.toolCallId === pendingAskUserInput.toolCallId) {
|
|
849
|
+
return askOverlayState;
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
toolCallId: pendingAskUserInput.toolCallId,
|
|
853
|
+
currentQuestionIndex: 0,
|
|
854
|
+
answers: pendingAskUserInput.questions.map(() => []),
|
|
855
|
+
};
|
|
856
|
+
}, [askOverlayState, pendingAskUserInput]);
|
|
857
|
+
const lastMessageHasContent = useMemo(() => {
|
|
858
|
+
if (controller.messages.length === 0)
|
|
859
|
+
return false;
|
|
860
|
+
const lastMsg = controller.messages[controller.messages.length - 1];
|
|
861
|
+
if (lastMsg.role !== "assistant")
|
|
862
|
+
return false;
|
|
863
|
+
const hasText = (lastMsg.parts || []).some((part) => part.type === "text" && part.text.trim().length > 0);
|
|
864
|
+
const hasTools = (lastMsg.toolInvocations?.length || 0) > 0 || (lastMsg.parts || []).some((part) => part.type === "tool");
|
|
865
|
+
return hasText || hasTools;
|
|
866
|
+
}, [controller.messages]);
|
|
867
|
+
const dismissAskOverlay = useCallback((toolCallId) => {
|
|
868
|
+
setDismissedAskToolCallIds((prev) => (prev.includes(toolCallId) ? prev : [...prev, toolCallId]));
|
|
869
|
+
setAskOverlayState(null);
|
|
870
|
+
}, []);
|
|
871
|
+
const commitAskAnswersToComposer = useCallback((toolCallId, answers) => {
|
|
872
|
+
if (!pendingAskUserInput || pendingAskUserInput.toolCallId !== toolCallId)
|
|
873
|
+
return;
|
|
874
|
+
const formatted = formatAskUserInputAnswers(pendingAskUserInput.questions, answers);
|
|
875
|
+
if (formatted.length > 0) {
|
|
876
|
+
const nextDraft = draft.trim().length > 0 ? `${draft.trim()}\n\n${formatted}` : formatted;
|
|
877
|
+
setDraft(nextDraft);
|
|
878
|
+
}
|
|
879
|
+
dismissAskOverlay(toolCallId);
|
|
880
|
+
requestAnimationFrame(() => {
|
|
881
|
+
inputRef.current?.focus();
|
|
882
|
+
});
|
|
883
|
+
}, [dismissAskOverlay, draft, pendingAskUserInput, setDraft]);
|
|
884
|
+
const updateAskAnswer = useCallback((option) => {
|
|
885
|
+
if (!pendingAskUserInput || !effectiveAskOverlayState)
|
|
886
|
+
return;
|
|
887
|
+
if (effectiveAskOverlayState.toolCallId !== pendingAskUserInput.toolCallId)
|
|
888
|
+
return;
|
|
889
|
+
const questionIndex = effectiveAskOverlayState.currentQuestionIndex;
|
|
890
|
+
const question = pendingAskUserInput.questions[questionIndex];
|
|
891
|
+
if (!question)
|
|
892
|
+
return;
|
|
893
|
+
const nextAnswers = effectiveAskOverlayState.answers.map((answers) => [...answers]);
|
|
894
|
+
const currentAnswers = nextAnswers[questionIndex] || [];
|
|
895
|
+
if (question.type === "single_select") {
|
|
896
|
+
nextAnswers[questionIndex] = [option];
|
|
897
|
+
if (questionIndex >= pendingAskUserInput.questions.length - 1) {
|
|
898
|
+
commitAskAnswersToComposer(effectiveAskOverlayState.toolCallId, nextAnswers);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
setAskOverlayState({
|
|
902
|
+
...effectiveAskOverlayState,
|
|
903
|
+
answers: nextAnswers,
|
|
904
|
+
currentQuestionIndex: questionIndex + 1,
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
nextAnswers[questionIndex] = currentAnswers.includes(option)
|
|
909
|
+
? currentAnswers.filter((entry) => entry !== option)
|
|
910
|
+
: [...currentAnswers, option];
|
|
911
|
+
setAskOverlayState({
|
|
912
|
+
...effectiveAskOverlayState,
|
|
913
|
+
answers: nextAnswers,
|
|
914
|
+
});
|
|
915
|
+
}, [commitAskAnswersToComposer, effectiveAskOverlayState, pendingAskUserInput]);
|
|
916
|
+
const continueAskQuestions = useCallback(() => {
|
|
917
|
+
if (!pendingAskUserInput || !effectiveAskOverlayState)
|
|
918
|
+
return;
|
|
919
|
+
if (effectiveAskOverlayState.toolCallId !== pendingAskUserInput.toolCallId)
|
|
920
|
+
return;
|
|
921
|
+
const questionIndex = effectiveAskOverlayState.currentQuestionIndex;
|
|
922
|
+
const answers = effectiveAskOverlayState.answers[questionIndex] || [];
|
|
923
|
+
if (answers.length === 0)
|
|
924
|
+
return;
|
|
925
|
+
if (questionIndex >= pendingAskUserInput.questions.length - 1) {
|
|
926
|
+
commitAskAnswersToComposer(effectiveAskOverlayState.toolCallId, effectiveAskOverlayState.answers);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
setAskOverlayState({
|
|
930
|
+
...effectiveAskOverlayState,
|
|
931
|
+
currentQuestionIndex: questionIndex + 1,
|
|
932
|
+
});
|
|
933
|
+
}, [commitAskAnswersToComposer, effectiveAskOverlayState, pendingAskUserInput]);
|
|
934
|
+
const handleSubmit = useCallback(async () => {
|
|
935
|
+
if (!draft.trim() || isConversationBusy)
|
|
936
|
+
return;
|
|
937
|
+
const message = draft.trim();
|
|
938
|
+
setDraft("");
|
|
939
|
+
scrollToLatest("smooth");
|
|
940
|
+
await controller.sendMessage(message);
|
|
941
|
+
}, [controller, draft, isConversationBusy, scrollToLatest, setDraft]);
|
|
942
|
+
const handleWidgetSendPrompt = useCallback(async (prompt) => {
|
|
943
|
+
const message = prompt.trim();
|
|
944
|
+
if (!message)
|
|
945
|
+
return;
|
|
946
|
+
if (isConversationBusy) {
|
|
947
|
+
setDraft(message);
|
|
948
|
+
requestAnimationFrame(() => {
|
|
949
|
+
inputRef.current?.focus();
|
|
950
|
+
});
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
scrollToLatest("smooth");
|
|
954
|
+
await controller.sendMessage(message);
|
|
955
|
+
}, [controller, isConversationBusy, scrollToLatest, setDraft]);
|
|
956
|
+
const handleUploadSelection = useCallback(async (files) => {
|
|
957
|
+
const selectedFiles = files ? Array.from(files) : [];
|
|
958
|
+
if (selectedFiles.length === 0)
|
|
959
|
+
return;
|
|
960
|
+
try {
|
|
961
|
+
await controller.uploadFiles(selectedFiles);
|
|
962
|
+
}
|
|
963
|
+
finally {
|
|
964
|
+
if (fileInputRef.current) {
|
|
965
|
+
fileInputRef.current.value = "";
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}, [controller]);
|
|
969
|
+
const handleKeyDown = useCallback((event) => {
|
|
970
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
971
|
+
event.preventDefault();
|
|
972
|
+
void handleSubmit();
|
|
973
|
+
}
|
|
974
|
+
}, [handleSubmit]);
|
|
975
|
+
const handleModelChange = useCallback(async (nextModel) => {
|
|
976
|
+
if (isUpdatingModel)
|
|
977
|
+
return;
|
|
978
|
+
setIsUpdatingModel(true);
|
|
979
|
+
try {
|
|
980
|
+
await controller.setConversationModel(nextModel);
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
setIsUpdatingModel(false);
|
|
984
|
+
}
|
|
985
|
+
}, [controller, isUpdatingModel]);
|
|
986
|
+
const activeAskQuestion = pendingAskUserInput
|
|
987
|
+
&& effectiveAskOverlayState
|
|
988
|
+
&& effectiveAskOverlayState.toolCallId === pendingAskUserInput.toolCallId
|
|
989
|
+
? pendingAskUserInput.questions[effectiveAskOverlayState.currentQuestionIndex]
|
|
990
|
+
: null;
|
|
991
|
+
const activeAskAnswers = activeAskQuestion && effectiveAskOverlayState
|
|
992
|
+
? effectiveAskOverlayState.answers[effectiveAskOverlayState.currentQuestionIndex] || []
|
|
993
|
+
: [];
|
|
994
|
+
const canContinueAsk = activeAskAnswers.length > 0;
|
|
995
|
+
return (_jsxs("div", { className: cx("flex h-full min-h-0 flex-col gap-3 font-sans antialiased", showConversationList && "lg:grid lg:grid-cols-[280px_minmax(0,1fr)] lg:gap-3"), children: [showConversationList ? (_jsxs("aside", { className: "hidden min-h-0 overflow-hidden rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] shadow-[var(--shadow-lg)] lg:flex lg:flex-col", children: [_jsx("div", { className: "border-b border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[13px] font-semibold text-[var(--text-primary)]", children: "Conversations" }), _jsxs("div", { className: "mt-1 text-[11px] text-[var(--text-tertiary)]", children: [controller.conversations.length, " total"] })] }), _jsx("button", { type: "button", onClick: controller.clearMessages, className: "rounded-full border border-[var(--border-default)] bg-[var(--bg-surface)] px-3 py-1.5 text-[11px] font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]", children: "New" })] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-2", children: controller.conversations.map((conversation) => {
|
|
996
|
+
const isActive = conversation.id === controller.activeConversationId;
|
|
997
|
+
return (_jsxs("button", { type: "button", onClick: () => controller.selectConversation(conversation.id), className: cx("w-full rounded-xl border px-3 py-2.5 text-left transition-colors", isActive
|
|
998
|
+
? "border-[color:color-mix(in_srgb,_var(--brand-primary)_44%,_var(--border-default))] bg-[color:color-mix(in_srgb,_var(--brand-glow)_42%,_var(--bg-surface))]"
|
|
999
|
+
: "border-[var(--border-default)] bg-[var(--bg-surface)] hover:bg-[var(--bg-subtle)]"), children: [_jsx("div", { className: "text-[12px] font-medium text-[var(--text-primary)]", children: renderConversationLabel({ conversation, isActive }) }), _jsx("div", { className: "mt-1 text-[10px] uppercase tracking-[0.08em] text-[var(--text-tertiary)]", children: (conversation.status || "waiting").toLowerCase() })] }, conversation.id));
|
|
1000
|
+
}) })] })) : null, _jsxs("div", { className: "flex h-full min-h-0 flex-col gap-3", children: [_jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] shadow-[var(--shadow-lg)]", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "h-7 w-7 rounded-full bg-[linear-gradient(135deg,var(--brand-primary),var(--brand-secondary))] flex items-center justify-center shadow-[var(--shadow-xs)]", children: _jsx("span", { className: "text-[var(--text-on-brand)] text-xs", children: "\u2728" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-[var(--text-primary)] text-[13px] leading-tight", children: title }), _jsx("p", { className: "text-[11px] text-[var(--text-tertiary)]", children: subtitle })] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("select", { value: controller.conversationModel || "", onChange: (event) => { void handleModelChange(event.target.value || null); }, disabled: isConversationBusy || isUpdatingModel, className: "h-8 rounded-full border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] px-3 text-[11px] text-[var(--text-secondary)]", children: [_jsx("option", { value: "", children: "Auto" }), availableModels.map((availableModel) => (_jsx("option", { value: availableModel, children: availableModel }, availableModel)))] }), _jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "inline-flex h-8 w-8 items-center justify-center rounded-full text-[var(--text-tertiary)] hover:bg-[var(--bg-subtle)] hover:text-[var(--text-secondary)]", children: "\u21BA" })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-3 min-h-[180px] bg-[var(--bg-surface)]", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || _jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); } })) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "flex justify-center py-6", children: _jsx("span", { className: "text-[var(--text-tertiary)] text-sm", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "flex justify-center py-1", children: _jsx("span", { className: "text-[var(--text-tertiary)] text-xs", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
|
|
1001
|
+
const previousRow = index > 0 ? displayMessageRows[index - 1] : null;
|
|
1002
|
+
const showAssistantHeader = row.message.role !== "assistant"
|
|
1003
|
+
? false
|
|
1004
|
+
: previousRow?.message.role !== "assistant";
|
|
1005
|
+
const includesLastRawMessage = row.sourceIndexes.includes(controller.messages.length - 1);
|
|
1006
|
+
return (_jsx(MessageGroup, { message: row.message, onNavigateResource: onNavigateResource, onWidgetSendPrompt: handleWidgetSendPrompt, conversationId: controller.activeConversationId, isStreaming: isConversationBusy && includesLastRawMessage && row.message.role === "assistant", showAssistantHeader: showAssistantHeader, renderMessageContent: renderMessageContent, renderPresentedFile: renderPresentedFile, renderToolInvocation: renderToolInvocation }, row.id || index));
|
|
1007
|
+
}), isConversationBusy && controller.messages.length > 0 && !activeToolBanner && !lastMessageHasContent ? (_jsx(ThinkingIndicator, {})) : null, controller.error ? (_jsx("div", { className: "bg-[color:color-mix(in_srgb,_var(--state-error)_12%,_transparent)] border border-[color:color-mix(in_srgb,_var(--state-error)_48%,_var(--border-subtle))] rounded-lg p-3 text-xs text-[var(--state-error)] flex items-start gap-2.5", children: _jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Something went wrong" }), _jsx("p", { className: "text-[var(--state-error)] mt-1", children: controller.error })] }) })) : null, (controller.messages.length > 0 || isConversationBusy || !!controller.error) ? (_jsx("div", { "aria-hidden": "true", className: "h-14 shrink-0" })) : null] })] }), _jsxs("div", { className: "relative rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] p-2 shadow-[var(--shadow-md)]", children: [planSummary ? (_jsx("div", { className: "absolute bottom-[calc(100%+8px)] left-0 right-0 z-20", children: isPlanHidden ? (_jsxs("button", { type: "button", onClick: () => setIsPlanHidden(false), className: "inline-flex items-center gap-2 rounded-lg border border-[var(--border-default)] bg-[var(--bg-surface)] px-3 py-1.5 text-[11px] font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)] transition-colors", children: ["Show plan (", planSummary.completedCount, "/", planSummary.steps.length, ")"] })) : (_jsx(PlanSummaryStrip, { plan: planSummary, onHide: () => setIsPlanHidden(true) })) })) : null, isConversationBusy && activeToolBanner ? (_jsx("div", { className: "px-2 pb-1", children: _jsx("div", { className: "inline-flex max-w-full items-center gap-1.5 text-[11px] text-[var(--text-tertiary)] animate-in fade-in duration-200", children: _jsx("span", { className: "truncate", children: activeToolBanner.summary }) }) })) : null, activeAskQuestion && effectiveAskOverlayState && pendingAskUserInput ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsxs("div", { className: "text-[11px] uppercase tracking-[0.12em] text-[var(--text-tertiary)]", children: ["Question ", effectiveAskOverlayState.currentQuestionIndex + 1, " of ", pendingAskUserInput.questions.length] }), _jsx("p", { className: "mt-1 text-[14px] font-medium text-[var(--text-primary)] leading-6", children: activeAskQuestion.question })] }), _jsx("button", { type: "button", onClick: () => dismissAskOverlay(effectiveAskOverlayState.toolCallId), className: "rounded-md px-2 py-1 text-[12px] text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)] transition-colors", children: "Skip" })] }), _jsx("div", { className: "max-h-[260px] overflow-y-auto space-y-1.5 pr-1", children: activeAskQuestion.options.map((option, optionIndex) => {
|
|
1008
|
+
const isSelected = activeAskAnswers.includes(option);
|
|
1009
|
+
const rankLabel = activeAskQuestion.type === "rank_priorities" && isSelected
|
|
1010
|
+
? activeAskAnswers.indexOf(option) + 1
|
|
1011
|
+
: null;
|
|
1012
|
+
return (_jsx("button", { type: "button", onClick: () => updateAskAnswer(option), className: cx("w-full rounded-lg border px-2.5 py-2 text-left text-[13px] transition-colors", isSelected
|
|
1013
|
+
? "border-[color:color-mix(in_srgb,_var(--brand-primary)_64%,_var(--border-subtle))] bg-[color:color-mix(in_srgb,_var(--brand-primary)_14%,_transparent)] text-[var(--text-primary)]"
|
|
1014
|
+
: "border-[var(--border-default)] bg-[var(--bg-canvas)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)]"), children: _jsxs("span", { className: "inline-flex items-center gap-2", children: [rankLabel ? (_jsx("span", { className: "inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--brand-primary)] px-1 text-[10px] font-semibold text-[var(--text-on-brand)]", children: rankLabel })) : (_jsx("span", { className: cx("inline-block h-2.5 w-2.5 rounded-full border", isSelected
|
|
1015
|
+
? "border-[var(--brand-primary)] bg-[var(--brand-primary)]"
|
|
1016
|
+
: "border-[var(--border-default)] bg-transparent") })), option] }) }, `${option}-${optionIndex}`));
|
|
1017
|
+
}) }), (activeAskQuestion.type !== "single_select" || pendingAskUserInput.questions.length > 1) ? (_jsx("div", { className: "flex justify-end", children: _jsx("button", { type: "button", onClick: continueAskQuestions, disabled: !canContinueAsk, className: cx("rounded-md px-2.5 py-1.5 text-[12px] font-medium transition-colors", canContinueAsk
|
|
1018
|
+
? "bg-[var(--brand-primary)] text-[var(--text-on-brand)] hover:bg-[color:color-mix(in_srgb,_var(--brand-primary)_88%,_var(--text-primary))]"
|
|
1019
|
+
: "bg-[var(--bg-subtle)] text-[var(--text-tertiary)]"), children: effectiveAskOverlayState.currentQuestionIndex >= pendingAskUserInput.questions.length - 1 ? "Use answers" : "Continue" }) })) : null] })) : (_jsxs("div", { className: "space-y-1.5", children: [controller.pendingFiles.length > 0 ? (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 px-1", children: controller.pendingFiles.map((file) => {
|
|
1020
|
+
const fileKey = `${file.name}:${file.size}:${file.lastModified}`;
|
|
1021
|
+
return (_jsx("div", { children: renderPendingFile({
|
|
1022
|
+
file,
|
|
1023
|
+
remove: () => controller.removePendingFile(fileKey),
|
|
1024
|
+
}) }, fileKey));
|
|
1025
|
+
}) })) : null, _jsxs("div", { className: "relative flex items-end gap-2", children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: (event) => { void handleUploadSelection(event.target.files); } }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: isConversationBusy || controller.isUploadingFiles, className: cx("mb-1.5 ml-1 h-9 w-9 rounded-full flex items-center justify-center transition-colors", isConversationBusy || controller.isUploadingFiles
|
|
1026
|
+
? "bg-[var(--bg-subtle)] text-[var(--text-tertiary)]"
|
|
1027
|
+
: "bg-[var(--bg-subtle)] text-[var(--text-secondary)] hover:bg-[var(--bg-canvas)] hover:text-[var(--text-primary)]"), title: "Upload files", children: controller.isUploadingFiles ? "…" : "+" }), _jsx("textarea", { ref: inputRef, value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, className: "flex-1 resize-none border-0 bg-transparent px-3 py-2.5 text-[14px] text-[var(--text-primary)] leading-6 focus:ring-0 focus:outline-none placeholder:text-[var(--text-tertiary)] min-h-[48px] max-h-[220px]", rows: 1, disabled: isConversationBusy }), _jsx("div", { className: "pb-1.5 pr-1.5", children: _jsx("button", { onClick: isConversationBusy ? controller.stop : () => { void handleSubmit(); }, disabled: !isConversationBusy && !draft.trim(), className: cx("h-9 w-9 rounded-full flex items-center justify-center transition-all duration-200", isConversationBusy
|
|
1028
|
+
? "bg-[var(--text-primary)] text-[var(--text-inverse)] hover:bg-[color:color-mix(in_srgb,_var(--text-primary)_80%,_transparent)] hover:scale-105"
|
|
1029
|
+
: draft.trim()
|
|
1030
|
+
? "bg-[var(--brand-primary)] text-[var(--text-on-brand)] shadow-[var(--shadow-xs)] hover:bg-[color:color-mix(in_srgb,_var(--brand-primary)_88%,_var(--text-primary))]"
|
|
1031
|
+
: "bg-[var(--bg-subtle)] text-[var(--text-tertiary)]"), title: isConversationBusy ? "Stop generating" : "Send message", children: isConversationBusy ? "■" : "→" }) })] })] }))] })] })] }));
|
|
1032
|
+
}
|