@nextclaw/ui 0.5.48 → 0.6.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/ChannelsList-CkCpHSto.js +1 -0
  3. package/dist/assets/ChatPage-DM4XNsrW.js +32 -0
  4. package/dist/assets/DocBrowser-B5Aqiz6W.js +1 -0
  5. package/dist/assets/MarketplacePage-BIi0bBdW.js +49 -0
  6. package/dist/assets/ModelConfig-BTFiEAxQ.js +1 -0
  7. package/dist/assets/{ProvidersList-BXHpjVtO.js → ProvidersList-cdk1d-G_.js} +1 -1
  8. package/dist/assets/RuntimeConfig-CFqFsXmR.js +1 -0
  9. package/dist/assets/{SecretsConfig-KkgMzdt1.js → SecretsConfig-CIKasCek.js} +2 -2
  10. package/dist/assets/SessionsConfig-mnCLFtbo.js +2 -0
  11. package/dist/assets/{card-D7NY0Szf.js → card-C1BUfR85.js} +1 -1
  12. package/dist/assets/index-Dxas8MJ9.js +2 -0
  13. package/dist/assets/index-P4YzN9iS.css +1 -0
  14. package/dist/assets/{label-Ojs7Al6B.js → label-CwWfYbuj.js} +1 -1
  15. package/dist/assets/{logos-B1qBsCSi.js → logos-DDyjHSEU.js} +1 -1
  16. package/dist/assets/{page-layout-CUMMO0nN.js → page-layout-DKTRKcHL.js} +1 -1
  17. package/dist/assets/provider-models-y4mUDcGF.js +1 -0
  18. package/dist/assets/{switch-BdhS_16-.js → switch-Bi3yeYiC.js} +1 -1
  19. package/dist/assets/{tabs-custom-D261E5EA.js → tabs-custom-HZFNZrc0.js} +1 -1
  20. package/dist/assets/useConfig-CgzVQTZl.js +6 -0
  21. package/dist/assets/{useConfirmDialog-BUKGHDL6.js → useConfirmDialog-DwD21HlD.js} +2 -2
  22. package/dist/assets/{vendor-Dh04PGww.js → vendor-Ylg6Wdt_.js} +84 -69
  23. package/dist/index.html +3 -3
  24. package/package.json +2 -1
  25. package/src/App.tsx +10 -6
  26. package/src/api/config.ts +42 -1
  27. package/src/api/types.ts +29 -0
  28. package/src/components/chat/ChatConversationPanel.tsx +109 -85
  29. package/src/components/chat/ChatInputBar.tsx +245 -0
  30. package/src/components/chat/ChatPage.tsx +365 -187
  31. package/src/components/chat/ChatSidebar.tsx +242 -0
  32. package/src/components/chat/ChatThread.tsx +92 -25
  33. package/src/components/chat/ChatWelcome.tsx +61 -0
  34. package/src/components/chat/SkillsPicker.tsx +137 -0
  35. package/src/components/chat/useChatStreamController.ts +287 -56
  36. package/src/components/config/ChannelForm.tsx +1 -1
  37. package/src/components/config/ChannelsList.tsx +3 -3
  38. package/src/components/config/ModelConfig.tsx +11 -89
  39. package/src/components/config/RuntimeConfig.tsx +29 -1
  40. package/src/components/layout/AppLayout.tsx +42 -6
  41. package/src/components/layout/Sidebar.tsx +68 -62
  42. package/src/components/marketplace/MarketplacePage.tsx +13 -3
  43. package/src/components/ui/popover.tsx +31 -0
  44. package/src/hooks/useConfig.ts +18 -0
  45. package/src/lib/i18n.ts +53 -0
  46. package/src/lib/provider-models.ts +129 -0
  47. package/dist/assets/ChannelsList-C8cguFLc.js +0 -1
  48. package/dist/assets/ChatPage-BkHWNUNR.js +0 -32
  49. package/dist/assets/CronConfig-D-ESQlvk.js +0 -1
  50. package/dist/assets/DocBrowser-B9ZD6pAk.js +0 -1
  51. package/dist/assets/MarketplacePage-Ds_l9KTF.js +0 -49
  52. package/dist/assets/ModelConfig-N1tbLv9b.js +0 -1
  53. package/dist/assets/RuntimeConfig-KsKfkjgv.js +0 -1
  54. package/dist/assets/SessionsConfig-CWBp8IPf.js +0 -2
  55. package/dist/assets/index-BRBYYgR_.js +0 -2
  56. package/dist/assets/index-C5cdRzpO.css +0 -1
  57. package/dist/assets/useConfig-txxbxXnT.js +0 -6
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-BRBYYgR_.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-Dh04PGww.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-C5cdRzpO.css">
9
+ <script type="module" crossorigin src="/assets/index-Dxas8MJ9.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-Ylg6Wdt_.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-P4YzN9iS.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.48",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
8
  "@radix-ui/react-dialog": "^1.1.2",
9
9
  "@radix-ui/react-label": "^2.1.0",
10
+ "@radix-ui/react-popover": "^1.1.15",
10
11
  "@radix-ui/react-select": "^2.1.2",
11
12
  "@radix-ui/react-slot": "^1.1.0",
12
13
  "@radix-ui/react-switch": "^1.1.1",
package/src/App.tsx CHANGED
@@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
3
  import { AppLayout } from '@/components/layout/AppLayout';
4
4
  import { useWebSocket } from '@/hooks/useWebSocket';
5
5
  import { Toaster } from 'sonner';
6
- import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
6
+ import { Routes, Route, Navigate } from 'react-router-dom';
7
7
 
8
8
  const queryClient = new QueryClient({
9
9
  defaultOptions: {
@@ -20,7 +20,6 @@ const ProvidersListPage = lazy(async () => ({ default: (await import('@/componen
20
20
  const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
21
21
  const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
22
22
  const SessionsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SessionsConfig')).SessionsConfig }));
23
- const CronConfigPage = lazy(async () => ({ default: (await import('@/components/config/CronConfig')).CronConfig }));
24
23
  const SecretsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SecretsConfig')).SecretsConfig }));
25
24
  const MarketplacePage = lazy(async () => ({ default: (await import('@/components/marketplace/MarketplacePage')).MarketplacePage }));
26
25
 
@@ -34,21 +33,26 @@ function LazyRoute({ children }: { children: JSX.Element }) {
34
33
 
35
34
  function AppContent() {
36
35
  useWebSocket(queryClient); // Initialize WebSocket connection
37
- const location = useLocation();
38
36
 
39
37
  return (
40
38
  <QueryClientProvider client={queryClient}>
41
39
  <AppLayout>
42
- <div key={location.pathname} className="animate-fade-in w-full h-full">
40
+ <div className="w-full h-full">
43
41
  <Routes>
44
- <Route path="/chat" element={<LazyRoute><ChatPage /></LazyRoute>} />
42
+ <Route path="/chat/skills" element={<Navigate to="/skills" replace />} />
43
+ <Route path="/chat/cron" element={<Navigate to="/cron" replace />} />
44
+ <Route path="/chat/:sessionId" element={<LazyRoute><ChatPage view="chat" /></LazyRoute>} />
45
+ <Route path="/chat" element={<LazyRoute><ChatPage view="chat" /></LazyRoute>} />
46
+ <Route path="/skills" element={<LazyRoute><ChatPage view="skills" /></LazyRoute>} />
47
+ <Route path="/cron" element={<LazyRoute><ChatPage view="cron" /></LazyRoute>} />
45
48
  <Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
46
49
  <Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
47
50
  <Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
48
51
  <Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
49
52
  <Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
50
- <Route path="/cron" element={<LazyRoute><CronConfigPage /></LazyRoute>} />
51
53
  <Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
54
+ <Route path="/settings" element={<Navigate to="/model" replace />} />
55
+ <Route path="/marketplace/skills" element={<Navigate to="/skills" replace />} />
52
56
  <Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
53
57
  <Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
54
58
  <Route path="/" element={<Navigate to="/chat" replace />} />
package/src/api/config.ts CHANGED
@@ -21,6 +21,9 @@ import type {
21
21
  SessionPatchUpdate,
22
22
  ChatTurnRequest,
23
23
  ChatTurnView,
24
+ ChatCapabilitiesView,
25
+ ChatTurnStopRequest,
26
+ ChatTurnStopResult,
24
27
  CronListView,
25
28
  CronEnableRequest,
26
29
  CronRunRequest,
@@ -241,6 +244,32 @@ export async function sendChatTurn(data: ChatTurnRequest): Promise<ChatTurnView>
241
244
  return response.data;
242
245
  }
243
246
 
247
+ // GET /api/chat/capabilities
248
+ export async function fetchChatCapabilities(params?: { sessionKey?: string; agentId?: string }): Promise<ChatCapabilitiesView> {
249
+ const query = new URLSearchParams();
250
+ if (params?.sessionKey?.trim()) {
251
+ query.set('sessionKey', params.sessionKey.trim());
252
+ }
253
+ if (params?.agentId?.trim()) {
254
+ query.set('agentId', params.agentId.trim());
255
+ }
256
+ const suffix = query.toString();
257
+ const response = await api.get<ChatCapabilitiesView>(suffix ? `/api/chat/capabilities?${suffix}` : '/api/chat/capabilities');
258
+ if (!response.ok) {
259
+ throw new Error(response.error.message);
260
+ }
261
+ return response.data;
262
+ }
263
+
264
+ // POST /api/chat/turn/stop
265
+ export async function stopChatTurn(data: ChatTurnStopRequest): Promise<ChatTurnStopResult> {
266
+ const response = await api.post<ChatTurnStopResult>('/api/chat/turn/stop', data);
267
+ if (!response.ok) {
268
+ throw new Error(response.error.message);
269
+ }
270
+ return response.data;
271
+ }
272
+
244
273
  type ChatTurnStreamOptions = {
245
274
  signal?: AbortSignal;
246
275
  onReady?: (event: ChatTurnStreamReadyEvent) => void;
@@ -324,11 +353,23 @@ export async function sendChatTurnStream(
324
353
  const readyPayload = JSON.parse(parsed.data) as {
325
354
  sessionKey?: string;
326
355
  requestedAt?: string;
356
+ runId?: string;
357
+ stopSupported?: boolean;
358
+ stopReason?: string;
327
359
  };
328
360
  options.onReady?.({
329
361
  event: 'ready',
330
362
  sessionKey: String(readyPayload.sessionKey ?? ''),
331
- requestedAt: String(readyPayload.requestedAt ?? '')
363
+ requestedAt: String(readyPayload.requestedAt ?? ''),
364
+ ...(typeof readyPayload.runId === 'string' && readyPayload.runId.trim().length > 0
365
+ ? { runId: readyPayload.runId.trim() }
366
+ : {}),
367
+ ...(typeof readyPayload.stopSupported === 'boolean'
368
+ ? { stopSupported: readyPayload.stopSupported }
369
+ : {}),
370
+ ...(typeof readyPayload.stopReason === 'string' && readyPayload.stopReason.trim().length > 0
371
+ ? { stopReason: readyPayload.stopReason.trim() }
372
+ : {})
332
373
  });
333
374
  } catch {
334
375
  // ignore malformed ready event payload
package/src/api/types.ts CHANGED
@@ -74,6 +74,8 @@ export type AgentProfileView = {
74
74
  default?: boolean;
75
75
  workspace?: string;
76
76
  model?: string;
77
+ engine?: string;
78
+ engineConfig?: Record<string, unknown>;
77
79
  contextTokens?: number;
78
80
  maxToolIterations?: number;
79
81
  };
@@ -167,10 +169,31 @@ export type ChatTurnView = {
167
169
  durationMs: number;
168
170
  };
169
171
 
172
+ export type ChatCapabilitiesView = {
173
+ stopSupported: boolean;
174
+ stopReason?: string;
175
+ };
176
+
177
+ export type ChatTurnStopRequest = {
178
+ runId: string;
179
+ sessionKey?: string;
180
+ agentId?: string;
181
+ };
182
+
183
+ export type ChatTurnStopResult = {
184
+ stopped: boolean;
185
+ runId: string;
186
+ sessionKey?: string;
187
+ reason?: string;
188
+ };
189
+
170
190
  export type ChatTurnStreamReadyEvent = {
171
191
  event: "ready";
172
192
  sessionKey: string;
173
193
  requestedAt: string;
194
+ runId?: string;
195
+ stopSupported?: boolean;
196
+ stopReason?: string;
174
197
  };
175
198
 
176
199
  export type ChatTurnStreamDeltaEvent = {
@@ -242,6 +265,8 @@ export type RuntimeConfigUpdate = {
242
265
  agents?: {
243
266
  defaults?: {
244
267
  contextTokens?: number;
268
+ engine?: string;
269
+ engineConfig?: Record<string, unknown>;
245
270
  };
246
271
  list?: AgentProfileView[];
247
272
  };
@@ -307,6 +332,8 @@ export type ConfigView = {
307
332
  defaults: {
308
333
  model: string;
309
334
  workspace?: string;
335
+ engine?: string;
336
+ engineConfig?: Record<string, unknown>;
310
337
  contextTokens?: number;
311
338
  maxToolIterations?: number;
312
339
  };
@@ -530,6 +557,8 @@ export type MarketplaceInstalledRecord = {
530
557
  id?: string;
531
558
  spec: string;
532
559
  label?: string;
560
+ description?: string;
561
+ descriptionZh?: string;
533
562
  source?: string;
534
563
  installedAt?: string;
535
564
  enabled?: boolean;
@@ -1,19 +1,27 @@
1
1
  import type { MutableRefObject } from 'react';
2
- import type { SessionEventView } from '@/api/types';
2
+ import type { MarketplaceInstalledRecord, SessionEventView } from '@/api/types';
3
3
  import { Button } from '@/components/ui/button';
4
4
  import { ChatThread } from '@/components/chat/ChatThread';
5
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
5
+ import { ChatInputBar, type ChatModelOption } from '@/components/chat/ChatInputBar';
6
+ import { ChatWelcome } from '@/components/chat/ChatWelcome';
6
7
  import { t } from '@/lib/i18n';
7
- import { MessageSquareText, Send, Trash2 } from 'lucide-react';
8
+ import { Trash2 } from 'lucide-react';
8
9
 
9
10
  type ChatConversationPanelProps = {
10
- agentOptions: string[];
11
- selectedAgentId: string;
12
- onSelectedAgentIdChange: (value: string) => void;
11
+ modelOptions: ChatModelOption[];
12
+ selectedModel: string;
13
+ onSelectedModelChange: (value: string) => void;
14
+ onGoToProviders: () => void;
15
+ skillRecords: MarketplaceInstalledRecord[];
16
+ isSkillsLoading?: boolean;
17
+ selectedSkills: string[];
18
+ onSelectedSkillsChange: (next: string[]) => void;
13
19
  selectedSessionKey: string | null;
20
+ sessionDisplayName?: string;
14
21
  canDeleteSession: boolean;
15
22
  isDeletePending: boolean;
16
23
  onDeleteSession: () => void;
24
+ onCreateSession: () => void;
17
25
  threadRef: MutableRefObject<HTMLDivElement | null>;
18
26
  onThreadScroll: () => void;
19
27
  isHistoryLoading: boolean;
@@ -24,17 +32,28 @@ type ChatConversationPanelProps = {
24
32
  draft: string;
25
33
  onDraftChange: (value: string) => void;
26
34
  onSend: () => Promise<void> | void;
35
+ onStop: () => Promise<void> | void;
36
+ canStopGeneration: boolean;
37
+ stopDisabledReason?: string | null;
38
+ sendError?: string | null;
27
39
  queuedCount: number;
28
40
  };
29
41
 
30
42
  export function ChatConversationPanel({
31
- agentOptions,
32
- selectedAgentId,
33
- onSelectedAgentIdChange,
43
+ modelOptions,
44
+ selectedModel,
45
+ onSelectedModelChange,
46
+ onGoToProviders,
47
+ skillRecords,
48
+ isSkillsLoading = false,
49
+ selectedSkills,
50
+ onSelectedSkillsChange,
34
51
  selectedSessionKey,
52
+ sessionDisplayName,
35
53
  canDeleteSession,
36
54
  isDeletePending,
37
55
  onDeleteSession,
56
+ onCreateSession,
38
57
  threadRef,
39
58
  onThreadScroll,
40
59
  isHistoryLoading,
@@ -45,104 +64,109 @@ export function ChatConversationPanel({
45
64
  draft,
46
65
  onDraftChange,
47
66
  onSend,
48
- queuedCount
67
+ onStop,
68
+ canStopGeneration,
69
+ stopDisabledReason,
70
+ sendError,
71
+ queuedCount,
49
72
  }: ChatConversationPanelProps) {
50
- const showHistoryLoading =
73
+ const showWelcome = !selectedSessionKey && mergedEvents.length === 0;
74
+ const hasConfiguredModel = modelOptions.length > 0;
75
+ const hideEmptyHint =
51
76
  isHistoryLoading &&
52
77
  mergedEvents.length === 0 &&
53
78
  !isSending &&
54
79
  !isAwaitingAssistantOutput &&
55
80
  !streamingAssistantText.trim();
81
+ const shouldShowProviderSetup =
82
+ !hasConfiguredModel &&
83
+ !selectedSessionKey &&
84
+ mergedEvents.length === 0 &&
85
+ !hideEmptyHint &&
86
+ !isSending;
56
87
 
57
88
  return (
58
- <section className="flex-1 min-h-0 rounded-2xl border border-gray-200 bg-gradient-to-b from-gray-50/60 to-white shadow-card flex flex-col overflow-hidden">
59
- <div className="px-5 py-4 border-b border-gray-200/80 bg-white/80 backdrop-blur-sm">
60
- <div className="grid gap-3 lg:grid-cols-[minmax(220px,300px)_minmax(0,1fr)_auto] items-end">
61
- <div className="min-w-0">
62
- <div className="text-[11px] text-gray-500 mb-1">{t('chatAgentLabel')}</div>
63
- <Select value={selectedAgentId} onValueChange={onSelectedAgentIdChange}>
64
- <SelectTrigger className="h-9 rounded-lg">
65
- <SelectValue placeholder={t('chatSelectAgent')} />
66
- </SelectTrigger>
67
- <SelectContent>
68
- {agentOptions.map((agent) => (
69
- <SelectItem key={agent} value={agent}>
70
- {agent}
71
- </SelectItem>
72
- ))}
73
- </SelectContent>
74
- </Select>
75
- </div>
76
-
77
- <div className="min-w-0">
78
- <div className="text-[11px] text-gray-500 mb-1">{t('chatSessionLabel')}</div>
79
- <div className="h-9 rounded-lg border border-gray-200 bg-white px-3 text-xs text-gray-600 flex items-center truncate">
80
- {selectedSessionKey ?? t('chatNoSession')}
81
- </div>
89
+ <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
90
+ {/* Minimal top bar - only shown when session is active */}
91
+ {selectedSessionKey && (
92
+ <div className="px-5 py-3 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0">
93
+ <div className="min-w-0 flex-1">
94
+ <span className="text-sm font-medium text-gray-700 truncate">
95
+ {sessionDisplayName || selectedSessionKey}
96
+ </span>
82
97
  </div>
83
-
84
98
  <Button
85
- variant="outline"
86
- className="rounded-lg"
99
+ variant="ghost"
100
+ size="icon"
101
+ className="rounded-lg shrink-0 text-gray-400 hover:text-destructive"
87
102
  onClick={onDeleteSession}
88
103
  disabled={!canDeleteSession || isDeletePending}
89
104
  >
90
- <Trash2 className="h-3.5 w-3.5 mr-1.5" />
91
- {t('chatDeleteSession')}
105
+ <Trash2 className="h-4 w-4" />
92
106
  </Button>
93
107
  </div>
94
- </div>
108
+ )}
95
109
 
96
- <div ref={threadRef} onScroll={onThreadScroll} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-5 py-5">
97
- {!selectedSessionKey ? (
98
- <div className="h-full flex items-center justify-center">
99
- <div className="text-center text-gray-500">
100
- <MessageSquareText className="h-8 w-8 mx-auto mb-2 text-gray-300" />
101
- <div className="text-sm font-medium">{t('chatNoSession')}</div>
102
- <div className="text-xs mt-1">{t('chatNoSessionHint')}</div>
110
+ {!hasConfiguredModel && !showWelcome && (
111
+ <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
+ <span className="text-xs text-amber-800">{t('chatModelNoOptions')}</span>
113
+ <button
114
+ type="button"
115
+ onClick={onGoToProviders}
116
+ className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
117
+ >
118
+ {t('chatGoConfigureProvider')}
119
+ </button>
120
+ </div>
121
+ )}
122
+
123
+ {/* Message thread or welcome */}
124
+ <div ref={threadRef} onScroll={onThreadScroll} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
125
+ {showWelcome ? (
126
+ shouldShowProviderSetup ? (
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>
103
135
  </div>
104
- </div>
105
- ) : showHistoryLoading ? (
106
- <div className="text-sm text-gray-500">{t('chatHistoryLoading')}</div>
136
+ ) : (
137
+ <ChatWelcome onCreateSession={onCreateSession} />
138
+ )
139
+ ) : hideEmptyHint ? (
140
+ <div className="h-full" />
107
141
  ) : mergedEvents.length === 0 ? (
108
- <div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
142
+ <div className="px-5 py-5 text-sm text-gray-500">{t('chatNoMessages')}</div>
109
143
  ) : (
110
- <ChatThread events={mergedEvents} isSending={isSending && isAwaitingAssistantOutput} />
144
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
145
+ <ChatThread events={mergedEvents} isSending={isSending && isAwaitingAssistantOutput} />
146
+ </div>
111
147
  )}
112
148
  </div>
113
149
 
114
- <div className="border-t border-gray-200 bg-white p-4">
115
- <div className="rounded-xl border border-gray-200 bg-white p-2">
116
- <textarea
117
- value={draft}
118
- onChange={(event) => onDraftChange(event.target.value)}
119
- onKeyDown={(event) => {
120
- if (event.key === 'Enter' && !event.shiftKey) {
121
- event.preventDefault();
122
- void onSend();
123
- }
124
- }}
125
- placeholder={t('chatInputPlaceholder')}
126
- className="w-full min-h-[68px] max-h-[220px] resize-y bg-transparent outline-none text-sm px-2 py-1.5 text-gray-800 placeholder:text-gray-400"
127
- />
128
- <div className="flex items-center justify-between px-2 pb-1">
129
- <div className="text-[11px] text-gray-400">
130
- {isSending && queuedCount > 0
131
- ? `${t('chatQueuedHintPrefix')} ${queuedCount} ${t('chatQueuedHintSuffix')}`
132
- : t('chatInputHint')}
133
- </div>
134
- <Button
135
- size="sm"
136
- className="rounded-lg"
137
- onClick={() => void onSend()}
138
- disabled={draft.trim().length === 0}
139
- >
140
- <Send className="h-3.5 w-3.5 mr-1.5" />
141
- {isSending ? t('chatQueueSend') : t('chatSend')}
142
- </Button>
143
- </div>
144
- </div>
145
- </div>
150
+ {/* Enhanced input bar */}
151
+ <ChatInputBar
152
+ draft={draft}
153
+ onDraftChange={onDraftChange}
154
+ onSend={onSend}
155
+ onStop={onStop}
156
+ onGoToProviders={onGoToProviders}
157
+ canStopGeneration={canStopGeneration}
158
+ stopDisabledReason={stopDisabledReason}
159
+ sendError={sendError}
160
+ isSending={isSending}
161
+ queuedCount={queuedCount}
162
+ modelOptions={modelOptions}
163
+ selectedModel={selectedModel}
164
+ onSelectedModelChange={onSelectedModelChange}
165
+ skillRecords={skillRecords}
166
+ isSkillsLoading={isSkillsLoading}
167
+ selectedSkills={selectedSkills}
168
+ onSelectedSkillsChange={onSelectedSkillsChange}
169
+ />
146
170
  </section>
147
171
  );
148
172
  }