@nextclaw/ui 0.6.9 → 0.6.11
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/.eslintrc.cjs +10 -0
- package/CHANGELOG.md +15 -0
- package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
- package/dist/assets/ChatPage-DIx05c6s.js +36 -0
- package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
- package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
- package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
- package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
- package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
- package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
- package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
- package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
- package/dist/assets/index-Cn6_2To7.js +8 -0
- package/dist/assets/index-nEYGCJTC.css +1 -0
- package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
- package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
- package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
- package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
- package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
- package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
- package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
- package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/src/App.tsx +1 -2
- package/src/api/config.ts +205 -202
- package/src/api/types.ts +54 -24
- package/src/components/chat/ChatConversationPanel.tsx +102 -121
- package/src/components/chat/ChatPage.tsx +165 -437
- package/src/components/chat/ChatSidebar.tsx +30 -36
- package/src/components/chat/ChatThread.tsx +73 -131
- package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
- package/src/components/chat/chat-input.types.ts +15 -0
- package/src/components/chat/chat-page-data.ts +121 -0
- package/src/components/chat/chat-page-runtime.ts +221 -0
- package/src/components/chat/chat-session-route.ts +59 -0
- package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
- package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
- package/src/components/chat/chat-stream/transport.ts +159 -0
- package/src/components/chat/chat-stream/types.ts +76 -0
- package/src/components/chat/managers/chat-input.manager.ts +142 -0
- package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
- package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
- package/src/components/chat/managers/chat-thread.manager.ts +86 -0
- package/src/components/chat/managers/chat-ui.manager.ts +65 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
- package/src/components/chat/presenter/chat.presenter.ts +32 -0
- package/src/components/chat/stores/chat-input.store.ts +62 -0
- package/src/components/chat/stores/chat-run-status.store.ts +30 -0
- package/src/components/chat/stores/chat-session-list.store.ts +34 -0
- package/src/components/chat/stores/chat-thread.store.ts +52 -0
- package/src/components/chat/useChatRuntimeController.ts +134 -0
- package/src/components/chat/useChatSessionTypeState.ts +148 -0
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/config/ProviderForm.tsx +221 -14
- package/src/hooks/useConfig.ts +33 -2
- package/src/hooks/useObservable.ts +20 -0
- package/src/hooks/useWebSocket.ts +23 -1
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +11 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChatPage-iji0RkTR.js +0 -34
- package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
- package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
- package/dist/assets/index-C_DhisNo.css +0 -1
- package/dist/assets/index-dKTqKCJo.js +0 -7
- package/dist/assets/session-run-status-CllIZxNf.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
|
@@ -1,590 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { Button } from '@/components/ui/button';
|
|
3
|
-
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover';
|
|
4
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
5
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
6
|
-
import { SkillsPicker } from '@/components/chat/SkillsPicker';
|
|
7
|
-
import type { MarketplaceInstalledRecord } from '@/api/types';
|
|
8
|
-
import { t } from '@/lib/i18n';
|
|
9
|
-
import { Paperclip, Send, Sparkles, Square, X } from 'lucide-react';
|
|
10
|
-
|
|
11
|
-
const SLASH_PANEL_MAX_WIDTH = 920;
|
|
12
|
-
|
|
13
|
-
export type ChatModelOption = {
|
|
14
|
-
value: string;
|
|
15
|
-
modelLabel: string;
|
|
16
|
-
providerLabel: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type ChatInputBarProps = {
|
|
20
|
-
isProviderStateResolved: boolean;
|
|
21
|
-
draft: string;
|
|
22
|
-
onDraftChange: (value: string) => void;
|
|
23
|
-
onSend: () => Promise<void> | void;
|
|
24
|
-
onStop: () => Promise<void> | void;
|
|
25
|
-
onGoToProviders: () => void;
|
|
26
|
-
canStopGeneration: boolean;
|
|
27
|
-
stopDisabledReason?: string | null;
|
|
28
|
-
sendError?: string | null;
|
|
29
|
-
isSending: boolean;
|
|
30
|
-
queuedCount: number;
|
|
31
|
-
modelOptions: ChatModelOption[];
|
|
32
|
-
selectedModel: string;
|
|
33
|
-
onSelectedModelChange: (value: string) => void;
|
|
34
|
-
skillRecords: MarketplaceInstalledRecord[];
|
|
35
|
-
isSkillsLoading?: boolean;
|
|
36
|
-
selectedSkills: string[];
|
|
37
|
-
onSelectedSkillsChange: (next: string[]) => void;
|
|
38
|
-
};
|
|
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
|
-
|
|
125
|
-
export function ChatInputBar({
|
|
126
|
-
isProviderStateResolved,
|
|
127
|
-
draft,
|
|
128
|
-
onDraftChange,
|
|
129
|
-
onSend,
|
|
130
|
-
onStop,
|
|
131
|
-
onGoToProviders,
|
|
132
|
-
canStopGeneration,
|
|
133
|
-
stopDisabledReason = null,
|
|
134
|
-
sendError = null,
|
|
135
|
-
isSending,
|
|
136
|
-
queuedCount,
|
|
137
|
-
modelOptions,
|
|
138
|
-
selectedModel,
|
|
139
|
-
onSelectedModelChange,
|
|
140
|
-
skillRecords,
|
|
141
|
-
isSkillsLoading = false,
|
|
142
|
-
selectedSkills,
|
|
143
|
-
onSelectedSkillsChange
|
|
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);
|
|
150
|
-
const hasModelOptions = modelOptions.length > 0;
|
|
151
|
-
const isModelOptionsLoading = !isProviderStateResolved && !hasModelOptions;
|
|
152
|
-
const isModelOptionsEmpty = isProviderStateResolved && !hasModelOptions;
|
|
153
|
-
const inputDisabled = (isModelOptionsLoading || isModelOptionsEmpty) && !isSending;
|
|
154
|
-
const selectedModelOption = modelOptions.find((option) => option.value === selectedModel);
|
|
155
|
-
const resolvedStopHint =
|
|
156
|
-
stopDisabledReason === '__preparing__'
|
|
157
|
-
? t('chatStopPreparing')
|
|
158
|
-
: stopDisabledReason?.trim() || t('chatStopUnavailable');
|
|
159
|
-
const selectedSkillRecords = selectedSkills.map((spec) => {
|
|
160
|
-
const matched = skillRecords.find((record) => record.spec === spec);
|
|
161
|
-
return {
|
|
162
|
-
spec,
|
|
163
|
-
label: matched?.label || spec
|
|
164
|
-
};
|
|
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
|
-
}, []);
|
|
281
|
-
|
|
282
|
-
return (
|
|
283
|
-
<div className="border-t border-gray-200/80 bg-white p-4">
|
|
284
|
-
<div className="mx-auto w-full max-w-[min(1120px,100%)]">
|
|
285
|
-
<div className="rounded-2xl border border-gray-200 bg-white shadow-card overflow-hidden">
|
|
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')
|
|
339
|
-
}
|
|
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>
|
|
419
|
-
{isModelOptionsLoading && (
|
|
420
|
-
<div className="px-4 pb-2">
|
|
421
|
-
<div className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
|
|
422
|
-
<span className="h-3 w-28 animate-pulse rounded bg-gray-200" />
|
|
423
|
-
<span className="h-3 w-16 animate-pulse rounded bg-gray-200" />
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
{isModelOptionsEmpty && (
|
|
428
|
-
<div className="px-4 pb-2">
|
|
429
|
-
<div className="inline-flex items-center gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs text-amber-800">
|
|
430
|
-
<span>{t('chatModelNoOptions')}</span>
|
|
431
|
-
<button
|
|
432
|
-
type="button"
|
|
433
|
-
onClick={onGoToProviders}
|
|
434
|
-
className="font-semibold text-amber-900 underline-offset-2 hover:underline"
|
|
435
|
-
>
|
|
436
|
-
{t('chatGoConfigureProvider')}
|
|
437
|
-
</button>
|
|
438
|
-
</div>
|
|
439
|
-
</div>
|
|
440
|
-
)}
|
|
441
|
-
{selectedSkillRecords.length > 0 && (
|
|
442
|
-
<div className="px-4 pb-2">
|
|
443
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
444
|
-
{selectedSkillRecords.map((record) => (
|
|
445
|
-
<button
|
|
446
|
-
key={record.spec}
|
|
447
|
-
type="button"
|
|
448
|
-
onClick={() => onSelectedSkillsChange(selectedSkills.filter((skill) => skill !== record.spec))}
|
|
449
|
-
className="inline-flex max-w-[200px] items-center gap-1.5 rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary"
|
|
450
|
-
>
|
|
451
|
-
<span className="truncate">{record.label}</span>
|
|
452
|
-
<X className="h-3 w-3 shrink-0" />
|
|
453
|
-
</button>
|
|
454
|
-
))}
|
|
455
|
-
</div>
|
|
456
|
-
</div>
|
|
457
|
-
)}
|
|
458
|
-
|
|
459
|
-
{/* Toolbar */}
|
|
460
|
-
<div className="flex items-center justify-between px-3 pb-3">
|
|
461
|
-
{/* Left group */}
|
|
462
|
-
<div className="flex items-center gap-1">
|
|
463
|
-
{/* Skills picker */}
|
|
464
|
-
<SkillsPicker
|
|
465
|
-
records={skillRecords}
|
|
466
|
-
isLoading={isSkillsLoading}
|
|
467
|
-
selectedSkills={selectedSkills}
|
|
468
|
-
onSelectedSkillsChange={onSelectedSkillsChange}
|
|
469
|
-
/>
|
|
470
|
-
|
|
471
|
-
{/* Model selector */}
|
|
472
|
-
<Select
|
|
473
|
-
value={hasModelOptions ? selectedModel : undefined}
|
|
474
|
-
onValueChange={onSelectedModelChange}
|
|
475
|
-
disabled={!hasModelOptions}
|
|
476
|
-
>
|
|
477
|
-
<SelectTrigger className="h-8 w-auto min-w-[220px] rounded-lg border-0 bg-transparent shadow-none text-xs font-medium text-gray-600 hover:bg-gray-100 focus:ring-0 px-3">
|
|
478
|
-
{selectedModelOption ? (
|
|
479
|
-
<div className="flex min-w-0 items-center gap-2 text-left">
|
|
480
|
-
<Sparkles className="h-3.5 w-3.5 shrink-0 text-primary" />
|
|
481
|
-
<span className="truncate text-xs font-semibold text-gray-700">
|
|
482
|
-
{selectedModelOption.providerLabel}/{selectedModelOption.modelLabel}
|
|
483
|
-
</span>
|
|
484
|
-
</div>
|
|
485
|
-
) : isModelOptionsLoading ? (
|
|
486
|
-
<div className="h-3 w-24 animate-pulse rounded bg-gray-200" />
|
|
487
|
-
) : (
|
|
488
|
-
<SelectValue placeholder={t('chatSelectModel')} />
|
|
489
|
-
)}
|
|
490
|
-
</SelectTrigger>
|
|
491
|
-
<SelectContent className="w-[320px]">
|
|
492
|
-
{modelOptions.length === 0 && (
|
|
493
|
-
isModelOptionsLoading ? (
|
|
494
|
-
<div className="space-y-2 px-3 py-2">
|
|
495
|
-
<div className="h-3 w-36 animate-pulse rounded bg-gray-200" />
|
|
496
|
-
<div className="h-3 w-28 animate-pulse rounded bg-gray-200" />
|
|
497
|
-
<div className="h-3 w-32 animate-pulse rounded bg-gray-200" />
|
|
498
|
-
</div>
|
|
499
|
-
) : (
|
|
500
|
-
<div className="px-3 py-2 text-xs text-gray-500">{t('chatModelNoOptions')}</div>
|
|
501
|
-
)
|
|
502
|
-
)}
|
|
503
|
-
{modelOptions.map((option) => (
|
|
504
|
-
<SelectItem key={option.value} value={option.value} className="py-2">
|
|
505
|
-
<div className="flex min-w-0 flex-col gap-0.5">
|
|
506
|
-
<span className="truncate text-xs font-semibold text-gray-800">{option.modelLabel}</span>
|
|
507
|
-
<span className="truncate text-[11px] text-gray-500">{option.providerLabel}</span>
|
|
508
|
-
</div>
|
|
509
|
-
</SelectItem>
|
|
510
|
-
))}
|
|
511
|
-
</SelectContent>
|
|
512
|
-
</Select>
|
|
513
|
-
|
|
514
|
-
{/* Attachment button (placeholder) */}
|
|
515
|
-
<TooltipProvider>
|
|
516
|
-
<Tooltip>
|
|
517
|
-
<TooltipTrigger asChild>
|
|
518
|
-
<button
|
|
519
|
-
type="button"
|
|
520
|
-
disabled
|
|
521
|
-
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-gray-400 cursor-not-allowed"
|
|
522
|
-
>
|
|
523
|
-
<Paperclip className="h-4 w-4" />
|
|
524
|
-
</button>
|
|
525
|
-
</TooltipTrigger>
|
|
526
|
-
<TooltipContent side="top">
|
|
527
|
-
<p className="text-xs">{t('chatInputAttachComingSoon')}</p>
|
|
528
|
-
</TooltipContent>
|
|
529
|
-
</Tooltip>
|
|
530
|
-
</TooltipProvider>
|
|
531
|
-
</div>
|
|
532
|
-
|
|
533
|
-
{/* Right group */}
|
|
534
|
-
<div className="flex flex-col items-end gap-1">
|
|
535
|
-
{sendError?.trim() && (
|
|
536
|
-
<div className="max-w-[420px] text-right text-[11px] text-red-600">{sendError}</div>
|
|
537
|
-
)}
|
|
538
|
-
<div className="flex items-center gap-2">
|
|
539
|
-
{isSending && queuedCount > 0 && (
|
|
540
|
-
<span className="text-[11px] text-gray-400">
|
|
541
|
-
{t('chatQueuedHintPrefix')} {queuedCount} {t('chatQueuedHintSuffix')}
|
|
542
|
-
</span>
|
|
543
|
-
)}
|
|
544
|
-
{isSending ? (
|
|
545
|
-
canStopGeneration ? (
|
|
546
|
-
<Button
|
|
547
|
-
size="sm"
|
|
548
|
-
variant="destructive"
|
|
549
|
-
className="rounded-lg"
|
|
550
|
-
onClick={() => void onStop()}
|
|
551
|
-
>
|
|
552
|
-
<Square className="h-3.5 w-3.5 mr-1.5" />
|
|
553
|
-
{t('chatStop')}
|
|
554
|
-
</Button>
|
|
555
|
-
) : (
|
|
556
|
-
<TooltipProvider>
|
|
557
|
-
<Tooltip>
|
|
558
|
-
<TooltipTrigger asChild>
|
|
559
|
-
<span>
|
|
560
|
-
<Button size="sm" className="rounded-lg" disabled>
|
|
561
|
-
<Square className="h-3.5 w-3.5 mr-1.5" />
|
|
562
|
-
{t('chatStop')}
|
|
563
|
-
</Button>
|
|
564
|
-
</span>
|
|
565
|
-
</TooltipTrigger>
|
|
566
|
-
<TooltipContent side="top">
|
|
567
|
-
<p className="text-xs">{resolvedStopHint}</p>
|
|
568
|
-
</TooltipContent>
|
|
569
|
-
</Tooltip>
|
|
570
|
-
</TooltipProvider>
|
|
571
|
-
)
|
|
572
|
-
) : (
|
|
573
|
-
<Button
|
|
574
|
-
size="sm"
|
|
575
|
-
className="rounded-lg"
|
|
576
|
-
onClick={() => void onSend()}
|
|
577
|
-
disabled={draft.trim().length === 0 || !hasModelOptions}
|
|
578
|
-
>
|
|
579
|
-
<Send className="h-3.5 w-3.5 mr-1.5" />
|
|
580
|
-
{t('chatSend')}
|
|
581
|
-
</Button>
|
|
582
|
-
)}
|
|
583
|
-
</div>
|
|
584
|
-
</div>
|
|
585
|
-
</div>
|
|
586
|
-
</div>
|
|
587
|
-
</div>
|
|
588
|
-
</div>
|
|
589
|
-
);
|
|
590
|
-
}
|