@nextclaw/ui 0.6.1 → 0.6.3
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/dist/assets/{ChannelsList-CkCpHSto.js → ChannelsList-Bga6n85j.js} +1 -1
- package/dist/assets/ChatPage-B-Yk3kkv.js +32 -0
- package/dist/assets/{DocBrowser-B5Aqiz6W.js → DocBrowser-dv57PRp5.js} +1 -1
- package/dist/assets/{MarketplacePage-BIi0bBdW.js → MarketplacePage-j6p73Hjo.js} +1 -1
- package/dist/assets/{ModelConfig-BTFiEAxQ.js → ModelConfig-BiKSDp5h.js} +1 -1
- package/dist/assets/{ProvidersList-cdk1d-G_.js → ProvidersList-B7ZfRUkD.js} +1 -1
- package/dist/assets/{RuntimeConfig-CFqFsXmR.js → RuntimeConfig-Bpt9UNb6.js} +1 -1
- package/dist/assets/{SecretsConfig-CIKasCek.js → SecretsConfig-Ds00G-_O.js} +2 -2
- package/dist/assets/{SessionsConfig-mnCLFtbo.js → SessionsConfig-Mjet4opU.js} +1 -1
- package/dist/assets/{card-C1BUfR85.js → card-C7JJ5BGA.js} +1 -1
- package/dist/assets/index-BiJ2xs5X.css +1 -0
- package/dist/assets/{index-Dxas8MJ9.js → index-Cb9xiqC5.js} +2 -2
- package/dist/assets/{label-CwWfYbuj.js → label-DHJKdaUl.js} +1 -1
- package/dist/assets/{logos-DDyjHSEU.js → logos-fPO_amyL.js} +1 -1
- package/dist/assets/{page-layout-DKTRKcHL.js → page-layout-CF0JQsWW.js} +1 -1
- package/dist/assets/{switch-Bi3yeYiC.js → switch-C1hgy-fE.js} +1 -1
- package/dist/assets/{tabs-custom-HZFNZrc0.js → tabs-custom-OyoLf5ZM.js} +1 -1
- package/dist/assets/useConfig-D_G46zbo.js +6 -0
- package/dist/assets/{useConfirmDialog-DwD21HlD.js → useConfirmDialog-_0u6i3cI.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/config.ts +77 -12
- package/src/api/types.ts +24 -0
- package/src/components/chat/ChatConversationPanel.tsx +5 -20
- package/src/components/chat/ChatPage.tsx +49 -8
- package/src/components/chat/useChatStreamController.ts +192 -115
- package/src/hooks/useConfig.ts +29 -0
- package/src/hooks/useWebSocket.ts +21 -0
- package/dist/assets/ChatPage-DM4XNsrW.js +0 -32
- package/dist/assets/index-P4YzN9iS.css +0 -1
- package/dist/assets/useConfig-CgzVQTZl.js +0 -6
package/src/api/config.ts
CHANGED
|
@@ -24,6 +24,9 @@ import type {
|
|
|
24
24
|
ChatCapabilitiesView,
|
|
25
25
|
ChatTurnStopRequest,
|
|
26
26
|
ChatTurnStopResult,
|
|
27
|
+
ChatRunListView,
|
|
28
|
+
ChatRunState,
|
|
29
|
+
ChatRunView,
|
|
27
30
|
CronListView,
|
|
28
31
|
CronEnableRequest,
|
|
29
32
|
CronRunRequest,
|
|
@@ -270,6 +273,39 @@ export async function stopChatTurn(data: ChatTurnStopRequest): Promise<ChatTurnS
|
|
|
270
273
|
return response.data;
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
// GET /api/chat/runs
|
|
277
|
+
export async function fetchChatRuns(params?: {
|
|
278
|
+
sessionKey?: string;
|
|
279
|
+
states?: ChatRunState[];
|
|
280
|
+
limit?: number;
|
|
281
|
+
}): Promise<ChatRunListView> {
|
|
282
|
+
const query = new URLSearchParams();
|
|
283
|
+
if (params?.sessionKey?.trim()) {
|
|
284
|
+
query.set('sessionKey', params.sessionKey.trim());
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(params?.states) && params.states.length > 0) {
|
|
287
|
+
query.set('states', params.states.join(','));
|
|
288
|
+
}
|
|
289
|
+
if (typeof params?.limit === 'number' && Number.isFinite(params.limit)) {
|
|
290
|
+
query.set('limit', String(Math.max(0, Math.trunc(params.limit))));
|
|
291
|
+
}
|
|
292
|
+
const suffix = query.toString();
|
|
293
|
+
const response = await api.get<ChatRunListView>(suffix ? `/api/chat/runs?${suffix}` : '/api/chat/runs');
|
|
294
|
+
if (!response.ok) {
|
|
295
|
+
throw new Error(response.error.message);
|
|
296
|
+
}
|
|
297
|
+
return response.data;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// GET /api/chat/runs/:runId
|
|
301
|
+
export async function fetchChatRun(runId: string): Promise<ChatRunView> {
|
|
302
|
+
const response = await api.get<ChatRunView>(`/api/chat/runs/${encodeURIComponent(runId)}`);
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
throw new Error(response.error.message);
|
|
305
|
+
}
|
|
306
|
+
return response.data;
|
|
307
|
+
}
|
|
308
|
+
|
|
273
309
|
type ChatTurnStreamOptions = {
|
|
274
310
|
signal?: AbortSignal;
|
|
275
311
|
onReady?: (event: ChatTurnStreamReadyEvent) => void;
|
|
@@ -304,20 +340,10 @@ function parseSseFrame(frame: string): SseParsedEvent | null {
|
|
|
304
340
|
};
|
|
305
341
|
}
|
|
306
342
|
|
|
307
|
-
|
|
308
|
-
|
|
343
|
+
async function consumeChatTurnSseStream(
|
|
344
|
+
response: Response,
|
|
309
345
|
options: ChatTurnStreamOptions = {}
|
|
310
346
|
): Promise<ChatTurnView> {
|
|
311
|
-
const response = await fetch(`${API_BASE}/api/chat/turn/stream`, {
|
|
312
|
-
method: 'POST',
|
|
313
|
-
headers: {
|
|
314
|
-
'Content-Type': 'application/json',
|
|
315
|
-
Accept: 'text/event-stream'
|
|
316
|
-
},
|
|
317
|
-
body: JSON.stringify(data),
|
|
318
|
-
signal: options.signal
|
|
319
|
-
});
|
|
320
|
-
|
|
321
347
|
if (!response.ok) {
|
|
322
348
|
let message = `chat stream failed (${response.status} ${response.statusText})`;
|
|
323
349
|
try {
|
|
@@ -449,6 +475,45 @@ export async function sendChatTurnStream(
|
|
|
449
475
|
return finalView;
|
|
450
476
|
}
|
|
451
477
|
|
|
478
|
+
export async function sendChatTurnStream(
|
|
479
|
+
data: ChatTurnRequest,
|
|
480
|
+
options: ChatTurnStreamOptions = {}
|
|
481
|
+
): Promise<ChatTurnView> {
|
|
482
|
+
const response = await fetch(`${API_BASE}/api/chat/turn/stream`, {
|
|
483
|
+
method: 'POST',
|
|
484
|
+
headers: {
|
|
485
|
+
'Content-Type': 'application/json',
|
|
486
|
+
Accept: 'text/event-stream'
|
|
487
|
+
},
|
|
488
|
+
body: JSON.stringify(data),
|
|
489
|
+
signal: options.signal
|
|
490
|
+
});
|
|
491
|
+
return consumeChatTurnSseStream(response, options);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function streamChatRun(
|
|
495
|
+
params: {
|
|
496
|
+
runId: string;
|
|
497
|
+
fromEventIndex?: number;
|
|
498
|
+
},
|
|
499
|
+
options: ChatTurnStreamOptions = {}
|
|
500
|
+
): Promise<ChatTurnView> {
|
|
501
|
+
const query = new URLSearchParams();
|
|
502
|
+
if (typeof params.fromEventIndex === 'number' && Number.isFinite(params.fromEventIndex)) {
|
|
503
|
+
query.set('fromEventIndex', String(Math.max(0, Math.trunc(params.fromEventIndex))));
|
|
504
|
+
}
|
|
505
|
+
const suffix = query.toString();
|
|
506
|
+
const url = `${API_BASE}/api/chat/runs/${encodeURIComponent(params.runId)}/stream${suffix ? `?${suffix}` : ''}`;
|
|
507
|
+
const response = await fetch(url, {
|
|
508
|
+
method: 'GET',
|
|
509
|
+
headers: {
|
|
510
|
+
Accept: 'text/event-stream'
|
|
511
|
+
},
|
|
512
|
+
signal: options.signal
|
|
513
|
+
});
|
|
514
|
+
return consumeChatTurnSseStream(response, options);
|
|
515
|
+
}
|
|
516
|
+
|
|
452
517
|
// GET /api/cron
|
|
453
518
|
export async function fetchCronJobs(params?: { all?: boolean }): Promise<CronListView> {
|
|
454
519
|
const query = new URLSearchParams();
|
package/src/api/types.ts
CHANGED
|
@@ -187,6 +187,29 @@ export type ChatTurnStopResult = {
|
|
|
187
187
|
reason?: string;
|
|
188
188
|
};
|
|
189
189
|
|
|
190
|
+
export type ChatRunState = 'queued' | 'running' | 'completed' | 'failed' | 'aborted';
|
|
191
|
+
|
|
192
|
+
export type ChatRunView = {
|
|
193
|
+
runId: string;
|
|
194
|
+
sessionKey: string;
|
|
195
|
+
agentId?: string;
|
|
196
|
+
model?: string;
|
|
197
|
+
state: ChatRunState;
|
|
198
|
+
requestedAt: string;
|
|
199
|
+
startedAt?: string;
|
|
200
|
+
completedAt?: string;
|
|
201
|
+
stopSupported: boolean;
|
|
202
|
+
stopReason?: string;
|
|
203
|
+
error?: string;
|
|
204
|
+
reply?: string;
|
|
205
|
+
eventCount: number;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type ChatRunListView = {
|
|
209
|
+
runs: ChatRunView[];
|
|
210
|
+
total: number;
|
|
211
|
+
};
|
|
212
|
+
|
|
190
213
|
export type ChatTurnStreamReadyEvent = {
|
|
191
214
|
event: "ready";
|
|
192
215
|
sessionKey: string;
|
|
@@ -469,6 +492,7 @@ export type ConfigActionExecuteResult = {
|
|
|
469
492
|
// WebSocket events
|
|
470
493
|
export type WsEvent =
|
|
471
494
|
| { type: 'config.updated'; payload: { path: string } }
|
|
495
|
+
| { type: 'run.updated'; payload: { run: ChatRunView } }
|
|
472
496
|
| { type: 'config.reload.started'; payload?: Record<string, unknown> }
|
|
473
497
|
| { type: 'config.reload.finished'; payload?: Record<string, unknown> }
|
|
474
498
|
| { type: 'error'; payload: { message: string; code?: string } }
|
|
@@ -8,6 +8,7 @@ import { t } from '@/lib/i18n';
|
|
|
8
8
|
import { Trash2 } from 'lucide-react';
|
|
9
9
|
|
|
10
10
|
type ChatConversationPanelProps = {
|
|
11
|
+
isProviderStateResolved: boolean;
|
|
11
12
|
modelOptions: ChatModelOption[];
|
|
12
13
|
selectedModel: string;
|
|
13
14
|
onSelectedModelChange: (value: string) => void;
|
|
@@ -40,6 +41,7 @@ type ChatConversationPanelProps = {
|
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export function ChatConversationPanel({
|
|
44
|
+
isProviderStateResolved,
|
|
43
45
|
modelOptions,
|
|
44
46
|
selectedModel,
|
|
45
47
|
onSelectedModelChange,
|
|
@@ -72,18 +74,13 @@ export function ChatConversationPanel({
|
|
|
72
74
|
}: ChatConversationPanelProps) {
|
|
73
75
|
const showWelcome = !selectedSessionKey && mergedEvents.length === 0;
|
|
74
76
|
const hasConfiguredModel = modelOptions.length > 0;
|
|
77
|
+
const shouldShowProviderHint = isProviderStateResolved && !hasConfiguredModel;
|
|
75
78
|
const hideEmptyHint =
|
|
76
79
|
isHistoryLoading &&
|
|
77
80
|
mergedEvents.length === 0 &&
|
|
78
81
|
!isSending &&
|
|
79
82
|
!isAwaitingAssistantOutput &&
|
|
80
83
|
!streamingAssistantText.trim();
|
|
81
|
-
const shouldShowProviderSetup =
|
|
82
|
-
!hasConfiguredModel &&
|
|
83
|
-
!selectedSessionKey &&
|
|
84
|
-
mergedEvents.length === 0 &&
|
|
85
|
-
!hideEmptyHint &&
|
|
86
|
-
!isSending;
|
|
87
84
|
|
|
88
85
|
return (
|
|
89
86
|
<section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
@@ -107,7 +104,7 @@ export function ChatConversationPanel({
|
|
|
107
104
|
</div>
|
|
108
105
|
)}
|
|
109
106
|
|
|
110
|
-
{
|
|
107
|
+
{shouldShowProviderHint && (
|
|
111
108
|
<div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
|
|
112
109
|
<span className="text-xs text-amber-800">{t('chatModelNoOptions')}</span>
|
|
113
110
|
<button
|
|
@@ -123,19 +120,7 @@ export function ChatConversationPanel({
|
|
|
123
120
|
{/* Message thread or welcome */}
|
|
124
121
|
<div ref={threadRef} onScroll={onThreadScroll} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
|
|
125
122
|
{showWelcome ? (
|
|
126
|
-
|
|
127
|
-
<div className="h-full flex items-center justify-center p-8">
|
|
128
|
-
<div className="w-full max-w-xl rounded-2xl border border-amber-200 bg-amber-50/70 p-6 text-center">
|
|
129
|
-
<h2 className="text-lg font-semibold text-amber-900">{t('chatProviderSetupTitle')}</h2>
|
|
130
|
-
<p className="mt-2 text-sm text-amber-800">{t('chatProviderSetupDescription')}</p>
|
|
131
|
-
<Button className="mt-4" onClick={onGoToProviders}>
|
|
132
|
-
{t('chatGoConfigureProvider')}
|
|
133
|
-
</Button>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
) : (
|
|
137
|
-
<ChatWelcome onCreateSession={onCreateSession} />
|
|
138
|
-
)
|
|
123
|
+
<ChatWelcome onCreateSession={onCreateSession} />
|
|
139
124
|
) : hideEmptyHint ? (
|
|
140
125
|
<div className="h-full" />
|
|
141
126
|
) : mergedEvents.length === 0 ? (
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
useConfigMeta,
|
|
8
8
|
useDeleteSession,
|
|
9
9
|
useSessionHistory,
|
|
10
|
-
useSessions
|
|
10
|
+
useSessions,
|
|
11
|
+
useChatRuns
|
|
11
12
|
} from '@/hooks/useConfig';
|
|
12
13
|
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
13
14
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
@@ -242,6 +243,9 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
242
243
|
|
|
243
244
|
const configQuery = useConfig();
|
|
244
245
|
const configMetaQuery = useConfigMeta();
|
|
246
|
+
const isProviderStateResolved =
|
|
247
|
+
(configQuery.isFetched || configQuery.isSuccess) &&
|
|
248
|
+
(configMetaQuery.isFetched || configMetaQuery.isSuccess);
|
|
245
249
|
const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
|
|
246
250
|
const installedSkillsQuery = useMarketplaceInstalled('skill');
|
|
247
251
|
const chatCapabilitiesQuery = useChatCapabilities({
|
|
@@ -334,6 +338,8 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
334
338
|
stopDisabledReason,
|
|
335
339
|
lastSendError,
|
|
336
340
|
sendMessage,
|
|
341
|
+
resumeRun,
|
|
342
|
+
activeBackendRunId,
|
|
337
343
|
stopCurrentRun,
|
|
338
344
|
resetStreamState
|
|
339
345
|
} = useChatStreamController({
|
|
@@ -345,17 +351,51 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
345
351
|
refetchHistory: historyQuery.refetch
|
|
346
352
|
});
|
|
347
353
|
|
|
354
|
+
const activeRunsQuery = useChatRuns(
|
|
355
|
+
selectedSessionKey
|
|
356
|
+
? {
|
|
357
|
+
sessionKey: selectedSessionKey,
|
|
358
|
+
states: ['queued', 'running'],
|
|
359
|
+
limit: 5
|
|
360
|
+
}
|
|
361
|
+
: undefined
|
|
362
|
+
);
|
|
363
|
+
const activeRun = useMemo(() => {
|
|
364
|
+
const candidates = activeRunsQuery.data?.runs ?? [];
|
|
365
|
+
if (!selectedSessionKey) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
return candidates.find((entry) => entry.sessionKey === selectedSessionKey) ?? null;
|
|
369
|
+
}, [activeRunsQuery.data?.runs, selectedSessionKey]);
|
|
370
|
+
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (view !== 'chat' || !selectedSessionKey || !activeRun) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (activeBackendRunId === activeRun.runId) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
void resumeRun(activeRun);
|
|
379
|
+
}, [activeBackendRunId, activeRun, resumeRun, selectedSessionKey, view]);
|
|
380
|
+
|
|
348
381
|
const mergedEvents = useMemo(() => {
|
|
349
|
-
const
|
|
382
|
+
const bySeq = new Map<number, SessionEventView>();
|
|
383
|
+
const append = (event: SessionEventView) => {
|
|
384
|
+
if (!Number.isFinite(event.seq)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
bySeq.set(event.seq, event);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
historyEvents.forEach(append);
|
|
350
391
|
if (optimisticUserEvent) {
|
|
351
|
-
|
|
392
|
+
append(optimisticUserEvent);
|
|
352
393
|
}
|
|
353
|
-
|
|
394
|
+
streamingSessionEvents.forEach(append);
|
|
395
|
+
|
|
396
|
+
const next = [...bySeq.values()].sort((left, right) => left.seq - right.seq);
|
|
354
397
|
if (streamingAssistantText.trim()) {
|
|
355
|
-
const maxSeq = next.reduce((max, event) =>
|
|
356
|
-
const seq = Number.isFinite(event.seq) ? event.seq : 0;
|
|
357
|
-
return seq > max ? seq : max;
|
|
358
|
-
}, 0);
|
|
398
|
+
const maxSeq = next.reduce((max, event) => (event.seq > max ? event.seq : max), 0);
|
|
359
399
|
next.push({
|
|
360
400
|
seq: maxSeq + 1,
|
|
361
401
|
type: 'stream.assistant_delta',
|
|
@@ -483,6 +523,7 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
483
523
|
};
|
|
484
524
|
|
|
485
525
|
const conversationProps: ComponentProps<typeof ChatConversationPanel> = {
|
|
526
|
+
isProviderStateResolved,
|
|
486
527
|
modelOptions,
|
|
487
528
|
selectedModel,
|
|
488
529
|
onSelectedModelChange: setSelectedModel,
|