@steerable/agent-ui 0.2.3 → 0.2.4
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/dist/cards/ActionSegmentCard.d.ts +18 -0
- package/dist/cards/ActionSegmentCard.d.ts.map +1 -0
- package/dist/cards/ActionSegmentCard.js +26 -0
- package/dist/cards/ActionSegmentCard.js.map +1 -0
- package/dist/cards/AnalysisDocumentCard.d.ts +33 -0
- package/dist/cards/AnalysisDocumentCard.d.ts.map +1 -0
- package/dist/cards/AnalysisDocumentCard.js +11 -0
- package/dist/cards/AnalysisDocumentCard.js.map +1 -0
- package/dist/cards/AskUserQuestionsCard.d.ts +43 -0
- package/dist/cards/AskUserQuestionsCard.d.ts.map +1 -0
- package/dist/cards/AskUserQuestionsCard.js +171 -0
- package/dist/cards/AskUserQuestionsCard.js.map +1 -0
- package/dist/cards/CoverageReportCard.d.ts +41 -0
- package/dist/cards/CoverageReportCard.d.ts.map +1 -0
- package/dist/cards/CoverageReportCard.js +55 -0
- package/dist/cards/CoverageReportCard.js.map +1 -0
- package/dist/cards/OrchestrationPlanCard.d.ts +60 -0
- package/dist/cards/OrchestrationPlanCard.d.ts.map +1 -0
- package/dist/cards/OrchestrationPlanCard.js +103 -0
- package/dist/cards/OrchestrationPlanCard.js.map +1 -0
- package/dist/cards/PlanSelectorCard.d.ts +40 -0
- package/dist/cards/PlanSelectorCard.d.ts.map +1 -0
- package/dist/cards/PlanSelectorCard.js +106 -0
- package/dist/cards/PlanSelectorCard.js.map +1 -0
- package/dist/cards/PlanStepsCard.d.ts +18 -0
- package/dist/cards/PlanStepsCard.d.ts.map +1 -0
- package/dist/cards/PlanStepsCard.js +25 -0
- package/dist/cards/PlanStepsCard.js.map +1 -0
- package/dist/cards/QuizCard.d.ts +44 -0
- package/dist/cards/QuizCard.d.ts.map +1 -0
- package/dist/cards/QuizCard.js +113 -0
- package/dist/cards/QuizCard.js.map +1 -0
- package/dist/cards/ResearchPlanCard.d.ts +23 -0
- package/dist/cards/ResearchPlanCard.d.ts.map +1 -0
- package/dist/cards/ResearchPlanCard.js +50 -0
- package/dist/cards/ResearchPlanCard.js.map +1 -0
- package/dist/cards/SearchSourcesCard.d.ts +29 -0
- package/dist/cards/SearchSourcesCard.d.ts.map +1 -0
- package/dist/cards/SearchSourcesCard.js +83 -0
- package/dist/cards/SearchSourcesCard.js.map +1 -0
- package/dist/cards/SuggestedRepliesCard.d.ts +18 -0
- package/dist/cards/SuggestedRepliesCard.d.ts.map +1 -0
- package/dist/cards/SuggestedRepliesCard.js +14 -0
- package/dist/cards/SuggestedRepliesCard.js.map +1 -0
- package/dist/cards/SummaryMessageCard.d.ts +40 -0
- package/dist/cards/SummaryMessageCard.d.ts.map +1 -0
- package/dist/cards/SummaryMessageCard.js +56 -0
- package/dist/cards/SummaryMessageCard.js.map +1 -0
- package/dist/cards/ThinkingProcessCard.d.ts +46 -0
- package/dist/cards/ThinkingProcessCard.d.ts.map +1 -0
- package/dist/cards/ThinkingProcessCard.js +79 -0
- package/dist/cards/ThinkingProcessCard.js.map +1 -0
- package/dist/cards/ToolExecutionCard.d.ts +21 -0
- package/dist/cards/ToolExecutionCard.d.ts.map +1 -0
- package/dist/cards/ToolExecutionCard.js +65 -0
- package/dist/cards/ToolExecutionCard.js.map +1 -0
- package/dist/cards/icons.d.ts +38 -0
- package/dist/cards/icons.d.ts.map +1 -0
- package/dist/cards/icons.js +87 -0
- package/dist/cards/icons.js.map +1 -0
- package/dist/cards/index.d.ts +38 -0
- package/dist/cards/index.d.ts.map +1 -0
- package/dist/cards/index.js +24 -0
- package/dist/cards/index.js.map +1 -0
- package/dist/cards/types.d.ts +24 -0
- package/dist/cards/types.d.ts.map +1 -0
- package/dist/cards/types.js +7 -0
- package/dist/cards/types.js.map +1 -0
- package/dist/components/AgentSelector.d.ts +23 -0
- package/dist/components/AgentSelector.d.ts.map +1 -0
- package/dist/components/AgentSelector.js +63 -0
- package/dist/components/AgentSelector.js.map +1 -0
- package/dist/components/AgentSelector.stories.d.ts +12 -0
- package/dist/components/AgentSelector.stories.d.ts.map +1 -0
- package/dist/components/AgentSelector.stories.js +95 -0
- package/dist/components/AgentSelector.stories.js.map +1 -0
- package/dist/components/ChatPanel.d.ts +138 -21
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +181 -26
- package/dist/components/ChatPanel.js.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/state/ChatSessionProvider.d.ts +59 -0
- package/dist/state/ChatSessionProvider.d.ts.map +1 -0
- package/dist/state/ChatSessionProvider.js +71 -0
- package/dist/state/ChatSessionProvider.js.map +1 -0
- package/dist/state/MockChatStreamTransport.d.ts +54 -0
- package/dist/state/MockChatStreamTransport.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.js +86 -0
- package/dist/state/MockChatStreamTransport.js.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts +13 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.js +60 -0
- package/dist/state/MockChatStreamTransport.stories.js.map +1 -0
- package/dist/state/bridgeLegacySSE.d.ts +42 -0
- package/dist/state/bridgeLegacySSE.d.ts.map +1 -0
- package/dist/state/bridgeLegacySSE.js +102 -0
- package/dist/state/bridgeLegacySSE.js.map +1 -0
- package/dist/state/index.d.ts +32 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +24 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/parseSSE.d.ts +50 -0
- package/dist/state/parseSSE.d.ts.map +1 -0
- package/dist/state/parseSSE.js +112 -0
- package/dist/state/parseSSE.js.map +1 -0
- package/dist/state/useChatComposer.d.ts +51 -0
- package/dist/state/useChatComposer.d.ts.map +1 -0
- package/dist/state/useChatComposer.js +82 -0
- package/dist/state/useChatComposer.js.map +1 -0
- package/dist/state/useChatList.d.ts +71 -0
- package/dist/state/useChatList.d.ts.map +1 -0
- package/dist/state/useChatList.js +158 -0
- package/dist/state/useChatList.js.map +1 -0
- package/dist/state/useChatSession.d.ts +29 -0
- package/dist/state/useChatSession.d.ts.map +1 -0
- package/dist/state/useChatSession.js +37 -0
- package/dist/state/useChatSession.js.map +1 -0
- package/dist/state/useScrollLock.d.ts +35 -0
- package/dist/state/useScrollLock.d.ts.map +1 -0
- package/dist/state/useScrollLock.js +74 -0
- package/dist/state/useScrollLock.js.map +1 -0
- package/dist/state/useToolCallStream.d.ts +38 -0
- package/dist/state/useToolCallStream.d.ts.map +1 -0
- package/dist/state/useToolCallStream.js +74 -0
- package/dist/state/useToolCallStream.js.map +1 -0
- package/package.json +10 -2
- package/src/cards/ActionSegmentCard.tsx +60 -0
- package/src/cards/AnalysisDocumentCard.tsx +81 -0
- package/src/cards/AskUserQuestionsCard.tsx +354 -0
- package/src/cards/CoverageReportCard.tsx +215 -0
- package/src/cards/OrchestrationPlanCard.tsx +328 -0
- package/src/cards/PlanSelectorCard.tsx +312 -0
- package/src/cards/PlanStepsCard.tsx +52 -0
- package/src/cards/QuizCard.tsx +309 -0
- package/src/cards/ResearchPlanCard.tsx +169 -0
- package/src/cards/SearchSourcesCard.tsx +244 -0
- package/src/cards/SuggestedRepliesCard.tsx +48 -0
- package/src/cards/SummaryMessageCard.tsx +145 -0
- package/src/cards/ThinkingProcessCard.tsx +155 -0
- package/src/cards/ToolExecutionCard.tsx +152 -0
- package/src/cards/cards.test.tsx +492 -0
- package/src/cards/icons.tsx +249 -0
- package/src/cards/index.ts +53 -0
- package/src/cards/types.ts +27 -0
- package/src/components/AgentSelector.stories.tsx +113 -0
- package/src/components/AgentSelector.test.tsx +70 -0
- package/src/components/AgentSelector.tsx +147 -0
- package/src/components/ChatPanel.test.tsx +130 -0
- package/src/components/ChatPanel.tsx +612 -138
- package/src/components/index.ts +13 -1
- package/src/index.ts +43 -0
- package/src/state/ChatSessionProvider.test.tsx +85 -0
- package/src/state/ChatSessionProvider.tsx +98 -0
- package/src/state/MockChatStreamTransport.stories.tsx +95 -0
- package/src/state/MockChatStreamTransport.test.ts +163 -0
- package/src/state/MockChatStreamTransport.ts +123 -0
- package/src/state/bridgeLegacySSE.test.ts +134 -0
- package/src/state/bridgeLegacySSE.ts +134 -0
- package/src/state/index.ts +82 -0
- package/src/state/parseSSE.test.ts +123 -0
- package/src/state/parseSSE.ts +123 -0
- package/src/state/useChatComposer.test.ts +145 -0
- package/src/state/useChatComposer.ts +117 -0
- package/src/state/useChatList.test.ts +135 -0
- package/src/state/useChatList.ts +217 -0
- package/src/state/useChatSession.test.ts +61 -0
- package/src/state/useChatSession.ts +64 -0
- package/src/state/useScrollLock.test.ts +99 -0
- package/src/state/useScrollLock.ts +105 -0
- package/src/state/useToolCallStream.test.ts +86 -0
- package/src/state/useToolCallStream.ts +110 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<QuizCard />`
|
|
3
|
+
*
|
|
4
|
+
* Renders a multi-question quiz: choice / fill / judge / short_answer types.
|
|
5
|
+
* The card manages an internal draft, calls `onSubmit(answers)` once, and
|
|
6
|
+
* switches to read-only mode if `isComplete` is true (or the backend re-emits
|
|
7
|
+
* with `payload.submittedAnswers` filled in).
|
|
8
|
+
*
|
|
9
|
+
* Variants:
|
|
10
|
+
* - `choiceVariant`: 'pills' (default; chip-style buttons) | 'radio' (native
|
|
11
|
+
* radio/checkbox inputs in a vertical list).
|
|
12
|
+
* - `questionVariant`: 'flat' (default) | 'card' (each question gets its own
|
|
13
|
+
* bordered inner card).
|
|
14
|
+
*
|
|
15
|
+
* Slots:
|
|
16
|
+
* - `judgeLabels`: tuple for "true"/"false" buttons. Default `['对', '错']`.
|
|
17
|
+
* - `submitHint`, `submittedMessage`, `submittingLabel`: localisable
|
|
18
|
+
* status strings.
|
|
19
|
+
* - `renderQuestionMeta(question)`: append metadata under the stem (e.g.
|
|
20
|
+
* difficulty + points badges).
|
|
21
|
+
*/
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
import type { QuizPayload } from '@steerable/agent-protocol';
|
|
24
|
+
import { ClipboardIcon, LoaderIcon, CheckIcon } from './icons.js';
|
|
25
|
+
import { asReactNode, type RenderSlotResult } from './types.js';
|
|
26
|
+
|
|
27
|
+
type AnswerValue = string | string[];
|
|
28
|
+
type Question = QuizPayload['questions'][number];
|
|
29
|
+
|
|
30
|
+
export type QuizChoiceVariant = 'pills' | 'radio';
|
|
31
|
+
export type QuizQuestionVariant = 'flat' | 'card';
|
|
32
|
+
|
|
33
|
+
export interface QuizCardProps {
|
|
34
|
+
payload: QuizPayload;
|
|
35
|
+
isComplete?: boolean;
|
|
36
|
+
onSubmit?: (answers: Record<string, AnswerValue>) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
choiceVariant?: QuizChoiceVariant;
|
|
39
|
+
questionVariant?: QuizQuestionVariant;
|
|
40
|
+
judgeLabels?: [string, string];
|
|
41
|
+
submitHint?: React.ReactNode;
|
|
42
|
+
submittedMessage?: React.ReactNode;
|
|
43
|
+
submittingLabel?: React.ReactNode;
|
|
44
|
+
renderQuestionMeta?: (question: Question) => RenderSlotResult;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emptyAnswer(q: Question): AnswerValue {
|
|
48
|
+
if (q.type === 'choice' && q.allowMultiple) return [];
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const QuizCard: React.FC<QuizCardProps> = ({
|
|
53
|
+
payload,
|
|
54
|
+
isComplete,
|
|
55
|
+
onSubmit,
|
|
56
|
+
className,
|
|
57
|
+
choiceVariant = 'pills',
|
|
58
|
+
questionVariant = 'flat',
|
|
59
|
+
judgeLabels = ['对', '错'],
|
|
60
|
+
submitHint,
|
|
61
|
+
submittedMessage,
|
|
62
|
+
submittingLabel = '提交中...',
|
|
63
|
+
renderQuestionMeta,
|
|
64
|
+
}) => {
|
|
65
|
+
const isReadOnly = Boolean(
|
|
66
|
+
isComplete ||
|
|
67
|
+
(payload.submittedAnswers && Object.keys(payload.submittedAnswers).length > 0),
|
|
68
|
+
);
|
|
69
|
+
const initial = React.useMemo(() => {
|
|
70
|
+
const seed: Record<string, AnswerValue> = {};
|
|
71
|
+
for (const q of payload.questions ?? []) {
|
|
72
|
+
seed[q.id] = (payload.submittedAnswers?.[q.id] ?? emptyAnswer(q)) as AnswerValue;
|
|
73
|
+
}
|
|
74
|
+
return seed;
|
|
75
|
+
}, [payload.questions, payload.submittedAnswers]);
|
|
76
|
+
const [draft, setDraft] = React.useState(initial);
|
|
77
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
78
|
+
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
setDraft(initial);
|
|
81
|
+
}, [initial]);
|
|
82
|
+
|
|
83
|
+
const setSingle = (qId: string, value: string) => setDraft((p) => ({ ...p, [qId]: value }));
|
|
84
|
+
const toggleMulti = (qId: string, opt: string) =>
|
|
85
|
+
setDraft((p) => {
|
|
86
|
+
const cur = (p[qId] ?? []) as string[];
|
|
87
|
+
return { ...p, [qId]: cur.includes(opt) ? cur.filter((v) => v !== opt) : [...cur, opt] };
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const handleSubmit = async () => {
|
|
91
|
+
if (!onSubmit) return;
|
|
92
|
+
setSubmitting(true);
|
|
93
|
+
try {
|
|
94
|
+
onSubmit(draft);
|
|
95
|
+
} finally {
|
|
96
|
+
setSubmitting(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const renderChoice = (q: Question, value: AnswerValue) => {
|
|
101
|
+
const options = q.options ?? [];
|
|
102
|
+
if (choiceVariant === 'radio') {
|
|
103
|
+
return (
|
|
104
|
+
<div className="space-y-1.5">
|
|
105
|
+
{options.map((opt) => {
|
|
106
|
+
const checked = Array.isArray(value) ? value.includes(opt) : value === opt;
|
|
107
|
+
return (
|
|
108
|
+
<label key={opt} className="flex cursor-pointer items-start gap-2 text-sm">
|
|
109
|
+
<input
|
|
110
|
+
type={q.allowMultiple ? 'checkbox' : 'radio'}
|
|
111
|
+
name={q.id}
|
|
112
|
+
checked={checked}
|
|
113
|
+
disabled={isReadOnly}
|
|
114
|
+
className="mt-[3px] h-4 w-4 shrink-0"
|
|
115
|
+
onChange={() => (q.allowMultiple ? toggleMulti(q.id, opt) : setSingle(q.id, opt))}
|
|
116
|
+
/>
|
|
117
|
+
<span className="min-w-0 break-words leading-relaxed">{opt}</span>
|
|
118
|
+
</label>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex flex-wrap gap-2">
|
|
126
|
+
{options.map((opt) => {
|
|
127
|
+
const selected = q.allowMultiple
|
|
128
|
+
? (Array.isArray(value) ? value : []).includes(opt)
|
|
129
|
+
: value === opt;
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
key={opt}
|
|
133
|
+
type="button"
|
|
134
|
+
disabled={isReadOnly}
|
|
135
|
+
onClick={() => (q.allowMultiple ? toggleMulti(q.id, opt) : setSingle(q.id, opt))}
|
|
136
|
+
className={[
|
|
137
|
+
'rounded-full border px-3 py-1 text-xs transition-colors duration-150',
|
|
138
|
+
selected
|
|
139
|
+
? 'border-[var(--agent-primary,#18181b)] bg-[var(--agent-primary,#18181b)] text-[var(--agent-primary-foreground,#fff)]'
|
|
140
|
+
: 'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] hover:bg-[var(--agent-accent,#f3f4f6)]',
|
|
141
|
+
isReadOnly ? 'cursor-default opacity-80' : '',
|
|
142
|
+
].filter(Boolean).join(' ')}
|
|
143
|
+
>
|
|
144
|
+
{opt}
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const renderJudge = (q: Question, value: AnswerValue) => {
|
|
153
|
+
if (choiceVariant === 'radio') {
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
|
|
156
|
+
{judgeLabels.map((opt) => (
|
|
157
|
+
<label key={opt} className="flex cursor-pointer items-center gap-2">
|
|
158
|
+
<input
|
|
159
|
+
type="radio"
|
|
160
|
+
name={q.id}
|
|
161
|
+
checked={value === opt}
|
|
162
|
+
disabled={isReadOnly}
|
|
163
|
+
onChange={() => setSingle(q.id, opt)}
|
|
164
|
+
className="h-4 w-4 shrink-0"
|
|
165
|
+
/>
|
|
166
|
+
<span>{opt}</span>
|
|
167
|
+
</label>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return (
|
|
173
|
+
<div className="flex gap-2">
|
|
174
|
+
{judgeLabels.map((opt) => (
|
|
175
|
+
<button
|
|
176
|
+
key={opt}
|
|
177
|
+
type="button"
|
|
178
|
+
disabled={isReadOnly}
|
|
179
|
+
onClick={() => setSingle(q.id, opt)}
|
|
180
|
+
className={[
|
|
181
|
+
'rounded-full border px-3 py-1 text-xs transition-colors duration-150',
|
|
182
|
+
value === opt
|
|
183
|
+
? 'border-[var(--agent-primary,#18181b)] bg-[var(--agent-primary,#18181b)] text-[var(--agent-primary-foreground,#fff)]'
|
|
184
|
+
: 'border-[var(--agent-border,#e5e7eb)] hover:bg-[var(--agent-accent,#f3f4f6)]',
|
|
185
|
+
].filter(Boolean).join(' ')}
|
|
186
|
+
>
|
|
187
|
+
{opt}
|
|
188
|
+
</button>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const renderTextarea = (q: Question, value: AnswerValue) => (
|
|
195
|
+
<textarea
|
|
196
|
+
rows={q.type === 'short_answer' ? 4 : 2}
|
|
197
|
+
value={typeof value === 'string' ? value : ''}
|
|
198
|
+
disabled={isReadOnly}
|
|
199
|
+
placeholder={q.placeholder ?? '请输入你的答案'}
|
|
200
|
+
onChange={(e) => setSingle(q.id, e.target.value)}
|
|
201
|
+
className="w-full rounded-md border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] px-3 py-2 text-sm text-[var(--agent-foreground,#111827)] outline-none transition-all duration-200 focus:border-[var(--agent-primary,#18181b)]"
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const defaultMeta = (q: Question) => {
|
|
206
|
+
if (!q.difficulty && !q.points) return null;
|
|
207
|
+
return (
|
|
208
|
+
<>
|
|
209
|
+
{q.difficulty ? <span>难度 {q.difficulty}</span> : null}
|
|
210
|
+
{q.points ? <span>{q.points} 分</span> : null}
|
|
211
|
+
</>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
className={[
|
|
218
|
+
'steerable-quiz overflow-hidden rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] text-sm text-[var(--agent-foreground,#111827)]',
|
|
219
|
+
className,
|
|
220
|
+
].filter(Boolean).join(' ')}
|
|
221
|
+
>
|
|
222
|
+
<div className="space-y-3 p-3">
|
|
223
|
+
<div className="flex items-center gap-2">
|
|
224
|
+
<ClipboardIcon size={14} className="shrink-0 text-blue-500" />
|
|
225
|
+
<span className="break-words font-medium">{payload.title}</span>
|
|
226
|
+
{isReadOnly && <CheckIcon size={12} className="text-emerald-600" />}
|
|
227
|
+
</div>
|
|
228
|
+
{payload.description && (
|
|
229
|
+
<p className="break-words text-xs leading-relaxed text-[var(--agent-muted-foreground,#6b7280)]">
|
|
230
|
+
{payload.description}
|
|
231
|
+
</p>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<div className={questionVariant === 'card' ? 'space-y-3' : 'space-y-4'}>
|
|
235
|
+
{(payload.questions ?? []).map((q, idx) => {
|
|
236
|
+
const value = draft[q.id] ?? emptyAnswer(q);
|
|
237
|
+
const metaNode = renderQuestionMeta
|
|
238
|
+
? asReactNode(renderQuestionMeta(q))
|
|
239
|
+
: defaultMeta(q);
|
|
240
|
+
const inner = (
|
|
241
|
+
<>
|
|
242
|
+
<div className="mb-2">
|
|
243
|
+
<p className="break-words text-sm font-medium leading-relaxed">
|
|
244
|
+
{idx + 1}. {q.stem}
|
|
245
|
+
</p>
|
|
246
|
+
{metaNode && (
|
|
247
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-[var(--agent-muted-foreground,#6b7280)]">
|
|
248
|
+
{metaNode}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
{q.type === 'choice' && q.options ? renderChoice(q, value) : null}
|
|
253
|
+
{q.type === 'judge' ? renderJudge(q, value) : null}
|
|
254
|
+
{q.type === 'fill' || q.type === 'short_answer' ? renderTextarea(q, value) : null}
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
return questionVariant === 'card' ? (
|
|
258
|
+
<div
|
|
259
|
+
key={q.id}
|
|
260
|
+
className="rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-background,#fff)] p-3"
|
|
261
|
+
>
|
|
262
|
+
{inner}
|
|
263
|
+
</div>
|
|
264
|
+
) : (
|
|
265
|
+
<div key={q.id} className="space-y-1.5">
|
|
266
|
+
{inner}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
})}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 pt-1">
|
|
273
|
+
{isReadOnly ? (
|
|
274
|
+
<div className="inline-flex min-w-0 items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
|
|
275
|
+
<CheckIcon size={12} />
|
|
276
|
+
<span className="truncate">{submittedMessage ?? '已提交'}</span>
|
|
277
|
+
</div>
|
|
278
|
+
) : (
|
|
279
|
+
submitHint && (
|
|
280
|
+
<span className="min-w-0 flex-1 break-words text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
281
|
+
{submitHint}
|
|
282
|
+
</span>
|
|
283
|
+
)
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{!isReadOnly && onSubmit && (
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={handleSubmit}
|
|
290
|
+
disabled={submitting || isReadOnly}
|
|
291
|
+
className="ml-auto inline-flex h-[28px] shrink-0 items-center gap-1 rounded-full bg-[var(--agent-primary,#18181b)] px-3 text-xs font-medium text-[var(--agent-primary-foreground,#fff)] transition-all duration-200 hover:opacity-90 disabled:opacity-50"
|
|
292
|
+
>
|
|
293
|
+
{submitting ? (
|
|
294
|
+
<>
|
|
295
|
+
<LoaderIcon size={10} className="animate-spin" />
|
|
296
|
+
{submittingLabel}
|
|
297
|
+
</>
|
|
298
|
+
) : (
|
|
299
|
+
payload.submitActionLabel || '提交'
|
|
300
|
+
)}
|
|
301
|
+
</button>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export default QuizCard;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<ResearchPlanCard />`
|
|
3
|
+
*
|
|
4
|
+
* Snapshot of a research agent's current sub-question tree, with per-question
|
|
5
|
+
* status badges (searching / strong evidence / conflicted / ...) and the
|
|
6
|
+
* coordinator's decision for the next round.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - progress bar (round / totalRounds);
|
|
10
|
+
* - sub-question note rendering when `note` is present;
|
|
11
|
+
* - boxed "next decision" footer with the agent's reasoning.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import type { ResearchPlanPayload } from '@steerable/agent-protocol';
|
|
15
|
+
import { GitBranchIcon, ListChecksIcon } from './icons.js';
|
|
16
|
+
|
|
17
|
+
type Kind = 'fact' | 'compare' | 'conclusion' | 'risk';
|
|
18
|
+
type Status =
|
|
19
|
+
| 'pending'
|
|
20
|
+
| 'searching'
|
|
21
|
+
| 'evidenced_strong'
|
|
22
|
+
| 'evidenced_medium'
|
|
23
|
+
| 'evidenced_weak'
|
|
24
|
+
| 'conflicted'
|
|
25
|
+
| 'exhausted';
|
|
26
|
+
|
|
27
|
+
const KIND_LABELS: Record<Kind, string> = {
|
|
28
|
+
fact: '事实',
|
|
29
|
+
compare: '对比',
|
|
30
|
+
conclusion: '结论',
|
|
31
|
+
risk: '风险',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const STATUS_LABELS: Record<Status, string> = {
|
|
35
|
+
pending: '待开始',
|
|
36
|
+
searching: '检索中',
|
|
37
|
+
evidenced_strong: '强证据',
|
|
38
|
+
evidenced_medium: '中证据',
|
|
39
|
+
evidenced_weak: '弱证据',
|
|
40
|
+
conflicted: '证据冲突',
|
|
41
|
+
exhausted: '已穷尽',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const STATUS_COLOR: Record<Status, string> = {
|
|
45
|
+
pending: 'bg-[var(--agent-muted,#f3f4f6)] text-[var(--agent-muted-foreground,#6b7280)]',
|
|
46
|
+
searching: 'bg-[var(--agent-muted,#f3f4f6)] text-[var(--agent-muted-foreground,#6b7280)]',
|
|
47
|
+
evidenced_strong: 'bg-green-500/15 text-green-700 dark:text-green-400',
|
|
48
|
+
evidenced_medium: 'bg-blue-500/15 text-blue-700 dark:text-blue-400',
|
|
49
|
+
evidenced_weak: 'bg-amber-500/15 text-amber-700 dark:text-amber-400',
|
|
50
|
+
conflicted: 'bg-red-500/15 text-red-700 dark:text-red-400',
|
|
51
|
+
exhausted: 'bg-zinc-500/15 text-zinc-700 dark:text-zinc-400',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const DECISION_LABEL: Record<'continue' | 'expand' | 'converge', string> = {
|
|
55
|
+
continue: '继续深挖',
|
|
56
|
+
expand: '横向扩展',
|
|
57
|
+
converge: '进入收敛',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface ResearchPlanCardProps {
|
|
61
|
+
payload: ResearchPlanPayload;
|
|
62
|
+
/** Total expected rounds (used to compute the progress bar). Defaults to 3. */
|
|
63
|
+
totalRounds?: number;
|
|
64
|
+
className?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const ResearchPlanCard: React.FC<ResearchPlanCardProps> = ({
|
|
68
|
+
payload,
|
|
69
|
+
totalRounds = 3,
|
|
70
|
+
className,
|
|
71
|
+
}) => {
|
|
72
|
+
const subQuestions = payload.subQuestions ?? [];
|
|
73
|
+
const round = Math.max(0, Math.min(totalRounds, payload.round ?? 0));
|
|
74
|
+
const progressPercent = totalRounds > 0
|
|
75
|
+
? Math.max(0, Math.min(100, (round / totalRounds) * 100))
|
|
76
|
+
: 0;
|
|
77
|
+
const decisionLabel =
|
|
78
|
+
payload.decision &&
|
|
79
|
+
DECISION_LABEL[payload.decision.next as 'continue' | 'expand' | 'converge'];
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
className={[
|
|
84
|
+
'steerable-research-plan rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] text-sm text-[var(--agent-foreground,#111827)]',
|
|
85
|
+
className,
|
|
86
|
+
].filter(Boolean).join(' ')}
|
|
87
|
+
>
|
|
88
|
+
<div className="space-y-3 p-3">
|
|
89
|
+
<div className="space-y-2">
|
|
90
|
+
<div className="flex items-center justify-between gap-2">
|
|
91
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
92
|
+
<GitBranchIcon size={14} className="shrink-0 text-blue-500" />
|
|
93
|
+
<span className="truncate text-sm font-medium">{payload.topic}</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="inline-flex shrink-0 items-center gap-1">
|
|
96
|
+
<span className="rounded-full bg-[var(--agent-muted,#f3f4f6)] px-2 py-0.5 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
97
|
+
第 {round}/{totalRounds} 轮
|
|
98
|
+
</span>
|
|
99
|
+
{payload.final && (
|
|
100
|
+
<span className="rounded-full bg-green-500/15 px-2 py-0.5 text-xs text-green-700 dark:text-green-400">
|
|
101
|
+
已收敛
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[var(--agent-muted,#f3f4f6)]">
|
|
107
|
+
<div
|
|
108
|
+
className="h-full bg-blue-500 transition-all duration-200"
|
|
109
|
+
style={{ width: `${progressPercent}%` }}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="space-y-2">
|
|
115
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
116
|
+
<ListChecksIcon size={14} className="text-blue-500" />
|
|
117
|
+
子问题状态
|
|
118
|
+
</div>
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
{subQuestions.map((q) => {
|
|
121
|
+
const kind = (q.kind ?? 'fact') as Kind;
|
|
122
|
+
const status = (q.status ?? 'pending') as Status;
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
key={q.id}
|
|
126
|
+
className="rounded-md border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] p-2.5"
|
|
127
|
+
>
|
|
128
|
+
<div className="mb-1.5 flex flex-wrap items-center gap-1.5">
|
|
129
|
+
<span className="rounded-full bg-[var(--agent-muted,#f3f4f6)] px-1.5 py-0.5 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
130
|
+
{KIND_LABELS[kind] ?? kind}
|
|
131
|
+
</span>
|
|
132
|
+
<span className={`rounded-full px-1.5 py-0.5 text-xs ${STATUS_COLOR[status]}`}>
|
|
133
|
+
{STATUS_LABELS[status] ?? status}
|
|
134
|
+
</span>
|
|
135
|
+
<span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
136
|
+
证据 {q.evidenceCount ?? 0}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="text-sm">{q.question}</div>
|
|
140
|
+
{q.note && (
|
|
141
|
+
<div className="mt-1 whitespace-pre-wrap text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
142
|
+
{q.note}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{payload.decision && (
|
|
152
|
+
<div className="rounded-md border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-muted,#f3f4f6)]/40 px-2.5 py-2">
|
|
153
|
+
<div className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">下一步决策</div>
|
|
154
|
+
<div className="text-sm font-medium">
|
|
155
|
+
{decisionLabel ?? payload.decision.next}
|
|
156
|
+
</div>
|
|
157
|
+
{payload.decision.reason && (
|
|
158
|
+
<div className="mt-0.5 whitespace-pre-wrap text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
159
|
+
{payload.decision.reason}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default ResearchPlanCard;
|