@nextclaw/ui 0.6.14 → 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 +18 -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-BxZJ9BJT.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-9OABNssV.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-BIjHWZUm.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-BZEH0QZp.js → session-run-status-TkIuGbVw.js} +1 -1
- package/dist/assets/skeleton-CWbsNx2h.js +1 -0
- package/dist/assets/{switch-CnGQpdTp.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 -10
- 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 +29 -1
- package/src/api/websocket.ts +2 -0
- package/src/components/auth/login-page.tsx +69 -0
- package/src/components/chat/ChatConversationPanel.tsx +12 -54
- package/src/components/chat/ChatSidebar.tsx +7 -1
- 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/common/BrandHeader.tsx +4 -1
- package/src/components/common/StatusBadge.tsx +32 -20
- 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/hooks/useWebSocket.ts +53 -1
- 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/.eslintrc.cjs +0 -48
- package/dist/assets/ChannelsList-DiSnpiW0.js +0 -1
- package/dist/assets/ChatPage-DsaIrNHN.js +0 -36
- package/dist/assets/DocBrowser-CnfcptGM.js +0 -1
- package/dist/assets/MarketplacePage-BI_J_DBQ.js +0 -49
- package/dist/assets/ModelConfig-DfL8F4tN.js +0 -1
- package/dist/assets/ProvidersList-DpT_oFHZ.js +0 -1
- package/dist/assets/RuntimeConfig-BNYR_Iag.js +0 -1
- package/dist/assets/SearchConfig-TDBl7Fjh.js +0 -1
- package/dist/assets/SessionsConfig-BRwntUDz.js +0 -2
- package/dist/assets/card-BYnT3Mxo.js +0 -1
- package/dist/assets/index-BCfS4UY1.css +0 -1
- package/dist/assets/index-BnUxgevr.js +0 -8
- package/dist/assets/input-oaepEtqu.js +0 -1
- package/dist/assets/page-layout-B6JXiSQB.js +0 -1
- package/dist/assets/popover-LJQgv5l1.js +0 -1
- package/dist/assets/tabs-custom-CpSv7pDl.js +0 -1
- package/dist/assets/useConfirmDialog-pqAlPdQZ.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
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-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DK4TS5ev.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-B7ozqnFC.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BLeJkJ0o.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,39 +20,38 @@
|
|
|
20
20
|
"react": "^18.3.1",
|
|
21
21
|
"react-dom": "^18.3.1",
|
|
22
22
|
"react-hook-form": "^7.53.2",
|
|
23
|
-
"react-markdown": "^10.1.0",
|
|
24
23
|
"react-router-dom": "^7.13.0",
|
|
25
24
|
"rehype-sanitize": "^6.0.0",
|
|
26
|
-
"remark-gfm": "^4.0.1",
|
|
27
25
|
"rxjs": "^7.8.2",
|
|
28
26
|
"sonner": "^1.7.1",
|
|
29
27
|
"tailwind-merge": "^2.5.4",
|
|
30
28
|
"zod": "^3.23.8",
|
|
31
29
|
"zustand": "^5.0.2",
|
|
30
|
+
"@nextclaw/agent-chat-ui": "0.1.1",
|
|
32
31
|
"@nextclaw/agent-chat": "0.1.1"
|
|
33
32
|
},
|
|
34
33
|
"devDependencies": {
|
|
34
|
+
"@testing-library/react": "^16.3.0",
|
|
35
|
+
"@testing-library/user-event": "^14.6.1",
|
|
35
36
|
"@types/react": "^18.3.12",
|
|
36
37
|
"@types/react-dom": "^18.3.1",
|
|
37
|
-
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
38
|
-
"@typescript-eslint/parser": "^7.18.0",
|
|
39
38
|
"@vitejs/plugin-react": "^4.3.4",
|
|
40
39
|
"autoprefixer": "^10.4.20",
|
|
41
|
-
"
|
|
42
|
-
"eslint-config-prettier": "^9.1.0",
|
|
43
|
-
"eslint-plugin-react-hooks": "^7.0.1",
|
|
40
|
+
"jsdom": "^25.0.1",
|
|
44
41
|
"postcss": "^8.4.49",
|
|
45
42
|
"prettier": "^3.3.3",
|
|
46
43
|
"tailwindcss": "^3.4.15",
|
|
47
44
|
"tailwindcss-animate": "^1.0.7",
|
|
48
45
|
"typescript": "^5.6.3",
|
|
49
|
-
"vite": "^6.0.1"
|
|
46
|
+
"vite": "^6.0.1",
|
|
47
|
+
"vitest": "^2.1.2"
|
|
50
48
|
},
|
|
51
49
|
"scripts": {
|
|
52
50
|
"dev": "vite",
|
|
53
51
|
"build": "tsc && vite build",
|
|
54
52
|
"preview": "vite preview",
|
|
55
53
|
"lint": "eslint .",
|
|
56
|
-
"tsc": "tsc --noEmit"
|
|
54
|
+
"tsc": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run"
|
|
57
56
|
}
|
|
58
57
|
}
|
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { lazy, Suspense } from 'react';
|
|
2
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { LoginPage } from '@/components/auth/login-page';
|
|
3
4
|
import { AppLayout } from '@/components/layout/AppLayout';
|
|
5
|
+
import { useAuthStatus } from '@/hooks/use-auth';
|
|
4
6
|
import { useWebSocket } from '@/hooks/useWebSocket';
|
|
5
7
|
import { Toaster } from 'sonner';
|
|
6
8
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
@@ -20,6 +22,7 @@ const SearchConfigPage = lazy(async () => ({ default: (await import('@/component
|
|
|
20
22
|
const ProvidersListPage = lazy(async () => ({ default: (await import('@/components/config/ProvidersList')).ProvidersList }));
|
|
21
23
|
const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
|
|
22
24
|
const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
|
|
25
|
+
const SecurityConfigPage = lazy(async () => ({ default: (await import('@/components/config/security-config')).SecurityConfig }));
|
|
23
26
|
const SessionsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SessionsConfig')).SessionsConfig }));
|
|
24
27
|
const SecretsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SecretsConfig')).SecretsConfig }));
|
|
25
28
|
const MarketplacePage = lazy(async () => ({ default: (await import('@/components/marketplace/MarketplacePage')).MarketplacePage }));
|
|
@@ -32,38 +35,57 @@ function LazyRoute({ children }: { children: JSX.Element }) {
|
|
|
32
35
|
return <Suspense fallback={<RouteFallback />}>{children}</Suspense>;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function
|
|
38
|
+
function ProtectedApp() {
|
|
36
39
|
useWebSocket(queryClient); // Initialize WebSocket connection
|
|
37
40
|
|
|
41
|
+
return (
|
|
42
|
+
<AppLayout>
|
|
43
|
+
<div className="w-full h-full">
|
|
44
|
+
<Routes>
|
|
45
|
+
<Route path="/chat/skills" element={<Navigate to="/skills" replace />} />
|
|
46
|
+
<Route path="/chat/cron" element={<Navigate to="/cron" replace />} />
|
|
47
|
+
<Route path="/chat/:sessionId?" element={<LazyRoute><ChatPage view="chat" /></LazyRoute>} />
|
|
48
|
+
<Route path="/skills" element={<LazyRoute><ChatPage view="skills" /></LazyRoute>} />
|
|
49
|
+
<Route path="/cron" element={<LazyRoute><ChatPage view="cron" /></LazyRoute>} />
|
|
50
|
+
<Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
|
|
51
|
+
<Route path="/search" element={<LazyRoute><SearchConfigPage /></LazyRoute>} />
|
|
52
|
+
<Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
|
|
53
|
+
<Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
|
|
54
|
+
<Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
|
|
55
|
+
<Route path="/security" element={<LazyRoute><SecurityConfigPage /></LazyRoute>} />
|
|
56
|
+
<Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
|
|
57
|
+
<Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
|
|
58
|
+
<Route path="/settings" element={<Navigate to="/model" replace />} />
|
|
59
|
+
<Route path="/marketplace/skills" element={<Navigate to="/skills" replace />} />
|
|
60
|
+
<Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
|
|
61
|
+
<Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
|
|
62
|
+
<Route path="/" element={<Navigate to="/chat" replace />} />
|
|
63
|
+
<Route path="*" element={<Navigate to="/chat" replace />} />
|
|
64
|
+
</Routes>
|
|
65
|
+
</div>
|
|
66
|
+
</AppLayout>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function AuthGate() {
|
|
71
|
+
const authStatus = useAuthStatus();
|
|
72
|
+
|
|
73
|
+
if (authStatus.isLoading && !authStatus.isError) {
|
|
74
|
+
return <RouteFallback />;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (authStatus.data?.enabled && !authStatus.data.authenticated) {
|
|
78
|
+
return <LoginPage username={authStatus.data.username} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return <ProtectedApp />;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function AppContent() {
|
|
38
85
|
return (
|
|
39
86
|
<QueryClientProvider client={queryClient}>
|
|
40
|
-
<
|
|
41
|
-
<div className="w-full h-full">
|
|
42
|
-
<Routes>
|
|
43
|
-
<Route path="/chat/skills" element={<Navigate to="/skills" replace />} />
|
|
44
|
-
<Route path="/chat/cron" element={<Navigate to="/cron" replace />} />
|
|
45
|
-
<Route path="/chat/:sessionId?" 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>} />
|
|
48
|
-
<Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
|
|
49
|
-
<Route path="/search" element={<LazyRoute><SearchConfigPage /></LazyRoute>} />
|
|
50
|
-
<Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
|
|
51
|
-
<Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
|
|
52
|
-
<Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
|
|
53
|
-
<Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
|
|
54
|
-
<Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
|
|
55
|
-
<Route path="/settings" element={<Navigate to="/model" replace />} />
|
|
56
|
-
<Route path="/marketplace/skills" element={<Navigate to="/skills" replace />} />
|
|
57
|
-
<Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
|
|
58
|
-
<Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
|
|
59
|
-
<Route path="/" element={<Navigate to="/chat" replace />} />
|
|
60
|
-
<Route path="*" element={<Navigate to="/chat" replace />} />
|
|
61
|
-
</Routes>
|
|
62
|
-
</div>
|
|
63
|
-
</AppLayout>
|
|
87
|
+
<AuthGate />
|
|
64
88
|
<Toaster position="top-right" richColors />
|
|
65
89
|
</QueryClientProvider>
|
|
66
90
|
);
|
|
67
91
|
}
|
|
68
|
-
|
|
69
|
-
export default AppContent;
|
package/src/api/client.ts
CHANGED
package/src/api/config.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { api, API_BASE } from './client';
|
|
2
2
|
import type {
|
|
3
|
+
AuthEnabledUpdateRequest,
|
|
4
|
+
AuthLoginRequest,
|
|
5
|
+
AuthPasswordUpdateRequest,
|
|
6
|
+
AuthSetupRequest,
|
|
7
|
+
AuthStatusView,
|
|
3
8
|
AppMetaView,
|
|
4
9
|
ConfigView,
|
|
5
10
|
ConfigMetaView,
|
|
@@ -46,6 +51,60 @@ import type {
|
|
|
46
51
|
CronActionResult
|
|
47
52
|
} from './types';
|
|
48
53
|
|
|
54
|
+
// GET /api/auth/status
|
|
55
|
+
export async function fetchAuthStatus(): Promise<AuthStatusView> {
|
|
56
|
+
const response = await api.get<AuthStatusView>('/api/auth/status');
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(response.error.message);
|
|
59
|
+
}
|
|
60
|
+
return response.data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// POST /api/auth/setup
|
|
64
|
+
export async function setupAuth(data: AuthSetupRequest): Promise<AuthStatusView> {
|
|
65
|
+
const response = await api.post<AuthStatusView>('/api/auth/setup', data);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(response.error.message);
|
|
68
|
+
}
|
|
69
|
+
return response.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// POST /api/auth/login
|
|
73
|
+
export async function loginAuth(data: AuthLoginRequest): Promise<AuthStatusView> {
|
|
74
|
+
const response = await api.post<AuthStatusView>('/api/auth/login', data);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(response.error.message);
|
|
77
|
+
}
|
|
78
|
+
return response.data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// POST /api/auth/logout
|
|
82
|
+
export async function logoutAuth(): Promise<{ success: boolean }> {
|
|
83
|
+
const response = await api.post<{ success: boolean }>('/api/auth/logout', {});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(response.error.message);
|
|
86
|
+
}
|
|
87
|
+
return response.data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// PUT /api/auth/password
|
|
91
|
+
export async function updateAuthPassword(data: AuthPasswordUpdateRequest): Promise<AuthStatusView> {
|
|
92
|
+
const response = await api.put<AuthStatusView>('/api/auth/password', data);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(response.error.message);
|
|
95
|
+
}
|
|
96
|
+
return response.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// PUT /api/auth/enabled
|
|
100
|
+
export async function updateAuthEnabled(data: AuthEnabledUpdateRequest): Promise<AuthStatusView> {
|
|
101
|
+
const response = await api.put<AuthStatusView>('/api/auth/enabled', data);
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(response.error.message);
|
|
104
|
+
}
|
|
105
|
+
return response.data;
|
|
106
|
+
}
|
|
107
|
+
|
|
49
108
|
// GET /api/app/meta
|
|
50
109
|
export async function fetchAppMeta(): Promise<AppMetaView> {
|
|
51
110
|
const response = await api.get<AppMetaView>('/api/app/meta');
|
|
@@ -356,6 +415,7 @@ async function readSseStream(params: {
|
|
|
356
415
|
}): Promise<{ sessionKey: string; reply: string }> {
|
|
357
416
|
const response = await fetch(`${API_BASE}${params.path}`, {
|
|
358
417
|
method: params.method,
|
|
418
|
+
credentials: 'include',
|
|
359
419
|
headers: {
|
|
360
420
|
'Content-Type': 'application/json',
|
|
361
421
|
Accept: 'text/event-stream'
|
package/src/api/types.ts
CHANGED
|
@@ -158,6 +158,31 @@ export type ProviderAuthImportResult = {
|
|
|
158
158
|
expiresAt?: string;
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
+
export type AuthStatusView = {
|
|
162
|
+
enabled: boolean;
|
|
163
|
+
configured: boolean;
|
|
164
|
+
authenticated: boolean;
|
|
165
|
+
username?: string;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type AuthSetupRequest = {
|
|
169
|
+
username: string;
|
|
170
|
+
password: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type AuthLoginRequest = {
|
|
174
|
+
username: string;
|
|
175
|
+
password: string;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export type AuthPasswordUpdateRequest = {
|
|
179
|
+
password: string;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type AuthEnabledUpdateRequest = {
|
|
183
|
+
enabled: boolean;
|
|
184
|
+
};
|
|
185
|
+
|
|
161
186
|
export type AgentProfileView = {
|
|
162
187
|
id: string;
|
|
163
188
|
default?: boolean;
|
|
@@ -501,6 +526,7 @@ export type ConfigView = {
|
|
|
501
526
|
session?: SessionConfigView;
|
|
502
527
|
tools?: Record<string, unknown>;
|
|
503
528
|
gateway?: Record<string, unknown>;
|
|
529
|
+
ui?: Record<string, unknown>;
|
|
504
530
|
secrets?: SecretsView;
|
|
505
531
|
};
|
|
506
532
|
|
|
@@ -653,7 +679,9 @@ export type WsEvent =
|
|
|
653
679
|
| { type: 'config.reload.started'; payload?: Record<string, unknown> }
|
|
654
680
|
| { type: 'config.reload.finished'; payload?: Record<string, unknown> }
|
|
655
681
|
| { type: 'error'; payload: { message: string; code?: string } }
|
|
656
|
-
| { type: 'connection.open'; payload?: Record<string, unknown> }
|
|
682
|
+
| { type: 'connection.open'; payload?: Record<string, unknown> }
|
|
683
|
+
| { type: 'connection.close'; payload?: Record<string, unknown> }
|
|
684
|
+
| { type: 'connection.error'; payload?: { message?: string } };
|
|
657
685
|
|
|
658
686
|
export type MarketplaceItemType = 'plugin' | 'skill';
|
|
659
687
|
|
package/src/api/websocket.ts
CHANGED
|
@@ -30,6 +30,7 @@ export class ConfigWebSocket {
|
|
|
30
30
|
|
|
31
31
|
this.ws.onclose = () => {
|
|
32
32
|
console.log('WebSocket disconnected');
|
|
33
|
+
this.emit({ type: 'connection.close', payload: {} });
|
|
33
34
|
if (!this.manualClose) {
|
|
34
35
|
this.scheduleReconnect();
|
|
35
36
|
}
|
|
@@ -37,6 +38,7 @@ export class ConfigWebSocket {
|
|
|
37
38
|
|
|
38
39
|
this.ws.onerror = (error) => {
|
|
39
40
|
console.error('WebSocket error:', error);
|
|
41
|
+
this.emit({ type: 'connection.error', payload: { message: 'websocket error' } });
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
+
import { Input } from '@/components/ui/input';
|
|
5
|
+
import { useLoginAuth } from '@/hooks/use-auth';
|
|
6
|
+
import { t } from '@/lib/i18n';
|
|
7
|
+
|
|
8
|
+
type LoginPageProps = {
|
|
9
|
+
username?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function LoginPage({ username }: LoginPageProps) {
|
|
13
|
+
const loginMutation = useLoginAuth();
|
|
14
|
+
const [formUsername, setFormUsername] = useState(username ?? '');
|
|
15
|
+
const [password, setPassword] = useState('');
|
|
16
|
+
const canSubmit = formUsername.trim().length > 0 && password.length > 0 && !loginMutation.isPending;
|
|
17
|
+
|
|
18
|
+
const handleSubmit = (event?: FormEvent<HTMLFormElement>) => {
|
|
19
|
+
event?.preventDefault();
|
|
20
|
+
if (!canSubmit) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
loginMutation.mutate({
|
|
24
|
+
username: formUsername.trim(),
|
|
25
|
+
password
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<main className="flex min-h-screen items-center justify-center bg-secondary px-6 py-10">
|
|
31
|
+
<Card hover={false} className="w-full max-w-md shadow-card-hover">
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-gray-500">{t('authBrand')}</p>
|
|
34
|
+
<CardTitle className="text-2xl">{t('authLoginTitle')}</CardTitle>
|
|
35
|
+
<CardDescription>{t('authLoginDescription')}</CardDescription>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent>
|
|
38
|
+
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
39
|
+
<div className="space-y-2">
|
|
40
|
+
<label className="text-sm font-medium text-gray-800">{t('authUsername')}</label>
|
|
41
|
+
<Input
|
|
42
|
+
value={formUsername}
|
|
43
|
+
onChange={(event) => setFormUsername(event.target.value)}
|
|
44
|
+
placeholder={t('authUsernamePlaceholder')}
|
|
45
|
+
autoFocus
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="space-y-2">
|
|
49
|
+
<label className="text-sm font-medium text-gray-800">{t('authPassword')}</label>
|
|
50
|
+
<Input
|
|
51
|
+
type="password"
|
|
52
|
+
value={password}
|
|
53
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
54
|
+
placeholder={t('authPasswordPlaceholder')}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<Button
|
|
58
|
+
type="submit"
|
|
59
|
+
className="w-full"
|
|
60
|
+
disabled={!canSubmit}
|
|
61
|
+
>
|
|
62
|
+
{loginMutation.isPending ? t('authLoggingIn') : t('authLoginAction')}
|
|
63
|
+
</Button>
|
|
64
|
+
</form>
|
|
65
|
+
</CardContent>
|
|
66
|
+
</Card>
|
|
67
|
+
</main>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { useStickyBottomScroll } from '@nextclaw/agent-chat-ui';
|
|
2
3
|
import { Button } from '@/components/ui/button';
|
|
3
|
-
import {
|
|
4
|
+
import { ChatInputBarContainer, ChatMessageListContainer } from '@/components/chat/nextclaw';
|
|
4
5
|
import { ChatWelcome } from '@/components/chat/ChatWelcome';
|
|
5
|
-
import { ChatInputBarView } from '@/components/chat/chat-input/ChatInputBarView';
|
|
6
6
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
7
7
|
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
8
8
|
import { t } from '@/lib/i18n';
|
|
9
9
|
import { cn } from '@/lib/utils';
|
|
10
10
|
import { Trash2 } from 'lucide-react';
|
|
11
11
|
|
|
12
|
-
const STICKY_BOTTOM_THRESHOLD_PX = 10;
|
|
13
|
-
|
|
14
12
|
function ChatConversationSkeleton() {
|
|
15
13
|
return (
|
|
16
14
|
<section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
@@ -45,12 +43,6 @@ export function ChatConversationPanel() {
|
|
|
45
43
|
const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
|
|
46
44
|
const threadRef = snapshot.threadRef ?? fallbackThreadRef;
|
|
47
45
|
|
|
48
|
-
// --- Sticky-to-bottom scroll state ---
|
|
49
|
-
const isStickyRef = useRef(true);
|
|
50
|
-
const isProgrammaticScrollRef = useRef(false);
|
|
51
|
-
const previousSessionKeyRef = useRef<string | null>(null);
|
|
52
|
-
const pendingInitialScrollRef = useRef(false);
|
|
53
|
-
|
|
54
46
|
const showWelcome = !snapshot.selectedSessionKey && snapshot.uiMessages.length === 0 && !snapshot.isSending;
|
|
55
47
|
const hasConfiguredModel = snapshot.modelOptions.length > 0;
|
|
56
48
|
const shouldShowProviderHint = snapshot.isProviderStateResolved && !hasConfiguredModel;
|
|
@@ -60,47 +52,13 @@ export function ChatConversationPanel() {
|
|
|
60
52
|
!snapshot.isSending &&
|
|
61
53
|
!snapshot.isAwaitingAssistantOutput;
|
|
62
54
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const handleScroll = useCallback(() => {
|
|
71
|
-
// Skip sticky check for programmatic scrolls
|
|
72
|
-
if (isProgrammaticScrollRef.current) {
|
|
73
|
-
isProgrammaticScrollRef.current = false;
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const el = threadRef.current;
|
|
77
|
-
if (!el) return;
|
|
78
|
-
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
79
|
-
isStickyRef.current = distanceFromBottom <= STICKY_BOTTOM_THRESHOLD_PX;
|
|
80
|
-
}, [threadRef]);
|
|
81
|
-
|
|
82
|
-
// Session change → force sticky + schedule initial scroll
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (previousSessionKeyRef.current === snapshot.selectedSessionKey) return;
|
|
85
|
-
previousSessionKeyRef.current = snapshot.selectedSessionKey;
|
|
86
|
-
isStickyRef.current = true;
|
|
87
|
-
pendingInitialScrollRef.current = true;
|
|
88
|
-
}, [snapshot.selectedSessionKey]);
|
|
89
|
-
|
|
90
|
-
// Initial scroll after history loads for a new session
|
|
91
|
-
useLayoutEffect(() => {
|
|
92
|
-
if (!pendingInitialScrollRef.current) return;
|
|
93
|
-
if (snapshot.isHistoryLoading || snapshot.uiMessages.length === 0) return;
|
|
94
|
-
pendingInitialScrollRef.current = false;
|
|
95
|
-
scrollToBottom();
|
|
96
|
-
}, [scrollToBottom, snapshot.isHistoryLoading, snapshot.uiMessages]);
|
|
97
|
-
|
|
98
|
-
// Streaming updates: keep bottom visible while still sticky.
|
|
99
|
-
useLayoutEffect(() => {
|
|
100
|
-
if (!isStickyRef.current) return;
|
|
101
|
-
if (snapshot.uiMessages.length === 0) return;
|
|
102
|
-
scrollToBottom();
|
|
103
|
-
}, [scrollToBottom, snapshot.uiMessages]);
|
|
55
|
+
const { onScroll: handleScroll } = useStickyBottomScroll({
|
|
56
|
+
scrollRef: threadRef,
|
|
57
|
+
resetKey: snapshot.selectedSessionKey,
|
|
58
|
+
isLoading: snapshot.isHistoryLoading,
|
|
59
|
+
hasContent: snapshot.uiMessages.length > 0,
|
|
60
|
+
contentVersion: snapshot.uiMessages
|
|
61
|
+
});
|
|
104
62
|
|
|
105
63
|
if (!snapshot.isProviderStateResolved) {
|
|
106
64
|
return <ChatConversationSkeleton />;
|
|
@@ -160,12 +118,12 @@ export function ChatConversationPanel() {
|
|
|
160
118
|
<div className="px-5 py-5 text-sm text-gray-500">{t('chatNoMessages')}</div>
|
|
161
119
|
) : (
|
|
162
120
|
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
|
|
163
|
-
<
|
|
121
|
+
<ChatMessageListContainer uiMessages={snapshot.uiMessages} isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput} />
|
|
164
122
|
</div>
|
|
165
123
|
)}
|
|
166
124
|
</div>
|
|
167
125
|
|
|
168
|
-
<
|
|
126
|
+
<ChatInputBarContainer />
|
|
169
127
|
</section>
|
|
170
128
|
);
|
|
171
129
|
}
|
|
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import type { SessionEntryView } from '@/api/types';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
4
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
5
|
+
import { StatusBadge } from '@/components/common/StatusBadge';
|
|
5
6
|
import { Input } from '@/components/ui/input';
|
|
6
7
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
7
8
|
import { SessionRunBadge } from '@/components/common/SessionRunBadge';
|
|
@@ -14,6 +15,7 @@ import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
|
14
15
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
15
16
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
16
17
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
18
|
+
import { useUiStore } from '@/stores/ui.store';
|
|
17
19
|
import { NavLink } from 'react-router-dom';
|
|
18
20
|
import { AlarmClock, BookOpen, BrainCircuit, Languages, MessageSquareText, Palette, Plus, Search, Settings } from 'lucide-react';
|
|
19
21
|
|
|
@@ -72,6 +74,7 @@ export function ChatSidebar() {
|
|
|
72
74
|
const docBrowser = useDocBrowser();
|
|
73
75
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
74
76
|
const runSnapshot = useChatRunStatusStore((state) => state.snapshot);
|
|
77
|
+
const connectionStatus = useUiStore((state) => state.connectionStatus);
|
|
75
78
|
const { language, setLanguage } = useI18n();
|
|
76
79
|
const { theme, setTheme } = useTheme();
|
|
77
80
|
const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
|
|
@@ -88,7 +91,10 @@ export function ChatSidebar() {
|
|
|
88
91
|
return (
|
|
89
92
|
<aside className="w-[280px] shrink-0 flex flex-col h-full bg-secondary border-r border-gray-200/60">
|
|
90
93
|
<div className="px-5 pt-5 pb-3">
|
|
91
|
-
<BrandHeader
|
|
94
|
+
<BrandHeader
|
|
95
|
+
className="flex items-center gap-2.5 min-w-0"
|
|
96
|
+
suffix={<StatusBadge status={connectionStatus} />}
|
|
97
|
+
/>
|
|
92
98
|
</div>
|
|
93
99
|
|
|
94
100
|
<div className="px-4 pb-3">
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { buildChatSlashItems, buildSelectedSkillItems, buildSkillPickerModel } from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
2
|
+
import type { ChatSkillRecord } from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
3
|
+
|
|
4
|
+
function createSkillRecord(partial: Partial<ChatSkillRecord>): ChatSkillRecord {
|
|
5
|
+
return {
|
|
6
|
+
key: 'demo.skill',
|
|
7
|
+
label: 'Demo Skill',
|
|
8
|
+
...partial
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('buildChatSlashItems', () => {
|
|
13
|
+
const texts = {
|
|
14
|
+
slashSkillSubtitle: 'Skill',
|
|
15
|
+
slashSkillSpecLabel: 'Spec',
|
|
16
|
+
noSkillDescription: 'No description'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it('sorts exact spec matches ahead of weaker matches', () => {
|
|
20
|
+
const items = buildChatSlashItems(
|
|
21
|
+
[
|
|
22
|
+
createSkillRecord({ key: 'web-search', label: 'Web Search' }),
|
|
23
|
+
createSkillRecord({ key: 'weather', label: 'Web Weather' })
|
|
24
|
+
],
|
|
25
|
+
'web',
|
|
26
|
+
texts
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(items.map((item) => item.value)).toEqual(['web-search', 'weather']);
|
|
30
|
+
expect(items[0]?.detailLines).toContain('Spec: web-search');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns an empty list when nothing matches', () => {
|
|
34
|
+
const items = buildChatSlashItems([createSkillRecord({ key: 'weather' })], 'terminal', texts);
|
|
35
|
+
expect(items).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('buildSelectedSkillItems', () => {
|
|
40
|
+
it('keeps selected specs and resolves labels when available', () => {
|
|
41
|
+
const chips = buildSelectedSkillItems(
|
|
42
|
+
['web-search', 'missing-skill'],
|
|
43
|
+
[createSkillRecord({ key: 'web-search', label: 'Web Search' })]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(chips).toEqual([
|
|
47
|
+
{ key: 'web-search', label: 'Web Search' },
|
|
48
|
+
{ key: 'missing-skill', label: 'missing-skill' }
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('buildSkillPickerModel', () => {
|
|
54
|
+
it('builds a stable semantic model for toolbar skill picker', () => {
|
|
55
|
+
const onSelectedKeysChange = vi.fn();
|
|
56
|
+
const model = buildSkillPickerModel({
|
|
57
|
+
skillRecords: [createSkillRecord({ key: 'web-search', label: 'Web Search', description: 'Search web' })],
|
|
58
|
+
selectedSkills: ['web-search'],
|
|
59
|
+
isLoading: false,
|
|
60
|
+
onSelectedKeysChange,
|
|
61
|
+
texts: {
|
|
62
|
+
title: 'Skills',
|
|
63
|
+
searchPlaceholder: 'Search skills',
|
|
64
|
+
emptyLabel: 'No skills',
|
|
65
|
+
loadingLabel: 'Loading',
|
|
66
|
+
manageLabel: 'Manage'
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(model).toMatchObject({
|
|
71
|
+
title: 'Skills',
|
|
72
|
+
selectedKeys: ['web-search'],
|
|
73
|
+
manageHref: '/marketplace/skills'
|
|
74
|
+
});
|
|
75
|
+
expect(model.options[0]).toMatchObject({
|
|
76
|
+
key: 'web-search',
|
|
77
|
+
label: 'Web Search'
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|