@nextclaw/ui 0.6.7 → 0.6.9
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/dist/assets/{ChannelsList-Dz8AGmaQ.js → ChannelsList-DACqpUYZ.js} +1 -1
- package/dist/assets/ChatPage-iji0RkTR.js +34 -0
- package/dist/assets/{DocBrowser-CkKvzF7m.js → DocBrowser-D7mjKkGe.js} +1 -1
- package/dist/assets/{LogoBadge-C_ygxoGB.js → LogoBadge-BlDT-g9R.js} +1 -1
- package/dist/assets/{MarketplacePage-DEvRs-Jc.js → MarketplacePage-CZq3jVgg.js} +3 -3
- package/dist/assets/{ModelConfig-BGfliN2Z.js → ModelConfig-DwRU5qrw.js} +1 -1
- package/dist/assets/{ProvidersList-BHLGLSvs.js → ProvidersList-DFxN3pjx.js} +1 -1
- package/dist/assets/{RuntimeConfig-Clltld_h.js → RuntimeConfig-C7BRLGSC.js} +1 -1
- package/dist/assets/{SecretsConfig-CaJLf7oJ.js → SecretsConfig-D5xZh7VF.js} +1 -1
- package/dist/assets/{SessionsConfig-3QF7K9wm.js → SessionsConfig-ovpj_otA.js} +1 -1
- package/dist/assets/{card-DXo3NsaB.js → card-Bf4CtrW8.js} +1 -1
- package/dist/assets/index-C_DhisNo.css +1 -0
- package/dist/assets/{index-CGo5Vnh0.js → index-dKTqKCJo.js} +5 -5
- package/dist/assets/{input-CzTldMKo.js → input-CaKJyoWZ.js} +1 -1
- package/dist/assets/{label-De__vsU7.js → label-BaXSWTKI.js} +1 -1
- package/dist/assets/{page-layout-BOgLC2tK.js → page-layout-DA6PFRtQ.js} +1 -1
- package/dist/assets/{session-run-status-DQVCDxTb.js → session-run-status-CllIZxNf.js} +1 -1
- package/dist/assets/{switch-pMrS4heA.js → switch-Cvd5wZs-.js} +1 -1
- package/dist/assets/{tabs-custom-DhOxWfCb.js → tabs-custom-0PybLkXs.js} +1 -1
- package/dist/assets/{useConfirmDialog-CseKBGh5.js → useConfirmDialog-DdtpSju1.js} +2 -2
- package/dist/assets/{vendor-D33xZtEC.js → vendor-C--HHaLf.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/api/types.ts +22 -4
- package/src/components/chat/ChatInputBar.tsx +341 -24
- package/src/components/chat/ChatPage.tsx +13 -5
- package/src/components/chat/useChatStreamController.ts +18 -3
- package/src/components/marketplace/MarketplacePage.tsx +48 -44
- package/src/lib/i18n.ts +11 -1
- package/dist/assets/ChatPage-BXDyt7BL.js +0 -34
- package/dist/assets/index-DcxYzrFm.css +0 -1
|
@@ -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';
|
|
@@ -209,11 +209,19 @@ function ChatPageLayout({ view, sidebarProps, conversationProps, confirmDialog }
|
|
|
209
209
|
<ChatConversationPanel {...conversationProps} />
|
|
210
210
|
) : (
|
|
211
211
|
<section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
212
|
-
|
|
213
|
-
<div className="
|
|
214
|
-
|
|
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>
|
|
215
217
|
</div>
|
|
216
|
-
|
|
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
|
+
)}
|
|
217
225
|
</section>
|
|
218
226
|
)}
|
|
219
227
|
|
|
@@ -178,7 +178,7 @@ type ExecuteStreamRunParams = {
|
|
|
178
178
|
onReady: (event: { runId?: string; stopSupported?: boolean; stopReason?: string; sessionKey: string }) => void;
|
|
179
179
|
onDelta: (event: { delta: string }) => void;
|
|
180
180
|
onSessionEvent: (event: { data: SessionEventView }) => void;
|
|
181
|
-
}) => Promise<{ sessionKey: string }>;
|
|
181
|
+
}) => Promise<{ sessionKey: string; reply: string }>;
|
|
182
182
|
setters: StreamSetters;
|
|
183
183
|
};
|
|
184
184
|
|
|
@@ -226,6 +226,7 @@ async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
|
|
|
226
226
|
let streamText = '';
|
|
227
227
|
try {
|
|
228
228
|
let hasAssistantSessionEvent = false;
|
|
229
|
+
let hasUserSessionEvent = false;
|
|
229
230
|
const streamTimestamp = new Date().toISOString();
|
|
230
231
|
setters.setStreamingAssistantTimestamp(streamTimestamp);
|
|
231
232
|
|
|
@@ -269,6 +270,7 @@ async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
|
|
|
269
270
|
return;
|
|
270
271
|
}
|
|
271
272
|
if (event.data.message?.role === 'user') {
|
|
273
|
+
hasUserSessionEvent = true;
|
|
272
274
|
setters.setOptimisticUserEvent(null);
|
|
273
275
|
}
|
|
274
276
|
upsertStreamingEvent(setters.setStreamingSessionEvents, event.data);
|
|
@@ -288,7 +290,13 @@ async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
|
|
|
288
290
|
setSelectedSessionKey(result.sessionKey);
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
const
|
|
293
|
+
const finalReply = typeof result.reply === 'string' ? result.reply.trim() : '';
|
|
294
|
+
const localAssistantText = !hasAssistantSessionEvent ? (streamText.trim() || finalReply) : '';
|
|
295
|
+
const isSlashCommandMessage = typeof sourceMessage === 'string' && sourceMessage.trim().startsWith('/');
|
|
296
|
+
const shouldKeepLocalUserCommand =
|
|
297
|
+
!hasUserSessionEvent &&
|
|
298
|
+
optimisticUserEvent?.message?.role === 'user' &&
|
|
299
|
+
isSlashCommandMessage;
|
|
292
300
|
await refetchIfSessionVisible({
|
|
293
301
|
selectedSessionKeyRef,
|
|
294
302
|
currentSessionKey: sourceSessionKey,
|
|
@@ -297,7 +305,14 @@ async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
|
|
|
297
305
|
refetchHistory
|
|
298
306
|
});
|
|
299
307
|
|
|
300
|
-
|
|
308
|
+
const localEvents: SessionEventView[] = [];
|
|
309
|
+
if (shouldKeepLocalUserCommand && optimisticUserEvent) {
|
|
310
|
+
localEvents.push(optimisticUserEvent);
|
|
311
|
+
}
|
|
312
|
+
if (localAssistantText) {
|
|
313
|
+
localEvents.push(buildLocalAssistantEvent(localAssistantText));
|
|
314
|
+
}
|
|
315
|
+
setters.setStreamingSessionEvents(localEvents);
|
|
301
316
|
|
|
302
317
|
setters.setStreamingAssistantText('');
|
|
303
318
|
setters.setStreamingAssistantTimestamp(null);
|
|
@@ -824,7 +824,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
824
824
|
};
|
|
825
825
|
|
|
826
826
|
return (
|
|
827
|
-
<PageLayout>
|
|
827
|
+
<PageLayout className="flex h-full min-h-0 flex-col pb-0">
|
|
828
828
|
<PageHeader title={t(copyKeys.pageTitle)} description={t(copyKeys.pageDescription)} />
|
|
829
829
|
|
|
830
830
|
<Tabs
|
|
@@ -849,7 +849,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
849
849
|
}}
|
|
850
850
|
/>
|
|
851
851
|
|
|
852
|
-
<section>
|
|
852
|
+
<section className="flex min-h-0 flex-1 flex-col">
|
|
853
853
|
<div className="flex items-center justify-between mb-3">
|
|
854
854
|
<h3 className="text-[14px] font-semibold text-gray-900">
|
|
855
855
|
{scope === 'installed' ? t(copyKeys.sectionInstalled) : t(copyKeys.sectionCatalog)}
|
|
@@ -868,52 +868,56 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
868
868
|
</div>
|
|
869
869
|
)}
|
|
870
870
|
|
|
871
|
-
<div className="
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
871
|
+
<div className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1">
|
|
872
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
|
|
873
|
+
{scope === 'all' && allItems.map((item) => (
|
|
874
|
+
<MarketplaceListCard
|
|
875
|
+
key={item.id}
|
|
876
|
+
item={item}
|
|
877
|
+
record={findInstalledRecordForItem(item, installedRecordLookup)}
|
|
878
|
+
localeFallbacks={localeFallbacks}
|
|
879
|
+
installState={installState}
|
|
880
|
+
manageState={manageState}
|
|
881
|
+
onOpen={() => void openItemDetail(item, findInstalledRecordForItem(item, installedRecordLookup))}
|
|
882
|
+
onInstall={handleInstall}
|
|
883
|
+
onManage={handleManage}
|
|
884
|
+
/>
|
|
885
|
+
))}
|
|
886
|
+
|
|
887
|
+
{scope === 'installed' && installedEntries.map((entry) => (
|
|
888
|
+
<MarketplaceListCard
|
|
889
|
+
key={entry.key}
|
|
890
|
+
item={entry.item}
|
|
891
|
+
record={entry.record}
|
|
892
|
+
localeFallbacks={localeFallbacks}
|
|
893
|
+
installState={installState}
|
|
894
|
+
manageState={manageState}
|
|
895
|
+
onOpen={() => void openItemDetail(entry.item, entry.record)}
|
|
896
|
+
onInstall={handleInstall}
|
|
897
|
+
onManage={handleManage}
|
|
898
|
+
/>
|
|
899
|
+
))}
|
|
900
|
+
</div>
|
|
900
901
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
902
|
+
{scope === 'all' && !itemsQuery.isLoading && !itemsQuery.isError && allItems.length === 0 && (
|
|
903
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyData)}</div>
|
|
904
|
+
)}
|
|
905
|
+
{scope === 'installed' && !installedQuery.isLoading && !installedQuery.isError && installedEntries.length === 0 && (
|
|
906
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
|
|
907
|
+
)}
|
|
908
|
+
</div>
|
|
907
909
|
</section>
|
|
908
910
|
|
|
909
911
|
{scope === 'all' && (
|
|
910
|
-
<
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
912
|
+
<div className="shrink-0">
|
|
913
|
+
<PaginationBar
|
|
914
|
+
page={page}
|
|
915
|
+
totalPages={totalPages}
|
|
916
|
+
busy={itemsQuery.isFetching}
|
|
917
|
+
onPrev={() => setPage((current) => Math.max(1, current - 1))}
|
|
918
|
+
onNext={() => setPage((current) => (totalPages > 0 ? Math.min(totalPages, current + 1) : current + 1))}
|
|
919
|
+
/>
|
|
920
|
+
</div>
|
|
917
921
|
)}
|
|
918
922
|
<ConfirmDialog />
|
|
919
923
|
</PageLayout>
|
package/src/lib/i18n.ts
CHANGED
|
@@ -517,8 +517,18 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
517
517
|
chatHistoryLoading: { zh: '加载会话历史中...', en: 'Loading session history...' },
|
|
518
518
|
chatNoMessages: { zh: '暂无消息,发送一条开始对话。', en: 'No messages yet. Send one to start.' },
|
|
519
519
|
chatTyping: { zh: 'Agent 正在思考...', en: 'Agent is thinking...' },
|
|
520
|
-
chatInputPlaceholder: { zh: '
|
|
520
|
+
chatInputPlaceholder: { zh: '输入消息,输入 / 选择技能,Enter 发送,Shift + Enter 换行', en: 'Type a message, type / to select skills, Enter to send, Shift + Enter for newline' },
|
|
521
521
|
chatInputHint: { zh: '支持多轮上下文,默认走当前会话。', en: 'Multi-turn context is preserved in the current session.' },
|
|
522
|
+
chatSlashSectionCommands: { zh: '命令', en: 'Commands' },
|
|
523
|
+
chatSlashSectionSkills: { zh: '技能', en: 'Skills' },
|
|
524
|
+
chatSlashTypeCommand: { zh: '命令', en: 'Command' },
|
|
525
|
+
chatSlashTypeSkill: { zh: '技能', en: 'Skill' },
|
|
526
|
+
chatSlashSkillSpec: { zh: '标识', en: 'Spec' },
|
|
527
|
+
chatSlashLoading: { zh: '加载命令与技能中…', en: 'Loading commands and skills…' },
|
|
528
|
+
chatSlashNoResult: { zh: '无匹配项', en: 'No matches' },
|
|
529
|
+
chatSlashHint: { zh: '输入 / 触发命令或技能选择', en: 'Type / to access commands and skills' },
|
|
530
|
+
chatSlashCommandHint: { zh: '回车插入命令,继续输入参数后发送。', en: 'Press Enter to insert command, then add args and send.' },
|
|
531
|
+
chatSlashSkillHint: { zh: '回车把该技能加入本轮请求。', en: 'Press Enter to add this skill for the next turn.' },
|
|
522
532
|
chatSend: { zh: '发送', en: 'Send' },
|
|
523
533
|
chatStop: { zh: '停止', en: 'Stop' },
|
|
524
534
|
chatStopPreparing: { zh: '正在建立可停止会话,请稍候…', en: 'Preparing stoppable run…' },
|