@nextclaw/ui 0.6.6 → 0.6.8
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-DH5fzlPu.js +1 -0
- package/dist/assets/ChatPage-BrLCnJSb.js +34 -0
- package/dist/assets/DocBrowser-DPQHJVsZ.js +1 -0
- package/dist/assets/LogoBadge-FEb4_vSq.js +1 -0
- package/dist/assets/{MarketplacePage--wFfsNH0.js → MarketplacePage-BAVXYeZA.js} +2 -2
- package/dist/assets/ModelConfig-BqPXe7nw.js +1 -0
- package/dist/assets/ProvidersList-vpKPuIxV.js +1 -0
- package/dist/assets/RuntimeConfig-DTYSU4_d.js +1 -0
- package/dist/assets/{SecretsConfig-B25P3J7V.js → SecretsConfig-nNzs3YDm.js} +2 -2
- package/dist/assets/SessionsConfig-CHjeyqEQ.js +2 -0
- package/dist/assets/{card-CCSDsedj.js → card-73MmEZi7.js} +1 -1
- package/dist/assets/index-CTLvVlk8.js +7 -0
- package/dist/assets/index-DI6BuShn.css +1 -0
- package/dist/assets/input-1MCMs6Yf.js +1 -0
- package/dist/assets/{label-BxzAKPzU.js → label-C4Q8RlBJ.js} +1 -1
- package/dist/assets/{page-layout-DaLNSFKw.js → page-layout-CK0vcVmV.js} +1 -1
- package/dist/assets/session-run-status-BaNlKvi6.js +5 -0
- package/dist/assets/{switch-DHOCEi5L.js → switch-Bf8w_cF1.js} +1 -1
- package/dist/assets/{tabs-custom-zdFy3fnK.js → tabs-custom-B6Gw8gax.js} +1 -1
- package/dist/assets/{useConfirmDialog-D3ZVa92J.js → useConfirmDialog-B5CZ4EDN.js} +1 -1
- package/dist/assets/{vendor-Dj2ULvht.js → vendor-C--HHaLf.js} +6 -6
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/api/config.ts +53 -0
- package/src/api/types.ts +48 -0
- package/src/components/chat/ChatInputBar.tsx +341 -24
- package/src/components/chat/ChatPage.tsx +28 -12
- package/src/components/chat/ChatSidebar.tsx +12 -7
- package/src/components/common/BrandHeader.tsx +23 -0
- package/src/components/common/SessionRunBadge.tsx +23 -0
- package/src/components/config/ProviderForm.tsx +193 -29
- package/src/components/config/ProvidersList.tsx +1 -2
- package/src/components/config/SessionsConfig.tsx +22 -2
- package/src/components/layout/Sidebar.tsx +2 -6
- package/src/hooks/useConfig.ts +31 -0
- package/src/lib/i18n.ts +28 -1
- package/src/lib/logos.ts +0 -19
- package/src/lib/session-run-status.ts +63 -0
- package/dist/assets/ChannelsList-VqzbAMCc.js +0 -1
- package/dist/assets/ChatPage-CjZqsBmn.js +0 -34
- package/dist/assets/DocBrowser-DvU-iUeB.js +0 -1
- package/dist/assets/ModelConfig-cY5UsbfA.js +0 -1
- package/dist/assets/ProvidersList-qZwaFoFt.js +0 -1
- package/dist/assets/RuntimeConfig-BY2Axlte.js +0 -1
- package/dist/assets/SessionsConfig-CxA9gIBw.js +0 -2
- package/dist/assets/chat-message-pw9oafI4.js +0 -5
- package/dist/assets/index-CD8a2KMH.js +0 -2
- package/dist/assets/index-DKOXGZc8.css +0 -1
- package/dist/assets/logos-C3oHQ9kv.js +0 -1
- package/dist/assets/useConfig-CDl9UK5m.js +0 -6
package/package.json
CHANGED
package/src/api/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { api, API_BASE } from './client';
|
|
2
2
|
import type {
|
|
3
|
+
AppMetaView,
|
|
3
4
|
ConfigView,
|
|
4
5
|
ConfigMetaView,
|
|
5
6
|
ConfigSchemaResponse,
|
|
@@ -8,6 +9,10 @@ import type {
|
|
|
8
9
|
ProviderConfigUpdate,
|
|
9
10
|
ProviderConnectionTestRequest,
|
|
10
11
|
ProviderConnectionTestResult,
|
|
12
|
+
ProviderAuthStartResult,
|
|
13
|
+
ProviderAuthPollRequest,
|
|
14
|
+
ProviderAuthPollResult,
|
|
15
|
+
ProviderAuthImportResult,
|
|
11
16
|
ProviderCreateRequest,
|
|
12
17
|
ProviderCreateResult,
|
|
13
18
|
ProviderDeleteResult,
|
|
@@ -36,6 +41,15 @@ import type {
|
|
|
36
41
|
ChatTurnStreamSessionEvent
|
|
37
42
|
} from './types';
|
|
38
43
|
|
|
44
|
+
// GET /api/app/meta
|
|
45
|
+
export async function fetchAppMeta(): Promise<AppMetaView> {
|
|
46
|
+
const response = await api.get<AppMetaView>('/api/app/meta');
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(response.error.message);
|
|
49
|
+
}
|
|
50
|
+
return response.data;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
// GET /api/config
|
|
40
54
|
export async function fetchConfig(): Promise<ConfigView> {
|
|
41
55
|
const response = await api.get<ConfigView>('/api/config');
|
|
@@ -129,6 +143,45 @@ export async function testProviderConnection(
|
|
|
129
143
|
return response.data;
|
|
130
144
|
}
|
|
131
145
|
|
|
146
|
+
// POST /api/config/providers/:provider/auth/start
|
|
147
|
+
export async function startProviderAuth(provider: string): Promise<ProviderAuthStartResult> {
|
|
148
|
+
const response = await api.post<ProviderAuthStartResult>(
|
|
149
|
+
`/api/config/providers/${provider}/auth/start`,
|
|
150
|
+
{}
|
|
151
|
+
);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(response.error.message);
|
|
154
|
+
}
|
|
155
|
+
return response.data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// POST /api/config/providers/:provider/auth/poll
|
|
159
|
+
export async function pollProviderAuth(
|
|
160
|
+
provider: string,
|
|
161
|
+
data: ProviderAuthPollRequest
|
|
162
|
+
): Promise<ProviderAuthPollResult> {
|
|
163
|
+
const response = await api.post<ProviderAuthPollResult>(
|
|
164
|
+
`/api/config/providers/${provider}/auth/poll`,
|
|
165
|
+
data
|
|
166
|
+
);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(response.error.message);
|
|
169
|
+
}
|
|
170
|
+
return response.data;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// POST /api/config/providers/:provider/auth/import-cli
|
|
174
|
+
export async function importProviderAuthFromCli(provider: string): Promise<ProviderAuthImportResult> {
|
|
175
|
+
const response = await api.post<ProviderAuthImportResult>(
|
|
176
|
+
`/api/config/providers/${provider}/auth/import-cli`,
|
|
177
|
+
{}
|
|
178
|
+
);
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(response.error.message);
|
|
181
|
+
}
|
|
182
|
+
return response.data;
|
|
183
|
+
}
|
|
184
|
+
|
|
132
185
|
// PUT /api/config/channels/:channel
|
|
133
186
|
export async function updateChannel(
|
|
134
187
|
channel: string,
|
package/src/api/types.ts
CHANGED
|
@@ -9,6 +9,11 @@ export type ApiResponse<T> =
|
|
|
9
9
|
| { ok: true; data: T }
|
|
10
10
|
| { ok: false; error: ApiError };
|
|
11
11
|
|
|
12
|
+
export type AppMetaView = {
|
|
13
|
+
name: string;
|
|
14
|
+
productVersion: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
12
17
|
export type ProviderConfigView = {
|
|
13
18
|
displayName?: string;
|
|
14
19
|
apiKeySet: boolean;
|
|
@@ -69,6 +74,35 @@ export type ProviderConnectionTestResult = {
|
|
|
69
74
|
hint?: string;
|
|
70
75
|
};
|
|
71
76
|
|
|
77
|
+
export type ProviderAuthStartResult = {
|
|
78
|
+
provider: string;
|
|
79
|
+
kind: "device_code";
|
|
80
|
+
sessionId: string;
|
|
81
|
+
verificationUri: string;
|
|
82
|
+
userCode: string;
|
|
83
|
+
expiresAt: string;
|
|
84
|
+
intervalMs: number;
|
|
85
|
+
note?: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ProviderAuthPollRequest = {
|
|
89
|
+
sessionId: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type ProviderAuthPollResult = {
|
|
93
|
+
provider: string;
|
|
94
|
+
status: "pending" | "authorized" | "denied" | "expired" | "error";
|
|
95
|
+
message?: string;
|
|
96
|
+
nextPollMs?: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type ProviderAuthImportResult = {
|
|
100
|
+
provider: string;
|
|
101
|
+
status: "imported";
|
|
102
|
+
source: "cli";
|
|
103
|
+
expiresAt?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
72
106
|
export type AgentProfileView = {
|
|
73
107
|
id: string;
|
|
74
108
|
default?: boolean;
|
|
@@ -394,6 +428,20 @@ export type ProviderSpecView = {
|
|
|
394
428
|
isGateway?: boolean;
|
|
395
429
|
isLocal?: boolean;
|
|
396
430
|
defaultApiBase?: string;
|
|
431
|
+
logo?: string;
|
|
432
|
+
apiBaseHelp?: {
|
|
433
|
+
en?: string;
|
|
434
|
+
zh?: string;
|
|
435
|
+
};
|
|
436
|
+
auth?: {
|
|
437
|
+
kind: "device_code";
|
|
438
|
+
displayName?: string;
|
|
439
|
+
note?: {
|
|
440
|
+
en?: string;
|
|
441
|
+
zh?: string;
|
|
442
|
+
};
|
|
443
|
+
supportsCliImport?: boolean;
|
|
444
|
+
};
|
|
397
445
|
defaultModels?: string[];
|
|
398
446
|
supportsWireApi?: boolean;
|
|
399
447
|
wireApiOptions?: Array<"auto" | "chat" | "responses">;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
1
2
|
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover';
|
|
2
4
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
3
5
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
4
6
|
import { SkillsPicker } from '@/components/chat/SkillsPicker';
|
|
@@ -6,6 +8,8 @@ import type { MarketplaceInstalledRecord } from '@/api/types';
|
|
|
6
8
|
import { t } from '@/lib/i18n';
|
|
7
9
|
import { Paperclip, Send, Sparkles, Square, X } from 'lucide-react';
|
|
8
10
|
|
|
11
|
+
const SLASH_PANEL_MAX_WIDTH = 920;
|
|
12
|
+
|
|
9
13
|
export type ChatModelOption = {
|
|
10
14
|
value: string;
|
|
11
15
|
modelLabel: string;
|
|
@@ -33,6 +37,91 @@ type ChatInputBarProps = {
|
|
|
33
37
|
onSelectedSkillsChange: (next: string[]) => void;
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
type SlashPanelItem = {
|
|
41
|
+
kind: 'skill';
|
|
42
|
+
key: string;
|
|
43
|
+
title: string;
|
|
44
|
+
subtitle: string;
|
|
45
|
+
description: string;
|
|
46
|
+
detailLines: string[];
|
|
47
|
+
skillSpec?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type RankedSkill = {
|
|
51
|
+
record: MarketplaceInstalledRecord;
|
|
52
|
+
score: number;
|
|
53
|
+
order: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function resolveSlashQuery(draft: string): string | null {
|
|
57
|
+
const match = /^\/([^\s]*)$/.exec(draft);
|
|
58
|
+
if (!match) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return (match[1] ?? '').trim().toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeSearchText(value: string | null | undefined): string {
|
|
65
|
+
return (value ?? '').trim().toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSubsequenceMatch(query: string, target: string): boolean {
|
|
69
|
+
if (!query || !target) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
let pointer = 0;
|
|
73
|
+
for (const char of target) {
|
|
74
|
+
if (char === query[pointer]) {
|
|
75
|
+
pointer += 1;
|
|
76
|
+
if (pointer >= query.length) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scoreSkillRecord(record: MarketplaceInstalledRecord, query: string): number {
|
|
85
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
86
|
+
if (!normalizedQuery) {
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const spec = normalizeSearchText(record.spec);
|
|
91
|
+
const label = normalizeSearchText(record.label || record.spec);
|
|
92
|
+
const description = normalizeSearchText(`${record.descriptionZh ?? ''} ${record.description ?? ''}`);
|
|
93
|
+
const labelTokens = label.split(/[\s/_-]+/).filter(Boolean);
|
|
94
|
+
|
|
95
|
+
if (spec === normalizedQuery) {
|
|
96
|
+
return 1200;
|
|
97
|
+
}
|
|
98
|
+
if (label === normalizedQuery) {
|
|
99
|
+
return 1150;
|
|
100
|
+
}
|
|
101
|
+
if (spec.startsWith(normalizedQuery)) {
|
|
102
|
+
return 1000;
|
|
103
|
+
}
|
|
104
|
+
if (label.startsWith(normalizedQuery)) {
|
|
105
|
+
return 950;
|
|
106
|
+
}
|
|
107
|
+
if (labelTokens.some((token) => token.startsWith(normalizedQuery))) {
|
|
108
|
+
return 900;
|
|
109
|
+
}
|
|
110
|
+
if (spec.includes(normalizedQuery)) {
|
|
111
|
+
return 800;
|
|
112
|
+
}
|
|
113
|
+
if (label.includes(normalizedQuery)) {
|
|
114
|
+
return 760;
|
|
115
|
+
}
|
|
116
|
+
if (description.includes(normalizedQuery)) {
|
|
117
|
+
return 500;
|
|
118
|
+
}
|
|
119
|
+
if (isSubsequenceMatch(normalizedQuery, label) || isSubsequenceMatch(normalizedQuery, spec)) {
|
|
120
|
+
return 300;
|
|
121
|
+
}
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
36
125
|
export function ChatInputBar({
|
|
37
126
|
isProviderStateResolved,
|
|
38
127
|
draft,
|
|
@@ -53,6 +142,11 @@ export function ChatInputBar({
|
|
|
53
142
|
selectedSkills,
|
|
54
143
|
onSelectedSkillsChange
|
|
55
144
|
}: ChatInputBarProps) {
|
|
145
|
+
const [activeSlashIndex, setActiveSlashIndex] = useState(0);
|
|
146
|
+
const [dismissedSlashPanel, setDismissedSlashPanel] = useState(false);
|
|
147
|
+
const [slashPanelWidth, setSlashPanelWidth] = useState<number | null>(null);
|
|
148
|
+
const slashAnchorRef = useRef<HTMLDivElement | null>(null);
|
|
149
|
+
const slashListRef = useRef<HTMLDivElement | null>(null);
|
|
56
150
|
const hasModelOptions = modelOptions.length > 0;
|
|
57
151
|
const isModelOptionsLoading = !isProviderStateResolved && !hasModelOptions;
|
|
58
152
|
const isModelOptionsEmpty = isProviderStateResolved && !hasModelOptions;
|
|
@@ -69,36 +163,259 @@ export function ChatInputBar({
|
|
|
69
163
|
label: matched?.label || spec
|
|
70
164
|
};
|
|
71
165
|
});
|
|
166
|
+
const slashQuery = useMemo(() => resolveSlashQuery(draft), [draft]);
|
|
167
|
+
const startsWithSlash = draft.startsWith('/');
|
|
168
|
+
const normalizedSlashQuery = slashQuery ?? '';
|
|
169
|
+
const skillSortCollator = useMemo(
|
|
170
|
+
() => new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }),
|
|
171
|
+
[]
|
|
172
|
+
);
|
|
173
|
+
const skillSlashItems = useMemo<SlashPanelItem[]>(() => {
|
|
174
|
+
const rankedRecords: RankedSkill[] = skillRecords
|
|
175
|
+
.map((record, order) => ({
|
|
176
|
+
record,
|
|
177
|
+
score: scoreSkillRecord(record, normalizedSlashQuery),
|
|
178
|
+
order
|
|
179
|
+
}))
|
|
180
|
+
.filter((entry) => entry.score > 0)
|
|
181
|
+
.sort((left, right) => {
|
|
182
|
+
if (right.score !== left.score) {
|
|
183
|
+
return right.score - left.score;
|
|
184
|
+
}
|
|
185
|
+
const leftLabel = (left.record.label || left.record.spec).trim();
|
|
186
|
+
const rightLabel = (right.record.label || right.record.spec).trim();
|
|
187
|
+
const labelCompare = skillSortCollator.compare(leftLabel, rightLabel);
|
|
188
|
+
if (labelCompare !== 0) {
|
|
189
|
+
return labelCompare;
|
|
190
|
+
}
|
|
191
|
+
return left.order - right.order;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return rankedRecords
|
|
195
|
+
.map((entry) => entry.record)
|
|
196
|
+
.map((record) => ({
|
|
197
|
+
kind: 'skill',
|
|
198
|
+
key: `skill:${record.spec}`,
|
|
199
|
+
title: record.label || record.spec,
|
|
200
|
+
subtitle: t('chatSlashTypeSkill'),
|
|
201
|
+
description: (record.descriptionZh ?? record.description ?? '').trim() || t('chatSkillsPickerNoDescription'),
|
|
202
|
+
detailLines: [`${t('chatSlashSkillSpec')}: ${record.spec}`],
|
|
203
|
+
skillSpec: record.spec
|
|
204
|
+
}));
|
|
205
|
+
}, [normalizedSlashQuery, skillRecords, skillSortCollator]);
|
|
206
|
+
const slashItems = useMemo(() => [...skillSlashItems], [skillSlashItems]);
|
|
207
|
+
const isSlashPanelOpen = slashQuery !== null && !dismissedSlashPanel;
|
|
208
|
+
const activeSlashItem = slashItems[activeSlashIndex] ?? null;
|
|
209
|
+
const isSlashPanelLoading = isSkillsLoading;
|
|
210
|
+
const resolvedSlashPanelWidth = slashPanelWidth ? Math.min(slashPanelWidth, SLASH_PANEL_MAX_WIDTH) : undefined;
|
|
211
|
+
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const anchor = slashAnchorRef.current;
|
|
214
|
+
if (!anchor || typeof ResizeObserver === 'undefined') {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const update = () => {
|
|
218
|
+
setSlashPanelWidth(anchor.getBoundingClientRect().width);
|
|
219
|
+
};
|
|
220
|
+
update();
|
|
221
|
+
const observer = new ResizeObserver(() => update());
|
|
222
|
+
observer.observe(anchor);
|
|
223
|
+
return () => {
|
|
224
|
+
observer.disconnect();
|
|
225
|
+
};
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (!isSlashPanelOpen) {
|
|
230
|
+
setActiveSlashIndex(0);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (slashItems.length === 0) {
|
|
234
|
+
setActiveSlashIndex(0);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
setActiveSlashIndex((current) => {
|
|
238
|
+
if (current < 0) {
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
if (current >= slashItems.length) {
|
|
242
|
+
return slashItems.length - 1;
|
|
243
|
+
}
|
|
244
|
+
return current;
|
|
245
|
+
});
|
|
246
|
+
}, [isSlashPanelOpen, slashItems.length]);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (!startsWithSlash && dismissedSlashPanel) {
|
|
250
|
+
setDismissedSlashPanel(false);
|
|
251
|
+
}
|
|
252
|
+
}, [dismissedSlashPanel, startsWithSlash]);
|
|
253
|
+
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (!isSlashPanelOpen || isSlashPanelLoading || slashItems.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const container = slashListRef.current;
|
|
259
|
+
if (!container) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const active = container.querySelector<HTMLElement>(`[data-slash-index="${activeSlashIndex}"]`);
|
|
263
|
+
active?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
264
|
+
}, [activeSlashIndex, isSlashPanelLoading, isSlashPanelOpen, slashItems.length]);
|
|
265
|
+
|
|
266
|
+
const handleSelectSlashItem = useCallback((item: SlashPanelItem) => {
|
|
267
|
+
if (item.kind === 'skill' && item.skillSpec) {
|
|
268
|
+
if (!selectedSkills.includes(item.skillSpec)) {
|
|
269
|
+
onSelectedSkillsChange([...selectedSkills, item.skillSpec]);
|
|
270
|
+
}
|
|
271
|
+
onDraftChange('');
|
|
272
|
+
setDismissedSlashPanel(false);
|
|
273
|
+
}
|
|
274
|
+
}, [onDraftChange, onSelectedSkillsChange, selectedSkills]);
|
|
275
|
+
|
|
276
|
+
const handleSlashPanelOpenChange = useCallback((open: boolean) => {
|
|
277
|
+
if (!open) {
|
|
278
|
+
setDismissedSlashPanel(true);
|
|
279
|
+
}
|
|
280
|
+
}, []);
|
|
72
281
|
|
|
73
282
|
return (
|
|
74
283
|
<div className="border-t border-gray-200/80 bg-white p-4">
|
|
75
284
|
<div className="mx-auto w-full max-w-[min(1120px,100%)]">
|
|
76
285
|
<div className="rounded-2xl border border-gray-200 bg-white shadow-card overflow-hidden">
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
e.
|
|
85
|
-
|
|
86
|
-
|
|
286
|
+
<div className="relative">
|
|
287
|
+
{/* Textarea */}
|
|
288
|
+
<textarea
|
|
289
|
+
value={draft}
|
|
290
|
+
onChange={(e) => onDraftChange(e.target.value)}
|
|
291
|
+
disabled={inputDisabled}
|
|
292
|
+
onKeyDown={(e) => {
|
|
293
|
+
if (isSlashPanelOpen && !e.nativeEvent.isComposing && (e.key === ' ' || e.code === 'Space')) {
|
|
294
|
+
setDismissedSlashPanel(true);
|
|
295
|
+
}
|
|
296
|
+
if (isSlashPanelOpen && slashItems.length > 0) {
|
|
297
|
+
if (e.key === 'ArrowDown') {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
setActiveSlashIndex((current) => (current + 1) % slashItems.length);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (e.key === 'ArrowUp') {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
setActiveSlashIndex((current) => (current - 1 + slashItems.length) % slashItems.length);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if ((e.key === 'Enter' && !e.shiftKey) || e.key === 'Tab') {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
const selected = slashItems[activeSlashIndex];
|
|
310
|
+
if (selected) {
|
|
311
|
+
handleSelectSlashItem(selected);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (e.key === 'Escape') {
|
|
317
|
+
if (isSlashPanelOpen) {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
setDismissedSlashPanel(true);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (isSending && canStopGeneration) {
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
void onStop();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
void onSend();
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
placeholder={
|
|
334
|
+
isModelOptionsLoading
|
|
335
|
+
? ''
|
|
336
|
+
: hasModelOptions
|
|
337
|
+
? t('chatInputPlaceholder')
|
|
338
|
+
: t('chatModelNoOptions')
|
|
87
339
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
340
|
+
className="w-full min-h-[68px] max-h-[220px] resize-y bg-transparent outline-none text-sm px-4 py-3 text-gray-800 placeholder:text-gray-400"
|
|
341
|
+
/>
|
|
342
|
+
<Popover open={isSlashPanelOpen} onOpenChange={handleSlashPanelOpenChange}>
|
|
343
|
+
<PopoverAnchor asChild>
|
|
344
|
+
<div ref={slashAnchorRef} className="pointer-events-none absolute left-3 right-3 bottom-full h-0" />
|
|
345
|
+
</PopoverAnchor>
|
|
346
|
+
<PopoverContent
|
|
347
|
+
side="top"
|
|
348
|
+
align="start"
|
|
349
|
+
sideOffset={10}
|
|
350
|
+
className="z-[70] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-2xl border border-gray-200 bg-white/95 p-0 shadow-2xl backdrop-blur-md"
|
|
351
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
352
|
+
style={resolvedSlashPanelWidth ? { width: `${resolvedSlashPanelWidth}px` } : undefined}
|
|
353
|
+
>
|
|
354
|
+
<div className="grid min-h-[240px] grid-cols-[minmax(260px,340px)_minmax(0,1fr)]">
|
|
355
|
+
<div ref={slashListRef} className="max-h-[320px] overflow-y-auto border-r border-gray-200 p-3 custom-scrollbar">
|
|
356
|
+
{isSlashPanelLoading ? (
|
|
357
|
+
<div className="p-2 text-xs text-gray-500">{t('chatSlashLoading')}</div>
|
|
358
|
+
) : (
|
|
359
|
+
<>
|
|
360
|
+
<div className="mb-2 px-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500">
|
|
361
|
+
{t('chatSlashSectionSkills')}
|
|
362
|
+
</div>
|
|
363
|
+
{skillSlashItems.length === 0 ? (
|
|
364
|
+
<div className="px-2 text-xs text-gray-400">{t('chatSlashNoResult')}</div>
|
|
365
|
+
) : (
|
|
366
|
+
<div className="space-y-1">
|
|
367
|
+
{skillSlashItems.map((item, index) => {
|
|
368
|
+
const isActive = index === activeSlashIndex;
|
|
369
|
+
return (
|
|
370
|
+
<button
|
|
371
|
+
key={item.key}
|
|
372
|
+
type="button"
|
|
373
|
+
data-slash-index={index}
|
|
374
|
+
onMouseEnter={() => setActiveSlashIndex(index)}
|
|
375
|
+
onClick={() => handleSelectSlashItem(item)}
|
|
376
|
+
className={`flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition ${
|
|
377
|
+
isActive ? 'bg-gray-100 text-gray-900' : 'text-gray-700 hover:bg-gray-50'
|
|
378
|
+
}`}
|
|
379
|
+
>
|
|
380
|
+
<span className="truncate text-xs font-semibold">{item.title}</span>
|
|
381
|
+
<span className="truncate text-xs text-gray-500">{item.subtitle}</span>
|
|
382
|
+
</button>
|
|
383
|
+
);
|
|
384
|
+
})}
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
<div className="p-4">
|
|
391
|
+
{activeSlashItem ? (
|
|
392
|
+
<div className="space-y-3">
|
|
393
|
+
<div className="flex items-center gap-2">
|
|
394
|
+
<span className="inline-flex rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-semibold text-primary">
|
|
395
|
+
{activeSlashItem.subtitle}
|
|
396
|
+
</span>
|
|
397
|
+
<span className="text-sm font-semibold text-gray-900">{activeSlashItem.title}</span>
|
|
398
|
+
</div>
|
|
399
|
+
<p className="text-xs leading-5 text-gray-600">{activeSlashItem.description}</p>
|
|
400
|
+
<div className="space-y-1">
|
|
401
|
+
{activeSlashItem.detailLines.map((line) => (
|
|
402
|
+
<div key={line} className="rounded-md bg-gray-50 px-2 py-1 text-[11px] text-gray-600">
|
|
403
|
+
{line}
|
|
404
|
+
</div>
|
|
405
|
+
))}
|
|
406
|
+
</div>
|
|
407
|
+
<div className="pt-1 text-[11px] text-gray-500">
|
|
408
|
+
{t('chatSlashSkillHint')}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
) : (
|
|
412
|
+
<div className="text-xs text-gray-500">{t('chatSlashHint')}</div>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</PopoverContent>
|
|
417
|
+
</Popover>
|
|
418
|
+
</div>
|
|
102
419
|
{isModelOptionsLoading && (
|
|
103
420
|
<div className="px-4 pb-2">
|
|
104
421
|
<div className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
useDeleteSession,
|
|
9
9
|
useSessionHistory,
|
|
10
10
|
useSessions,
|
|
11
|
-
useChatRuns
|
|
11
|
+
useChatRuns,
|
|
12
12
|
} from '@/hooks/useConfig';
|
|
13
13
|
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
14
14
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
@@ -20,6 +20,7 @@ import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
|
20
20
|
import { useChatStreamController } from '@/components/chat/useChatStreamController';
|
|
21
21
|
import { buildFallbackEventsFromMessages } from '@/lib/chat-message';
|
|
22
22
|
import { buildProviderModelCatalog, composeProviderModel } from '@/lib/provider-models';
|
|
23
|
+
import { buildActiveRunBySessionKey, buildSessionRunStatusByKey } from '@/lib/session-run-status';
|
|
23
24
|
import { t } from '@/lib/i18n';
|
|
24
25
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
25
26
|
|
|
@@ -208,11 +209,19 @@ function ChatPageLayout({ view, sidebarProps, conversationProps, confirmDialog }
|
|
|
208
209
|
<ChatConversationPanel {...conversationProps} />
|
|
209
210
|
) : (
|
|
210
211
|
<section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
211
|
-
|
|
212
|
-
<div className="
|
|
213
|
-
|
|
212
|
+
{view === 'cron' ? (
|
|
213
|
+
<div className="h-full overflow-auto custom-scrollbar">
|
|
214
|
+
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
|
|
215
|
+
<CronConfig />
|
|
216
|
+
</div>
|
|
214
217
|
</div>
|
|
215
|
-
|
|
218
|
+
) : (
|
|
219
|
+
<div className="h-full overflow-hidden">
|
|
220
|
+
<div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
|
|
221
|
+
<MarketplacePage forcedType="skills" />
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
216
225
|
</section>
|
|
217
226
|
)}
|
|
218
227
|
|
|
@@ -351,22 +360,28 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
351
360
|
refetchHistory: historyQuery.refetch
|
|
352
361
|
});
|
|
353
362
|
|
|
354
|
-
const
|
|
355
|
-
|
|
363
|
+
const sessionStatusRunsQuery = useChatRuns(
|
|
364
|
+
view === 'chat'
|
|
356
365
|
? {
|
|
357
|
-
sessionKey: selectedSessionKey,
|
|
358
366
|
states: ['queued', 'running'],
|
|
359
|
-
limit:
|
|
367
|
+
limit: 200
|
|
360
368
|
}
|
|
361
369
|
: undefined
|
|
362
370
|
);
|
|
371
|
+
const activeRunBySessionKey = useMemo(
|
|
372
|
+
() => buildActiveRunBySessionKey(sessionStatusRunsQuery.data?.runs ?? []),
|
|
373
|
+
[sessionStatusRunsQuery.data?.runs]
|
|
374
|
+
);
|
|
375
|
+
const sessionRunStatusByKey = useMemo(
|
|
376
|
+
() => buildSessionRunStatusByKey(activeRunBySessionKey),
|
|
377
|
+
[activeRunBySessionKey]
|
|
378
|
+
);
|
|
363
379
|
const activeRun = useMemo(() => {
|
|
364
|
-
const candidates = activeRunsQuery.data?.runs ?? [];
|
|
365
380
|
if (!selectedSessionKey) {
|
|
366
381
|
return null;
|
|
367
382
|
}
|
|
368
|
-
return
|
|
369
|
-
}, [
|
|
383
|
+
return activeRunBySessionKey.get(selectedSessionKey) ?? null;
|
|
384
|
+
}, [activeRunBySessionKey, selectedSessionKey]);
|
|
370
385
|
|
|
371
386
|
useEffect(() => {
|
|
372
387
|
if (view !== 'chat' || !selectedSessionKey || !activeRun) {
|
|
@@ -513,6 +528,7 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
513
528
|
|
|
514
529
|
const sidebarProps: ComponentProps<typeof ChatSidebar> = {
|
|
515
530
|
sessions,
|
|
531
|
+
sessionRunStatusByKey,
|
|
516
532
|
selectedSessionKey,
|
|
517
533
|
onSelectSession: handleSelectSession,
|
|
518
534
|
onCreateSession: createNewSession,
|