Merge knowledge library controls and responsive layout
This commit is contained in:
commit
e123b75069
6 files changed
+89
-25
No files matched your search
@@ -28,6 +28,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [documentPending, setDocumentPending] = useState(false);
|
||||
const [uploadTarget, setUploadTarget] = useState<HTMLDivElement | null>(null);
|
||||
const createIntent = useRef<{ operation_id: string; name: string; embedding_model: string } | null>(null);
|
||||
usePendingCloudInput(busy || documentPending || Boolean(createIntent.current) || (creating && Boolean(name.trim())));
|
||||
const operationInFlight = useRef(false);
|
||||
@@ -63,8 +64,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
|
||||
const selectedKnowledge = manageable.find(kb => kb.kb_id === kbId);
|
||||
const managementDisabled = disabled || busy || catalogLoading || Boolean(catalogError);
|
||||
const bindable = (id: string) => selectableIds === undefined || Boolean(selectableIds?.includes(id));
|
||||
const creationControls = <div className="space-y-3">
|
||||
<Button type="button" variant="outline" disabled={disabled || busy || documentPending || Boolean(createIntent.current)} onClick={() => setCreating(v => !v)}>新建知识库</Button>
|
||||
const creationDetails = <div className="space-y-3">
|
||||
{creating && <div className="space-y-3 rounded-md bg-muted/30 p-3">
|
||||
<Input aria-label="知识库名称" placeholder="知识库名称" value={name} disabled={disabled || busy || !!createIntent.current} onChange={e => setName(e.target.value)} />
|
||||
<select aria-label="向量模型" className="h-10 w-full rounded-md border bg-background px-3" value={model}
|
||||
@@ -93,19 +93,20 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
|
||||
}
|
||||
}, 'creation')}>{createIntent.current ? '重试创建' : '创建知识库'}</Button>
|
||||
</div>}
|
||||
{errorTarget === 'creation' && error && <p role="alert" className="text-sm text-destructive">{error}</p>}
|
||||
</div>;
|
||||
return <div className="agent-knowledge-owned-workspace">
|
||||
<section aria-label="上传自己的文件" className="agent-knowledge-upload space-y-4">
|
||||
<section aria-label="上传自己的文件" className="agent-knowledge-upload">
|
||||
<div className="agent-knowledge-panel-heading"><Upload aria-hidden="true" className="size-5" /><h3>上传自己的文件</h3></div>
|
||||
{creationControls}
|
||||
{errorTarget === 'creation' && error && <p role="alert" className="text-sm text-destructive">{error}</p>}
|
||||
<div className="agent-knowledge-create-action">
|
||||
<Button type="button" variant="outline" disabled={disabled || busy || documentPending || Boolean(createIntent.current)} onClick={() => setCreating(v => !v)}>新建知识库</Button>
|
||||
</div>
|
||||
{creationDetails}
|
||||
</section>
|
||||
<section aria-label="查看我的知识库" className="agent-knowledge-management space-y-4 text-sm">
|
||||
<section aria-label="查看我的知识库" className="agent-knowledge-management text-sm">
|
||||
<div className="agent-knowledge-panel-heading"><Library aria-hidden="true" className="size-5" /><h3>查看我的知识库</h3></div>
|
||||
{errorTarget === 'management' && error && <p role="alert" className="text-destructive">{error}</p>}
|
||||
{catalogError && <p role="alert" className="text-destructive">知识库与模型目录加载失败:{catalogError}。请刷新重试。</p>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<select aria-label="管理的知识库" className="h-10 min-w-0 flex-1 basis-48 rounded-md border bg-background px-3" value={kbId} disabled={managementDisabled || documentPending}
|
||||
<div className="agent-knowledge-management-controls">
|
||||
<select aria-label="管理的知识库" value={kbId} disabled={managementDisabled || documentPending}
|
||||
onChange={e => {
|
||||
const selected = manageable.find(kb => kb.kb_id === e.target.value);
|
||||
if (managementDisabled || documentPending) return;
|
||||
@@ -117,8 +118,14 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
|
||||
{kb.name}{selectableIds === null ? '(选用状态待确认)' : !bindable(kb.kb_id) ? '(当前暂不可选用)' : ''}
|
||||
</option>)}
|
||||
</select>
|
||||
<Button type="button" variant="ghost" disabled={disabled || busy || catalogLoading} onClick={() => setRefresh(v => v + 1)}>刷新知识库与模型</Button>
|
||||
<Button type="button" variant="outline" disabled={disabled || busy || catalogLoading} onClick={() => setRefresh(v => v + 1)}>刷新知识库与模型</Button>
|
||||
<div ref={setUploadTarget} className="agent-knowledge-upload-action">
|
||||
{!selectedKnowledge && <Button type="button" disabled title="请先选择或新建自己的知识库">从电脑上传</Button>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{errorTarget === 'management' && error && <p role="alert" className="text-destructive">{error}</p>}
|
||||
{catalogError && <p role="alert" className="text-destructive">知识库与模型目录加载失败:{catalogError}。请刷新重试。</p>}
|
||||
{documentPending && selectedKnowledge && <p role="status" className="text-xs leading-5 text-amber-800">有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。</p>}
|
||||
{!catalogLoading && !catalogError && !manageable.length && <p className="text-xs leading-5 text-muted-foreground">还没有可管理的知识库,可以新建一个保存自己的文件。MakeLore知识仅供检索。</p>}
|
||||
{selectedKnowledge && selectableIds !== undefined && !bindable(selectedKnowledge.kb_id) && <p role="status" className="text-xs leading-5 text-amber-800">
|
||||
@@ -131,12 +138,11 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
|
||||
</Button>
|
||||
{documentPending && <p className="text-xs leading-5 text-muted-foreground">放弃只清除本地重试请求,不会删除已保存的文件。重新进入后可确认服务器状态。</p>}
|
||||
</div>}
|
||||
{!selectedKnowledge && <div>
|
||||
<Button type="button" disabled title="请先选择或新建自己的知识库">从电脑上传</Button>
|
||||
</div>}
|
||||
{kbId && <div hidden={!selectedKnowledge}><CloudKnowledgeDocuments key={slug + ':' + kbId} slug={slug} kbId={kbId} busy={managementDisabled || !selectedKnowledge}
|
||||
uploadTarget={selectedKnowledge ? uploadTarget : null}
|
||||
canRead={Boolean(selectedKnowledge)} act={act} onPendingChange={setDocumentPending}
|
||||
onDeleteKnowledge={() => { callbacks.current.onDeleted?.(kbId); setKbId(''); setDocumentPending(false); setRefresh(v => v + 1); }} /></div>}
|
||||
</div>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { CheckCircle2, CircleAlert, FileText, Loader2 } from 'lucide-react';
|
||||
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -47,11 +48,12 @@ function documentState(file: CloudKnowledgeFile) {
|
||||
return { label: '状态待确认', detail: '请刷新状态后再操作。', stage: 0 };
|
||||
}
|
||||
|
||||
export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange }: {
|
||||
export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange, uploadTarget }: {
|
||||
slug: string; kbId: string; busy: boolean;
|
||||
act: (operation: () => Promise<void>) => Promise<void>;
|
||||
onDeleteKnowledge: () => void;
|
||||
canRead?: boolean; onPendingChange?: (pending: boolean) => void;
|
||||
uploadTarget?: HTMLElement | null;
|
||||
}) {
|
||||
const [files, setFiles] = useState<CloudKnowledgeFile[]>([]);
|
||||
const [pageCount, setPageCount] = useState(1);
|
||||
@@ -150,9 +152,10 @@ export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowled
|
||||
});
|
||||
const states = files.map(documentState);
|
||||
const active = states.some(state => state.active) || submitting !== null;
|
||||
const uploadButton = <Button type="button" disabled={busy || !canRead || (unresolved && !uploadIntents.current.new)} onClick={() => void upload()}>{uploadIntents.current.new ? '重新选择同一文件并重试' : '从电脑上传'}</Button>;
|
||||
return <div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button type="button" disabled={busy || !canRead || (unresolved && !uploadIntents.current.new)} onClick={() => void upload()}>{uploadIntents.current.new ? '重新选择同一文件并重试' : '从电脑上传'}</Button>
|
||||
{uploadTarget ? createPortal(uploadButton, uploadTarget) : uploadButton}
|
||||
<Button type="button" variant="ghost" disabled={loading || !canRead} onClick={refresh}>刷新状态</Button>
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-muted-foreground">上传后自动解析并建立索引。最大 5 MB;支持 TXT、Markdown、CSV、JSON、Word、Excel、文本 PDF。</p>
|
||||
|
||||
@@ -24,21 +24,21 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load
|
||||
<section aria-label="选择知识库" className="agent-knowledge-selection space-y-4">
|
||||
<div className="agent-knowledge-panel-heading"><Library aria-hidden="true" className="size-5" /><h3>选择知识库</h3></div>
|
||||
<p className="text-xs leading-5 text-muted-foreground">从 MakeLore知识和我的知识库中选择,可同时使用多个。</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select aria-label="选择知识库" className="min-w-0 flex-1 basis-48" disabled={disabled || loading || Boolean(error) || selectionFull || available.length === 0} value=""
|
||||
<div className="agent-knowledge-selection-controls">
|
||||
<select aria-label="选择知识库" disabled={disabled || loading || Boolean(error) || selectionFull || available.length === 0} value=""
|
||||
onChange={event => { if (!disabled && !loading && !error && event.target.value && available.some(option => option.key === event.target.value)) add(event.target.value); }}>
|
||||
<option value="">{loading ? '正在读取知识库…' : error ? '知识库目录暂不可用' : available.length ? '选择知识库,可选多个' : options.length ? '已选用全部可用知识库' : '暂无可选知识库'}</option>
|
||||
{shared.length > 0 && <optgroup label="MakeLore知识">{shared.map(option => <option key={option.key} value={option.key}>{option.name}</option>)}</optgroup>}
|
||||
{personal.length > 0 && <optgroup label="我的知识库">{personal.map(option => <option key={option.key} value={option.key}>{option.name}</option>)}</optgroup>}
|
||||
{unknown.length > 0 && <optgroup label="可选知识库">{unknown.map(option => <option key={option.key} value={option.key}>{option.name}</option>)}</optgroup>}
|
||||
</select>
|
||||
<Button type="button" variant="ghost" size="sm" disabled={disabled || loading || owned.loading} onClick={onRefresh}>刷新知识库</Button>
|
||||
<Button type="button" variant="outline" disabled={disabled || loading || owned.loading} onClick={onRefresh}>刷新知识库</Button>
|
||||
</div>
|
||||
{selectionFull && <p className="text-xs leading-5 text-muted-foreground">已选满100个知识库,请先移除一个再选择。</p>}
|
||||
{error ? <p role="alert" className="text-sm text-destructive">知识库目录读取失败,请刷新重试。</p>
|
||||
: !loading && ownedIds !== null && options.every(option => ownedIds.includes(option.key)) && <p className="text-xs leading-5 text-muted-foreground">暂无可选的MakeLore知识。平台开放给你读取的知识库会出现在这里。</p>}
|
||||
{owned.error && <p className="text-xs leading-5 text-muted-foreground">我的知识库目录暂不可用,仍可选用可读取的知识;上传文件请刷新重试。</p>}
|
||||
{selected.length > 0 ? <ul aria-label="已选知识库" className="space-y-2">
|
||||
{selected.length > 0 ? <ul aria-label="已选知识库" className="agent-knowledge-selected space-y-2">
|
||||
{selected.map(id => {
|
||||
const option = options.find(item => item.key === id);
|
||||
const ownKnowledge = owned.databases?.find(item => item.kb_id === id);
|
||||
|
||||
@@ -43,16 +43,35 @@
|
||||
.agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; }
|
||||
.agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; }
|
||||
.agent-knowledge-workspace { display: grid; gap: 18px; min-width: 0; }
|
||||
.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: stretch; gap: 18px; min-width: 0; }
|
||||
.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); grid-template-rows: auto minmax(96px, auto) minmax(43px, auto); align-items: stretch; gap: 18px; min-width: 0; }
|
||||
.agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; }
|
||||
.agent-knowledge-upload { border-color: #dce5f3; }
|
||||
.agent-knowledge-upload, .agent-knowledge-management { display: grid; grid-template-rows: subgrid; grid-row: span 3; }
|
||||
.agent-knowledge-create-action { display: flex; align-items: center; justify-content: center; }
|
||||
.agent-knowledge-management-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; min-width: 0; }
|
||||
.agent-config-card .agent-knowledge-management-controls select { width: calc((100% - 144px - 8px) / 2); flex: none; padding-inline: 8px; font-size: 14px; }
|
||||
.agent-knowledge-upload-action { display: flex; min-width: 0; }
|
||||
.agent-knowledge-create-action button, .agent-knowledge-management-controls button, .agent-knowledge-selection-controls button { min-height: 48px; height: auto; border-radius: 13px; }
|
||||
.agent-knowledge-management-controls button { flex-shrink: 0; }
|
||||
.agent-knowledge-selection { --knowledge-choice-width: calc((100% - 102px) * 2 / 3); }
|
||||
.agent-knowledge-selection-controls { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
.agent-config-card .agent-knowledge-selection-controls select { width: var(--knowledge-choice-width); flex: none; }
|
||||
.agent-knowledge-selected { width: var(--knowledge-choice-width); }
|
||||
.agent-knowledge-panel-heading { display: flex; align-items: center; gap: 9px; color: #33485c; }
|
||||
.agent-knowledge-panel-heading h3 { font-size: 16px; font-weight: 650; line-height: 1.5; }
|
||||
.agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; }
|
||||
.agent-knowledge-upload .agent-knowledge-panel-heading svg { color: #4a6e9c; }
|
||||
.agent-knowledge-selection ul { max-height: 480px; overflow: auto; overscroll-behavior: contain; }
|
||||
.agent-knowledge-selection button { flex-shrink: 0; }
|
||||
@container (max-width: 620px) { .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } }
|
||||
@container (max-width: 840px) {
|
||||
.agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
|
||||
.agent-knowledge-upload, .agent-knowledge-management { display: flex; flex-direction: column; gap: 16px; grid-row: auto; }
|
||||
.agent-knowledge-create-action { min-height: 80px; }
|
||||
.agent-knowledge-management-controls { flex-wrap: wrap; }
|
||||
.agent-config-card .agent-knowledge-management-controls select { width: 100%; }
|
||||
.agent-knowledge-selection { --knowledge-choice-width: 100%; }
|
||||
.agent-knowledge-selection-controls { flex-wrap: wrap; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.agent-editor-tabs { padding: 10px 12px; }
|
||||
.agent-editor-tabs [role="tab"] { gap: 6px; padding-inline: 8px; font-size: 14px; }
|
||||
@@ -65,7 +84,7 @@
|
||||
.agent-config-icon { width: 38px; height: 38px; border-radius: 13px; }
|
||||
.agent-config-section-top > button { margin-left: 48px; }
|
||||
.agent-knowledge-workspace, .agent-knowledge-owned-workspace { gap: 14px; }
|
||||
.agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); }
|
||||
.agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
|
||||
.agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { padding: 14px; border-radius: 14px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .agent-resource-option { transition: none; } }
|
||||
Reference in new issue
Block a user