@springbrand/message-panel 0.1.3-alpha.10 → 0.1.3-alpha.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/cloud-os/README.md +2 -2
- package/cloud-os/chat/cloud-os-chat-messages.tsx +4 -1
- package/cloud-os/chat/rich-blocks.tsx +265 -108
- package/cloud-os/chat/tool-presentation.ts +26 -10
- package/cloud-os/styles/cloud-os.css +29 -0
- package/demo/camel-chat-showcase.tsx +11 -12
- package/demo/chat-scenarios.ts +48 -31
- package/demo/cloud-os-chat-showcase.tsx +11 -9
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +1 -1
- package/src/camel/camel-chat-messages.tsx +77 -77
- package/src/camel/camel-tool-presentation.tsx +17 -13
- package/src/camel/camel-turn.ts +1 -17
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +0 -23
- package/src/parts/index.tsx +102 -63
package/cloud-os/README.md
CHANGED
|
@@ -57,8 +57,8 @@ pnpm dev:cloud-os # http://localhost:9992
|
|
|
57
57
|
|
|
58
58
|
- `update_plan` 是**整份快照覆盖**,不是增量事件。整条会话只保留最后一份计划快照
|
|
59
59
|
(`dropSupersededPlans`),否则同一份计划会被画上四遍。
|
|
60
|
-
- `ask_user`
|
|
61
|
-
|
|
60
|
+
- `ask_user` 的「已回答」只取该 Tool Part 的 `output.answers[]`;
|
|
61
|
+
不扫描后续用户消息,问题与答案按数组下标一一对应。
|
|
62
62
|
- `approval-requested` 的工具是否渲染成原位授权卡,由宿主的 `approvalIdOf` 决定;
|
|
63
63
|
返回 `undefined` 就退回普通工具行(本仓的持久化审批走输入框上方那条队列)。
|
|
64
64
|
|
|
@@ -154,7 +154,10 @@ function UserBubble({
|
|
|
154
154
|
{entry.attachments.map((attachment, index) => (
|
|
155
155
|
<Fragment key={`${attachment.url}:${index}`}>
|
|
156
156
|
{renderFileView(renderFile, {
|
|
157
|
-
file:
|
|
157
|
+
file: {
|
|
158
|
+
...attachment,
|
|
159
|
+
url: resolveUrl?.(attachment.url) ?? attachment.url,
|
|
160
|
+
},
|
|
158
161
|
placement: "user-message",
|
|
159
162
|
})}
|
|
160
163
|
</Fragment>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArrowLeft,
|
|
3
|
+
ArrowRight,
|
|
2
4
|
Bell,
|
|
3
5
|
CaretRight,
|
|
4
6
|
Check,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
UsersThree,
|
|
10
12
|
WarningCircle,
|
|
11
13
|
} from "@phosphor-icons/react";
|
|
12
|
-
import { useState } from "react";
|
|
14
|
+
import { useRef, useState } from "react";
|
|
13
15
|
import { Tooltip } from "../primitives/tooltip";
|
|
14
16
|
import { WorkshopButton } from "../primitives/workshop-controls";
|
|
15
17
|
import { MarkdownMessage } from "./markdown-message";
|
|
@@ -124,33 +126,32 @@ interface AskQuestion {
|
|
|
124
126
|
prompt: string;
|
|
125
127
|
options: string[];
|
|
126
128
|
multi: boolean;
|
|
129
|
+
allowCustom: boolean;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
function questionsOf(input: Record<string, unknown>): AskQuestion[] {
|
|
130
|
-
if (Array.isArray(input.questions))
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
return [];
|
|
133
|
+
if (!Array.isArray(input.questions)) return [];
|
|
134
|
+
return input.questions.map((item) => {
|
|
135
|
+
const question = (item ?? {}) as Record<string, unknown>;
|
|
136
|
+
const options = Array.isArray(question.options)
|
|
137
|
+
? question.options.filter((option): option is string =>
|
|
138
|
+
typeof option === "string"
|
|
139
|
+
)
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
prompt: typeof question.question === "string"
|
|
143
|
+
? question.question
|
|
144
|
+
: "Choose an option",
|
|
145
|
+
options,
|
|
146
|
+
multi: question.multiSelect === true,
|
|
147
|
+
allowCustom: question.allowCustom === true || options.length === 0,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface DraftAnswer {
|
|
153
|
+
selections: string[];
|
|
154
|
+
text: string;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export function AskUserBlock({
|
|
@@ -162,37 +163,54 @@ export function AskUserBlock({
|
|
|
162
163
|
onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
|
|
163
164
|
}) {
|
|
164
165
|
const questions = questionsOf(call.input);
|
|
165
|
-
// 不能用 `useState(() => questions.map(
|
|
166
|
-
// 首帧 questions 往往还是空数组,那之后每次
|
|
166
|
+
// 不能用 `useState(() => questions.map(...))` 初始化:工具入参是**流式**到达的,
|
|
167
|
+
// 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
|
|
167
168
|
// 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
|
|
168
|
-
const [
|
|
169
|
-
const
|
|
169
|
+
const [answers, setAnswers] = useState<DraftAnswer[]>([]);
|
|
170
|
+
const [current, setCurrent] = useState(0);
|
|
171
|
+
const direction = useRef<"back" | "forward">("forward");
|
|
172
|
+
const answerAt = (index: number): DraftAnswer =>
|
|
173
|
+
answers[index] ?? { selections: [], text: "" };
|
|
170
174
|
const toggle = (questionIndex: number, option: string, multi: boolean) =>
|
|
171
|
-
|
|
172
|
-
const next = questions.map((_, index) =>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
setAnswers((current) => {
|
|
176
|
+
const next = questions.map((_, index) =>
|
|
177
|
+
current[index] ?? { selections: [], text: "" }
|
|
178
|
+
);
|
|
179
|
+
const answer = next[questionIndex]!;
|
|
180
|
+
next[questionIndex] = {
|
|
181
|
+
...answer,
|
|
182
|
+
selections: !multi
|
|
183
|
+
? [option]
|
|
184
|
+
: answer.selections.includes(option)
|
|
185
|
+
? answer.selections.filter((value) => value !== option)
|
|
186
|
+
: [...answer.selections, option],
|
|
187
|
+
};
|
|
188
|
+
return next;
|
|
189
|
+
});
|
|
190
|
+
const setText = (questionIndex: number, text: string) =>
|
|
191
|
+
setAnswers((current) => {
|
|
192
|
+
const next = questions.map((_, index) =>
|
|
193
|
+
current[index] ?? { selections: [], text: "" }
|
|
194
|
+
);
|
|
195
|
+
next[questionIndex] = { ...next[questionIndex]!, text };
|
|
179
196
|
return next;
|
|
180
197
|
});
|
|
181
198
|
// 在途只用于按钮转圈;权威状态永远是 part 的 state。
|
|
182
199
|
const [inFlight, setInFlight] = useState(false);
|
|
183
200
|
const outcome = askUserOutcome(call);
|
|
184
|
-
const
|
|
185
|
-
const answeredSelections = new Set(answer?.selections ?? []);
|
|
186
|
-
const answeredText = [
|
|
187
|
-
...(answer?.selections ?? []),
|
|
188
|
-
...(answer?.text.trim() ? [answer.text.trim()] : []),
|
|
189
|
-
].join(" / ");
|
|
201
|
+
const settledAnswers = outcome.kind === "answered" ? outcome.answers : [];
|
|
190
202
|
// 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
|
|
191
203
|
const resolved = outcome.kind !== "pending" || inFlight;
|
|
192
204
|
const valid = questions.length > 0 && questions.every(
|
|
193
|
-
(
|
|
194
|
-
|
|
205
|
+
(_question, index) =>
|
|
206
|
+
answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
|
|
195
207
|
);
|
|
208
|
+
const currentIndex = Math.min(current, Math.max(questions.length - 1, 0));
|
|
209
|
+
const currentQuestion = questions[currentIndex];
|
|
210
|
+
const currentAnswer = answerAt(currentIndex);
|
|
211
|
+
const currentValid = currentAnswer.selections.length > 0 ||
|
|
212
|
+
Boolean(currentAnswer.text.trim());
|
|
213
|
+
const isLast = currentIndex === questions.length - 1;
|
|
196
214
|
const buttonLabel = outcome.kind === "answered"
|
|
197
215
|
? "Submitted"
|
|
198
216
|
: outcome.kind === "closed"
|
|
@@ -200,32 +218,30 @@ export function AskUserBlock({
|
|
|
200
218
|
: inFlight
|
|
201
219
|
? "Submitting…"
|
|
202
220
|
: "Submit";
|
|
203
|
-
const footnote = outcome.kind === "
|
|
204
|
-
? answeredText && `Answered: ${answeredText}`
|
|
205
|
-
: outcome.kind === "closed"
|
|
221
|
+
const footnote = outcome.kind === "closed"
|
|
206
222
|
? outcome.reason === "user_replied_freeform"
|
|
207
223
|
? "Answered in the chat instead."
|
|
208
224
|
: "Closed without an answer."
|
|
209
225
|
: "";
|
|
210
226
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
const submit = () => {
|
|
228
|
+
if (!onRespond) return;
|
|
229
|
+
setInFlight(true);
|
|
230
|
+
// 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
|
|
231
|
+
void onRespond(call.toolCallId, {
|
|
232
|
+
answers: questions.map((_, index) => ({
|
|
233
|
+
selections: answerAt(index).selections,
|
|
234
|
+
text: answerAt(index).text.trim(),
|
|
235
|
+
})),
|
|
236
|
+
}).then((ok) => {
|
|
237
|
+
if (!ok) setInFlight(false);
|
|
238
|
+
}, () => setInFlight(false));
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const settledButton = (
|
|
215
242
|
<WorkshopButton
|
|
216
243
|
tone="primary"
|
|
217
|
-
disabled
|
|
218
|
-
onClick={() => {
|
|
219
|
-
if (!onRespond) return;
|
|
220
|
-
setInFlight(true);
|
|
221
|
-
// 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
|
|
222
|
-
void onRespond(call.toolCallId, {
|
|
223
|
-
selections: questions.flatMap((_, index) => selectionAt(index)),
|
|
224
|
-
text: "",
|
|
225
|
-
}).then((ok) => {
|
|
226
|
-
if (!ok) setInFlight(false);
|
|
227
|
-
});
|
|
228
|
-
}}
|
|
244
|
+
disabled
|
|
229
245
|
className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
230
246
|
>
|
|
231
247
|
{buttonLabel}
|
|
@@ -234,52 +250,193 @@ export function AskUserBlock({
|
|
|
234
250
|
|
|
235
251
|
return (
|
|
236
252
|
// 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
|
|
237
|
-
<div
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
253
|
+
<div
|
|
254
|
+
className="w-fit min-w-[15rem] max-w-[560px]"
|
|
255
|
+
data-part-type="ask_user"
|
|
256
|
+
data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
|
|
257
|
+
>
|
|
242
258
|
<div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-kumo-base px-3 py-2.5">
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
259
|
+
{outcome.kind === "pending" ? (
|
|
260
|
+
<>
|
|
261
|
+
{questions.length > 1 && (
|
|
262
|
+
<div className="flex flex-col gap-2">
|
|
263
|
+
<div className="flex items-center justify-between text-[11px] font-medium leading-4 text-kumo-inactive">
|
|
264
|
+
<span>Clarifying questions</span>
|
|
265
|
+
<span className="tabular-nums">
|
|
266
|
+
{currentIndex + 1}/{questions.length}
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
<div className="flex gap-1" aria-hidden="true">
|
|
270
|
+
{questions.map((question, index) => (
|
|
271
|
+
<span
|
|
272
|
+
key={`${question.prompt}:${index}`}
|
|
273
|
+
className={`h-1 flex-1 rounded-full ${
|
|
274
|
+
index < currentIndex
|
|
275
|
+
? "bg-kumo-brand"
|
|
276
|
+
: index === currentIndex
|
|
277
|
+
? "bg-kumo-brand/60"
|
|
278
|
+
: "bg-kumo-fill"
|
|
279
|
+
}`}
|
|
280
|
+
/>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
{currentQuestion && (
|
|
286
|
+
<div className="overflow-hidden">
|
|
287
|
+
<div
|
|
288
|
+
key={currentIndex}
|
|
289
|
+
className="cos-ask-user-question flex flex-col gap-2.5"
|
|
290
|
+
data-direction={direction.current}
|
|
291
|
+
>
|
|
292
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
293
|
+
{currentQuestion.prompt}
|
|
294
|
+
</p>
|
|
295
|
+
{currentQuestion.options.length > 0 && (
|
|
296
|
+
<ul className="m-0 flex list-none flex-col gap-1.5 p-0">
|
|
297
|
+
{currentQuestion.options.map((option) => {
|
|
298
|
+
const selected = currentAnswer.selections.includes(
|
|
299
|
+
option,
|
|
300
|
+
);
|
|
301
|
+
return (
|
|
302
|
+
<li key={option} className="m-0 p-0">
|
|
303
|
+
<label
|
|
304
|
+
className={`flex min-h-8 items-center gap-2 rounded-lg border px-2.5 py-1.5 text-[13px] leading-[18px] tracking-[-0.25px] transition-colors duration-150 ease-out ${
|
|
305
|
+
resolved || !onRespond
|
|
306
|
+
? "cursor-default text-kumo-inactive"
|
|
307
|
+
: "cursor-pointer text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
308
|
+
} ${
|
|
309
|
+
selected
|
|
310
|
+
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
311
|
+
: "border-kumo-line bg-kumo-base"
|
|
312
|
+
}`}
|
|
313
|
+
>
|
|
314
|
+
<input
|
|
315
|
+
type={currentQuestion.multi
|
|
316
|
+
? "checkbox"
|
|
317
|
+
: "radio"}
|
|
318
|
+
name={`${call.toolCallId}-${currentIndex}`}
|
|
319
|
+
checked={selected}
|
|
320
|
+
disabled={resolved || !onRespond}
|
|
321
|
+
onChange={() =>
|
|
322
|
+
toggle(
|
|
323
|
+
currentIndex,
|
|
324
|
+
option,
|
|
325
|
+
currentQuestion.multi,
|
|
326
|
+
)}
|
|
327
|
+
className="h-4 w-4 shrink-0 accent-[var(--color-kumo-brand)]"
|
|
328
|
+
/>
|
|
329
|
+
<span>{option}</span>
|
|
330
|
+
</label>
|
|
331
|
+
</li>
|
|
332
|
+
);
|
|
333
|
+
})}
|
|
334
|
+
</ul>
|
|
335
|
+
)}
|
|
336
|
+
{currentQuestion.allowCustom && (
|
|
337
|
+
<label className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-subtle">
|
|
338
|
+
<span>
|
|
339
|
+
{currentQuestion.options.length > 0 ? "Other" : "Answer"}
|
|
340
|
+
</span>
|
|
341
|
+
<input
|
|
342
|
+
type="text"
|
|
343
|
+
value={currentAnswer.text}
|
|
344
|
+
maxLength={2_000}
|
|
345
|
+
disabled={resolved || !onRespond}
|
|
346
|
+
aria-label={`自定义回答:${currentQuestion.prompt}`}
|
|
347
|
+
placeholder={currentQuestion.options.length > 0
|
|
348
|
+
? "Add a custom answer"
|
|
349
|
+
: "Type your answer"}
|
|
350
|
+
onChange={(event) =>
|
|
351
|
+
setText(currentIndex, event.currentTarget.value)}
|
|
352
|
+
className="h-8 rounded-lg border border-kumo-line bg-kumo-base px-2.5 text-[13px] text-kumo-default outline-none placeholder:text-kumo-inactive focus:border-kumo-brand focus:ring-2 focus:ring-kumo-brand/20 disabled:cursor-default"
|
|
353
|
+
/>
|
|
354
|
+
</label>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
<div className="flex items-center gap-2">
|
|
360
|
+
{questions.length > 1 && (
|
|
361
|
+
<WorkshopButton
|
|
362
|
+
disabled={currentIndex === 0 || resolved}
|
|
363
|
+
onClick={() => {
|
|
364
|
+
direction.current = "back";
|
|
365
|
+
setCurrent((index) => Math.max(0, index - 1));
|
|
366
|
+
}}
|
|
367
|
+
className="!h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
368
|
+
>
|
|
369
|
+
<ArrowLeft size={13} weight="bold" />
|
|
370
|
+
Back
|
|
371
|
+
</WorkshopButton>
|
|
372
|
+
)}
|
|
373
|
+
<WorkshopButton
|
|
374
|
+
tone="primary"
|
|
375
|
+
disabled={
|
|
376
|
+
resolved || !onRespond ||
|
|
377
|
+
(questions.length > 1 && !isLast ? !currentValid : !valid)
|
|
378
|
+
}
|
|
379
|
+
onClick={() => {
|
|
380
|
+
if (questions.length > 1 && !isLast) {
|
|
381
|
+
direction.current = "forward";
|
|
382
|
+
setCurrent((index) =>
|
|
383
|
+
Math.min(questions.length - 1, index + 1)
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
submit();
|
|
388
|
+
}}
|
|
389
|
+
className="ml-auto !h-7 gap-1 !rounded-lg px-2.5 text-[12px]"
|
|
390
|
+
>
|
|
391
|
+
{questions.length > 1 && !isLast ? (
|
|
392
|
+
<>
|
|
393
|
+
Next
|
|
394
|
+
<ArrowRight size={13} weight="bold" />
|
|
395
|
+
</>
|
|
396
|
+
) : buttonLabel}
|
|
397
|
+
</WorkshopButton>
|
|
272
398
|
</div>
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
399
|
+
</>
|
|
400
|
+
) : (
|
|
401
|
+
<>
|
|
402
|
+
{outcome.kind === "answered" &&
|
|
403
|
+
questions.map((question, questionIndex) => (
|
|
404
|
+
<div
|
|
405
|
+
key={`${question.prompt}:${questionIndex}`}
|
|
406
|
+
className="flex flex-col gap-1.5"
|
|
407
|
+
>
|
|
408
|
+
<p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
409
|
+
{question.prompt}
|
|
410
|
+
</p>
|
|
411
|
+
{settledAnswers[questionIndex] && (
|
|
412
|
+
<div className="flex flex-col gap-1 text-[12px] leading-4 text-kumo-inactive">
|
|
413
|
+
{settledAnswers[questionIndex]!.selections.length > 0 && (
|
|
414
|
+
<p className="m-0">
|
|
415
|
+
Answered: {settledAnswers[questionIndex]!.selections
|
|
416
|
+
.join(" / ")}
|
|
417
|
+
</p>
|
|
418
|
+
)}
|
|
419
|
+
{settledAnswers[questionIndex]!.text.trim() && (
|
|
420
|
+
<p className="m-0">
|
|
421
|
+
{settledAnswers[questionIndex]!.selections.length > 0
|
|
422
|
+
? `补充:${settledAnswers[questionIndex]!.text.trim()}`
|
|
423
|
+
: `Answered: ${settledAnswers[questionIndex]!.text.trim()}`}
|
|
424
|
+
</p>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
))}
|
|
430
|
+
<div className="flex items-center gap-3">
|
|
431
|
+
{footnote && (
|
|
432
|
+
<p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
|
|
433
|
+
{footnote}
|
|
434
|
+
</p>
|
|
435
|
+
)}
|
|
436
|
+
{settledButton}
|
|
437
|
+
</div>
|
|
438
|
+
</>
|
|
439
|
+
)}
|
|
283
440
|
</div>
|
|
284
441
|
</div>
|
|
285
442
|
);
|
|
@@ -498,7 +498,7 @@ export function toCloudOsToolCall(
|
|
|
498
498
|
*
|
|
499
499
|
* - `pending`:还等着用户操作,选项可点。**只有 input 态才算 pending** ——
|
|
500
500
|
* 服务端一旦结算,这次 interaction 就不再接受响应。
|
|
501
|
-
* - `answered`:`ask_user` 的 settle
|
|
501
|
+
* - `answered`:`ask_user` 的 settle 写了按问题对应的 `{ answers }`。
|
|
502
502
|
* - `closed`:被结算掉了但不是一个答案 —— 用户改成直接发言(`{ cancelled: true }`)、
|
|
503
503
|
* Turn 提前结束、或工具出错。必须和 pending 分开,否则卡片会留一个点了拿
|
|
504
504
|
* `{ ok: false }` 的假 Submit,读起来就是「点了没反应」。
|
|
@@ -508,7 +508,14 @@ export function toCloudOsToolCall(
|
|
|
508
508
|
*/
|
|
509
509
|
export type AskUserOutcome =
|
|
510
510
|
| { kind: "pending" }
|
|
511
|
-
| {
|
|
511
|
+
| {
|
|
512
|
+
kind: "answered";
|
|
513
|
+
answers: Array<{
|
|
514
|
+
question: string;
|
|
515
|
+
selections: string[];
|
|
516
|
+
text: string;
|
|
517
|
+
}>;
|
|
518
|
+
}
|
|
512
519
|
| { kind: "closed"; reason: "user_replied_freeform" | "other" };
|
|
513
520
|
|
|
514
521
|
export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
@@ -516,14 +523,23 @@ export function askUserOutcome(call: CloudOsToolCall): AskUserOutcome {
|
|
|
516
523
|
return { kind: "pending" };
|
|
517
524
|
}
|
|
518
525
|
const record = recordOf(call.output);
|
|
519
|
-
if (Array.isArray(record.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
|
|
526
|
+
if (Array.isArray(record.answers) && record.answers.length > 0) {
|
|
527
|
+
const answers = record.answers.map(recordOf);
|
|
528
|
+
if (answers.every((answer) =>
|
|
529
|
+
typeof answer.question === "string" &&
|
|
530
|
+
Array.isArray(answer.selections) &&
|
|
531
|
+
answer.selections.every((value) => typeof value === "string") &&
|
|
532
|
+
(answer.text === undefined || typeof answer.text === "string")
|
|
533
|
+
)) {
|
|
534
|
+
return {
|
|
535
|
+
kind: "answered",
|
|
536
|
+
answers: answers.map((answer) => ({
|
|
537
|
+
question: answer.question as string,
|
|
538
|
+
selections: answer.selections as string[],
|
|
539
|
+
text: typeof answer.text === "string" ? answer.text : "",
|
|
540
|
+
})),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
527
543
|
}
|
|
528
544
|
return {
|
|
529
545
|
kind: "closed",
|
|
@@ -535,6 +535,35 @@
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
/* ── 原位提问:逐题横向切换 ─────────────────────────────────────────────── */
|
|
539
|
+
.cos-ask-user-question[data-direction="forward"] {
|
|
540
|
+
animation: cos-ask-user-forward 180ms ease-out;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.cos-ask-user-question[data-direction="back"] {
|
|
544
|
+
animation: cos-ask-user-back 180ms ease-out;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@keyframes cos-ask-user-forward {
|
|
548
|
+
from {
|
|
549
|
+
opacity: 0;
|
|
550
|
+
transform: translateX(24px);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
@keyframes cos-ask-user-back {
|
|
555
|
+
from {
|
|
556
|
+
opacity: 0;
|
|
557
|
+
transform: translateX(-24px);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
@media (prefers-reduced-motion: reduce) {
|
|
562
|
+
.cos-ask-user-question {
|
|
563
|
+
animation: none !important;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
538
567
|
/* ── 观察类 markdown(行内、紧凑)────────────────────────────────────────── */
|
|
539
568
|
.cos-observation-markdown {
|
|
540
569
|
line-height: 1.5;
|
|
@@ -15,11 +15,13 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
commands as sharedCommands,
|
|
17
17
|
defaultPrompt,
|
|
18
|
+
demoAskUserResponse,
|
|
18
19
|
demoModels,
|
|
19
20
|
finalToolNames,
|
|
20
21
|
flowSteps,
|
|
21
22
|
messagesForScenario,
|
|
22
23
|
scenarios,
|
|
24
|
+
type DemoAskUserResponse,
|
|
23
25
|
type DemoScenario,
|
|
24
26
|
type DemoScenarioMeta,
|
|
25
27
|
} from "./chat-scenarios";
|
|
@@ -42,7 +44,7 @@ export function CamelChatShowcase() {
|
|
|
42
44
|
const [theme, setTheme] = useState<"light" | "dark">("dark");
|
|
43
45
|
const [scenario, setScenario] = useState<DemoScenario>(initialScenario);
|
|
44
46
|
const [prompt, setPrompt] = useState(defaultPrompt);
|
|
45
|
-
const [humanAnswer, setHumanAnswer] = useState(
|
|
47
|
+
const [humanAnswer, setHumanAnswer] = useState(demoAskUserResponse);
|
|
46
48
|
const [model, setModel] = useState<string>(demoModels[0].value);
|
|
47
49
|
const [value, setValue] = useState("");
|
|
48
50
|
const [attachments, setAttachments] =
|
|
@@ -274,20 +276,17 @@ export function CamelChatShowcase() {
|
|
|
274
276
|
onAnswer={(payload) => {
|
|
275
277
|
const data =
|
|
276
278
|
payload && typeof payload === "object"
|
|
277
|
-
? (payload as
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
...(data.selections?.flat() ?? []),
|
|
284
|
-
data.text?.trim() ?? "",
|
|
285
|
-
]
|
|
279
|
+
? (payload as DemoAskUserResponse)
|
|
280
|
+
: { answers: [] };
|
|
281
|
+
const answer = data.answers.flatMap((item) => [
|
|
282
|
+
...item.selections,
|
|
283
|
+
item.text?.trim() ?? "",
|
|
284
|
+
])
|
|
286
285
|
.filter(Boolean)
|
|
287
286
|
.join("、");
|
|
288
|
-
if (
|
|
287
|
+
if (data.answers.length > 0) setHumanAnswer(data);
|
|
289
288
|
selectScenario("planning");
|
|
290
|
-
setLastEvent(`已回答并继续:${answer
|
|
289
|
+
setLastEvent(`已回答并继续:${answer}`);
|
|
291
290
|
}}
|
|
292
291
|
getToolApproval={approvalView}
|
|
293
292
|
/>
|