@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.31
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 +6 -6
- package/cloud-os/assets/followup-arrow.svg +3 -0
- package/cloud-os/assets/loading-corner.svg +3 -0
- package/cloud-os/assets/loading-mark.svg +16 -0
- package/cloud-os/assets/loading-spark.svg +3 -0
- package/cloud-os/assets/model-selected.svg +5 -0
- package/cloud-os/assets/thinking-spark.svg +9 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +2 -2
- package/cloud-os/chat/cloud-os-chat-messages.tsx +374 -63
- package/cloud-os/chat/image-generation.tsx +76 -0
- package/cloud-os/chat/loading-state.tsx +25 -0
- package/cloud-os/chat/markdown-message.tsx +144 -41
- package/cloud-os/chat/rich-blocks.tsx +548 -246
- package/cloud-os/chat/tool-presentation.ts +35 -16
- package/cloud-os/chat/tool-rows.tsx +330 -85
- package/cloud-os/chat/transcript-model.ts +428 -56
- package/cloud-os/composer/cloud-os-chat-input.tsx +228 -142
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +441 -0
- package/cloud-os/index.ts +35 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +58 -15
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +100 -20
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +223 -43
- package/demo/cloud-os-chat-showcase.tsx +47 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +3 -4
- package/src/camel/camel-chat-messages.tsx +78 -78
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- package/src/composer/composer-attachments.tsx +1 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
package/src/message-panel.tsx
CHANGED
|
@@ -93,7 +93,6 @@ interface MessageRowProps<T> {
|
|
|
93
93
|
slots: MessagePanelSlots;
|
|
94
94
|
status: MessagePanelProps<T>["status"];
|
|
95
95
|
latestActivityByKind: ReadonlyMap<string, string>;
|
|
96
|
-
replyText?: string;
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
function MessageRow<T>({
|
|
@@ -107,7 +106,6 @@ function MessageRow<T>({
|
|
|
107
106
|
slots,
|
|
108
107
|
status,
|
|
109
108
|
latestActivityByKind,
|
|
110
|
-
replyText,
|
|
111
109
|
}: MessageRowProps<T>) {
|
|
112
110
|
const role = roleOf(adapter, message);
|
|
113
111
|
const parts = adapter.getParts(message);
|
|
@@ -186,7 +184,6 @@ function MessageRow<T>({
|
|
|
186
184
|
isLatestOfKind:
|
|
187
185
|
key === undefined ||
|
|
188
186
|
latestActivityByKind.get(key) === activityId,
|
|
189
|
-
replyText,
|
|
190
187
|
approval: adapter.getToolApproval?.(part),
|
|
191
188
|
status,
|
|
192
189
|
actions,
|
|
@@ -255,25 +252,6 @@ function MessagePanelInner<T>(
|
|
|
255
252
|
});
|
|
256
253
|
return latest;
|
|
257
254
|
}, [adapter, ids, messages]);
|
|
258
|
-
const replyTextByMessageId = useMemo(() => {
|
|
259
|
-
const replies = new Map<string, string>();
|
|
260
|
-
messages.forEach((message, messageIndex) => {
|
|
261
|
-
if (roleOf(adapter, message) !== "assistant") return;
|
|
262
|
-
for (
|
|
263
|
-
let replyIndex = messageIndex + 1;
|
|
264
|
-
replyIndex < messages.length;
|
|
265
|
-
replyIndex += 1
|
|
266
|
-
) {
|
|
267
|
-
const reply = messages[replyIndex];
|
|
268
|
-
if (roleOf(adapter, reply) !== "user") continue;
|
|
269
|
-
const text = messageText(adapter, reply).trim();
|
|
270
|
-
if (text) replies.set(ids[messageIndex], text);
|
|
271
|
-
break;
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
return replies;
|
|
275
|
-
}, [adapter, ids, messages]);
|
|
276
|
-
|
|
277
255
|
return (
|
|
278
256
|
<div
|
|
279
257
|
ref={ref}
|
|
@@ -299,7 +277,7 @@ function MessagePanelInner<T>(
|
|
|
299
277
|
? slots.error(error)
|
|
300
278
|
: slots.error ?? (
|
|
301
279
|
<PanelStatus kind="error">
|
|
302
|
-
{error instanceof Error ? error.message : "
|
|
280
|
+
{error instanceof Error ? error.message : "This turn failed. Please try again."}
|
|
303
281
|
</PanelStatus>
|
|
304
282
|
))}
|
|
305
283
|
<Conversation>
|
|
@@ -332,7 +310,6 @@ function MessagePanelInner<T>(
|
|
|
332
310
|
slots={slots}
|
|
333
311
|
status={status}
|
|
334
312
|
latestActivityByKind={latestActivityByKind}
|
|
335
|
-
replyText={replyTextByMessageId.get(ids[index])}
|
|
336
313
|
/>
|
|
337
314
|
))}
|
|
338
315
|
{showPending && (
|
package/src/parts/index.tsx
CHANGED
|
@@ -833,59 +833,67 @@ export function DashboardPart(data: DashboardData) {
|
|
|
833
833
|
}
|
|
834
834
|
|
|
835
835
|
export interface AskQuestion {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
836
|
+
question: string;
|
|
837
|
+
options?: string[];
|
|
838
|
+
multiSelect?: boolean;
|
|
839
|
+
allowCustom?: boolean;
|
|
839
840
|
}
|
|
840
841
|
|
|
841
842
|
export function AskUserPart({
|
|
842
843
|
questions,
|
|
843
|
-
|
|
844
|
-
submittedAnswer,
|
|
844
|
+
submittedAnswers = [],
|
|
845
845
|
onSubmit,
|
|
846
846
|
}: {
|
|
847
847
|
questions: readonly AskQuestion[];
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
848
|
+
submittedAnswers?: ReadonlyArray<{ selections: string[]; text?: string }>;
|
|
849
|
+
onSubmit?: (payload: {
|
|
850
|
+
answers: Array<{ selections: string[]; text: string }>;
|
|
851
|
+
}) => void | Promise<void>;
|
|
851
852
|
}) {
|
|
852
|
-
const [
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
),
|
|
863
|
-
[externalAnswer],
|
|
864
|
-
);
|
|
865
|
-
const resolvedSubmitted = submitted || Boolean(externalAnswer);
|
|
866
|
-
const valid = questions.every(
|
|
867
|
-
(question, index) => question.kind !== "single" || selections[index].length === 1,
|
|
853
|
+
const [answers, setAnswers] = useState<Array<{
|
|
854
|
+
selections: string[];
|
|
855
|
+
text: string;
|
|
856
|
+
}>>([]);
|
|
857
|
+
const answerAt = (index: number) =>
|
|
858
|
+
answers[index] ?? { selections: [], text: "" };
|
|
859
|
+
const [inFlight, setInFlight] = useState(false);
|
|
860
|
+
const resolvedSubmitted = submittedAnswers.length > 0;
|
|
861
|
+
const valid = questions.length > 0 && questions.every(
|
|
862
|
+
(_question, index) =>
|
|
863
|
+
answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
|
|
868
864
|
);
|
|
869
865
|
const toggle = (questionIndex: number, option: string) => {
|
|
870
|
-
if (resolvedSubmitted) return;
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
866
|
+
if (resolvedSubmitted || inFlight || !onSubmit) return;
|
|
867
|
+
setAnswers((current) => {
|
|
868
|
+
const next = questions.map((_, index) =>
|
|
869
|
+
current[index] ?? { selections: [], text: "" }
|
|
870
|
+
);
|
|
871
|
+
const answer = next[questionIndex]!;
|
|
872
|
+
next[questionIndex] = {
|
|
873
|
+
...answer,
|
|
874
|
+
selections: questions[questionIndex]!.multiSelect
|
|
875
|
+
? answer.selections.includes(option)
|
|
876
|
+
? answer.selections.filter((item) => item !== option)
|
|
877
|
+
: [...answer.selections, option]
|
|
878
|
+
: [option],
|
|
879
|
+
};
|
|
880
|
+
return next;
|
|
881
|
+
});
|
|
880
882
|
};
|
|
881
883
|
const submit = async () => {
|
|
882
|
-
if (!valid || resolvedSubmitted) return;
|
|
883
|
-
|
|
884
|
-
|
|
884
|
+
if (!valid || resolvedSubmitted || !onSubmit) return;
|
|
885
|
+
setInFlight(true);
|
|
886
|
+
try {
|
|
887
|
+
await onSubmit({
|
|
888
|
+
answers: questions.map((_, index) => ({
|
|
889
|
+
selections: answerAt(index).selections,
|
|
890
|
+
text: answerAt(index).text.trim(),
|
|
891
|
+
})),
|
|
892
|
+
});
|
|
893
|
+
} catch {
|
|
894
|
+
setInFlight(false);
|
|
895
|
+
}
|
|
885
896
|
};
|
|
886
|
-
const resolvedAnswer =
|
|
887
|
-
externalAnswer ||
|
|
888
|
-
[...selections.flat(), text.trim()].filter(Boolean).join("、");
|
|
889
897
|
return (
|
|
890
898
|
<section
|
|
891
899
|
className="sb-ask-user"
|
|
@@ -894,15 +902,15 @@ export function AskUserPart({
|
|
|
894
902
|
>
|
|
895
903
|
{questions.map((question, questionIndex) => (
|
|
896
904
|
<fieldset
|
|
897
|
-
key={`${questionIndex}:${question.
|
|
898
|
-
disabled={resolvedSubmitted}
|
|
905
|
+
key={`${questionIndex}:${question.question}`}
|
|
906
|
+
disabled={resolvedSubmitted || inFlight || !onSubmit}
|
|
899
907
|
>
|
|
900
|
-
<legend>{question.
|
|
908
|
+
<legend>{question.question}</legend>
|
|
901
909
|
<div>
|
|
902
|
-
{question.options.map((option) => {
|
|
903
|
-
const selected =
|
|
904
|
-
?
|
|
905
|
-
:
|
|
910
|
+
{(question.options ?? []).map((option) => {
|
|
911
|
+
const selected = submittedAnswers[questionIndex]
|
|
912
|
+
? submittedAnswers[questionIndex]!.selections.includes(option)
|
|
913
|
+
: answerAt(questionIndex).selections.includes(option);
|
|
906
914
|
return (
|
|
907
915
|
<button
|
|
908
916
|
key={option}
|
|
@@ -910,27 +918,53 @@ export function AskUserPart({
|
|
|
910
918
|
aria-pressed={selected}
|
|
911
919
|
onClick={() => toggle(questionIndex, option)}
|
|
912
920
|
>
|
|
913
|
-
{question.
|
|
914
|
-
{selected && question.
|
|
921
|
+
{question.multiSelect && <span>{selected && <CheckIcon size={12} />}</span>}
|
|
922
|
+
{selected && !question.multiSelect && <CheckIcon size={14} />}
|
|
915
923
|
{option}
|
|
916
924
|
</button>
|
|
917
925
|
);
|
|
918
926
|
})}
|
|
919
927
|
</div>
|
|
928
|
+
{!resolvedSubmitted &&
|
|
929
|
+
(question.allowCustom === true || !question.options?.length) && (
|
|
930
|
+
<textarea
|
|
931
|
+
value={answerAt(questionIndex).text}
|
|
932
|
+
maxLength={2_000}
|
|
933
|
+
aria-label={`Custom answer: ${question.question}`}
|
|
934
|
+
onChange={(event) =>
|
|
935
|
+
setAnswers((current) => {
|
|
936
|
+
const next = questions.map((_, index) =>
|
|
937
|
+
current[index] ?? { selections: [], text: "" }
|
|
938
|
+
);
|
|
939
|
+
next[questionIndex] = {
|
|
940
|
+
...next[questionIndex]!,
|
|
941
|
+
text: event.currentTarget.value,
|
|
942
|
+
};
|
|
943
|
+
return next;
|
|
944
|
+
})}
|
|
945
|
+
placeholder={question.options?.length ? "Other" : "Type your answer"}
|
|
946
|
+
/>
|
|
947
|
+
)}
|
|
948
|
+
{submittedAnswers[questionIndex] && (
|
|
949
|
+
<small>
|
|
950
|
+
{[
|
|
951
|
+
...submittedAnswers[questionIndex]!.selections,
|
|
952
|
+
...(submittedAnswers[questionIndex]!.text?.trim()
|
|
953
|
+
? [submittedAnswers[questionIndex]!.text!.trim()]
|
|
954
|
+
: []),
|
|
955
|
+
].join(" / ")}
|
|
956
|
+
</small>
|
|
957
|
+
)}
|
|
920
958
|
</fieldset>
|
|
921
959
|
))}
|
|
922
|
-
{freeTextPlaceholder && !resolvedSubmitted && (
|
|
923
|
-
<textarea value={text} onChange={(event) => setText(event.currentTarget.value)} placeholder={freeTextPlaceholder} />
|
|
924
|
-
)}
|
|
925
960
|
{resolvedSubmitted ? (
|
|
926
961
|
<div className="sb-ask-user__submitted">
|
|
927
962
|
<CheckIcon size={15} />
|
|
928
963
|
<span>Submitted</span>
|
|
929
|
-
{resolvedAnswer && <small>{resolvedAnswer}</small>}
|
|
930
964
|
</div>
|
|
931
965
|
) : (
|
|
932
|
-
<button type="button" className="sb-button" disabled={!valid} onClick={() => void submit()}>
|
|
933
|
-
|
|
966
|
+
<button type="button" className="sb-button" disabled={!valid || inFlight || !onSubmit} onClick={() => void submit()}>
|
|
967
|
+
{inFlight ? "Submitting…" : "提交"}<ArrowRightIcon size={15} />
|
|
934
968
|
</button>
|
|
935
969
|
)}
|
|
936
970
|
</section>
|
|
@@ -1081,7 +1115,7 @@ export function DefaultPartRenderer({
|
|
|
1081
1115
|
);
|
|
1082
1116
|
}
|
|
1083
1117
|
if (kind === "error") {
|
|
1084
|
-
return <ErrorPart title={String(data.title ?? "Error")} body={String(data.body ?? data.message ?? "
|
|
1118
|
+
return <ErrorPart title={String(data.title ?? "Error")} body={String(data.body ?? data.message ?? "Request failed")} />;
|
|
1085
1119
|
}
|
|
1086
1120
|
if (kind === "image") {
|
|
1087
1121
|
return <ImagePart src={String(record.src ?? record.url ?? data.src ?? data.url ?? "")} alt={String(record.alt ?? record.filename ?? data.alt ?? data.filename ?? "图片")} onPreview={() => context.actions.onPreview?.(part)} />;
|
|
@@ -1128,17 +1162,22 @@ export function DefaultPartRenderer({
|
|
|
1128
1162
|
if (kind === "ask_user") {
|
|
1129
1163
|
const questions: AskQuestion[] = Array.isArray(data.questions)
|
|
1130
1164
|
? data.questions as AskQuestion[]
|
|
1131
|
-
: [
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1165
|
+
: [];
|
|
1166
|
+
const output = objectOf(record.output);
|
|
1167
|
+
const submittedAnswers = Array.isArray(output.answers)
|
|
1168
|
+
? output.answers.map((answer) => {
|
|
1169
|
+
const value = objectOf(answer);
|
|
1170
|
+
return {
|
|
1171
|
+
selections: strings(value.selections),
|
|
1172
|
+
text: typeof value.text === "string" ? value.text : "",
|
|
1173
|
+
};
|
|
1174
|
+
})
|
|
1175
|
+
: [];
|
|
1136
1176
|
return withToolApproval(
|
|
1137
1177
|
<AskUserPart
|
|
1138
1178
|
questions={questions}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
onSubmit={(payload) => context.actions.onAnswer?.(payload)}
|
|
1179
|
+
submittedAnswers={submittedAnswers}
|
|
1180
|
+
onSubmit={context.actions.onAnswer}
|
|
1142
1181
|
/>,
|
|
1143
1182
|
);
|
|
1144
1183
|
}
|