@nextclaw/ui 0.10.0 → 0.10.2
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 +24 -1
- package/dist/assets/{ChannelsList-VSRZzxx2.js → ChannelsList-DSMuOmMG.js} +4 -4
- package/dist/assets/ChatPage-do9TwNxj.js +38 -0
- package/dist/assets/{DocBrowser-C65Hbvnb.js → DocBrowser-BjoTblYl.js} +1 -1
- package/dist/assets/{LogoBadge-4qtguXEJ.js → LogoBadge-2yDaYdxw.js} +1 -1
- package/dist/assets/MarketplacePage-DVVk4dlH.js +49 -0
- package/dist/assets/{McpMarketplacePage-CHLkD8yX.js → McpMarketplacePage-B4WUzuLw.js} +1 -1
- package/dist/assets/{ModelConfig-CjsGdmZa.js → ModelConfig-Dr0eI9nN.js} +1 -1
- package/dist/assets/ProvidersList-C7A-mIbe.js +1 -0
- package/dist/assets/{RemoteAccessPage-rOZCnH1x.js → RemoteAccessPage-CI3Am3w1.js} +1 -1
- package/dist/assets/{RuntimeConfig-CmJh6g0R.js → RuntimeConfig-DvSNVSs8.js} +1 -1
- package/dist/assets/{SearchConfig-C_hUuzR4.js → SearchConfig-B6TGIZow.js} +1 -1
- package/dist/assets/{SecretsConfig-Bu_zIRlQ.js → SecretsConfig-CpxaKU1j.js} +1 -1
- package/dist/assets/{SessionsConfig-DA_nqkM_.js → SessionsConfig-B-VHnv4G.js} +1 -1
- package/dist/assets/{chat-message-BOdA4h43.js → chat-message-BMqngrjp.js} +1 -1
- package/dist/assets/index-C6MeoecJ.js +8 -0
- package/dist/assets/index-DdXzLuNG.css +1 -0
- package/dist/assets/{label-BYZ62ajO.js → label-s2ILtQeP.js} +1 -1
- package/dist/assets/{page-layout-UC-h92sU.js → page-layout-BX5Ro4Sj.js} +1 -1
- package/dist/assets/{popover-DASCEr3G.js → popover-qmNpQSIy.js} +1 -1
- package/dist/assets/{security-config-Cvujq4fH.js → security-config--F-f-nDl.js} +1 -1
- package/dist/assets/skeleton-DthPOKSc.js +1 -0
- package/dist/assets/{status-dot-C1AvPwDD.js → status-dot-DWj7aUy8.js} +1 -1
- package/dist/assets/{switch-D3wVuCSh.js → switch-62r7L4Lj.js} +1 -1
- package/dist/assets/tabs-custom-DEmoGMsc.js +1 -0
- package/dist/assets/useConfirmDialog-DzT94nC_.js +1 -0
- package/dist/assets/{vendor-DJt0Azq5.js → vendor-CNhxtHCf.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +5 -5
- package/src/App.test.tsx +41 -0
- package/src/App.tsx +37 -0
- package/src/api/client.test.ts +12 -0
- package/src/api/client.ts +4 -2
- package/src/api/config.ts +1 -1
- package/src/components/chat/ChatSidebar.tsx +41 -69
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +32 -1
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +6 -3
- package/src/components/chat/adapters/chat-message.adapter.test.ts +141 -163
- package/src/components/chat/adapters/chat-message.adapter.ts +35 -0
- package/src/components/chat/chat-composer-state.ts +38 -0
- package/src/components/chat/chat-stream/types.ts +2 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +116 -55
- package/src/components/chat/containers/chat-message-list.container.tsx +2 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +16 -1
- package/src/components/chat/managers/chat-session-list.manager.ts +0 -2
- package/src/components/chat/managers/chat-thread.manager.ts +0 -1
- package/src/components/chat/ncp/NcpChatPage.tsx +18 -18
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +50 -33
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +5 -123
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +56 -1
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +8 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +0 -1
- package/src/components/chat/presenter/chat-presenter-context.tsx +6 -0
- package/src/components/chat/stores/chat-input.store.ts +3 -0
- package/src/components/config/ChannelsList.test.tsx +2 -1
- package/src/components/config/weixin-channel-auth-section.test.tsx +2 -1
- package/src/components/layout/Sidebar.tsx +62 -102
- package/src/components/layout/sidebar-items.tsx +172 -0
- package/src/components/layout/sidebar.layout.test.tsx +11 -4
- package/src/hooks/use-auth.ts +1 -2
- package/src/lib/i18n.chat.ts +117 -0
- package/src/lib/i18n.remote.ts +1 -1
- package/src/lib/i18n.ts +2 -112
- package/src/transport/local.transport.ts +28 -7
- package/src/transport/remote.transport.test.ts +135 -0
- package/src/transport/remote.transport.ts +14 -1
- package/src/transport/transport.types.ts +1 -0
- package/dist/assets/ChatPage-CX0ZKE5i.js +0 -41
- package/dist/assets/MarketplacePage-DPCYptfD.js +0 -49
- package/dist/assets/ProvidersList-aXp_mo4J.js +0 -1
- package/dist/assets/index-C63mHRbE.css +0 -1
- package/dist/assets/index-DS7D1-KS.js +0 -8
- package/dist/assets/skeleton-DlYEKkkj.js +0 -1
- package/dist/assets/tabs-custom-CbgS7tu0.js +0 -1
- package/dist/assets/useConfirmDialog-BYbFEIbQ.js +0 -1
package/src/App.tsx
CHANGED
|
@@ -3,8 +3,10 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|
|
3
3
|
import { AccountPanel } from '@/account/components/account-panel';
|
|
4
4
|
import { appQueryClient } from '@/app-query-client';
|
|
5
5
|
import { LoginPage } from '@/components/auth/login-page';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
6
7
|
import { AppLayout } from '@/components/layout/AppLayout';
|
|
7
8
|
import { useAuthStatus } from '@/hooks/use-auth';
|
|
9
|
+
import { t } from '@/lib/i18n';
|
|
8
10
|
import { useRealtimeQueryBridge } from '@/hooks/use-realtime-query-bridge';
|
|
9
11
|
import { AppPresenterProvider } from '@/presenter/app-presenter-context';
|
|
10
12
|
import { Toaster } from 'sonner';
|
|
@@ -31,6 +33,29 @@ function LazyRoute({ children }: { children: JSX.Element }) {
|
|
|
31
33
|
return <Suspense fallback={<RouteFallback />}>{children}</Suspense>;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function AuthBootstrapErrorState(props: {
|
|
37
|
+
message: string;
|
|
38
|
+
retrying: boolean;
|
|
39
|
+
onRetry: () => void;
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<main className="flex min-h-screen items-center justify-center bg-secondary px-6 py-10">
|
|
43
|
+
<div className="w-full max-w-lg rounded-3xl border border-gray-200 bg-white p-8 shadow-card">
|
|
44
|
+
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-gray-500">{t('authBrand')}</p>
|
|
45
|
+
<h1 className="mt-3 text-2xl font-semibold text-gray-900">{t('authStatusLoadFailed')}</h1>
|
|
46
|
+
<p className="mt-3 text-sm leading-6 text-gray-600">
|
|
47
|
+
{props.message}
|
|
48
|
+
</p>
|
|
49
|
+
<div className="mt-6 flex gap-3">
|
|
50
|
+
<Button onClick={props.onRetry} disabled={props.retrying}>
|
|
51
|
+
{t('authRetryStatus')}
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</main>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
function ProtectedApp() {
|
|
35
60
|
useRealtimeQueryBridge(appQueryClient);
|
|
36
61
|
|
|
@@ -75,6 +100,18 @@ function AuthGate() {
|
|
|
75
100
|
return <RouteFallback />;
|
|
76
101
|
}
|
|
77
102
|
|
|
103
|
+
if (authStatus.isError) {
|
|
104
|
+
return (
|
|
105
|
+
<AuthBootstrapErrorState
|
|
106
|
+
message={authStatus.error instanceof Error ? authStatus.error.message : t('authStatusLoadFailed')}
|
|
107
|
+
retrying={authStatus.isRefetching}
|
|
108
|
+
onRetry={() => {
|
|
109
|
+
void authStatus.refetch();
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
78
115
|
if (authStatus.data?.enabled && !authStatus.data.authenticated) {
|
|
79
116
|
return <LoginPage username={authStatus.data.username} />;
|
|
80
117
|
}
|
package/src/api/client.test.ts
CHANGED
|
@@ -30,6 +30,18 @@ describe('api/client', () => {
|
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it('forwards timeout overrides to appClient.request', async () => {
|
|
34
|
+
mocks.request.mockResolvedValue({ ok: true });
|
|
35
|
+
|
|
36
|
+
await api.get<{ ok: boolean }>('/api/auth/status', { timeoutMs: 5000 });
|
|
37
|
+
|
|
38
|
+
expect(mocks.request).toHaveBeenCalledWith({
|
|
39
|
+
method: 'GET',
|
|
40
|
+
path: '/api/auth/status',
|
|
41
|
+
timeoutMs: 5000
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
33
45
|
it('parses JSON request bodies before sending to appClient.request', async () => {
|
|
34
46
|
mocks.request.mockResolvedValue({ success: true });
|
|
35
47
|
|
package/src/api/client.ts
CHANGED
|
@@ -3,13 +3,14 @@ import type { ApiResponse } from './types';
|
|
|
3
3
|
|
|
4
4
|
export async function requestApiResponse<T>(
|
|
5
5
|
endpoint: string,
|
|
6
|
-
options: RequestInit = {}
|
|
6
|
+
options: RequestInit & { timeoutMs?: number } = {}
|
|
7
7
|
): Promise<ApiResponse<T>> {
|
|
8
8
|
const method = (options.method || 'GET').toUpperCase();
|
|
9
9
|
try {
|
|
10
10
|
const data = await appClient.request<T>({
|
|
11
11
|
method: method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
12
12
|
path: endpoint,
|
|
13
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
13
14
|
...(options.body !== undefined ? { body: parseRequestBody(options.body) } : {})
|
|
14
15
|
});
|
|
15
16
|
return {
|
|
@@ -32,7 +33,8 @@ export async function requestApiResponse<T>(
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export const api = {
|
|
35
|
-
get: <T>(path: string
|
|
36
|
+
get: <T>(path: string, options: RequestInit & { timeoutMs?: number } = {}) =>
|
|
37
|
+
requestApiResponse<T>(path, { ...options, method: 'GET' }),
|
|
36
38
|
put: <T>(path: string, body: unknown) =>
|
|
37
39
|
requestApiResponse<T>(path, {
|
|
38
40
|
method: 'PUT',
|
package/src/api/config.ts
CHANGED
|
@@ -54,7 +54,7 @@ import type {
|
|
|
54
54
|
|
|
55
55
|
// GET /api/auth/status
|
|
56
56
|
export async function fetchAuthStatus(): Promise<AuthStatusView> {
|
|
57
|
-
const response = await api.get<AuthStatusView>('/api/auth/status');
|
|
57
|
+
const response = await api.get<AuthStatusView>('/api/auth/status', { timeoutMs: 5_000 });
|
|
58
58
|
if (!response.ok) {
|
|
59
59
|
throw new Error(response.error.message);
|
|
60
60
|
}
|
|
@@ -5,7 +5,7 @@ import { BrandHeader } from '@/components/common/BrandHeader';
|
|
|
5
5
|
import { StatusBadge } from '@/components/common/StatusBadge';
|
|
6
6
|
import { Input } from '@/components/ui/input';
|
|
7
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
8
|
-
import {
|
|
8
|
+
import { SelectItem } from '@/components/ui/select';
|
|
9
9
|
import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
|
|
10
10
|
import { useChatSessionLabelService } from '@/components/chat/chat-session-label.service';
|
|
11
11
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
@@ -19,8 +19,9 @@ import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
|
19
19
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
20
20
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
21
21
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
22
|
+
import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/components/layout/sidebar-items';
|
|
22
23
|
import { useUiStore } from '@/stores/ui.store';
|
|
23
|
-
import {
|
|
24
|
+
import { useLocation } from 'react-router-dom';
|
|
24
25
|
import {
|
|
25
26
|
AlarmClock,
|
|
26
27
|
BookOpen,
|
|
@@ -284,28 +285,14 @@ export function ChatSidebar() {
|
|
|
284
285
|
<div className="px-3 pb-2">
|
|
285
286
|
<ul className="space-y-0.5">
|
|
286
287
|
{navItems.map((item) => {
|
|
287
|
-
const Icon = item.icon;
|
|
288
288
|
return (
|
|
289
289
|
<li key={item.target}>
|
|
290
|
-
<
|
|
290
|
+
<SidebarNavLinkItem
|
|
291
291
|
to={item.target}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
: 'text-gray-600 hover:bg-gray-200/60 hover:text-gray-900'
|
|
297
|
-
)}
|
|
298
|
-
>
|
|
299
|
-
{({ isActive }) => (
|
|
300
|
-
<>
|
|
301
|
-
<Icon className={cn(
|
|
302
|
-
'h-4 w-4 transition-colors',
|
|
303
|
-
isActive ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-800'
|
|
304
|
-
)} />
|
|
305
|
-
<span>{item.label()}</span>
|
|
306
|
-
</>
|
|
307
|
-
)}
|
|
308
|
-
</NavLink>
|
|
292
|
+
label={item.label()}
|
|
293
|
+
icon={item.icon}
|
|
294
|
+
density="compact"
|
|
295
|
+
/>
|
|
309
296
|
</li>
|
|
310
297
|
);
|
|
311
298
|
})}
|
|
@@ -363,57 +350,42 @@ export function ChatSidebar() {
|
|
|
363
350
|
</div>
|
|
364
351
|
|
|
365
352
|
<div className="px-3 py-3 border-t border-gray-200/60 space-y-0.5">
|
|
366
|
-
<
|
|
353
|
+
<SidebarNavLinkItem
|
|
367
354
|
to="/settings"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)}
|
|
374
|
-
>
|
|
375
|
-
{({ isActive }) => (
|
|
376
|
-
<>
|
|
377
|
-
<Settings className={cn('h-4 w-4 transition-colors', isActive ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-800')} />
|
|
378
|
-
<span>{t('settings')}</span>
|
|
379
|
-
</>
|
|
380
|
-
)}
|
|
381
|
-
</NavLink>
|
|
382
|
-
<button
|
|
355
|
+
label={t('settings')}
|
|
356
|
+
icon={Settings}
|
|
357
|
+
density="compact"
|
|
358
|
+
/>
|
|
359
|
+
<SidebarActionItem
|
|
383
360
|
onClick={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
384
|
-
|
|
361
|
+
icon={BookOpen}
|
|
362
|
+
label={t('docBrowserHelp')}
|
|
363
|
+
density="compact"
|
|
364
|
+
/>
|
|
365
|
+
<SidebarSelectItem
|
|
366
|
+
value={theme}
|
|
367
|
+
onValueChange={(value) => setTheme(value as UiTheme)}
|
|
368
|
+
icon={Palette}
|
|
369
|
+
label={t('theme')}
|
|
370
|
+
valueLabel={currentThemeLabel}
|
|
371
|
+
density="compact"
|
|
385
372
|
>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
</Select>
|
|
403
|
-
<Select value={language} onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}>
|
|
404
|
-
<SelectTrigger className="w-full h-auto rounded-xl border-0 bg-transparent shadow-none px-3 py-2 text-[13px] font-medium text-gray-600 hover:bg-gray-200/60 focus:ring-0">
|
|
405
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
406
|
-
<Languages className="h-4 w-4 text-gray-400" />
|
|
407
|
-
<span>{t('language')}</span>
|
|
408
|
-
</div>
|
|
409
|
-
<span className="ml-auto text-[11px] text-gray-500">{currentLanguageLabel}</span>
|
|
410
|
-
</SelectTrigger>
|
|
411
|
-
<SelectContent>
|
|
412
|
-
{LANGUAGE_OPTIONS.map((option) => (
|
|
413
|
-
<SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
|
|
414
|
-
))}
|
|
415
|
-
</SelectContent>
|
|
416
|
-
</Select>
|
|
373
|
+
{THEME_OPTIONS.map((option) => (
|
|
374
|
+
<SelectItem key={option.value} value={option.value} className="text-xs">{t(option.labelKey)}</SelectItem>
|
|
375
|
+
))}
|
|
376
|
+
</SidebarSelectItem>
|
|
377
|
+
<SidebarSelectItem
|
|
378
|
+
value={language}
|
|
379
|
+
onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}
|
|
380
|
+
icon={Languages}
|
|
381
|
+
label={t('language')}
|
|
382
|
+
valueLabel={currentLanguageLabel}
|
|
383
|
+
density="compact"
|
|
384
|
+
>
|
|
385
|
+
{LANGUAGE_OPTIONS.map((option) => (
|
|
386
|
+
<SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
|
|
387
|
+
))}
|
|
388
|
+
</SidebarSelectItem>
|
|
417
389
|
</div>
|
|
418
390
|
</aside>
|
|
419
391
|
);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildChatSlashItems,
|
|
3
|
+
buildModelToolbarSelect,
|
|
4
|
+
buildSelectedSkillItems,
|
|
5
|
+
buildSkillPickerModel
|
|
6
|
+
} from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
2
7
|
import type { ChatSkillRecord } from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
3
8
|
|
|
4
9
|
function createSkillRecord(partial: Partial<ChatSkillRecord>): ChatSkillRecord {
|
|
@@ -78,3 +83,29 @@ describe('buildSkillPickerModel', () => {
|
|
|
78
83
|
});
|
|
79
84
|
});
|
|
80
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('buildModelToolbarSelect', () => {
|
|
88
|
+
it('falls back to the first available option when the selected model is missing', () => {
|
|
89
|
+
const onValueChange = vi.fn();
|
|
90
|
+
const select = buildModelToolbarSelect({
|
|
91
|
+
modelOptions: [
|
|
92
|
+
{
|
|
93
|
+
value: 'minimax/MiniMax-M2.7',
|
|
94
|
+
modelLabel: 'MiniMax-M2.7',
|
|
95
|
+
providerLabel: 'MiniMax'
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
selectedModel: 'dashscope/qwen3-coder-next',
|
|
99
|
+
isModelOptionsLoading: false,
|
|
100
|
+
hasModelOptions: true,
|
|
101
|
+
onValueChange,
|
|
102
|
+
texts: {
|
|
103
|
+
modelSelectPlaceholder: 'Select model',
|
|
104
|
+
modelNoOptionsLabel: 'No models'
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(select.value).toBe('minimax/MiniMax-M2.7');
|
|
109
|
+
expect(select.selectedLabel).toBe('MiniMax/MiniMax-M2.7');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -238,13 +238,16 @@ export function buildModelToolbarSelect(params: {
|
|
|
238
238
|
texts: Pick<ChatInputBarAdapterTexts, 'modelSelectPlaceholder' | 'modelNoOptionsLabel'>;
|
|
239
239
|
}): ChatToolbarSelect {
|
|
240
240
|
const selectedModelOption = params.modelOptions.find((option) => option.value === params.selectedModel);
|
|
241
|
+
const fallbackModelOption = params.modelOptions[0];
|
|
242
|
+
const resolvedModelOption = selectedModelOption ?? fallbackModelOption;
|
|
243
|
+
const resolvedValue = params.hasModelOptions ? resolvedModelOption?.value : undefined;
|
|
241
244
|
|
|
242
245
|
return {
|
|
243
246
|
key: 'model',
|
|
244
|
-
value:
|
|
247
|
+
value: resolvedValue,
|
|
245
248
|
placeholder: params.texts.modelSelectPlaceholder,
|
|
246
|
-
selectedLabel:
|
|
247
|
-
? `${
|
|
249
|
+
selectedLabel: resolvedModelOption
|
|
250
|
+
? `${resolvedModelOption.providerLabel}/${resolvedModelOption.modelLabel}`
|
|
248
251
|
: undefined,
|
|
249
252
|
icon: 'sparkles',
|
|
250
253
|
options: params.modelOptions.map((option) => ({
|
|
@@ -6,183 +6,161 @@ function toSource(uiMessages: UiMessage[]): ChatMessageSource[] {
|
|
|
6
6
|
return uiMessages as unknown as ChatMessageSource[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
];
|
|
9
|
+
const defaultTexts = {
|
|
10
|
+
roleLabels: {
|
|
11
|
+
user: "You",
|
|
12
|
+
assistant: "Assistant",
|
|
13
|
+
tool: "Tool",
|
|
14
|
+
system: "System",
|
|
15
|
+
fallback: "Message",
|
|
16
|
+
},
|
|
17
|
+
reasoningLabel: "Reasoning",
|
|
18
|
+
toolCallLabel: "Tool Call",
|
|
19
|
+
toolResultLabel: "Tool Result",
|
|
20
|
+
toolNoOutputLabel: "No output",
|
|
21
|
+
toolOutputLabel: "View Output",
|
|
22
|
+
imageAttachmentLabel: "Image attachment",
|
|
23
|
+
fileAttachmentLabel: "File attachment",
|
|
24
|
+
unknownPartLabel: "Unknown Part",
|
|
25
|
+
};
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
63
|
-
"formatted:2026-03-17T10:00:00.000Z",
|
|
64
|
-
);
|
|
65
|
-
expect(adapted[0]?.parts.map((part) => part.type)).toEqual([
|
|
66
|
-
"markdown",
|
|
67
|
-
"reasoning",
|
|
68
|
-
"tool-card",
|
|
69
|
-
]);
|
|
70
|
-
expect(adapted[0]?.parts[1]).toMatchObject({
|
|
71
|
-
type: "reasoning",
|
|
72
|
-
label: "Reasoning",
|
|
73
|
-
text: "internal reasoning",
|
|
74
|
-
});
|
|
75
|
-
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
76
|
-
type: "tool-card",
|
|
77
|
-
card: {
|
|
78
|
-
titleLabel: "Tool Result",
|
|
79
|
-
outputLabel: "View Output",
|
|
80
|
-
},
|
|
81
|
-
});
|
|
27
|
+
function adapt(uiMessages: ChatMessageSource[]) {
|
|
28
|
+
return adaptChatMessages({
|
|
29
|
+
uiMessages,
|
|
30
|
+
formatTimestamp: (value) => `formatted:${value}`,
|
|
31
|
+
texts: defaultTexts,
|
|
82
32
|
});
|
|
33
|
+
}
|
|
83
34
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
35
|
+
it("maps markdown, reasoning, and tool parts into UI view models", () => {
|
|
36
|
+
const messages: UiMessage[] = [
|
|
37
|
+
{
|
|
38
|
+
id: "assistant-1",
|
|
39
|
+
role: "assistant",
|
|
40
|
+
meta: {
|
|
41
|
+
status: "final",
|
|
42
|
+
timestamp: "2026-03-17T10:00:00.000Z",
|
|
43
|
+
},
|
|
44
|
+
parts: [
|
|
45
|
+
{ type: "text", text: "hello world" },
|
|
87
46
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
47
|
+
type: "reasoning",
|
|
48
|
+
reasoning: "internal reasoning",
|
|
49
|
+
details: [],
|
|
91
50
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
51
|
+
{
|
|
52
|
+
type: "tool-invocation",
|
|
53
|
+
toolInvocation: {
|
|
54
|
+
status: ToolInvocationStatus.RESULT,
|
|
55
|
+
toolCallId: "call-1",
|
|
56
|
+
toolName: "web_search",
|
|
57
|
+
args: '{"q":"hello"}',
|
|
58
|
+
result: { ok: true },
|
|
59
|
+
},
|
|
101
60
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const adapted = adapt(toSource(messages));
|
|
66
|
+
|
|
67
|
+
expect(adapted).toHaveLength(1);
|
|
68
|
+
expect(adapted[0]?.roleLabel).toBe("Assistant");
|
|
69
|
+
expect(adapted[0]?.timestampLabel).toBe(
|
|
70
|
+
"formatted:2026-03-17T10:00:00.000Z",
|
|
71
|
+
);
|
|
72
|
+
expect(adapted[0]?.parts.map((part) => part.type)).toEqual([
|
|
73
|
+
"markdown",
|
|
74
|
+
"reasoning",
|
|
75
|
+
"tool-card",
|
|
76
|
+
]);
|
|
77
|
+
expect(adapted[0]?.parts[1]).toMatchObject({
|
|
78
|
+
type: "reasoning",
|
|
79
|
+
label: "Reasoning",
|
|
80
|
+
text: "internal reasoning",
|
|
81
|
+
});
|
|
82
|
+
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
83
|
+
type: "tool-card",
|
|
84
|
+
card: {
|
|
85
|
+
titleLabel: "Tool Result",
|
|
86
|
+
outputLabel: "View Output",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("maps non-standard roles back to the generic message role", () => {
|
|
92
|
+
const adapted = adapt([
|
|
93
|
+
{
|
|
94
|
+
id: "data-1",
|
|
95
|
+
role: "data",
|
|
96
|
+
parts: [{ type: "text", text: "payload" }],
|
|
97
|
+
},
|
|
98
|
+
] as unknown as ChatMessageSource[]);
|
|
110
99
|
|
|
111
|
-
|
|
112
|
-
|
|
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 = adapt([
|
|
106
|
+
{
|
|
107
|
+
id: "x-1",
|
|
108
|
+
role: "assistant",
|
|
109
|
+
parts: [{ type: "step-start", value: "x" }],
|
|
110
|
+
},
|
|
111
|
+
] as unknown as ChatMessageSource[]);
|
|
112
|
+
|
|
113
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
114
|
+
type: "unknown",
|
|
115
|
+
rawType: "step-start",
|
|
116
|
+
label: "Unknown Part",
|
|
113
117
|
});
|
|
118
|
+
});
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
user: "You",
|
|
128
|
-
assistant: "Assistant",
|
|
129
|
-
tool: "Tool",
|
|
130
|
-
system: "System",
|
|
131
|
-
fallback: "Message",
|
|
132
|
-
},
|
|
133
|
-
reasoningLabel: "Reasoning",
|
|
134
|
-
toolCallLabel: "Tool Call",
|
|
135
|
-
toolResultLabel: "Tool Result",
|
|
136
|
-
toolNoOutputLabel: "No output",
|
|
137
|
-
toolOutputLabel: "View Output",
|
|
138
|
-
unknownPartLabel: "Unknown Part",
|
|
139
|
-
},
|
|
140
|
-
});
|
|
120
|
+
it("drops empty and zero-width text parts during adaptation", () => {
|
|
121
|
+
const adapted = adapt([
|
|
122
|
+
{
|
|
123
|
+
id: "assistant-mixed",
|
|
124
|
+
role: "assistant",
|
|
125
|
+
parts: [
|
|
126
|
+
{ type: "text", text: " " },
|
|
127
|
+
{ type: "text", text: "\u200B\u200B" },
|
|
128
|
+
{ type: "text", text: "\u200Bhello\u200B" },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
] as unknown as ChatMessageSource[]);
|
|
141
132
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
expect(adapted).toHaveLength(1);
|
|
134
|
+
expect(adapted[0]?.id).toBe("assistant-mixed");
|
|
135
|
+
expect(adapted[0]?.parts).toHaveLength(1);
|
|
136
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
137
|
+
type: "markdown",
|
|
138
|
+
text: "\u200Bhello\u200B",
|
|
147
139
|
});
|
|
140
|
+
});
|
|
148
141
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
it("maps file parts into previewable attachment view models", () => {
|
|
143
|
+
const adapted = adapt([
|
|
144
|
+
{
|
|
145
|
+
id: "assistant-file",
|
|
146
|
+
role: "assistant",
|
|
147
|
+
parts: [
|
|
152
148
|
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{ type: "text", text: " " },
|
|
157
|
-
{ type: "text", text: "\u200B\u200B" },
|
|
158
|
-
{ type: "text", text: "\u200Bhello\u200B" },
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
] as unknown as ChatMessageSource[],
|
|
162
|
-
formatTimestamp: () => "formatted",
|
|
163
|
-
texts: {
|
|
164
|
-
roleLabels: {
|
|
165
|
-
user: "You",
|
|
166
|
-
assistant: "Assistant",
|
|
167
|
-
tool: "Tool",
|
|
168
|
-
system: "System",
|
|
169
|
-
fallback: "Message",
|
|
149
|
+
type: "file",
|
|
150
|
+
mimeType: "image/png",
|
|
151
|
+
data: "ZmFrZS1pbWFnZQ==",
|
|
170
152
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
toolNoOutputLabel: "No output",
|
|
175
|
-
toolOutputLabel: "View Output",
|
|
176
|
-
unknownPartLabel: "Unknown Part",
|
|
177
|
-
},
|
|
178
|
-
});
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
] as unknown as ChatMessageSource[]);
|
|
179
156
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
157
|
+
expect(adapted[0]?.parts[0]).toEqual({
|
|
158
|
+
type: "file",
|
|
159
|
+
file: {
|
|
160
|
+
label: "Image attachment",
|
|
161
|
+
mimeType: "image/png",
|
|
162
|
+
dataUrl: "data:image/png;base64,ZmFrZS1pbWFnZQ==",
|
|
163
|
+
isImage: true,
|
|
164
|
+
},
|
|
187
165
|
});
|
|
188
166
|
});
|