feat(agents): 完成云智能体工作台与交互流程
This commit is contained in:
1 parent
46fbae9dec
commit
64a3cf4934
26 files changed
+1955
-49
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Download, Folder, Paperclip } from 'lucide-react';
|
||||
import { usePendingCloudInput } from './CloudPending';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
|
||||
import type { CloudAttachment, CloudFile, CloudUpload } from '../../../shared/cloud-agents';
|
||||
|
||||
export function CloudFiles({ threadId, ensureThread, selected, onSelected, onBusy, disabled }: {
|
||||
threadId?: string; ensureThread: () => Promise<string>; selected: string[]; onSelected: (ids: string[]) => void;
|
||||
onBusy: (busy: boolean) => void; disabled: boolean;
|
||||
}) {
|
||||
const [attachments, setAttachments] = useState<CloudAttachment[]>([]);
|
||||
const [files, setFiles] = useState<CloudFile[]>([]);
|
||||
const [path, setPath] = useState('/');
|
||||
const [upload, setUpload] = useState<CloudUpload | null>(null);
|
||||
const [parseMethod, setParseMethod] = useState('disable');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
usePendingCloudInput(busy || Boolean(upload));
|
||||
useEffect(() => {
|
||||
if (!threadId) return;
|
||||
let live = true;
|
||||
Promise.all([cloudAgentsApi.call('attachments', { thread_id: threadId }), cloudAgentsApi.call('files', { thread_id: threadId, path })])
|
||||
.then(([a, f]) => { if (live) { setAttachments(a.attachments); setFiles(f.files); } })
|
||||
.catch(e => { if (live) setError(e instanceof Error ? e.message : '读取文件失败'); });
|
||||
return () => { live = false; };
|
||||
}, [threadId, path, refresh]);
|
||||
const act = async (task: () => Promise<unknown>) => {
|
||||
setBusy(true); onBusy(true); setError('');
|
||||
try { await task(); setRefresh(v => v + 1); }
|
||||
catch(e) { setError(e instanceof Error ? e.message : '文件操作失败'); }
|
||||
finally { setBusy(false); onBusy(false); }
|
||||
};
|
||||
return <details className="my-3 rounded-lg border border-border px-3 py-2 text-sm">
|
||||
<summary className="min-h-8 cursor-pointer py-1">附件与产物 {selected.length ? '· 本条消息附带 ' + selected.length + ' 个文件' : ''}</summary>
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="flex items-center gap-2"><Button variant="outline" size="sm" disabled={disabled || busy || Boolean(upload)} onClick={() => void act(async () => {
|
||||
const picked = await cloudAgentsApi.upload();
|
||||
if (picked) { setUpload(picked); setParseMethod('disable'); }
|
||||
})}><Paperclip className="mr-2 h-4 w-4" />添加附件</Button>
|
||||
<Button variant="ghost" size="sm" disabled={busy} onClick={() => setRefresh(v => v + 1)}>刷新文件</Button></div>
|
||||
{upload && <div className="space-y-2 rounded-lg bg-muted/40 p-3"><p>{upload.file_name}</p>
|
||||
{upload.parse_supported && <label className="block text-xs">读取方式<select aria-label="附件读取方式" className="ml-2 h-10 rounded-md border border-input bg-background px-2" value={parseMethod} disabled={busy} onChange={e => setParseMethod(e.target.value)}>
|
||||
<option value="disable">保留原始文件</option>{upload.parse_methods.filter(m => m !== 'disable').map(method => <option key={method} value={method}>{method === 'rapid_ocr' ? '本地文字识别' : method}</option>)}
|
||||
</select></label>}
|
||||
<div className="flex gap-2"><Button size="sm" disabled={busy} onClick={() => void act(async () => {
|
||||
const id = await ensureThread();
|
||||
const parsed = parseMethod === 'disable' ? undefined : await cloudAgentsApi.call('parseAttachment', { object_name: upload.object_name, parse_method: parseMethod });
|
||||
const result = await cloudAgentsApi.call('confirmAttachment', { thread_id: id, attachments: [{
|
||||
object_name: upload.object_name, file_type: upload.file_type, ...(parsed ? { parsed_object_name: parsed.parsed_object_name } : {}),
|
||||
}] });
|
||||
onSelected([...selected, ...result.attachments.map(a => a.file_id)]); setUpload(null);
|
||||
})}>加入此对话</Button><Button variant="ghost" size="sm" disabled={busy} onClick={() => setUpload(null)}>取消</Button></div></div>}
|
||||
{attachments.map(file => <label key={file.file_id} className="flex min-h-10 items-center gap-2">
|
||||
<input type="checkbox" disabled={disabled || busy} checked={selected.includes(file.file_id)} onChange={e => onSelected(e.target.checked ? [...selected, file.file_id] : selected.filter(id => id !== file.file_id))} />
|
||||
<span className="min-w-0 flex-1 truncate">{file.file_name}</span><span className="text-xs text-muted-foreground tabular-nums">{Math.ceil(file.file_size / 1024)} KB</span>
|
||||
<Button variant="ghost" size="sm" disabled={disabled || busy} aria-label={'删除附件 ' + file.file_name} onClick={event => { event.preventDefault(); void act(async () => {
|
||||
await cloudAgentsApi.call('deleteAttachment', { thread_id: threadId!, file_id: file.file_id });
|
||||
onSelected(selected.filter(id => id !== file.file_id));
|
||||
}); }}>删除</Button>
|
||||
</label>)}
|
||||
{threadId && <div className="space-y-2 border-t border-border pt-3">
|
||||
<div className="flex items-center justify-between"><p className="break-all text-xs text-muted-foreground">会话文件 · {path}</p>
|
||||
{path !== '/' && <Button variant="ghost" size="sm" onClick={() => setPath(path.split('/').slice(0, -1).join('/') || '/')}>上一级</Button>}</div>
|
||||
{files.map(file => <div key={file.name} className="flex min-h-10 items-center justify-between gap-3">
|
||||
{file.is_dir ? <Button variant="ghost" className="min-w-0 justify-start" onClick={() => setPath(file.directory_path)}><Folder className="mr-2 h-4 w-4" /><span className="truncate">{file.name}</span></Button>
|
||||
: <><span className="min-w-0 truncate">{file.name}</span><Button variant="ghost" aria-label={'保存 ' + file.name} disabled={busy}
|
||||
onClick={() => void act(() => cloudAgentsApi.download(threadId, file.path))}><Download className="h-4 w-4" /></Button></>}
|
||||
</div>)}
|
||||
</div>}
|
||||
{error && <p role="alert" className="text-xs text-destructive">{error}。可刷新文件列表核对已上传的附件。</p>}
|
||||
</div>
|
||||
</details>;
|
||||
}
|
||||
Reference in new issue
Block a user