@nextclaw/ui 0.6.15 → 0.7.0
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/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/dist/assets/ChannelsList-DF2U-LY1.js +1 -0
- package/dist/assets/ChatPage-BX39y0U5.js +36 -0
- package/dist/assets/DocBrowser-B9ws5JL7.js +1 -0
- package/dist/assets/{LogoBadge-Cer0jX6t.js → LogoBadge-DvGAzkZ3.js} +1 -1
- package/dist/assets/MarketplacePage-DG5mHWJ8.js +49 -0
- package/dist/assets/ModelConfig-BL_HsOsm.js +1 -0
- package/dist/assets/ProvidersList-CH5z00YT.js +1 -0
- package/dist/assets/RuntimeConfig-BplBgkwo.js +1 -0
- package/dist/assets/SearchConfig-BhaI0fUf.js +1 -0
- package/dist/assets/{SecretsConfig-BnGVZiv4.js → SecretsConfig-CFoimOh9.js} +2 -2
- package/dist/assets/SessionsConfig-BHTAYn9T.js +2 -0
- package/dist/assets/index-BLeJkJ0o.css +1 -0
- package/dist/assets/index-DK4TS5ev.js +8 -0
- package/dist/assets/index-X5J6Mm--.js +1 -0
- package/dist/assets/{index-CkqvHQAt.js → index-uMsNsQX6.js} +1 -1
- package/dist/assets/{label-DkL14Jvl.js → label-D8ly4a2P.js} +1 -1
- package/dist/assets/page-layout-BSYfvwbp.js +1 -0
- package/dist/assets/security-config-DlKEYHNN.js +1 -0
- package/dist/assets/{session-run-status-tZ4ISNj-.js → session-run-status-TkIuGbVw.js} +1 -1
- package/dist/assets/skeleton-CWbsNx2h.js +1 -0
- package/dist/assets/{switch-CgbPbIX3.js → switch-Ce_g9lpN.js} +1 -1
- package/dist/assets/tabs-custom-Cf5azvT5.js +1 -0
- package/dist/assets/useConfirmDialog-A8Ek8Wu7.js +5 -0
- package/dist/assets/vendor-B7ozqnFC.js +412 -0
- package/dist/index.html +3 -3
- package/package.json +9 -5
- package/src/App.tsx +49 -27
- package/src/api/client.ts +1 -0
- package/src/api/config.ts +60 -0
- package/src/api/types.ts +26 -0
- package/src/components/auth/login-page.tsx +69 -0
- package/src/components/chat/ChatConversationPanel.tsx +12 -54
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +137 -0
- package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +67 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/managers/chat-thread.manager.ts +3 -1
- package/src/components/chat/nextclaw/index.ts +23 -0
- package/src/components/config/runtime-security-card.tsx +276 -0
- package/src/components/config/security-config.tsx +12 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
- package/src/components/marketplace/MarketplacePage.tsx +77 -28
- package/src/hooks/use-auth.ts +111 -0
- package/src/hooks/useMarketplace.ts +9 -0
- package/src/lib/i18n.ts +72 -0
- package/src/test/setup.ts +16 -0
- package/tsconfig.json +3 -2
- package/vite.config.ts +2 -1
- package/vitest.config.ts +16 -0
- package/dist/assets/ChannelsList-DzeVn-JC.js +0 -1
- package/dist/assets/ChatPage-BiFhIm1-.js +0 -36
- package/dist/assets/DocBrowser-By3lF9yN.js +0 -1
- package/dist/assets/MarketplacePage-EZxALdIz.js +0 -49
- package/dist/assets/ModelConfig-AchYxLft.js +0 -1
- package/dist/assets/ProvidersList-BsD-4kKX.js +0 -1
- package/dist/assets/RuntimeConfig-sKOERbFD.js +0 -1
- package/dist/assets/SearchConfig-DAfvDwX6.js +0 -1
- package/dist/assets/SessionsConfig-CzvrKDRs.js +0 -2
- package/dist/assets/card-BAM7vbMg.js +0 -1
- package/dist/assets/index-D9rRqOi8.css +0 -1
- package/dist/assets/index-DJZ5y7t1.js +0 -8
- package/dist/assets/input-BoelTiYL.js +0 -1
- package/dist/assets/page-layout-CERNdqzB.js +0 -1
- package/dist/assets/popover-uwYz3Chm.js +0 -1
- package/dist/assets/tabs-custom-pDyl95el.js +0 -1
- package/dist/assets/useConfirmDialog-DyP6Ac75.js +0 -5
- package/dist/assets/vendor-BKtTvQYU.js +0 -407
- package/src/components/chat/ChatThread.tsx +0 -402
- package/src/components/chat/SkillsPicker.tsx +0 -137
- package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
- package/src/components/chat/chat-input/useChatInputBarController.ts +0 -322
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChatInlineHint,
|
|
3
|
+
ChatSelectedItem,
|
|
4
|
+
ChatSkillPickerOption,
|
|
5
|
+
ChatSkillPickerProps,
|
|
6
|
+
ChatSlashItem,
|
|
7
|
+
ChatToolbarSelect
|
|
8
|
+
} from '@nextclaw/agent-chat-ui';
|
|
9
|
+
|
|
10
|
+
export type ChatThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive' | 'xhigh';
|
|
11
|
+
|
|
12
|
+
export type ChatSkillRecord = {
|
|
13
|
+
key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
descriptionZh?: string;
|
|
17
|
+
badgeLabel?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ChatModelRecord = {
|
|
21
|
+
value: string;
|
|
22
|
+
modelLabel: string;
|
|
23
|
+
providerLabel: string;
|
|
24
|
+
thinkingCapability?: {
|
|
25
|
+
supported: ChatThinkingLevel[];
|
|
26
|
+
default?: ChatThinkingLevel | null;
|
|
27
|
+
} | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const SLASH_ITEM_MATCH_SCORE = {
|
|
31
|
+
exactSpec: 1200,
|
|
32
|
+
exactLabel: 1150,
|
|
33
|
+
prefixSpec: 1000,
|
|
34
|
+
prefixLabel: 950,
|
|
35
|
+
prefixToken: 900,
|
|
36
|
+
containsSpec: 800,
|
|
37
|
+
containsLabel: 760,
|
|
38
|
+
containsDescription: 500,
|
|
39
|
+
subsequence: 300,
|
|
40
|
+
fallback: 1
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
export type ChatInputBarAdapterTexts = {
|
|
44
|
+
slashSkillSubtitle: string;
|
|
45
|
+
slashSkillSpecLabel: string;
|
|
46
|
+
noSkillDescription: string;
|
|
47
|
+
modelSelectPlaceholder: string;
|
|
48
|
+
modelNoOptionsLabel: string;
|
|
49
|
+
sessionTypePlaceholder: string;
|
|
50
|
+
thinkingLabels: Record<ChatThinkingLevel, string>;
|
|
51
|
+
noModelOptionsLabel: string;
|
|
52
|
+
configureProviderLabel: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function resolveSlashQuery(draft: string): string | null {
|
|
56
|
+
const match = /^\/([^\s]*)$/.exec(draft);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return (match[1] ?? '').trim().toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeSearchText(value: string | null | undefined): string {
|
|
64
|
+
return (value ?? '').trim().toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isSubsequenceMatch(query: string, target: string): boolean {
|
|
68
|
+
if (!query || !target) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
let pointer = 0;
|
|
72
|
+
for (const char of target) {
|
|
73
|
+
if (char === query[pointer]) {
|
|
74
|
+
pointer += 1;
|
|
75
|
+
if (pointer >= query.length) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function scoreSkillRecord(record: ChatSkillRecord, query: string): number {
|
|
84
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
85
|
+
if (!normalizedQuery) {
|
|
86
|
+
return SLASH_ITEM_MATCH_SCORE.fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const spec = normalizeSearchText(record.key);
|
|
90
|
+
const label = normalizeSearchText(record.label || record.key);
|
|
91
|
+
const description = normalizeSearchText(`${record.descriptionZh ?? ''} ${record.description ?? ''}`);
|
|
92
|
+
const labelTokens = label.split(/[\s/_-]+/).filter(Boolean);
|
|
93
|
+
|
|
94
|
+
if (spec === normalizedQuery) {
|
|
95
|
+
return SLASH_ITEM_MATCH_SCORE.exactSpec;
|
|
96
|
+
}
|
|
97
|
+
if (label === normalizedQuery) {
|
|
98
|
+
return SLASH_ITEM_MATCH_SCORE.exactLabel;
|
|
99
|
+
}
|
|
100
|
+
if (spec.startsWith(normalizedQuery)) {
|
|
101
|
+
return SLASH_ITEM_MATCH_SCORE.prefixSpec;
|
|
102
|
+
}
|
|
103
|
+
if (label.startsWith(normalizedQuery)) {
|
|
104
|
+
return SLASH_ITEM_MATCH_SCORE.prefixLabel;
|
|
105
|
+
}
|
|
106
|
+
if (labelTokens.some((token) => token.startsWith(normalizedQuery))) {
|
|
107
|
+
return SLASH_ITEM_MATCH_SCORE.prefixToken;
|
|
108
|
+
}
|
|
109
|
+
if (spec.includes(normalizedQuery)) {
|
|
110
|
+
return SLASH_ITEM_MATCH_SCORE.containsSpec;
|
|
111
|
+
}
|
|
112
|
+
if (label.includes(normalizedQuery)) {
|
|
113
|
+
return SLASH_ITEM_MATCH_SCORE.containsLabel;
|
|
114
|
+
}
|
|
115
|
+
if (description.includes(normalizedQuery)) {
|
|
116
|
+
return SLASH_ITEM_MATCH_SCORE.containsDescription;
|
|
117
|
+
}
|
|
118
|
+
if (isSubsequenceMatch(normalizedQuery, label) || isSubsequenceMatch(normalizedQuery, spec)) {
|
|
119
|
+
return SLASH_ITEM_MATCH_SCORE.subsequence;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildChatSlashItems(
|
|
125
|
+
skillRecords: ChatSkillRecord[],
|
|
126
|
+
normalizedSlashQuery: string,
|
|
127
|
+
texts: Pick<ChatInputBarAdapterTexts, 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'noSkillDescription'>
|
|
128
|
+
): ChatSlashItem[] {
|
|
129
|
+
const skillSortCollator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
|
|
130
|
+
|
|
131
|
+
return skillRecords
|
|
132
|
+
.map((record, order) => ({
|
|
133
|
+
record,
|
|
134
|
+
order,
|
|
135
|
+
score: scoreSkillRecord(record, normalizedSlashQuery)
|
|
136
|
+
}))
|
|
137
|
+
.filter((entry) => entry.score > 0)
|
|
138
|
+
.sort((left, right) => {
|
|
139
|
+
if (right.score !== left.score) {
|
|
140
|
+
return right.score - left.score;
|
|
141
|
+
}
|
|
142
|
+
const leftLabel = (left.record.label || left.record.key).trim();
|
|
143
|
+
const rightLabel = (right.record.label || right.record.key).trim();
|
|
144
|
+
const labelCompare = skillSortCollator.compare(leftLabel, rightLabel);
|
|
145
|
+
if (labelCompare !== 0) {
|
|
146
|
+
return labelCompare;
|
|
147
|
+
}
|
|
148
|
+
return left.order - right.order;
|
|
149
|
+
})
|
|
150
|
+
.map(({ record }) => ({
|
|
151
|
+
key: `skill:${record.key}`,
|
|
152
|
+
title: record.label || record.key,
|
|
153
|
+
subtitle: texts.slashSkillSubtitle,
|
|
154
|
+
description: (record.descriptionZh ?? record.description ?? '').trim() || texts.noSkillDescription,
|
|
155
|
+
detailLines: [`${texts.slashSkillSpecLabel}: ${record.key}`],
|
|
156
|
+
value: record.key
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildSelectedSkillItems(
|
|
161
|
+
selectedSkills: string[],
|
|
162
|
+
skillRecords: ChatSkillRecord[]
|
|
163
|
+
): ChatSelectedItem[] {
|
|
164
|
+
return selectedSkills.map((spec) => {
|
|
165
|
+
const matched = skillRecords.find((record) => record.key === spec);
|
|
166
|
+
return {
|
|
167
|
+
key: spec,
|
|
168
|
+
label: matched?.label || spec
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSkillPickerOption[] {
|
|
174
|
+
return skillRecords.map((record) => ({
|
|
175
|
+
key: record.key,
|
|
176
|
+
label: record.label,
|
|
177
|
+
description: record.descriptionZh || record.description || '',
|
|
178
|
+
badgeLabel: record.badgeLabel
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function buildSkillPickerModel(params: {
|
|
183
|
+
skillRecords: ChatSkillRecord[];
|
|
184
|
+
selectedSkills: string[];
|
|
185
|
+
isLoading: boolean;
|
|
186
|
+
onSelectedKeysChange: (next: string[]) => void;
|
|
187
|
+
texts: {
|
|
188
|
+
title: string;
|
|
189
|
+
searchPlaceholder: string;
|
|
190
|
+
emptyLabel: string;
|
|
191
|
+
loadingLabel: string;
|
|
192
|
+
manageLabel: string;
|
|
193
|
+
};
|
|
194
|
+
}): ChatSkillPickerProps {
|
|
195
|
+
return {
|
|
196
|
+
title: params.texts.title,
|
|
197
|
+
searchPlaceholder: params.texts.searchPlaceholder,
|
|
198
|
+
emptyLabel: params.texts.emptyLabel,
|
|
199
|
+
loadingLabel: params.texts.loadingLabel,
|
|
200
|
+
isLoading: params.isLoading,
|
|
201
|
+
manageLabel: params.texts.manageLabel,
|
|
202
|
+
manageHref: '/marketplace/skills',
|
|
203
|
+
options: buildSkillPickerOptions(params.skillRecords),
|
|
204
|
+
selectedKeys: params.selectedSkills,
|
|
205
|
+
onSelectedKeysChange: params.onSelectedKeysChange
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildModelStateHint(params: {
|
|
210
|
+
isModelOptionsLoading: boolean;
|
|
211
|
+
isModelOptionsEmpty: boolean;
|
|
212
|
+
onGoToProviders: () => void;
|
|
213
|
+
texts: Pick<ChatInputBarAdapterTexts, 'noModelOptionsLabel' | 'configureProviderLabel'>;
|
|
214
|
+
}): ChatInlineHint | null {
|
|
215
|
+
if (!params.isModelOptionsLoading && !params.isModelOptionsEmpty) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (params.isModelOptionsLoading) {
|
|
219
|
+
return {
|
|
220
|
+
tone: 'neutral',
|
|
221
|
+
loading: true
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
tone: 'warning',
|
|
226
|
+
text: params.texts.noModelOptionsLabel,
|
|
227
|
+
actionLabel: params.texts.configureProviderLabel,
|
|
228
|
+
onAction: params.onGoToProviders
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildModelToolbarSelect(params: {
|
|
233
|
+
modelOptions: ChatModelRecord[];
|
|
234
|
+
selectedModel: string;
|
|
235
|
+
isModelOptionsLoading: boolean;
|
|
236
|
+
hasModelOptions: boolean;
|
|
237
|
+
onValueChange: (value: string) => void;
|
|
238
|
+
texts: Pick<ChatInputBarAdapterTexts, 'modelSelectPlaceholder' | 'modelNoOptionsLabel'>;
|
|
239
|
+
}): ChatToolbarSelect {
|
|
240
|
+
const selectedModelOption = params.modelOptions.find((option) => option.value === params.selectedModel);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
key: 'model',
|
|
244
|
+
value: params.hasModelOptions ? params.selectedModel : undefined,
|
|
245
|
+
placeholder: params.texts.modelSelectPlaceholder,
|
|
246
|
+
selectedLabel: selectedModelOption
|
|
247
|
+
? `${selectedModelOption.providerLabel}/${selectedModelOption.modelLabel}`
|
|
248
|
+
: undefined,
|
|
249
|
+
icon: 'sparkles',
|
|
250
|
+
options: params.modelOptions.map((option) => ({
|
|
251
|
+
value: option.value,
|
|
252
|
+
label: option.modelLabel,
|
|
253
|
+
description: option.providerLabel
|
|
254
|
+
})),
|
|
255
|
+
disabled: !params.hasModelOptions,
|
|
256
|
+
loading: params.isModelOptionsLoading,
|
|
257
|
+
emptyLabel: params.texts.modelNoOptionsLabel,
|
|
258
|
+
onValueChange: params.onValueChange
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function buildSessionTypeToolbarSelect(params: {
|
|
263
|
+
selectedSessionType?: string;
|
|
264
|
+
selectedSessionTypeOption: { value: string; label: string } | null;
|
|
265
|
+
sessionTypeOptions: Array<{ value: string; label: string }>;
|
|
266
|
+
onValueChange: (value: string) => void;
|
|
267
|
+
canEditSessionType: boolean;
|
|
268
|
+
shouldShow: boolean;
|
|
269
|
+
texts: Pick<ChatInputBarAdapterTexts, 'sessionTypePlaceholder'>;
|
|
270
|
+
}): ChatToolbarSelect | null {
|
|
271
|
+
if (!params.shouldShow) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
key: 'session-type',
|
|
277
|
+
value: params.selectedSessionType,
|
|
278
|
+
placeholder: params.texts.sessionTypePlaceholder,
|
|
279
|
+
selectedLabel: params.selectedSessionTypeOption?.label,
|
|
280
|
+
options: params.sessionTypeOptions.map((option) => ({
|
|
281
|
+
value: option.value,
|
|
282
|
+
label: option.label
|
|
283
|
+
})),
|
|
284
|
+
disabled: !params.canEditSessionType,
|
|
285
|
+
onValueChange: params.onValueChange
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeThinkingLevels(levels: ChatThinkingLevel[]): ChatThinkingLevel[] {
|
|
290
|
+
const deduped: ChatThinkingLevel[] = [];
|
|
291
|
+
for (const level of ['off', ...levels] as ChatThinkingLevel[]) {
|
|
292
|
+
if (!deduped.includes(level)) {
|
|
293
|
+
deduped.push(level);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return deduped;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function buildThinkingToolbarSelect(params: {
|
|
300
|
+
supportedLevels: ChatThinkingLevel[];
|
|
301
|
+
selectedThinkingLevel: ChatThinkingLevel | null;
|
|
302
|
+
defaultThinkingLevel?: ChatThinkingLevel | null;
|
|
303
|
+
onValueChange: (value: ChatThinkingLevel) => void;
|
|
304
|
+
texts: Pick<ChatInputBarAdapterTexts, 'thinkingLabels'>;
|
|
305
|
+
}): ChatToolbarSelect | null {
|
|
306
|
+
if (params.supportedLevels.length === 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const options = normalizeThinkingLevels(params.supportedLevels);
|
|
311
|
+
const fallback = options.includes('off') ? 'off' : options[0];
|
|
312
|
+
const resolvedValue =
|
|
313
|
+
(params.selectedThinkingLevel && options.includes(params.selectedThinkingLevel) && params.selectedThinkingLevel) ||
|
|
314
|
+
(params.defaultThinkingLevel && options.includes(params.defaultThinkingLevel) && params.defaultThinkingLevel) ||
|
|
315
|
+
fallback;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
key: 'thinking',
|
|
319
|
+
value: resolvedValue,
|
|
320
|
+
placeholder: params.texts.thinkingLabels[resolvedValue],
|
|
321
|
+
selectedLabel: params.texts.thinkingLabels[resolvedValue],
|
|
322
|
+
icon: 'brain',
|
|
323
|
+
options: options.map((level) => ({
|
|
324
|
+
value: level,
|
|
325
|
+
label: params.texts.thinkingLabels[level]
|
|
326
|
+
})),
|
|
327
|
+
onValueChange: (value) => params.onValueChange(value as ChatThinkingLevel)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ToolInvocationStatus, type UiMessage } from '@nextclaw/agent-chat';
|
|
2
|
+
import { adaptChatMessages } from '@/components/chat/adapters/chat-message.adapter';
|
|
3
|
+
import type { ChatMessageSource } from '@/components/chat/adapters/chat-message.adapter';
|
|
4
|
+
|
|
5
|
+
function toSource(uiMessages: UiMessage[]): ChatMessageSource[] {
|
|
6
|
+
return uiMessages as unknown as ChatMessageSource[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('adaptChatMessages', () => {
|
|
10
|
+
it('maps markdown, reasoning, and tool parts into UI view models', () => {
|
|
11
|
+
const messages: UiMessage[] = [
|
|
12
|
+
{
|
|
13
|
+
id: 'assistant-1',
|
|
14
|
+
role: 'assistant',
|
|
15
|
+
meta: {
|
|
16
|
+
status: 'final',
|
|
17
|
+
timestamp: '2026-03-17T10:00:00.000Z'
|
|
18
|
+
},
|
|
19
|
+
parts: [
|
|
20
|
+
{ type: 'text', text: 'hello world' },
|
|
21
|
+
{
|
|
22
|
+
type: 'reasoning',
|
|
23
|
+
reasoning: 'internal reasoning',
|
|
24
|
+
details: []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'tool-invocation',
|
|
28
|
+
toolInvocation: {
|
|
29
|
+
status: ToolInvocationStatus.RESULT,
|
|
30
|
+
toolCallId: 'call-1',
|
|
31
|
+
toolName: 'web_search',
|
|
32
|
+
args: '{"q":"hello"}',
|
|
33
|
+
result: { ok: true }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const adapted = adaptChatMessages({
|
|
41
|
+
uiMessages: toSource(messages),
|
|
42
|
+
formatTimestamp: (value) => `formatted:${value}`,
|
|
43
|
+
texts: {
|
|
44
|
+
roleLabels: {
|
|
45
|
+
user: 'You',
|
|
46
|
+
assistant: 'Assistant',
|
|
47
|
+
tool: 'Tool',
|
|
48
|
+
system: 'System',
|
|
49
|
+
fallback: 'Message'
|
|
50
|
+
},
|
|
51
|
+
reasoningLabel: 'Reasoning',
|
|
52
|
+
toolCallLabel: 'Tool Call',
|
|
53
|
+
toolResultLabel: 'Tool Result',
|
|
54
|
+
toolNoOutputLabel: 'No output',
|
|
55
|
+
toolOutputLabel: 'View Output',
|
|
56
|
+
unknownPartLabel: 'Unknown Part'
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(adapted).toHaveLength(1);
|
|
61
|
+
expect(adapted[0]?.roleLabel).toBe('Assistant');
|
|
62
|
+
expect(adapted[0]?.timestampLabel).toBe('formatted:2026-03-17T10:00:00.000Z');
|
|
63
|
+
expect(adapted[0]?.parts.map((part) => part.type)).toEqual(['markdown', 'reasoning', 'tool-card']);
|
|
64
|
+
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
65
|
+
type: 'tool-card',
|
|
66
|
+
card: {
|
|
67
|
+
titleLabel: 'Tool Result',
|
|
68
|
+
outputLabel: 'View Output'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('maps non-standard roles back to the generic message role', () => {
|
|
74
|
+
const adapted = adaptChatMessages({
|
|
75
|
+
uiMessages: [
|
|
76
|
+
{
|
|
77
|
+
id: 'data-1',
|
|
78
|
+
role: 'data',
|
|
79
|
+
parts: [{ type: 'text', text: 'payload' }]
|
|
80
|
+
}
|
|
81
|
+
] as unknown as ChatMessageSource[],
|
|
82
|
+
formatTimestamp: () => 'formatted',
|
|
83
|
+
texts: {
|
|
84
|
+
roleLabels: {
|
|
85
|
+
user: 'You',
|
|
86
|
+
assistant: 'Assistant',
|
|
87
|
+
tool: 'Tool',
|
|
88
|
+
system: 'System',
|
|
89
|
+
fallback: 'Message'
|
|
90
|
+
},
|
|
91
|
+
reasoningLabel: 'Reasoning',
|
|
92
|
+
toolCallLabel: 'Tool Call',
|
|
93
|
+
toolResultLabel: 'Tool Result',
|
|
94
|
+
toolNoOutputLabel: 'No output',
|
|
95
|
+
toolOutputLabel: 'View Output',
|
|
96
|
+
unknownPartLabel: 'Unknown Part'
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(adapted[0]?.role).toBe('message');
|
|
101
|
+
expect(adapted[0]?.roleLabel).toBe('Message');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('maps unknown parts into a visible fallback part', () => {
|
|
105
|
+
const adapted = adaptChatMessages({
|
|
106
|
+
uiMessages: [
|
|
107
|
+
{
|
|
108
|
+
id: 'x-1',
|
|
109
|
+
role: 'assistant',
|
|
110
|
+
parts: [{ type: 'step-start', value: 'x' }]
|
|
111
|
+
}
|
|
112
|
+
] as unknown as ChatMessageSource[],
|
|
113
|
+
formatTimestamp: () => 'formatted',
|
|
114
|
+
texts: {
|
|
115
|
+
roleLabels: {
|
|
116
|
+
user: 'You',
|
|
117
|
+
assistant: 'Assistant',
|
|
118
|
+
tool: 'Tool',
|
|
119
|
+
system: 'System',
|
|
120
|
+
fallback: 'Message'
|
|
121
|
+
},
|
|
122
|
+
reasoningLabel: 'Reasoning',
|
|
123
|
+
toolCallLabel: 'Tool Call',
|
|
124
|
+
toolResultLabel: 'Tool Result',
|
|
125
|
+
toolNoOutputLabel: 'No output',
|
|
126
|
+
toolOutputLabel: 'View Output',
|
|
127
|
+
unknownPartLabel: 'Unknown Part'
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
132
|
+
type: 'unknown',
|
|
133
|
+
rawType: 'step-start',
|
|
134
|
+
label: 'Unknown Part'
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stringifyUnknown,
|
|
3
|
+
summarizeToolArgs,
|
|
4
|
+
type ToolCard
|
|
5
|
+
} from '@/lib/chat-message';
|
|
6
|
+
import type {
|
|
7
|
+
ChatMessageRole,
|
|
8
|
+
ChatMessageViewModel,
|
|
9
|
+
ChatToolPartViewModel
|
|
10
|
+
} from '@nextclaw/agent-chat-ui';
|
|
11
|
+
|
|
12
|
+
export type ChatMessagePartSource =
|
|
13
|
+
| {
|
|
14
|
+
type: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
type: 'reasoning';
|
|
19
|
+
reasoning: string;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
type: 'tool-invocation';
|
|
23
|
+
toolInvocation: {
|
|
24
|
+
status?: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
args?: unknown;
|
|
27
|
+
parsedArgs?: unknown;
|
|
28
|
+
result?: unknown;
|
|
29
|
+
error?: string;
|
|
30
|
+
toolCallId?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ChatMessageSource = {
|
|
39
|
+
id: string;
|
|
40
|
+
role: string;
|
|
41
|
+
meta?: {
|
|
42
|
+
timestamp?: string;
|
|
43
|
+
status?: string;
|
|
44
|
+
};
|
|
45
|
+
parts: ChatMessagePartSource[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ChatMessageAdapterTexts = {
|
|
49
|
+
roleLabels: {
|
|
50
|
+
user: string;
|
|
51
|
+
assistant: string;
|
|
52
|
+
tool: string;
|
|
53
|
+
system: string;
|
|
54
|
+
fallback: string;
|
|
55
|
+
};
|
|
56
|
+
reasoningLabel: string;
|
|
57
|
+
toolCallLabel: string;
|
|
58
|
+
toolResultLabel: string;
|
|
59
|
+
toolNoOutputLabel: string;
|
|
60
|
+
toolOutputLabel: string;
|
|
61
|
+
unknownPartLabel: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return typeof value === 'object' && value !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isTextPart(part: ChatMessagePartSource): part is Extract<ChatMessagePartSource, { type: 'text' }> {
|
|
69
|
+
return part.type === 'text' && typeof part.text === 'string';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isReasoningPart(
|
|
73
|
+
part: ChatMessagePartSource
|
|
74
|
+
): part is Extract<ChatMessagePartSource, { type: 'reasoning' }> {
|
|
75
|
+
return part.type === 'reasoning' && typeof part.reasoning === 'string';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isToolInvocationPart(
|
|
79
|
+
part: ChatMessagePartSource
|
|
80
|
+
): part is Extract<ChatMessagePartSource, { type: 'tool-invocation' }> {
|
|
81
|
+
if (part.type !== 'tool-invocation') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(part.toolInvocation)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return typeof part.toolInvocation.toolName === 'string';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveMessageTimestamp(message: ChatMessageSource): string {
|
|
91
|
+
const candidate = message.meta?.timestamp;
|
|
92
|
+
if (candidate && Number.isFinite(Date.parse(candidate))) {
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
return new Date().toISOString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveRoleLabel(role: string, texts: ChatMessageAdapterTexts['roleLabels']): string {
|
|
99
|
+
if (role === 'user') {
|
|
100
|
+
return texts.user;
|
|
101
|
+
}
|
|
102
|
+
if (role === 'assistant') {
|
|
103
|
+
return texts.assistant;
|
|
104
|
+
}
|
|
105
|
+
if (role === 'tool') {
|
|
106
|
+
return texts.tool;
|
|
107
|
+
}
|
|
108
|
+
if (role === 'system') {
|
|
109
|
+
return texts.system;
|
|
110
|
+
}
|
|
111
|
+
return texts.fallback;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveUiRole(role: string): ChatMessageRole {
|
|
115
|
+
if (role === 'user' || role === 'assistant' || role === 'tool' || role === 'system') {
|
|
116
|
+
return role;
|
|
117
|
+
}
|
|
118
|
+
return 'message';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildToolCard(toolCard: ToolCard, texts: ChatMessageAdapterTexts): ChatToolPartViewModel {
|
|
122
|
+
return {
|
|
123
|
+
kind: toolCard.kind,
|
|
124
|
+
toolName: toolCard.name,
|
|
125
|
+
summary: toolCard.detail,
|
|
126
|
+
output: toolCard.text,
|
|
127
|
+
hasResult: Boolean(toolCard.hasResult),
|
|
128
|
+
titleLabel: toolCard.kind === 'call' ? texts.toolCallLabel : texts.toolResultLabel,
|
|
129
|
+
outputLabel: texts.toolOutputLabel,
|
|
130
|
+
emptyLabel: texts.toolNoOutputLabel
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function adaptChatMessages(params: {
|
|
135
|
+
uiMessages: ChatMessageSource[];
|
|
136
|
+
texts: ChatMessageAdapterTexts;
|
|
137
|
+
formatTimestamp: (value: string) => string;
|
|
138
|
+
}): ChatMessageViewModel[] {
|
|
139
|
+
return params.uiMessages.map((message) => ({
|
|
140
|
+
id: message.id,
|
|
141
|
+
role: resolveUiRole(message.role),
|
|
142
|
+
roleLabel: resolveRoleLabel(message.role, params.texts.roleLabels),
|
|
143
|
+
timestampLabel: params.formatTimestamp(resolveMessageTimestamp(message)),
|
|
144
|
+
status: message.meta?.status,
|
|
145
|
+
parts: message.parts
|
|
146
|
+
.map((part) => {
|
|
147
|
+
if (isTextPart(part)) {
|
|
148
|
+
const text = part.text.trim();
|
|
149
|
+
if (!text) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
type: 'markdown' as const,
|
|
154
|
+
text
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (isReasoningPart(part)) {
|
|
158
|
+
const text = part.reasoning.trim();
|
|
159
|
+
if (!text) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
type: 'reasoning' as const,
|
|
164
|
+
text,
|
|
165
|
+
label: params.texts.reasoningLabel
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (isToolInvocationPart(part)) {
|
|
169
|
+
const invocation = part.toolInvocation;
|
|
170
|
+
const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
|
|
171
|
+
const rawResult = typeof invocation.error === 'string' && invocation.error.trim()
|
|
172
|
+
? invocation.error.trim()
|
|
173
|
+
: invocation.result != null
|
|
174
|
+
? stringifyUnknown(invocation.result).trim()
|
|
175
|
+
: '';
|
|
176
|
+
const hasResult =
|
|
177
|
+
invocation.status === 'result' || invocation.status === 'error' || invocation.status === 'cancelled';
|
|
178
|
+
const card: ToolCard = {
|
|
179
|
+
kind: hasResult ? 'result' : 'call',
|
|
180
|
+
name: invocation.toolName,
|
|
181
|
+
detail,
|
|
182
|
+
text: rawResult || undefined,
|
|
183
|
+
callId: invocation.toolCallId || undefined,
|
|
184
|
+
hasResult
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
type: 'tool-card' as const,
|
|
188
|
+
card: buildToolCard(card, params.texts)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
type: 'unknown' as const,
|
|
193
|
+
label: params.texts.unknownPartLabel,
|
|
194
|
+
rawType: typeof part.type === 'string' ? part.type : 'unknown',
|
|
195
|
+
text: stringifyUnknown(part)
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((part) => part !== null)
|
|
199
|
+
}));
|
|
200
|
+
}
|