@springbrand/message-panel 0.1.3-alpha.1 → 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 +71 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +663 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +944 -0
- package/cloud-os/chat/tool-presentation.ts +602 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +780 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +686 -0
- package/cloud-os/file-view.tsx +258 -0
- package/cloud-os/index.ts +123 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +228 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +582 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +25 -930
- package/demo/chat-scenarios.ts +943 -0
- package/demo/cloud-os-chat-showcase.tsx +483 -0
- package/demo/fixtures.ts +4 -5
- package/demo/index.ts +2 -0
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +17 -5
- package/src/camel/camel-chat-messages.tsx +118 -95
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/camel/camel-tool-presentation.tsx +17 -13
- package/src/camel/camel-turn.ts +1 -17
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +0 -23
- package/src/message.tsx +0 -8
- package/src/parts/index.tsx +102 -63
- package/src/styles/index.css +4 -1
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// camel 是 dev-only 的展示变体(App.tsx 的 `/__camel-chat` 路由),没有接生产宿主。
|
|
2
|
+
// 它的交互卡片仍用旧的 `onAnswer(payload)` 回调,**没有**迁到 client-settled tool 的
|
|
3
|
+
// `onRespond(toolCallId, response)` —— 生产聊天面板走的是 cloud-os,已经迁完。
|
|
4
|
+
// 哪天 camel 要上生产,这里必须一起改,否则卡片点了不会把答案回写给那次工具调用。
|
|
5
|
+
|
|
1
6
|
import type { UIMessage } from "ai";
|
|
2
7
|
import {
|
|
3
8
|
ArrowDownIcon,
|
|
@@ -71,10 +76,7 @@ export interface CamelChatMessagesProps {
|
|
|
71
76
|
finalToolNames?: readonly string[];
|
|
72
77
|
isHydrating?: boolean;
|
|
73
78
|
isRecovering?: boolean;
|
|
74
|
-
|
|
75
|
-
/** Show the opt-in activity bar for recovering or server-streaming states. */
|
|
76
|
-
showActivityStatus?: boolean;
|
|
77
|
-
activityStatusLabel?: string;
|
|
79
|
+
recoveryStatusLabel?: string;
|
|
78
80
|
hasPendingSteer?: boolean;
|
|
79
81
|
startedWithPending?: boolean;
|
|
80
82
|
error?: unknown;
|
|
@@ -164,6 +166,23 @@ export function CamelChatError({
|
|
|
164
166
|
);
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
export function CamelChatRecovery({
|
|
170
|
+
label = "模型响应暂时中断,正在重试…",
|
|
171
|
+
}: {
|
|
172
|
+
label?: string;
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
className="mt-4 flex items-center gap-2 rounded-lg border border-border/70 bg-muted/40 px-3 py-2 text-sm text-muted-foreground"
|
|
177
|
+
data-camel-chat-recovery=""
|
|
178
|
+
role="status"
|
|
179
|
+
>
|
|
180
|
+
<Loader2Icon className="size-4 shrink-0 animate-spin" aria-hidden="true" />
|
|
181
|
+
<span>{label}</span>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
export function CamelApprovalPrompts({
|
|
168
187
|
approvals,
|
|
169
188
|
error,
|
|
@@ -649,53 +668,30 @@ function CamelSchedule({
|
|
|
649
668
|
|
|
650
669
|
function CamelAskUser({
|
|
651
670
|
part,
|
|
652
|
-
submittedAnswer,
|
|
653
671
|
onAnswer,
|
|
654
672
|
}: {
|
|
655
673
|
part: unknown;
|
|
656
|
-
submittedAnswer?: string;
|
|
657
674
|
onAnswer: (payload: unknown) => void;
|
|
658
675
|
}) {
|
|
659
676
|
const record = recordOf(part);
|
|
660
677
|
const data = dataOf(part);
|
|
661
|
-
const output = record.output;
|
|
662
678
|
const questions = Array.isArray(data.questions)
|
|
663
679
|
? data.questions.map(recordOf)
|
|
664
|
-
:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const [text, setText] = useState("");
|
|
679
|
-
const [submitted, setSubmitted] = useState(false);
|
|
680
|
-
const outputRecord = recordOf(output);
|
|
681
|
-
const externalAnswer =
|
|
682
|
-
submittedAnswer?.trim() ||
|
|
683
|
-
(typeof output === "string"
|
|
684
|
-
? output.trim()
|
|
685
|
-
: typeof outputRecord.text === "string"
|
|
686
|
-
? outputRecord.text.trim()
|
|
687
|
-
: "");
|
|
688
|
-
const externalSelections = new Set(
|
|
689
|
-
externalAnswer
|
|
690
|
-
.split("、")
|
|
691
|
-
.map((value) => value.trim())
|
|
692
|
-
.filter(Boolean),
|
|
680
|
+
: [];
|
|
681
|
+
const output = recordOf(record.output);
|
|
682
|
+
const settledAnswers = Array.isArray(output.answers)
|
|
683
|
+
? output.answers.map(recordOf)
|
|
684
|
+
: [];
|
|
685
|
+
const [answers, setAnswers] = useState<Array<{
|
|
686
|
+
selections: string[];
|
|
687
|
+
text: string;
|
|
688
|
+
}>>([]);
|
|
689
|
+
const answerAt = (index: number) =>
|
|
690
|
+
answers[index] ?? { selections: [], text: "" };
|
|
691
|
+
const resolvedSubmitted = settledAnswers.length > 0;
|
|
692
|
+
const valid = questions.length > 0 && questions.every((_, index) =>
|
|
693
|
+
answerAt(index).selections.length > 0 || answerAt(index).text.trim()
|
|
693
694
|
);
|
|
694
|
-
const resolvedSubmitted = submitted || Boolean(externalAnswer);
|
|
695
|
-
const valid = questions.every((question, index) => {
|
|
696
|
-
const options = Array.isArray(question.options) ? question.options : [];
|
|
697
|
-
return options.length === 0 || (selections[index]?.length ?? 0) > 0;
|
|
698
|
-
});
|
|
699
695
|
|
|
700
696
|
return (
|
|
701
697
|
<section
|
|
@@ -704,21 +700,28 @@ function CamelAskUser({
|
|
|
704
700
|
data-state={resolvedSubmitted ? "submitted" : "idle"}
|
|
705
701
|
>
|
|
706
702
|
{questions.map((question, questionIndex) => {
|
|
707
|
-
const prompt = String(question.
|
|
703
|
+
const prompt = String(question.question ?? "Choose an option");
|
|
708
704
|
const options = Array.isArray(question.options)
|
|
709
705
|
? question.options.filter(
|
|
710
706
|
(option): option is string => typeof option === "string",
|
|
711
707
|
)
|
|
712
708
|
: [];
|
|
713
|
-
const multi = question.
|
|
709
|
+
const multi = question.multiSelect === true;
|
|
710
|
+
const allowCustom = question.allowCustom === true || options.length === 0;
|
|
711
|
+
const settledAnswer = settledAnswers[questionIndex];
|
|
712
|
+
const settledSelections = Array.isArray(settledAnswer?.selections)
|
|
713
|
+
? settledAnswer.selections.filter(
|
|
714
|
+
(value): value is string => typeof value === "string",
|
|
715
|
+
)
|
|
716
|
+
: [];
|
|
714
717
|
return (
|
|
715
718
|
<div className="space-y-2" key={`${prompt}:${questionIndex}`}>
|
|
716
719
|
<p className="text-sm text-foreground">{prompt}</p>
|
|
717
720
|
<div className="flex flex-wrap gap-2">
|
|
718
721
|
{options.map((option) => {
|
|
719
|
-
const selected =
|
|
720
|
-
?
|
|
721
|
-
: selections
|
|
722
|
+
const selected = settledAnswer
|
|
723
|
+
? settledSelections.includes(option)
|
|
724
|
+
: answerAt(questionIndex).selections.includes(option);
|
|
722
725
|
return (
|
|
723
726
|
<Button
|
|
724
727
|
key={option}
|
|
@@ -727,15 +730,21 @@ function CamelAskUser({
|
|
|
727
730
|
variant={selected ? "default" : "outline"}
|
|
728
731
|
disabled={resolvedSubmitted}
|
|
729
732
|
onClick={() =>
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
733
|
+
setAnswers((current) => {
|
|
734
|
+
const next = questions.map((_, index) =>
|
|
735
|
+
current[index] ?? { selections: [], text: "" }
|
|
736
|
+
);
|
|
737
|
+
const answer = next[questionIndex]!;
|
|
738
|
+
next[questionIndex] = {
|
|
739
|
+
...answer,
|
|
740
|
+
selections: !multi
|
|
741
|
+
? [option]
|
|
742
|
+
: answer.selections.includes(option)
|
|
743
|
+
? answer.selections.filter((value) => value !== option)
|
|
744
|
+
: [...answer.selections, option],
|
|
745
|
+
};
|
|
746
|
+
return next;
|
|
747
|
+
})
|
|
739
748
|
}
|
|
740
749
|
>
|
|
741
750
|
{option}
|
|
@@ -743,34 +752,54 @@ function CamelAskUser({
|
|
|
743
752
|
);
|
|
744
753
|
})}
|
|
745
754
|
</div>
|
|
755
|
+
{!resolvedSubmitted && allowCustom && (
|
|
756
|
+
<input
|
|
757
|
+
value={answerAt(questionIndex).text}
|
|
758
|
+
maxLength={2_000}
|
|
759
|
+
aria-label={`Custom answer: ${prompt}`}
|
|
760
|
+
onChange={(event) =>
|
|
761
|
+
setAnswers((current) => {
|
|
762
|
+
const next = questions.map((_, index) =>
|
|
763
|
+
current[index] ?? { selections: [], text: "" }
|
|
764
|
+
);
|
|
765
|
+
next[questionIndex] = {
|
|
766
|
+
...next[questionIndex]!,
|
|
767
|
+
text: event.currentTarget.value,
|
|
768
|
+
};
|
|
769
|
+
return next;
|
|
770
|
+
})}
|
|
771
|
+
placeholder={options.length > 0 ? "Other" : "Type your answer"}
|
|
772
|
+
className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring focus:ring-2 focus:ring-ring/30"
|
|
773
|
+
/>
|
|
774
|
+
)}
|
|
775
|
+
{settledAnswer && (
|
|
776
|
+
<p className="text-xs text-muted-foreground">
|
|
777
|
+
Answered: {[
|
|
778
|
+
...settledSelections,
|
|
779
|
+
...(typeof settledAnswer.text === "string" && settledAnswer.text.trim()
|
|
780
|
+
? [settledAnswer.text.trim()]
|
|
781
|
+
: []),
|
|
782
|
+
].join(" / ")}
|
|
783
|
+
</p>
|
|
784
|
+
)}
|
|
746
785
|
</div>
|
|
747
786
|
);
|
|
748
787
|
})}
|
|
749
|
-
{typeof data.freeTextPlaceholder === "string" && (
|
|
750
|
-
<input
|
|
751
|
-
value={text}
|
|
752
|
-
disabled={resolvedSubmitted}
|
|
753
|
-
onChange={(event) => setText(event.currentTarget.value)}
|
|
754
|
-
placeholder={data.freeTextPlaceholder}
|
|
755
|
-
className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring focus:ring-2 focus:ring-ring/30"
|
|
756
|
-
/>
|
|
757
|
-
)}
|
|
758
788
|
<Button
|
|
759
789
|
type="button"
|
|
760
790
|
size="sm"
|
|
761
791
|
disabled={resolvedSubmitted || !valid}
|
|
762
792
|
onClick={() => {
|
|
763
|
-
onAnswer({
|
|
764
|
-
|
|
793
|
+
onAnswer({
|
|
794
|
+
answers: questions.map((_, index) => ({
|
|
795
|
+
selections: answerAt(index).selections,
|
|
796
|
+
text: answerAt(index).text.trim(),
|
|
797
|
+
})),
|
|
798
|
+
});
|
|
765
799
|
}}
|
|
766
800
|
>
|
|
767
801
|
{resolvedSubmitted ? "Submitted" : "Submit"}
|
|
768
802
|
</Button>
|
|
769
|
-
{externalAnswer && (
|
|
770
|
-
<p className="text-xs text-muted-foreground">
|
|
771
|
-
已回答:{externalAnswer}
|
|
772
|
-
</p>
|
|
773
|
-
)}
|
|
774
803
|
</section>
|
|
775
804
|
);
|
|
776
805
|
}
|
|
@@ -781,7 +810,6 @@ function CamelRichPart({
|
|
|
781
810
|
onApprove,
|
|
782
811
|
onSuggestion,
|
|
783
812
|
onAnswer,
|
|
784
|
-
replyText,
|
|
785
813
|
getToolApproval,
|
|
786
814
|
}: {
|
|
787
815
|
part: unknown;
|
|
@@ -789,7 +817,6 @@ function CamelRichPart({
|
|
|
789
817
|
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
790
818
|
onSuggestion: (text: string) => void;
|
|
791
819
|
onAnswer: (payload: unknown) => void;
|
|
792
|
-
replyText?: string;
|
|
793
820
|
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
794
821
|
}) {
|
|
795
822
|
const kind = normalizedPartType(part);
|
|
@@ -805,7 +832,6 @@ function CamelRichPart({
|
|
|
805
832
|
return (
|
|
806
833
|
<CamelAskUser
|
|
807
834
|
part={part}
|
|
808
|
-
submittedAnswer={replyText}
|
|
809
835
|
onAnswer={onAnswer}
|
|
810
836
|
/>
|
|
811
837
|
);
|
|
@@ -905,7 +931,6 @@ function AssistantPart({
|
|
|
905
931
|
onApprove,
|
|
906
932
|
onSuggestion,
|
|
907
933
|
onAnswer,
|
|
908
|
-
replyText,
|
|
909
934
|
getToolApproval,
|
|
910
935
|
resolveUrl,
|
|
911
936
|
}: {
|
|
@@ -914,7 +939,6 @@ function AssistantPart({
|
|
|
914
939
|
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
915
940
|
onSuggestion: (text: string) => void;
|
|
916
941
|
onAnswer: (payload: unknown) => void;
|
|
917
|
-
replyText?: string;
|
|
918
942
|
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
919
943
|
resolveUrl?: MessageUrlResolver;
|
|
920
944
|
}) {
|
|
@@ -941,7 +965,6 @@ function AssistantPart({
|
|
|
941
965
|
onApprove={onApprove}
|
|
942
966
|
onSuggestion={onSuggestion}
|
|
943
967
|
onAnswer={onAnswer}
|
|
944
|
-
replyText={replyText}
|
|
945
968
|
getToolApproval={getToolApproval}
|
|
946
969
|
/>
|
|
947
970
|
);
|
|
@@ -1118,7 +1141,6 @@ function RenderAssistantPart({
|
|
|
1118
1141
|
onApprove,
|
|
1119
1142
|
onSuggestion,
|
|
1120
1143
|
onAnswer,
|
|
1121
|
-
replyText,
|
|
1122
1144
|
getToolApproval,
|
|
1123
1145
|
resolveUrl,
|
|
1124
1146
|
}: {
|
|
@@ -1127,7 +1149,6 @@ function RenderAssistantPart({
|
|
|
1127
1149
|
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
1128
1150
|
onSuggestion: (text: string) => void;
|
|
1129
1151
|
onAnswer: (payload: unknown) => void;
|
|
1130
|
-
replyText?: string;
|
|
1131
1152
|
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
1132
1153
|
resolveUrl?: MessageUrlResolver;
|
|
1133
1154
|
}) {
|
|
@@ -1141,7 +1162,6 @@ function RenderAssistantPart({
|
|
|
1141
1162
|
onApprove={onApprove}
|
|
1142
1163
|
onSuggestion={onSuggestion}
|
|
1143
1164
|
onAnswer={onAnswer}
|
|
1144
|
-
replyText={replyText}
|
|
1145
1165
|
getToolApproval={getToolApproval}
|
|
1146
1166
|
resolveUrl={resolveUrl}
|
|
1147
1167
|
/>
|
|
@@ -1151,6 +1171,7 @@ function RenderAssistantPart({
|
|
|
1151
1171
|
function AssistantTurn({
|
|
1152
1172
|
turn,
|
|
1153
1173
|
isActive,
|
|
1174
|
+
keepTraceExpanded,
|
|
1154
1175
|
hasPendingSteer,
|
|
1155
1176
|
hasPendingApproval,
|
|
1156
1177
|
onFork,
|
|
@@ -1162,6 +1183,7 @@ function AssistantTurn({
|
|
|
1162
1183
|
}: {
|
|
1163
1184
|
turn: CamelAssistantTurn;
|
|
1164
1185
|
isActive: boolean;
|
|
1186
|
+
keepTraceExpanded: boolean;
|
|
1165
1187
|
hasPendingSteer: boolean;
|
|
1166
1188
|
hasPendingApproval: boolean;
|
|
1167
1189
|
onFork?: (messageId: string) => void | Promise<void>;
|
|
@@ -1179,13 +1201,15 @@ function AssistantTurn({
|
|
|
1179
1201
|
onApprove={onApprove}
|
|
1180
1202
|
onSuggestion={onSuggestion}
|
|
1181
1203
|
onAnswer={onAnswer}
|
|
1182
|
-
replyText={turn.replyText}
|
|
1183
1204
|
getToolApproval={getToolApproval}
|
|
1184
1205
|
resolveUrl={resolveUrl}
|
|
1185
1206
|
/>
|
|
1186
1207
|
);
|
|
1187
1208
|
const showWorkInPlace =
|
|
1188
|
-
isActive ||
|
|
1209
|
+
isActive ||
|
|
1210
|
+
keepTraceExpanded ||
|
|
1211
|
+
hasPendingApproval ||
|
|
1212
|
+
turn.terminalState !== undefined;
|
|
1189
1213
|
const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
|
|
1190
1214
|
const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
|
|
1191
1215
|
const messageTime =
|
|
@@ -1294,9 +1318,7 @@ export function CamelChatMessages({
|
|
|
1294
1318
|
finalToolNames,
|
|
1295
1319
|
isHydrating = false,
|
|
1296
1320
|
isRecovering = false,
|
|
1297
|
-
|
|
1298
|
-
showActivityStatus = false,
|
|
1299
|
-
activityStatusLabel,
|
|
1321
|
+
recoveryStatusLabel,
|
|
1300
1322
|
hasPendingSteer = false,
|
|
1301
1323
|
startedWithPending = false,
|
|
1302
1324
|
error,
|
|
@@ -1328,6 +1350,10 @@ export function CamelChatMessages({
|
|
|
1328
1350
|
isActive && lastItem?.kind === "assistant"
|
|
1329
1351
|
? lastItem.actionMessageId
|
|
1330
1352
|
: null;
|
|
1353
|
+
const recoveringAssistantMessageId =
|
|
1354
|
+
isRecovering && lastItem?.kind === "assistant"
|
|
1355
|
+
? lastItem.actionMessageId
|
|
1356
|
+
: null;
|
|
1331
1357
|
|
|
1332
1358
|
const syncScrollButton = useCallback(() => {
|
|
1333
1359
|
const element = scrollRef.current;
|
|
@@ -1341,7 +1367,7 @@ export function CamelChatMessages({
|
|
|
1341
1367
|
const element = scrollRef.current;
|
|
1342
1368
|
if (!element || showScrollButton) return;
|
|
1343
1369
|
element.scrollTo({ top: element.scrollHeight });
|
|
1344
|
-
}, [messages, status, showScrollButton]);
|
|
1370
|
+
}, [isRecovering, messages, status, showScrollButton]);
|
|
1345
1371
|
|
|
1346
1372
|
return (
|
|
1347
1373
|
<div
|
|
@@ -1351,15 +1377,6 @@ export function CamelChatMessages({
|
|
|
1351
1377
|
data-status={status}
|
|
1352
1378
|
aria-busy={isActive || isHydrating}
|
|
1353
1379
|
>
|
|
1354
|
-
{showActivityStatus && (isRecovering || isServerStreaming) && (
|
|
1355
|
-
<div
|
|
1356
|
-
className="absolute inset-x-0 top-0 z-10 flex h-7 items-center justify-center gap-2 bg-background/85 text-xs text-muted-foreground backdrop-blur-sm"
|
|
1357
|
-
role="status"
|
|
1358
|
-
>
|
|
1359
|
-
<span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
|
|
1360
|
-
{activityStatusLabel ?? (isRecovering ? "Recovering…" : "Running…")}
|
|
1361
|
-
</div>
|
|
1362
|
-
)}
|
|
1363
1380
|
<div
|
|
1364
1381
|
ref={scrollRef}
|
|
1365
1382
|
onScroll={syncScrollButton}
|
|
@@ -1380,6 +1397,9 @@ export function CamelChatMessages({
|
|
|
1380
1397
|
key={item.key}
|
|
1381
1398
|
turn={item}
|
|
1382
1399
|
isActive={item.actionMessageId === activeAssistantMessageId}
|
|
1400
|
+
keepTraceExpanded={
|
|
1401
|
+
item.actionMessageId === recoveringAssistantMessageId
|
|
1402
|
+
}
|
|
1383
1403
|
hasPendingSteer={hasPendingSteer}
|
|
1384
1404
|
hasPendingApproval={item.parts.some((partRef) => {
|
|
1385
1405
|
const approval =
|
|
@@ -1398,6 +1418,9 @@ export function CamelChatMessages({
|
|
|
1398
1418
|
<StandaloneMessage key={item.key} item={item} />
|
|
1399
1419
|
),
|
|
1400
1420
|
)}
|
|
1421
|
+
{isRecovering && (
|
|
1422
|
+
<CamelChatRecovery label={recoveryStatusLabel} />
|
|
1423
|
+
)}
|
|
1401
1424
|
{error != null && <CamelChatError error={error} onRetry={onRetry} />}
|
|
1402
1425
|
{isActive && activeAssistantMessageId === null && (
|
|
1403
1426
|
<ThinkingIndicator
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type ReactNode,
|
|
40
40
|
} from "react";
|
|
41
41
|
import { ComposerTriggerPopover } from "../composer/composer-trigger-popover";
|
|
42
|
+
import { isComposingKeyEvent } from "../composer/key-rules";
|
|
42
43
|
import type { ComposerCommand } from "../composer/types";
|
|
43
44
|
import { cn } from "../internal/cn";
|
|
44
45
|
|
|
@@ -345,7 +346,7 @@ function CamelPromptInputContent({
|
|
|
345
346
|
if (
|
|
346
347
|
event.key === "Enter" &&
|
|
347
348
|
!event.shiftKey &&
|
|
348
|
-
!event.nativeEvent
|
|
349
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
349
350
|
) {
|
|
350
351
|
event.preventDefault();
|
|
351
352
|
submit();
|
|
@@ -77,6 +77,12 @@ function stringField(record: UnknownRecord, ...keys: string[]): string {
|
|
|
77
77
|
return "";
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function stringArray(value: unknown): string[] {
|
|
81
|
+
return Array.isArray(value)
|
|
82
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
83
|
+
: [];
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
function numberField(record: UnknownRecord, ...keys: string[]): number | undefined {
|
|
81
87
|
for (const key of keys) {
|
|
82
88
|
const value = record[key];
|
|
@@ -342,17 +348,7 @@ function fileSummary(
|
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
function questionsOf(input: UnknownRecord): UnknownRecord[] {
|
|
345
|
-
|
|
346
|
-
if (typeof input.question === "string") {
|
|
347
|
-
return [
|
|
348
|
-
{
|
|
349
|
-
question: input.question,
|
|
350
|
-
header: input.header,
|
|
351
|
-
options: input.options,
|
|
352
|
-
},
|
|
353
|
-
];
|
|
354
|
-
}
|
|
355
|
-
return [];
|
|
351
|
+
return Array.isArray(input.questions) ? input.questions.map(recordOf) : [];
|
|
356
352
|
}
|
|
357
353
|
|
|
358
354
|
function summaryFor(
|
|
@@ -1113,7 +1109,10 @@ function SkillDetails({
|
|
|
1113
1109
|
|
|
1114
1110
|
function AskUserDetails({ presentation }: CamelToolDetailsProps) {
|
|
1115
1111
|
const questions = questionsOf(presentation.input);
|
|
1116
|
-
const
|
|
1112
|
+
const output = recordOf(presentation.output);
|
|
1113
|
+
const answers = Array.isArray(output.answers)
|
|
1114
|
+
? output.answers.map(recordOf)
|
|
1115
|
+
: [];
|
|
1117
1116
|
return (
|
|
1118
1117
|
<div className="space-y-2">
|
|
1119
1118
|
{questions.length > 0 ? (
|
|
@@ -1123,7 +1122,12 @@ function AskUserDetails({ presentation }: CamelToolDetailsProps) {
|
|
|
1123
1122
|
{stringField(question, "question", "prompt") || "Question"}
|
|
1124
1123
|
</p>
|
|
1125
1124
|
<p className="text-muted-foreground/60">
|
|
1126
|
-
{
|
|
1125
|
+
{[
|
|
1126
|
+
...stringArray(answers[index]?.selections),
|
|
1127
|
+
...(typeof answers[index]?.text === "string" && answers[index].text
|
|
1128
|
+
? [answers[index].text]
|
|
1129
|
+
: []),
|
|
1130
|
+
].join(" / ") || "—"}
|
|
1127
1131
|
</p>
|
|
1128
1132
|
</div>
|
|
1129
1133
|
))
|
package/src/camel/camel-turn.ts
CHANGED
|
@@ -41,7 +41,6 @@ export interface CamelAssistantTurn {
|
|
|
41
41
|
};
|
|
42
42
|
stepCount: number;
|
|
43
43
|
copyText: string;
|
|
44
|
-
replyText?: string;
|
|
45
44
|
durationMs?: number;
|
|
46
45
|
completedAt?: number;
|
|
47
46
|
}
|
|
@@ -115,10 +114,6 @@ function standaloneKind(message: UIMessage): CamelStandaloneKind | null {
|
|
|
115
114
|
return message.role === "user" ? "user" : null;
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
function isDirectUserMessage(message: UIMessage): boolean {
|
|
119
|
-
return message.role === "user" && standaloneKind(message) === "user";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
117
|
export function camelMessageText(message: UIMessage): string {
|
|
123
118
|
return message.parts
|
|
124
119
|
.filter(isVisibleText)
|
|
@@ -228,7 +223,6 @@ function turnDuration(message: UIMessage): number | undefined {
|
|
|
228
223
|
export function projectCamelAssistantTurn(
|
|
229
224
|
message: UIMessage,
|
|
230
225
|
finalToolNames: readonly string[] = [],
|
|
231
|
-
replyText?: string,
|
|
232
226
|
): CamelAssistantTurn {
|
|
233
227
|
const parts = message.parts.map((part, partIndex) => ({
|
|
234
228
|
messageId: message.id,
|
|
@@ -294,7 +288,6 @@ export function projectCamelAssistantTurn(
|
|
|
294
288
|
parts.filter(({ part }) => part.type === "step-start").length,
|
|
295
289
|
),
|
|
296
290
|
copyText: camelMessageText(message),
|
|
297
|
-
replyText,
|
|
298
291
|
durationMs: turnDuration(message),
|
|
299
292
|
completedAt: camelCompletedAt(message),
|
|
300
293
|
};
|
|
@@ -309,16 +302,7 @@ export function projectCamelMessages(
|
|
|
309
302
|
if (message.role === "system") return;
|
|
310
303
|
const displayKind = standaloneKind(message);
|
|
311
304
|
if (message.role === "assistant" && displayKind === null) {
|
|
312
|
-
|
|
313
|
-
.slice(index + 1)
|
|
314
|
-
.find(isDirectUserMessage);
|
|
315
|
-
items.push(
|
|
316
|
-
projectCamelAssistantTurn(
|
|
317
|
-
message,
|
|
318
|
-
finalToolNames,
|
|
319
|
-
reply ? camelMessageText(reply).trim() || undefined : undefined,
|
|
320
|
-
),
|
|
321
|
-
);
|
|
305
|
+
items.push(projectCamelAssistantTurn(message, finalToolNames));
|
|
322
306
|
return;
|
|
323
307
|
}
|
|
324
308
|
if (displayKind !== null) {
|
|
@@ -103,7 +103,7 @@ export function ChatSummaryPanel({
|
|
|
103
103
|
return (
|
|
104
104
|
<aside
|
|
105
105
|
className={cn(
|
|
106
|
-
"w-[19rem] min-w-0 max-w-
|
|
106
|
+
"absolute right-3 top-11 z-30 w-[19rem] min-w-0 max-w-[calc(100%-1.5rem)] overflow-hidden rounded-2xl border border-border bg-popover shadow-card",
|
|
107
107
|
"animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-200 motion-reduce:animate-none",
|
|
108
108
|
className,
|
|
109
109
|
)}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type KeyboardEvent,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { cn } from "../internal/cn";
|
|
13
|
+
import { isComposingKeyEvent } from "./key-rules";
|
|
13
14
|
import { Composer } from "./composer";
|
|
14
15
|
import type {
|
|
15
16
|
ChatComposerProps,
|
|
@@ -152,7 +153,7 @@ function ChatComposerInner<TAttachment>(
|
|
|
152
153
|
if (
|
|
153
154
|
(event.key === "Enter" &&
|
|
154
155
|
!event.shiftKey &&
|
|
155
|
-
!event.nativeEvent
|
|
156
|
+
!isComposingKeyEvent(event.nativeEvent)) ||
|
|
156
157
|
event.key === "Tab"
|
|
157
158
|
) {
|
|
158
159
|
const command = filtered[active] ?? filtered[0];
|
|
@@ -156,7 +156,7 @@ function Items({
|
|
|
156
156
|
{item.label}
|
|
157
157
|
</span>
|
|
158
158
|
{item.description && (
|
|
159
|
-
<span className="ms-5.5 text-xs leading-tight text-muted-foreground">
|
|
159
|
+
<span className="ms-5.5 w-full truncate text-xs leading-tight text-muted-foreground">
|
|
160
160
|
{item.description}
|
|
161
161
|
</span>
|
|
162
162
|
)}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
Attachments,
|
|
27
27
|
} from "./composer-attachments";
|
|
28
28
|
import { useComposerDrop } from "./drop-controller";
|
|
29
|
+
import { isComposingKeyEvent } from "./key-rules";
|
|
29
30
|
import type {
|
|
30
31
|
ComposerAttachmentView,
|
|
31
32
|
ComposerProps,
|
|
@@ -247,7 +248,7 @@ function ComposerInner<TAttachment>(
|
|
|
247
248
|
if (
|
|
248
249
|
event.key === "Enter" &&
|
|
249
250
|
!event.shiftKey &&
|
|
250
|
-
!event.nativeEvent
|
|
251
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
251
252
|
) {
|
|
252
253
|
event.preventDefault();
|
|
253
254
|
if (canSend) void submit();
|
package/src/composer/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether Enter belongs to an input method rather than to the Composer.
|
|
3
|
+
*
|
|
4
|
+
* Both signals are honoured on their own: some Chinese input methods report only the
|
|
5
|
+
* legacy `keyCode === 229`, and either one alone means a word is still being composed.
|
|
6
|
+
*/
|
|
7
|
+
export function isComposingKeyEvent(event: {
|
|
8
|
+
isComposing?: boolean;
|
|
9
|
+
keyCode?: number;
|
|
10
|
+
}): boolean {
|
|
11
|
+
return event.isComposing === true || event.keyCode === 229;
|
|
12
|
+
}
|
package/src/contracts.ts
CHANGED
|
@@ -47,8 +47,6 @@ export interface PartRendererContext {
|
|
|
47
47
|
activityId: string;
|
|
48
48
|
isActive: boolean;
|
|
49
49
|
isLatestOfKind: boolean;
|
|
50
|
-
/** First user message after the assistant message that owns this part. */
|
|
51
|
-
replyText?: string;
|
|
52
50
|
approval?: ToolApprovalView;
|
|
53
51
|
status: MessagePanelStatus;
|
|
54
52
|
actions: MessagePanelActions;
|
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}
|
|
@@ -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 && (
|