@steerable/agent-ui 0.2.0 → 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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<SearchSourcesCard />`
|
|
3
|
+
*
|
|
4
|
+
* Renders a row of stacked favicons summarising the web sources an agent
|
|
5
|
+
* consulted, plus an expandable detail panel listing each `(title, domain,
|
|
6
|
+
* snippet)` triple.
|
|
7
|
+
*
|
|
8
|
+
* Appearance variants:
|
|
9
|
+
* - `card` (default): bordered card chrome with a divided expanded list.
|
|
10
|
+
* - `inline`: no chrome around the trigger; expanded items use a borderless
|
|
11
|
+
* `:hover` background. Matches the deeppath "above the input" aesthetic.
|
|
12
|
+
*
|
|
13
|
+
* Slots:
|
|
14
|
+
* - `renderSummary(count, more)` -- replace the default "{count} 个来源" label.
|
|
15
|
+
* - `faviconUrlFor(url)` -- replace the default favicon resolver.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
import type { SearchSourcesPayload } from '@steerable/agent-protocol';
|
|
19
|
+
import { GlobeIcon, ChevronDownIcon, ExternalLinkIcon } from './icons.js';
|
|
20
|
+
|
|
21
|
+
export type SearchSourcesCardAppearance = 'card' | 'inline';
|
|
22
|
+
|
|
23
|
+
export interface SearchSourcesCardProps {
|
|
24
|
+
payload: SearchSourcesPayload;
|
|
25
|
+
className?: string;
|
|
26
|
+
appearance?: SearchSourcesCardAppearance;
|
|
27
|
+
faviconUrlFor?: (url: string) => string | null;
|
|
28
|
+
renderSummary?: (uniqueCount: number, overflowCount: number) => React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAX_STACKED = 5;
|
|
32
|
+
|
|
33
|
+
function extractDomain(url: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
36
|
+
} catch {
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultFaviconUrl(url: string): string | null {
|
|
42
|
+
try {
|
|
43
|
+
const host = new URL(url).hostname;
|
|
44
|
+
return `https://www.google.com/s2/favicons?sz=64&domain=${host}`;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function FaviconCircle({ src, size = 18 }: { src: string | null; size?: number }) {
|
|
51
|
+
const [failed, setFailed] = React.useState(false);
|
|
52
|
+
const showFallback = !src || failed;
|
|
53
|
+
|
|
54
|
+
if (showFallback) {
|
|
55
|
+
return (
|
|
56
|
+
<span
|
|
57
|
+
className="inline-flex items-center justify-center rounded-full border-2 border-[var(--agent-background,#fff)] bg-[var(--agent-muted,#f3f4f6)]"
|
|
58
|
+
style={{ width: size, height: size }}
|
|
59
|
+
>
|
|
60
|
+
<GlobeIcon
|
|
61
|
+
size={Math.round(size * 0.55)}
|
|
62
|
+
className="text-[var(--agent-muted-foreground,#6b7280)]"
|
|
63
|
+
/>
|
|
64
|
+
</span>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<img
|
|
70
|
+
src={src}
|
|
71
|
+
alt=""
|
|
72
|
+
width={size}
|
|
73
|
+
height={size}
|
|
74
|
+
className="rounded-full border-2 border-[var(--agent-background,#fff)] bg-[var(--agent-background,#fff)] object-cover"
|
|
75
|
+
style={{ width: size, height: size }}
|
|
76
|
+
loading="lazy"
|
|
77
|
+
onError={() => setFailed(true)}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const SearchSourcesCard: React.FC<SearchSourcesCardProps> = ({
|
|
83
|
+
payload,
|
|
84
|
+
className,
|
|
85
|
+
appearance = 'card',
|
|
86
|
+
faviconUrlFor = defaultFaviconUrl,
|
|
87
|
+
renderSummary,
|
|
88
|
+
}) => {
|
|
89
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
90
|
+
const sources = payload.sources ?? [];
|
|
91
|
+
|
|
92
|
+
const unique = React.useMemo(() => {
|
|
93
|
+
const seen = new Set<string>();
|
|
94
|
+
return sources.filter((s) => {
|
|
95
|
+
if (!s?.url || seen.has(s.url)) return false;
|
|
96
|
+
seen.add(s.url);
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
}, [sources]);
|
|
100
|
+
|
|
101
|
+
if (unique.length === 0) return null;
|
|
102
|
+
|
|
103
|
+
const stacked = unique.slice(0, MAX_STACKED);
|
|
104
|
+
const more = unique.length - stacked.length;
|
|
105
|
+
|
|
106
|
+
const isCard = appearance === 'card';
|
|
107
|
+
|
|
108
|
+
const summaryLabel = renderSummary
|
|
109
|
+
? renderSummary(unique.length, more)
|
|
110
|
+
: isCard
|
|
111
|
+
? <>
|
|
112
|
+
{unique.length} 个来源{more > 0 ? `(+${more})` : ''}
|
|
113
|
+
</>
|
|
114
|
+
: <>搜索了 {unique.length} 个网站</>;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
className={[
|
|
119
|
+
'steerable-search-sources',
|
|
120
|
+
isCard
|
|
121
|
+
? 'rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)]'
|
|
122
|
+
: '',
|
|
123
|
+
className,
|
|
124
|
+
].filter(Boolean).join(' ')}
|
|
125
|
+
>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={() => setExpanded((v) => !v)}
|
|
129
|
+
className={
|
|
130
|
+
isCard
|
|
131
|
+
? 'flex w-full items-center justify-between gap-2 px-3 py-2 text-left'
|
|
132
|
+
: 'group flex items-center gap-2 rounded-md py-1 text-sm text-[var(--agent-muted-foreground,#6b7280)] transition-colors duration-150 hover:text-[var(--agent-foreground,#111827)]'
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
<span className={isCard ? 'flex items-center gap-2' : 'flex items-center'}>
|
|
136
|
+
{isCard ? (
|
|
137
|
+
<>
|
|
138
|
+
<span className="flex -space-x-2">
|
|
139
|
+
{stacked.map((s, idx) => (
|
|
140
|
+
<FaviconCircle key={`${idx}-${s.url}`} src={s.favicon ?? faviconUrlFor(s.url)} />
|
|
141
|
+
))}
|
|
142
|
+
</span>
|
|
143
|
+
<span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
144
|
+
{summaryLabel}
|
|
145
|
+
</span>
|
|
146
|
+
</>
|
|
147
|
+
) : (
|
|
148
|
+
<>
|
|
149
|
+
<span className="flex" style={{ paddingLeft: 0 }}>
|
|
150
|
+
{stacked.map((s, i) => (
|
|
151
|
+
<span
|
|
152
|
+
key={`${i}-${s.url}`}
|
|
153
|
+
className="inline-block"
|
|
154
|
+
style={{ marginLeft: i === 0 ? 0 : -6, zIndex: MAX_STACKED - i }}
|
|
155
|
+
>
|
|
156
|
+
<FaviconCircle src={s.favicon ?? faviconUrlFor(s.url)} />
|
|
157
|
+
</span>
|
|
158
|
+
))}
|
|
159
|
+
{more > 0 && (
|
|
160
|
+
<span
|
|
161
|
+
className="inline-flex items-center justify-center rounded-full border-2 border-[var(--agent-background,#fff)] bg-[var(--agent-muted,#f3f4f6)] text-[10px] font-medium text-[var(--agent-muted-foreground,#6b7280)]"
|
|
162
|
+
style={{ width: 18, height: 18, marginLeft: -6, zIndex: 0 }}
|
|
163
|
+
>
|
|
164
|
+
+{more}
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
</span>
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
</span>
|
|
171
|
+
{!isCard && <span className="ml-2">{summaryLabel}</span>}
|
|
172
|
+
<ChevronDownIcon
|
|
173
|
+
size={isCard ? 14 : 14}
|
|
174
|
+
className={[
|
|
175
|
+
isCard ? 'text-[var(--agent-muted-foreground,#6b7280)]' : 'h-3.5 w-3.5',
|
|
176
|
+
'transition-transform duration-200',
|
|
177
|
+
expanded ? 'rotate-180' : '',
|
|
178
|
+
].filter(Boolean).join(' ')}
|
|
179
|
+
/>
|
|
180
|
+
</button>
|
|
181
|
+
{expanded && (
|
|
182
|
+
isCard ? (
|
|
183
|
+
<ul className="divide-y divide-[var(--agent-border,#e5e7eb)] border-t border-[var(--agent-border,#e5e7eb)]">
|
|
184
|
+
{unique.map((s, idx) => (
|
|
185
|
+
<li key={`${idx}-${s.url}`} className="px-3 py-2 text-sm">
|
|
186
|
+
<a
|
|
187
|
+
href={s.url}
|
|
188
|
+
target="_blank"
|
|
189
|
+
rel="noopener noreferrer"
|
|
190
|
+
className="flex items-start gap-2 text-[var(--agent-foreground,#111827)] hover:underline"
|
|
191
|
+
>
|
|
192
|
+
<FaviconCircle src={s.favicon ?? faviconUrlFor(s.url)} size={16} />
|
|
193
|
+
<span className="min-w-0 flex-1">
|
|
194
|
+
<span className="block truncate font-medium">{s.title || extractDomain(s.url)}</span>
|
|
195
|
+
<span className="block truncate text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
196
|
+
{extractDomain(s.url)}
|
|
197
|
+
</span>
|
|
198
|
+
{s.snippet && (
|
|
199
|
+
<span className="mt-1 block text-xs text-[var(--agent-muted-foreground,#6b7280)] line-clamp-2">
|
|
200
|
+
{s.snippet}
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
</span>
|
|
204
|
+
<ExternalLinkIcon
|
|
205
|
+
size={12}
|
|
206
|
+
className="mt-1 flex-shrink-0 text-[var(--agent-muted-foreground,#6b7280)]"
|
|
207
|
+
/>
|
|
208
|
+
</a>
|
|
209
|
+
</li>
|
|
210
|
+
))}
|
|
211
|
+
</ul>
|
|
212
|
+
) : (
|
|
213
|
+
<div className="mt-1.5 flex flex-col gap-0.5">
|
|
214
|
+
{unique.map((s, idx) => (
|
|
215
|
+
<a
|
|
216
|
+
key={`${idx}-${s.url}`}
|
|
217
|
+
href={s.url}
|
|
218
|
+
target="_blank"
|
|
219
|
+
rel="noopener noreferrer"
|
|
220
|
+
className="group/item flex items-center gap-2.5 rounded-md px-2 py-1.5 transition-colors duration-150 hover:bg-[var(--agent-muted,#f3f4f6)]"
|
|
221
|
+
>
|
|
222
|
+
<FaviconCircle src={s.favicon ?? faviconUrlFor(s.url)} size={16} />
|
|
223
|
+
<div className="min-w-0 flex-1">
|
|
224
|
+
<div className="truncate text-xs text-[var(--agent-foreground,#111827)]">
|
|
225
|
+
{s.title || extractDomain(s.url)}
|
|
226
|
+
</div>
|
|
227
|
+
<div className="truncate text-[11px] text-[var(--agent-muted-foreground,#6b7280)]">
|
|
228
|
+
{extractDomain(s.url)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
<ExternalLinkIcon
|
|
232
|
+
size={12}
|
|
233
|
+
className="shrink-0 text-[var(--agent-muted-foreground,#6b7280)] opacity-0 transition-opacity group-hover/item:opacity-100"
|
|
234
|
+
/>
|
|
235
|
+
</a>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default SearchSourcesCard;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<SuggestedRepliesCard />`
|
|
3
|
+
*
|
|
4
|
+
* Quick-reply chips rendered below the composer. The card is pure presentation:
|
|
5
|
+
* the parent decides where to mount it and what `onSelect` does (typically:
|
|
6
|
+
* push the chosen text into the composer and submit).
|
|
7
|
+
*/
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import type { SuggestedRepliesPayload } from '@steerable/agent-protocol';
|
|
10
|
+
|
|
11
|
+
export interface SuggestedRepliesCardProps {
|
|
12
|
+
payload: SuggestedRepliesPayload;
|
|
13
|
+
onSelect: (text: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
buttonClassName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SuggestedRepliesCard: React.FC<SuggestedRepliesCardProps> = ({
|
|
19
|
+
payload,
|
|
20
|
+
onSelect,
|
|
21
|
+
className,
|
|
22
|
+
buttonClassName,
|
|
23
|
+
}) => {
|
|
24
|
+
const suggestions = payload.suggestions ?? [];
|
|
25
|
+
if (suggestions.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={['steerable-suggested-replies flex flex-wrap gap-2 py-2', className].filter(Boolean).join(' ')}>
|
|
29
|
+
{suggestions.map((text, idx) => (
|
|
30
|
+
<button
|
|
31
|
+
key={`${idx}-${text}`}
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() => onSelect(text)}
|
|
34
|
+
className={[
|
|
35
|
+
'rounded-full border px-3 py-1.5 text-sm transition-colors duration-150',
|
|
36
|
+
'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#f9fafb)]',
|
|
37
|
+
'text-[var(--agent-foreground,#111827)] hover:bg-[var(--agent-accent,#e5e7eb)]',
|
|
38
|
+
buttonClassName,
|
|
39
|
+
].filter(Boolean).join(' ')}
|
|
40
|
+
>
|
|
41
|
+
{text}
|
|
42
|
+
</button>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default SuggestedRepliesCard;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<SummaryMessageCard />`
|
|
3
|
+
*
|
|
4
|
+
* Compact summary of N previous messages (e.g. history compaction). Collapsed
|
|
5
|
+
* by default so it doesn't dominate the chat scrollback.
|
|
6
|
+
*
|
|
7
|
+
* Appearance variants:
|
|
8
|
+
* - `card` (default): bordered chrome.
|
|
9
|
+
* - `inline`: a small pill button + indented body block below. Matches the
|
|
10
|
+
* deeppath "above the input" aesthetic.
|
|
11
|
+
*
|
|
12
|
+
* Slots:
|
|
13
|
+
* - `renderMarkdown(body)` -- supply your own markdown renderer.
|
|
14
|
+
* - `renderLabel({ isPending, count })` -- replace the trigger label.
|
|
15
|
+
* - `renderToggle({ expanded, canToggle })` -- replace the toggle chevron + text.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
import type { SummaryMessagePayload } from '@steerable/agent-protocol';
|
|
19
|
+
import { ArchiveIcon, ChevronDownIcon, ChevronRightIcon, LoaderIcon } from './icons.js';
|
|
20
|
+
import { asReactNode, type RenderSlotResult } from './types.js';
|
|
21
|
+
|
|
22
|
+
export type SummaryMessageCardAppearance = 'card' | 'inline';
|
|
23
|
+
|
|
24
|
+
export interface SummaryMessageLabelArgs {
|
|
25
|
+
isPending: boolean;
|
|
26
|
+
count: number | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SummaryMessageToggleArgs {
|
|
30
|
+
expanded: boolean;
|
|
31
|
+
canToggle: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SummaryMessageCardProps {
|
|
35
|
+
payload: SummaryMessagePayload;
|
|
36
|
+
renderMarkdown?: (body: string) => RenderSlotResult;
|
|
37
|
+
renderLabel?: (args: SummaryMessageLabelArgs) => RenderSlotResult;
|
|
38
|
+
renderToggle?: (args: SummaryMessageToggleArgs) => RenderSlotResult;
|
|
39
|
+
appearance?: SummaryMessageCardAppearance;
|
|
40
|
+
className?: string;
|
|
41
|
+
defaultExpanded?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SummaryMessageCard: React.FC<SummaryMessageCardProps> = ({
|
|
45
|
+
payload,
|
|
46
|
+
renderMarkdown,
|
|
47
|
+
renderLabel,
|
|
48
|
+
renderToggle,
|
|
49
|
+
appearance = 'card',
|
|
50
|
+
className,
|
|
51
|
+
defaultExpanded = false,
|
|
52
|
+
}) => {
|
|
53
|
+
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
|
54
|
+
const isPending = payload.status === 'pending';
|
|
55
|
+
const count =
|
|
56
|
+
typeof payload.summarizedCount === 'number' && payload.summarizedCount > 0
|
|
57
|
+
? payload.summarizedCount
|
|
58
|
+
: null;
|
|
59
|
+
const hasBody = Boolean(payload.body && payload.body.trim());
|
|
60
|
+
const canToggle = !isPending && hasBody;
|
|
61
|
+
|
|
62
|
+
const defaultLabel = isPending
|
|
63
|
+
? '总结中…'
|
|
64
|
+
: `历史摘要${count ? `(${count} 条消息)` : ''}`;
|
|
65
|
+
const labelNode: React.ReactNode = renderLabel
|
|
66
|
+
? asReactNode(renderLabel({ isPending, count }))
|
|
67
|
+
: defaultLabel;
|
|
68
|
+
|
|
69
|
+
const defaultToggle = expanded
|
|
70
|
+
? <ChevronDownIcon size={12} />
|
|
71
|
+
: <ChevronRightIcon size={12} />;
|
|
72
|
+
const toggleNode: React.ReactNode = renderToggle
|
|
73
|
+
? asReactNode(renderToggle({ expanded, canToggle }))
|
|
74
|
+
: defaultToggle;
|
|
75
|
+
|
|
76
|
+
const body: React.ReactNode = expanded && canToggle ? (
|
|
77
|
+
renderMarkdown ? (
|
|
78
|
+
asReactNode(renderMarkdown(payload.body))
|
|
79
|
+
) : (
|
|
80
|
+
<pre className="whitespace-pre-wrap break-words font-sans">{payload.body}</pre>
|
|
81
|
+
)
|
|
82
|
+
) : null;
|
|
83
|
+
|
|
84
|
+
if (appearance === 'inline') {
|
|
85
|
+
return (
|
|
86
|
+
<div className={['steerable-summary-message', className].filter(Boolean).join(' ')}>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={() => canToggle && setExpanded((v) => !v)}
|
|
90
|
+
disabled={!canToggle}
|
|
91
|
+
className={[
|
|
92
|
+
'group inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[11px] leading-none text-[var(--agent-muted-foreground,#6b7280)] transition-colors',
|
|
93
|
+
canToggle
|
|
94
|
+
? 'cursor-pointer hover:bg-[var(--agent-muted,#f3f4f6)] hover:text-[var(--agent-foreground,#111827)]'
|
|
95
|
+
: 'cursor-default',
|
|
96
|
+
].join(' ')}
|
|
97
|
+
>
|
|
98
|
+
{isPending ? (
|
|
99
|
+
<LoaderIcon size={12} className="animate-spin" />
|
|
100
|
+
) : (
|
|
101
|
+
<ArchiveIcon size={12} />
|
|
102
|
+
)}
|
|
103
|
+
<span>{labelNode}</span>
|
|
104
|
+
{canToggle && (
|
|
105
|
+
<span className="inline-flex items-center gap-0.5 opacity-60 group-hover:opacity-100">
|
|
106
|
+
{toggleNode}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</button>
|
|
110
|
+
{body && (
|
|
111
|
+
<div className="ml-3 mt-1.5 rounded-md border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-muted,#f3f4f6)]/30 px-3 py-2 text-[12px] leading-relaxed text-[var(--agent-muted-foreground,#6b7280)]">
|
|
112
|
+
{body}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
className={[
|
|
122
|
+
'steerable-summary-message rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-muted,#f3f4f6)]/40 text-xs',
|
|
123
|
+
className,
|
|
124
|
+
].filter(Boolean).join(' ')}
|
|
125
|
+
>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={() => canToggle && setExpanded((v) => !v)}
|
|
129
|
+
disabled={!canToggle}
|
|
130
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-[var(--agent-muted-foreground,#6b7280)]"
|
|
131
|
+
>
|
|
132
|
+
{isPending ? <LoaderIcon size={12} /> : <ArchiveIcon size={12} />}
|
|
133
|
+
<span>{labelNode}</span>
|
|
134
|
+
{canToggle && <span className="ml-auto">{toggleNode}</span>}
|
|
135
|
+
</button>
|
|
136
|
+
{body && (
|
|
137
|
+
<div className="border-t border-[var(--agent-border,#e5e7eb)] px-3 py-2 text-[var(--agent-foreground,#111827)]">
|
|
138
|
+
{body}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default SummaryMessageCard;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<ThinkingProcessCard />`
|
|
3
|
+
*
|
|
4
|
+
* Collapsible chain-of-thought panel.
|
|
5
|
+
*
|
|
6
|
+
* Behaviour switches:
|
|
7
|
+
* - `isStreaming` paints the card with a soft pulsing border to convey
|
|
8
|
+
* "thoughts still arriving".
|
|
9
|
+
* - `autoExpandWhileStreaming` opens the panel automatically when streaming
|
|
10
|
+
* starts; `autoCollapseOnComplete` closes it once streaming finishes.
|
|
11
|
+
* - Controlled mode: pass `expanded` + `onExpandedChange` to delegate the
|
|
12
|
+
* open/close state (used by host apps that persist expansion in a
|
|
13
|
+
* context like deeppath's `MessageListContext`).
|
|
14
|
+
*
|
|
15
|
+
* Slots:
|
|
16
|
+
* - `renderMarkdown(body)`: replace the default `<pre>` body renderer.
|
|
17
|
+
* - `renderLabel({ isStreaming, isExpanded })`: replace the trigger label
|
|
18
|
+
* (deeppath uses "正在思考:...", "思考过程", "下一步推理"... variants).
|
|
19
|
+
* - `renderStreamingFooter`: extra content rendered below the body while
|
|
20
|
+
* streaming (deeppath shows a pulsing "思考中..." line).
|
|
21
|
+
* - `bodyClassName` overrides default compact body typography.
|
|
22
|
+
*/
|
|
23
|
+
import * as React from 'react';
|
|
24
|
+
import type { ThinkingProcessPayload } from '@steerable/agent-protocol';
|
|
25
|
+
import { ChevronDownIcon, ChevronRightIcon } from './icons.js';
|
|
26
|
+
import { asReactNode, type RenderSlotResult } from './types.js';
|
|
27
|
+
|
|
28
|
+
export interface ThinkingProcessLabelArgs {
|
|
29
|
+
isStreaming: boolean;
|
|
30
|
+
isExpanded: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ThinkingProcessCardProps {
|
|
34
|
+
payload: ThinkingProcessPayload;
|
|
35
|
+
renderMarkdown?: (body: string) => RenderSlotResult;
|
|
36
|
+
renderLabel?: (args: ThinkingProcessLabelArgs) => RenderSlotResult;
|
|
37
|
+
renderStreamingFooter?: () => RenderSlotResult;
|
|
38
|
+
className?: string;
|
|
39
|
+
bodyClassName?: string;
|
|
40
|
+
isStreaming?: boolean;
|
|
41
|
+
expanded?: boolean;
|
|
42
|
+
onExpandedChange?: (next: boolean) => void;
|
|
43
|
+
autoExpandWhileStreaming?: boolean;
|
|
44
|
+
autoCollapseOnComplete?: boolean;
|
|
45
|
+
durationMs?: number | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatDuration(ms?: number | null): string | null {
|
|
49
|
+
if (!ms || ms <= 0) return null;
|
|
50
|
+
return `${(ms / 1000).toFixed(1)}秒`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ThinkingProcessCard: React.FC<ThinkingProcessCardProps> = ({
|
|
54
|
+
payload,
|
|
55
|
+
renderMarkdown,
|
|
56
|
+
renderLabel,
|
|
57
|
+
renderStreamingFooter,
|
|
58
|
+
className,
|
|
59
|
+
bodyClassName,
|
|
60
|
+
isStreaming = false,
|
|
61
|
+
expanded: expandedProp,
|
|
62
|
+
onExpandedChange,
|
|
63
|
+
autoExpandWhileStreaming = false,
|
|
64
|
+
autoCollapseOnComplete = false,
|
|
65
|
+
durationMs = null,
|
|
66
|
+
}) => {
|
|
67
|
+
const isControlled = expandedProp !== undefined;
|
|
68
|
+
const [internalExpanded, setInternalExpanded] = React.useState(
|
|
69
|
+
Boolean(payload.defaultExpanded),
|
|
70
|
+
);
|
|
71
|
+
const expanded = isControlled ? Boolean(expandedProp) : internalExpanded;
|
|
72
|
+
const setExpanded = React.useCallback(
|
|
73
|
+
(next: boolean) => {
|
|
74
|
+
if (!isControlled) setInternalExpanded(next);
|
|
75
|
+
onExpandedChange?.(next);
|
|
76
|
+
},
|
|
77
|
+
[isControlled, onExpandedChange],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const prevStreamingRef = React.useRef(isStreaming);
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
const wasStreaming = prevStreamingRef.current;
|
|
83
|
+
if (autoExpandWhileStreaming && isStreaming && !expanded && payload.body?.trim()) {
|
|
84
|
+
setExpanded(true);
|
|
85
|
+
}
|
|
86
|
+
if (autoCollapseOnComplete && wasStreaming && !isStreaming && expanded) {
|
|
87
|
+
setExpanded(false);
|
|
88
|
+
}
|
|
89
|
+
prevStreamingRef.current = isStreaming;
|
|
90
|
+
}, [
|
|
91
|
+
isStreaming,
|
|
92
|
+
expanded,
|
|
93
|
+
autoExpandWhileStreaming,
|
|
94
|
+
autoCollapseOnComplete,
|
|
95
|
+
payload.body,
|
|
96
|
+
setExpanded,
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const showBody = expanded || (isStreaming && Boolean(payload.body));
|
|
100
|
+
const duration = formatDuration(durationMs);
|
|
101
|
+
|
|
102
|
+
const defaultLabel = isStreaming ? '正在思考...' : '思考过程';
|
|
103
|
+
const labelNode = renderLabel
|
|
104
|
+
? asReactNode(renderLabel({ isStreaming, isExpanded: expanded }))
|
|
105
|
+
: defaultLabel;
|
|
106
|
+
|
|
107
|
+
const defaultBodyClasses =
|
|
108
|
+
'whitespace-pre-wrap break-words font-sans text-xs leading-relaxed text-[var(--agent-muted-foreground,#6b7280)]';
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className={['steerable-thinking-process', className].filter(Boolean).join(' ')}>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => setExpanded(!expanded)}
|
|
115
|
+
className={[
|
|
116
|
+
'flex items-center gap-1 text-xs transition-colors duration-200',
|
|
117
|
+
isStreaming
|
|
118
|
+
? 'font-medium text-[var(--agent-foreground,#111827)]'
|
|
119
|
+
: 'text-[var(--agent-muted-foreground,#6b7280)] hover:text-[var(--agent-foreground,#111827)]',
|
|
120
|
+
].join(' ')}
|
|
121
|
+
>
|
|
122
|
+
{expanded ? <ChevronDownIcon size={14} /> : <ChevronRightIcon size={14} />}
|
|
123
|
+
<span>{labelNode}</span>
|
|
124
|
+
{duration && !isStreaming && (
|
|
125
|
+
<span className="ml-1 text-[var(--agent-muted-foreground,#6b7280)]">({duration})</span>
|
|
126
|
+
)}
|
|
127
|
+
{isStreaming && !expanded && (
|
|
128
|
+
<span
|
|
129
|
+
aria-hidden
|
|
130
|
+
className="ml-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--agent-muted-foreground,#6b7280)]"
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</button>
|
|
134
|
+
{showBody && (
|
|
135
|
+
<div
|
|
136
|
+
className={[
|
|
137
|
+
'mt-1 overflow-hidden rounded-lg border p-3',
|
|
138
|
+
isStreaming
|
|
139
|
+
? 'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-muted,#f3f4f6)]/30 shadow-sm animate-pulse'
|
|
140
|
+
: 'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-muted,#f3f4f6)]/30',
|
|
141
|
+
].join(' ')}
|
|
142
|
+
>
|
|
143
|
+
<div className={bodyClassName ?? defaultBodyClasses}>
|
|
144
|
+
{renderMarkdown ? asReactNode(renderMarkdown(payload.body)) : payload.body}
|
|
145
|
+
</div>
|
|
146
|
+
{isStreaming && renderStreamingFooter && (
|
|
147
|
+
<div className="mt-2">{asReactNode(renderStreamingFooter())}</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default ThinkingProcessCard;
|