Merge knowledge library controls and responsive layout

This commit is contained in:
鲨鱼辣椒 committed 2026-10-13 20:05:48 +08:00
commit e123b75069
6 files changed
+89 -25

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
历史待确认记录收起在智能体列表标题区的「创建记录/待处理记录」入口,只有点击后才打开记录窗口;配置、聊天和创建页不再置顶这些旧提醒。记录保留智能体名称和原请求身份;应用设置的待确认记录显示“网络可能不稳定,请再点一次「应用」。”,点击「应用」继续原来的提交,成功后显示“设置已应用。”。其他记录仍可明确「再试一次」或「不再提醒」,不会自动重试或清除。当前创建失败继续在创建表单内提示;关闭记录窗口保留已收到的结果和密钥,切换配置、聊天或创建页后可返回列表继续查看。密钥须明确关闭结果后,才可处理其他记录。 历史待确认记录收起在智能体列表标题区的「创建记录/待处理记录」入口,只有点击后才打开记录窗口;配置、聊天和创建页不再置顶这些旧提醒。记录保留智能体名称和原请求身份;应用设置的待确认记录显示“网络可能不稳定,请再点一次「应用」。”,点击「应用」继续原来的提交,成功后显示“设置已应用。”。其他记录仍可明确「再试一次」或「不再提醒」,不会自动重试或清除。当前创建失败继续在创建表单内提示;关闭记录窗口保留已收到的结果和密钥,切换配置、聊天或创建页后可返回列表继续查看。密钥须明确关闭结果后,才可处理其他记录。
高级配置的知识库支持下拉多选,分为「MakeLore知识」和「我的知识库」,已选项可单独移除。MakeLore知识来自当前账号有权读取的 Yuxi 共享目录,只显示名称与简介;移除仅取消当前智能体选用,不删除原库。知识库区域上方左右分区,两张卡片在并排时保持等高:左侧「上传自己的文件」仅放「新建知识库」入口及展开的创建表单,右侧「查看我的知识库」通过下拉框查看本人知识库、从电脑追加资料和管理文件;下方独立「选择知识库」,选择智能体使用的「MakeLore知识」和「我的知识库」。小窗口按新建、查看、选用顺序纵向排列。创建成功后在右侧打开新库;新建和查看均不会自动加入智能体配置,选用上限不影响管理与新建。仅提供「从电脑上传」作为添加文件入口,未选择管理目标时上传按钮置灰;聊天文件导入入口已移除。上传后自动解析、建立索引并持续显示真实处理状态;失败可重试且保留已有可用内容。暂不可选用的个人库仍可管理文件,已有绑定保留并提示不可用;未确认请求在当前库保留同一操作身份供重试。知识库选择只改草稿,显式保存/应用后生效;个人云端执行已自动接入所选知识库的检索,无需另外勾选 knowledge-base Skill。此交互复用现有接口,无需新增后端补丁。 高级配置的知识库支持下拉多选,分为「MakeLore知识」和「我的知识库」,已选项可单独移除。MakeLore知识来自当前账号有权读取的 Yuxi 共享目录,只显示名称与简介;移除仅取消当前智能体选用,不删除原库。知识库区域上方左右分区,两张卡片在并排时保持等高:左侧「上传自己的文件」仅放「新建知识库」入口及展开的创建表单,右侧「查看我的知识库」通过下拉框查看本人知识库、从电脑追加资料和管理文件。宽窗口中新建按钮与右侧选择、刷新、上传控件共用居中的水平线,管理下拉框约为原宽度的一半,两种目录刷新均显示为带边框按钮;下方独立「选择知识库」,选择智能体使用的「MakeLore知识」和「我的知识库」,下拉框缩至原宽度的约2/3,并与已选知识库框左右对齐。小窗口按新建、查看、选用顺序纵向排列,控件允许换行并保持可读。创建成功后在右侧打开新库;新建和查看均不会自动加入智能体配置,选用上限不影响管理与新建。仅提供「从电脑上传」作为添加文件入口,未选择管理目标时上传按钮置灰;聊天文件导入入口已移除。上传后自动解析、建立索引并持续显示真实处理状态;失败可重试且保留已有可用内容。暂不可选用的个人库仍可管理文件,已有绑定保留并提示不可用;未确认请求在当前库保留同一操作身份供重试。知识库选择只改草稿,显式保存/应用后生效;个人云端执行已自动接入所选知识库的检索,无需另外勾选 knowledge-base Skill。此交互复用现有接口,无需新增后端补丁。
云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
+20 -14
View File
@@ -28,6 +28,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
const [refresh, setRefresh] = useState(0); const [refresh, setRefresh] = useState(0);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [documentPending, setDocumentPending] = 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); const createIntent = useRef<{ operation_id: string; name: string; embedding_model: string } | null>(null);
usePendingCloudInput(busy || documentPending || Boolean(createIntent.current) || (creating && Boolean(name.trim()))); usePendingCloudInput(busy || documentPending || Boolean(createIntent.current) || (creating && Boolean(name.trim())));
const operationInFlight = useRef(false); 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 selectedKnowledge = manageable.find(kb => kb.kb_id === kbId);
const managementDisabled = disabled || busy || catalogLoading || Boolean(catalogError); const managementDisabled = disabled || busy || catalogLoading || Boolean(catalogError);
const bindable = (id: string) => selectableIds === undefined || Boolean(selectableIds?.includes(id)); const bindable = (id: string) => selectableIds === undefined || Boolean(selectableIds?.includes(id));
const creationControls = <div className="space-y-3"> const creationDetails = <div className="space-y-3">
<Button type="button" variant="outline" disabled={disabled || busy || documentPending || Boolean(createIntent.current)} onClick={() => setCreating(v => !v)}>新建知识库</Button>
{creating && <div className="space-y-3 rounded-md bg-muted/30 p-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)} /> <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} <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> }, 'creation')}>{createIntent.current ? '重试创建' : '创建知识库'}</Button>
</div>} </div>}
{errorTarget === 'creation' && error && <p role="alert" className="text-sm text-destructive">{error}</p>}
</div>; </div>;
return <div className="agent-knowledge-owned-workspace"> 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> <div className="agent-knowledge-panel-heading"><Upload aria-hidden="true" className="size-5" /><h3>上传自己的文件</h3></div>
{creationControls} <div className="agent-knowledge-create-action">
{errorTarget === 'creation' && error && <p role="alert" className="text-sm text-destructive">{error}</p>} <Button type="button" variant="outline" disabled={disabled || busy || documentPending || Boolean(createIntent.current)} onClick={() => setCreating(v => !v)}>新建知识库</Button>
</div>
{creationDetails}
</section> </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> <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>} <div className="agent-knowledge-management-controls">
{catalogError && <p role="alert" className="text-destructive">知识库与模型目录加载失败:{catalogError}。请刷新重试。</p>} <select aria-label="管理的知识库" value={kbId} disabled={managementDisabled || documentPending}
<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}
onChange={e => { onChange={e => {
const selected = manageable.find(kb => kb.kb_id === e.target.value); const selected = manageable.find(kb => kb.kb_id === e.target.value);
if (managementDisabled || documentPending) return; if (managementDisabled || documentPending) return;
@@ -117,8 +118,14 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
{kb.name}{selectableIds === null ? '(选用状态待确认)' : !bindable(kb.kb_id) ? '(当前暂不可选用)' : ''} {kb.name}{selectableIds === null ? '(选用状态待确认)' : !bindable(kb.kb_id) ? '(当前暂不可选用)' : ''}
</option>)} </option>)}
</select> </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>
<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>} {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>} {!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"> {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> </Button>
{documentPending && <p className="text-xs leading-5 text-muted-foreground">放弃只清除本地重试请求,不会删除已保存的文件。重新进入后可确认服务器状态。</p>} {documentPending && <p className="text-xs leading-5 text-muted-foreground">放弃只清除本地重试请求,不会删除已保存的文件。重新进入后可确认服务器状态。</p>}
</div>} </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} {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} canRead={Boolean(selectedKnowledge)} act={act} onPendingChange={setDocumentPending}
onDeleteKnowledge={() => { callbacks.current.onDeleted?.(kbId); setKbId(''); setDocumentPending(false); setRefresh(v => v + 1); }} /></div>} onDeleteKnowledge={() => { callbacks.current.onDeleted?.(kbId); setKbId(''); setDocumentPending(false); setRefresh(v => v + 1); }} /></div>}
</div>
</section> </section>
</div>; </div>;
} }
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { CheckCircle2, CircleAlert, FileText, Loader2 } from 'lucide-react'; import { CheckCircle2, CircleAlert, FileText, Loader2 } from 'lucide-react';
import { cloudAgentsApi } from '@/lib/cloud-agents-api'; import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -47,11 +48,12 @@ function documentState(file: CloudKnowledgeFile) {
return { label: '状态待确认', detail: '请刷新状态后再操作。', stage: 0 }; 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; slug: string; kbId: string; busy: boolean;
act: (operation: () => Promise<void>) => Promise<void>; act: (operation: () => Promise<void>) => Promise<void>;
onDeleteKnowledge: () => void; onDeleteKnowledge: () => void;
canRead?: boolean; onPendingChange?: (pending: boolean) => void; canRead?: boolean; onPendingChange?: (pending: boolean) => void;
uploadTarget?: HTMLElement | null;
}) { }) {
const [files, setFiles] = useState<CloudKnowledgeFile[]>([]); const [files, setFiles] = useState<CloudKnowledgeFile[]>([]);
const [pageCount, setPageCount] = useState(1); const [pageCount, setPageCount] = useState(1);
@@ -150,9 +152,10 @@ export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowled
}); });
const states = files.map(documentState); const states = files.map(documentState);
const active = states.some(state => state.active) || submitting !== null; 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"> return <div className="space-y-4">
<div className="flex flex-wrap items-center gap-2"> <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> <Button type="button" variant="ghost" disabled={loading || !canRead} onClick={refresh}>刷新状态</Button>
</div> </div>
<p className="text-xs leading-5 text-muted-foreground">上传后自动解析并建立索引。最大 5 MB;支持 TXT、Markdown、CSV、JSON、Word、Excel、文本 PDF。</p> <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"> <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> <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> <p className="text-xs leading-5 text-muted-foreground">从 MakeLore知识和我的知识库中选择,可同时使用多个。</p>
<div className="flex flex-wrap items-center gap-2"> <div className="agent-knowledge-selection-controls">
<select aria-label="选择知识库" className="min-w-0 flex-1 basis-48" disabled={disabled || loading || Boolean(error) || selectionFull || available.length === 0} value="" <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); }}> 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> <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>} {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>} {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>} {unknown.length > 0 && <optgroup label="可选知识库">{unknown.map(option => <option key={option.key} value={option.key}>{option.name}</option>)}</optgroup>}
</select> </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> </div>
{selectionFull && <p className="text-xs leading-5 text-muted-foreground">已选满100个知识库,请先移除一个再选择。</p>} {selectionFull && <p className="text-xs leading-5 text-muted-foreground">已选满100个知识库,请先移除一个再选择。</p>}
{error ? <p role="alert" className="text-sm text-destructive">知识库目录读取失败,请刷新重试。</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>} : !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>} {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 => { {selected.map(id => {
const option = options.find(item => item.key === id); const option = options.find(item => item.key === id);
const ownKnowledge = owned.databases?.find(item => item.kb_id === id); const ownKnowledge = owned.databases?.find(item => item.kb_id === id);
+22 -3
View File
@@ -43,16 +43,35 @@
.agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; } .agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; }
.agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; } .agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; }
.agent-knowledge-workspace { display: grid; gap: 18px; min-width: 0; } .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, .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 { 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 { 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 h3 { font-size: 16px; font-weight: 650; line-height: 1.5; }
.agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; } .agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; }
.agent-knowledge-upload .agent-knowledge-panel-heading svg { color: #4a6e9c; } .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 ul { max-height: 480px; overflow: auto; overscroll-behavior: contain; }
.agent-knowledge-selection button { flex-shrink: 0; } .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) { @media (max-width: 640px) {
.agent-editor-tabs { padding: 10px 12px; } .agent-editor-tabs { padding: 10px 12px; }
.agent-editor-tabs [role="tab"] { gap: 6px; padding-inline: 8px; font-size: 14px; } .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-icon { width: 38px; height: 38px; border-radius: 13px; }
.agent-config-section-top > button { margin-left: 48px; } .agent-config-section-top > button { margin-left: 48px; }
.agent-knowledge-workspace, .agent-knowledge-owned-workspace { gap: 14px; } .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; } .agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { padding: 14px; border-radius: 14px; }
} }
@media (prefers-reduced-motion: reduce) { .agent-resource-option { transition: none; } } @media (prefers-reduced-motion: reduce) { .agent-resource-option { transition: none; } }
+37 -1
View File
@@ -203,12 +203,24 @@ async function expectKnowledgeLayout(page: Page) {
const ownedBounds = await knowledge.locator('.agent-knowledge-owned-workspace').boundingBox(); const ownedBounds = await knowledge.locator('.agent-knowledge-owned-workspace').boundingBox();
expect(creationBounds).not.toBeNull(); expect(managementBounds).not.toBeNull(); expect(selectionBounds).not.toBeNull(); expect(creationBounds).not.toBeNull(); expect(managementBounds).not.toBeNull(); expect(selectionBounds).not.toBeNull();
expect(ownedBounds).not.toBeNull(); expect(ownedBounds).not.toBeNull();
if (ownedBounds!.width > 620) { if (ownedBounds!.width > 840) {
expect(creationBounds!.x + creationBounds!.width).toBeLessThanOrEqual(managementBounds!.x + 1); expect(creationBounds!.x + creationBounds!.width).toBeLessThanOrEqual(managementBounds!.x + 1);
expect(Math.abs(creationBounds!.y - managementBounds!.y)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.y - managementBounds!.y)).toBeLessThanOrEqual(1);
expect(Math.abs(creationBounds!.height - managementBounds!.height)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.height - managementBounds!.height)).toBeLessThanOrEqual(1);
expect(selectionBounds!.y).toBeGreaterThanOrEqual(Math.max(creationBounds!.y + creationBounds!.height, managementBounds!.y + managementBounds!.height)); expect(selectionBounds!.y).toBeGreaterThanOrEqual(Math.max(creationBounds!.y + creationBounds!.height, managementBounds!.y + managementBounds!.height));
expect(Math.abs(selectionBounds!.x + selectionBounds!.width - managementBounds!.x - managementBounds!.width)).toBeLessThanOrEqual(1); expect(Math.abs(selectionBounds!.x + selectionBounds!.width - managementBounds!.x - managementBounds!.width)).toBeLessThanOrEqual(1);
const create = await creation.getByRole('button', { name: '新建知识库', exact: true }).boundingBox();
const manager = await management.getByRole('combobox', { name: '管理的知识库', exact: true }).boundingBox();
const refresh = await management.getByRole('button', { name: '刷新知识库与模型', exact: true }).boundingBox();
const upload = await management.getByRole('button', { name: '从电脑上传', exact: true }).boundingBox();
const controls = await management.locator('.agent-knowledge-management-controls').boundingBox();
for (const control of [manager, refresh, upload]) {
expect(Math.abs(control!.y + control!.height / 2 - create!.y - create!.height / 2)).toBeLessThanOrEqual(1);
}
expect(Math.abs(manager!.width - (controls!.width - 144 - 8) / 2)).toBeLessThanOrEqual(2);
if (await creation.getByRole('textbox').count() === 0 && await management.getByRole('combobox', { name: '管理的知识库', exact: true }).inputValue() === '') {
expect(Math.abs(create!.y + create!.height / 2 - creationBounds!.y - creationBounds!.height / 2)).toBeLessThanOrEqual(2);
}
} else { } else {
expect(creationBounds!.y + creationBounds!.height).toBeLessThanOrEqual(managementBounds!.y + 1); expect(creationBounds!.y + creationBounds!.height).toBeLessThanOrEqual(managementBounds!.y + 1);
expect(managementBounds!.y + managementBounds!.height).toBeLessThanOrEqual(selectionBounds!.y + 1); expect(managementBounds!.y + managementBounds!.height).toBeLessThanOrEqual(selectionBounds!.y + 1);
@@ -217,6 +229,20 @@ async function expectKnowledgeLayout(page: Page) {
expect(Math.abs(creationBounds!.width - selectionBounds!.width)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.width - selectionBounds!.width)).toBeLessThanOrEqual(1);
} }
expect(Math.abs(creationBounds!.x - selectionBounds!.x)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.x - selectionBounds!.x)).toBeLessThanOrEqual(1);
const choice = await selection.getByRole('combobox', { name: '选择知识库', exact: true }).boundingBox();
const choiceRow = await selection.locator('.agent-knowledge-selection-controls').boundingBox();
if (ownedBounds!.width > 840) {
expect(Math.abs(choice!.width - (choiceRow!.width - 102) * 2 / 3)).toBeLessThanOrEqual(2);
}
const selectedList = selection.getByRole('list', { name: '已选知识库', exact: true });
if (await selectedList.count()) {
const selectedBounds = await selectedList.boundingBox();
expect(Math.abs(selectedBounds!.x - choice!.x)).toBeLessThanOrEqual(1);
expect(Math.abs(selectedBounds!.width - choice!.width)).toBeLessThanOrEqual(1);
}
for (const refresh of [management.getByRole('button', { name: '刷新知识库与模型', exact: true }), selection.getByRole('button', { name: '刷新知识库', exact: true })]) {
expect(await refresh.evaluate(element => getComputedStyle(element).borderTopWidth)).toBe('1px');
}
await expectNoClipping(page); await expectNoClipping(page);
} }
async function screenshotKnowledge(page: Page, name: string) { async function screenshotKnowledge(page: Page, name: string) {
@@ -356,6 +382,16 @@ test('cancelled and failed uploads preserve shared choices and the personal draf
// An uncertain upload response keeps its retry attached to the same library. // An uncertain upload response keeps its retry attached to the same library.
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeDisabled(); await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeDisabled();
await expect(knowledge.getByRole('button', { name: '新建知识库', exact: true })).toBeDisabled(); await expect(knowledge.getByRole('button', { name: '新建知识库', exact: true })).toBeDisabled();
await page.setViewportSize({ width: 1440, height: 850 });
const managementCard = knowledge.getByRole('region', { name: '查看我的知识库', exact: true });
const managementBounds = await managementCard.boundingBox();
for (const control of await managementCard.locator('.agent-knowledge-management-controls').getByRole('button').all()) {
const bounds = await control.boundingBox();
expect(bounds!.x).toBeGreaterThanOrEqual(managementBounds!.x);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(managementBounds!.x + managementBounds!.width);
}
await expectNoClipping(page);
await page.setViewportSize({ width: 375, height: 850 });
const failedUpload = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick')).at(-1)!; const failedUpload = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick')).at(-1)!;
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'cancel'; }); await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'cancel'; });
await knowledge.getByRole('button', { name: '重新选择同一文件并重试', exact: true }).click(); await knowledge.getByRole('button', { name: '重新选择同一文件并重试', exact: true }).click();