@springbrand/message-panel 0.1.3-alpha.2 → 0.1.3-alpha.21

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.
Files changed (41) hide show
  1. package/cloud-os/README.md +6 -6
  2. package/cloud-os/assets/followup-arrow.svg +3 -0
  3. package/cloud-os/assets/loading-corner.svg +3 -0
  4. package/cloud-os/assets/loading-mark.svg +16 -0
  5. package/cloud-os/assets/loading-spark.svg +3 -0
  6. package/cloud-os/assets/model-selected.svg +5 -0
  7. package/cloud-os/capability-chip.tsx +35 -0
  8. package/cloud-os/chat/activity-indicator.tsx +29 -0
  9. package/cloud-os/chat/cloud-os-chat-messages.tsx +123 -69
  10. package/cloud-os/chat/markdown-message.tsx +82 -40
  11. package/cloud-os/chat/rich-blocks.tsx +494 -190
  12. package/cloud-os/chat/tool-presentation.ts +67 -6
  13. package/cloud-os/chat/tool-rows.tsx +87 -43
  14. package/cloud-os/chat/transcript-model.ts +215 -23
  15. package/cloud-os/composer/cloud-os-chat-input.tsx +242 -127
  16. package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
  17. package/cloud-os/file-view.tsx +266 -0
  18. package/cloud-os/index.ts +29 -2
  19. package/cloud-os/layout/cloud-os-workspace-split.tsx +107 -24
  20. package/cloud-os/primitives/workshop-controls.tsx +2 -2
  21. package/cloud-os/styles/cloud-os.css +93 -19
  22. package/demo/camel-chat-showcase.tsx +21 -13
  23. package/demo/chat-scenarios.ts +100 -40
  24. package/demo/cloud-os-chat-showcase.tsx +51 -17
  25. package/demo/fixtures.ts +4 -5
  26. package/demo/message-panel-gallery.tsx +16 -2
  27. package/package.json +3 -4
  28. package/src/camel/camel-chat-messages.tsx +82 -77
  29. package/src/camel/camel-prompt-input.tsx +2 -1
  30. package/src/camel/camel-tool-presentation.tsx +19 -15
  31. package/src/camel/camel-turn.ts +1 -17
  32. package/src/chat-summary-panel.tsx +1 -1
  33. package/src/composer/chat-composer.tsx +2 -1
  34. package/src/composer/composer-trigger-popover.tsx +1 -1
  35. package/src/composer/composer.tsx +2 -1
  36. package/src/composer/index.ts +1 -0
  37. package/src/composer/key-rules.ts +12 -0
  38. package/src/contracts.ts +0 -2
  39. package/src/message-panel.tsx +0 -23
  40. package/src/parts/index.tsx +102 -63
  41. package/src/styles/index.css +4 -1
@@ -833,59 +833,67 @@ export function DashboardPart(data: DashboardData) {
833
833
  }
834
834
 
835
835
  export interface AskQuestion {
836
- prompt: string;
837
- kind: "single" | "multi";
838
- options: string[];
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
- freeTextPlaceholder,
844
- submittedAnswer,
844
+ submittedAnswers = [],
845
845
  onSubmit,
846
846
  }: {
847
847
  questions: readonly AskQuestion[];
848
- freeTextPlaceholder?: string;
849
- submittedAnswer?: string;
850
- onSubmit?: (payload: { selections: string[][]; text: string }) => void | Promise<void>;
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 [selections, setSelections] = useState<string[][]>(() => questions.map(() => []));
853
- const [text, setText] = useState("");
854
- const [submitted, setSubmitted] = useState(false);
855
- const externalAnswer = submittedAnswer?.trim() ?? "";
856
- const externalSelections = useMemo(
857
- () =>
858
- new Set(
859
- externalAnswer
860
- ? externalAnswer.split("、").map((item) => item.trim()).filter(Boolean)
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
- setSelections((current) =>
872
- current.map((selected, index) => {
873
- if (index !== questionIndex) return selected;
874
- if (questions[index].kind === "single") return [option];
875
- return selected.includes(option)
876
- ? selected.filter((item) => item !== option)
877
- : [...selected, option];
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
- await onSubmit?.({ selections, text: text.trim() });
884
- setSubmitted(true);
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.prompt}`}
898
- disabled={resolvedSubmitted}
905
+ key={`${questionIndex}:${question.question}`}
906
+ disabled={resolvedSubmitted || inFlight || !onSubmit}
899
907
  >
900
- <legend>{question.prompt}</legend>
908
+ <legend>{question.question}</legend>
901
909
  <div>
902
- {question.options.map((option) => {
903
- const selected = externalAnswer
904
- ? externalSelections.has(option)
905
- : selections[questionIndex].includes(option);
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.kind === "multi" && <span>{selected && <CheckIcon size={12} />}</span>}
914
- {selected && question.kind === "single" && <CheckIcon size={14} />}
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
- 提交<ArrowRightIcon size={15} />
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>
@@ -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
- prompt: String(data.question ?? "请选择"),
1133
- kind: data.multiSelect === true ? "multi" : "single",
1134
- options: strings(data.options),
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
- freeTextPlaceholder={typeof data.freeTextPlaceholder === "string" ? data.freeTextPlaceholder : undefined}
1140
- submittedAnswer={context.replyText}
1141
- onSubmit={(payload) => context.actions.onAnswer?.(payload)}
1179
+ submittedAnswers={submittedAnswers}
1180
+ onSubmit={context.actions.onAnswer}
1142
1181
  />,
1143
1182
  );
1144
1183
  }
@@ -2128,7 +2128,10 @@
2128
2128
  min-height: 120px;
2129
2129
  gap: 0;
2130
2130
  border-radius: 16px;
2131
- background: #1c1c1a;
2131
+ /* 这里曾写死 #1c1c1a,于是 light 档下输入框还是近黑的。
2132
+ 表面色一律走 token:--mp-card → --card(light #fff / dark #1c1c1c)。
2133
+ 本节其余几个写死色是状态/图标强调色(琥珀、蓝),两档背景上都读得出来,不在此列。 */
2134
+ background: var(--mp-card);
2132
2135
  box-shadow: none;
2133
2136
  }
2134
2137