fix: restore chat citations and tool details
This commit is contained in:
+24
-20
@@ -90,31 +90,39 @@ function Answer({ text }: { text: string }) {
|
||||
img: ({ alt, src }) => safeUrl(src) ? <a href={safeUrl(src)} target="_blank" rel="noopener noreferrer">{alt || '查看图片'}</a> : <span>{alt}</span> }}>
|
||||
{text || ''}</ReactMarkdown></div>;
|
||||
}
|
||||
function Citations({ items }: { items: any[] }) {
|
||||
function CitationCards({ items }: { items: any[] }) {
|
||||
return <div className="citation-grid">{items.map((item: any, index) => { const name = String(item.title || item.document || '未命名资料');
|
||||
const page = item.page ?? item.slide; const place = page == null ? '' : `第 ${page} ${/\.pptx?$/i.test(name) ? '张幻灯片' : '页'}`;
|
||||
const quote = typeof item.quote === 'string' ? item.quote.trim() : '';
|
||||
return <div className="citation-card" key={`${name}-${page}-${index}`}><div className="citation-heading"><Text strong>{name}</Text>{place && <Tag>{place}</Tag>}</div>
|
||||
{quote && (quote.length > 320 ? <details><summary>{quote.slice(0, 240)}… 展开原文</summary><p>{quote}</p></details> : <p>{quote}</p>)}
|
||||
<Space wrap size="small">{safeUrl(item.url) && <a href={safeUrl(item.url)} target="_blank" rel="noopener noreferrer">打开页面</a>}
|
||||
{safeUrl(item.image_url) && <a href={safeUrl(item.image_url)} target="_blank" rel="noopener noreferrer">查看图像</a>}</Space></div>;
|
||||
})}</div>;
|
||||
}
|
||||
function Citations({ items, messageId }: { items: any[]; messageId: number }) {
|
||||
const seen = new Set<string>();
|
||||
const unique = (items || []).filter(item => { if (!item || typeof item !== 'object') return false;
|
||||
const key = JSON.stringify([item.title || item.document, item.page, item.quote, item.url, item.image_url]);
|
||||
if (seen.has(key)) return false; seen.add(key); return true; });
|
||||
if (!unique.length) return null;
|
||||
return <section className="chat-citations" aria-label="引用资料"><Text strong>引用 · {unique.length}</Text><div className="citation-grid">
|
||||
{unique.map((item: any, index) => { const name = String(item.title || item.document || '未命名资料');
|
||||
const page = item.page ?? item.slide; const place = page == null ? '' : `第 ${page} ${/\.pptx?$/i.test(name) ? '张幻灯片' : '页'}`;
|
||||
const quote = typeof item.quote === 'string' ? item.quote.trim() : '';
|
||||
return <div className="citation-card" key={`${name}-${page}-${index}`}><div className="citation-heading"><Text strong>{name}</Text>{place && <Tag>{place}</Tag>}</div>
|
||||
{quote && (quote.length > 320 ? <details><summary>{quote.slice(0, 240)}… 展开原文</summary><p>{quote}</p></details> : <p>{quote}</p>)}
|
||||
<Space wrap size="small">{safeUrl(item.url) && <a href={safeUrl(item.url)} target="_blank" rel="noopener noreferrer">打开页面</a>}
|
||||
{safeUrl(item.image_url) && <a href={safeUrl(item.image_url)} target="_blank" rel="noopener noreferrer">查看图像</a>}</Space></div>;
|
||||
})}</div></section>;
|
||||
return <section className="chat-citations" aria-label="引用资料"><details id={`citations-${messageId}`} className="chat-citation-group"><summary><Text strong>引用 · {unique.length}</Text></summary><CitationCards items={unique} /></details></section>;
|
||||
}
|
||||
function Tools({ calls }: { calls: any[] }) {
|
||||
function Tools({ calls, messageId, legacyEvidence = [] }: { calls: any[]; messageId?: number; legacyEvidence?: any[] }) {
|
||||
if (!calls?.length) return null;
|
||||
return <section className="chat-tools" aria-label="工具调用"><Text type="secondary">工具调用 · {calls.length}</Text>
|
||||
{calls.map((call, index) => <details key={call.id || index} className="chat-tool"><summary><Space wrap size="small"><Text code>{call.name || '未知工具'}</Text>
|
||||
const active = calls.filter(call => call.status === 'running').length;
|
||||
const failed = calls.filter(call => call.status === 'failed').length;
|
||||
return <section className="chat-tools" aria-label="工具调用"><details className="chat-tool-group"><summary><Text strong>工具调用 · {calls.length}</Text>
|
||||
{active > 0 && <Tag color="processing">{active} 项进行中</Tag>}{failed > 0 && <Tag color="red">{failed} 项失败</Tag>}</summary>
|
||||
<div className="chat-tool-list">{calls.map((call, index) => <details key={call.id || index} className="chat-tool"><summary><Space wrap size="small"><Text code>{call.name || '未知工具'}</Text>
|
||||
<Tag color={statusColor(call.status || 'completed')}>{call.status === 'running' ? '进行中' : call.status === 'failed' ? '失败' : '成功'}</Tag>
|
||||
<Text type="secondary">{call.status === 'running' ? '正在执行' : call.error ? String(call.error).slice(0, 80) : call.summary || (typeof call.result === 'string' ? call.result : '已完成')}</Text></Space></summary>
|
||||
<div className="tool-detail"><Text strong>调用参数</Text><pre>{JSON.stringify(call.arguments || {}, null, 2)}</pre>
|
||||
<Text strong>{call.error ? '错误' : '调用结果'}</Text><pre>{call.error || (typeof call.result === 'string' ? call.result : JSON.stringify(call.result ?? '运行中', null, 2))}</pre></div>
|
||||
</details>)}</section>;
|
||||
<Text strong>{call.error ? '错误' : '调用结果'}</Text><pre>{call.error || (typeof call.result === 'string' ? call.result : JSON.stringify(call.result ?? '运行中', null, 2))}</pre>
|
||||
{call.result?.citations?.count !== undefined && <div><Text type="secondary">旧运行只保存了引用数量,无法还原各次检索对应的原文。</Text>{messageId && <Button type="link" size="small" onClick={() => { const target = document.getElementById(`citations-${messageId}`) as HTMLDetailsElement | null; if (target) { target.open = true; target.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }}>查看本轮已核验原文</Button>}
|
||||
{legacyEvidence.length > 0 && <div className="legacy-evidence"><Text strong>本轮回答已核验的相关原文(无法确认属于当前这一次检索)</Text><CitationCards items={legacyEvidence} /></div>}</div>}
|
||||
{call.result?.truncated && <Text type="secondary">旧运行的原始结果未保存。{call.result?.rechecked_current ? '附带的是当前数据库重查结果,不代表当时模型看到的内容。' : '再次提问可使用分页评分查询。'}</Text>}</div>
|
||||
</details>)}</div></details></section>;
|
||||
}
|
||||
export default function Chat() {
|
||||
const [conversations, setConversations] = useState<any[]>([]);
|
||||
@@ -131,7 +139,6 @@ export default function Chat() {
|
||||
const [versions, setVersions] = useState<any[]>([]);
|
||||
const [batches, setBatches] = useState<any[]>([]);
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
const [indexTasks, setIndexTasks] = useState<any[]>([]);
|
||||
const selection = useRef<string>(`draft-${token}`);
|
||||
const sending = useRef<Set<string>>(new Set());
|
||||
const deletedIds = useRef<Set<number>>(new Set());
|
||||
@@ -157,7 +164,6 @@ export default function Chat() {
|
||||
useEffect(() => { localStorage.removeItem('wisegrade_conversation_id'); refresh();
|
||||
api('/courses').then(async (courses: any[]) => setVersions((await Promise.all(courses.map(course => api(`/courses/${course.id}/versions`)))).flat())).catch(reportError);
|
||||
api('/scoring-batches').then(setBatches).catch(reportError); api('/projects').then(setProjects).catch(reportError);
|
||||
api('/index-tasks').then(setIndexTasks).catch(reportError);
|
||||
const timer = window.setInterval(refresh, 1800); return () => { window.clearInterval(timer); subscriptions.current.forEach(item => item.controller.abort()); subscriptions.current.clear(); };
|
||||
}, []);
|
||||
const watchRun = (id: number, runId: number) => {
|
||||
@@ -223,14 +229,12 @@ export default function Chat() {
|
||||
<Select aria-label="学生项目" allowClear disabled={locked || scopeSaving || running} placeholder="学生项目(可选)" value={currentScope.project_id} options={projects.filter(p => (!currentScope.course_version_id || p.course_version_id === currentScope.course_version_id) && (!currentScope.batch_id || !batches.find(b => b.id === currentScope.batch_id)?.project_ids || batches.find(b => b.id === currentScope.batch_id).project_ids.includes(p.id))).map(p => ({ value: p.id, label: `${p.student_no} ${p.name}` }))} onChange={changeProject} /></div>
|
||||
{locked && <Text type="secondary">首条消息已提交,课程版本、批次和学生项目范围不可修改。</Text>}
|
||||
</Card><div className="chat-messages">{detail?.messages?.map((item: any) => <Card key={item.id} size="small" className={`chat-message chat-${item.role}`} title={item.role === 'user' ? '教师' : '智评'}>
|
||||
{item.role === 'assistant' ? <Answer text={item.content} /> : <Paragraph className="chat-content">{item.content}</Paragraph>}
|
||||
{item.role === 'assistant' && <><Citations items={item.citations} /><Tools calls={item.tool_calls} /></>}
|
||||
{item.role === 'assistant' ? <><Tools calls={item.tool_calls} messageId={item.id} legacyEvidence={(item.citations || []).filter((source: any) => source.chunk_id)} /><Answer text={item.content} /><Citations items={item.citations} messageId={item.id} /></> : <Paragraph className="chat-content">{item.content}</Paragraph>}
|
||||
</Card>)}
|
||||
{detail?.runs?.filter((run: any) => run.status !== 'completed' && (run.tool_calls?.length || run.error || run.status === 'running')).map((run: any) => <Card key={`run-${run.id}`} size="small" className="chat-message chat-assistant" title={<Space>智评 <Tag color={statusColor(run.status)}>{run.status === 'running' ? '生成中' : run.status === 'failed' ? '失败' : '已中断'}</Tag></Space>}>
|
||||
<Tools calls={run.tool_calls} />{run.error && <Alert type="error" message={run.error} />}</Card>)}
|
||||
{!currentId && <Card size="small" className="chat-message"><Text type="secondary">输入问题,开始新的会话。</Text></Card>}</div>
|
||||
{detail?.actions?.map((action: any) => <ActionCard key={action.id} action={action} conversationId={detail.id} reload={() => loadDetail(detail.id).catch(reportError)} />)}
|
||||
{(indexTasks.length > 0 || batches.length > 0) && currentId && <Card size="small" className="top-gap" title="后台进度"><Space wrap>{indexTasks.slice(0, 5).map(task => <Tag key={`i${task.id}`} color={statusColor(task.status)}>索引 #{task.id} {task.status}{task.error ? `:${task.error}` : ''}</Tag>)}{batches.slice(0, 3).map(batch => <Tag key={`b${batch.id}`} color={statusColor(batch.status)}>批次 #{batch.id} {batch.status} {batch.progress_done}/{batch.progress_total}</Tag>)}</Space></Card>}
|
||||
<Space.Compact className="chat-compose"><Input.TextArea value={input} rows={3} disabled={running || !!pending[key]} placeholder="例如:查看批次 3 的未完成评分项;给评分项 42 打 8 分,评语……" onChange={e => setComposers(old => ({ ...old, [key]: e.target.value }))} onPressEnter={e => { if (!e.shiftKey) { e.preventDefault(); send(); } }} /><Button type="primary" loading={!!pending[key]} disabled={running || !input.trim()} onClick={send}>发送</Button></Space.Compact>
|
||||
</Col></Row><Modal open={deleteId !== undefined} title="删除会话?" okText="删除会话" okButtonProps={{ danger: true, loading: deleting }} cancelText="取消" onOk={remove} onCancel={() => { setDeleteError(''); setDeleteId(undefined); }}>
|
||||
<Paragraph>会话消息、工具记录和待确认卡片将被删除。已确认的业务操作与审计记录仍保留。</Paragraph>
|
||||
|
||||
@@ -46,12 +46,18 @@ body { margin: 0; }
|
||||
.chat-markdown table { display: block; width: max-content; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 10px 0; }
|
||||
.chat-markdown th, .chat-markdown td { border: 1px solid #d8e1eb; padding: 6px 9px; vertical-align: top; }
|
||||
.chat-citations { margin-top: 14px; padding-top: 10px; border-top: 1px solid #e2eaf3; }
|
||||
.chat-tool-group, .chat-citation-group { min-width: 0; }
|
||||
.chat-tool-group > summary, .chat-citation-group > summary { cursor: pointer; padding: 7px 2px; overflow-wrap: anywhere; }
|
||||
.chat-tool-group > summary .ant-tag { margin-left: 8px; }
|
||||
.chat-tool-list { padding: 0 0 5px 8px; border-left: 2px solid #d6e4f5; }
|
||||
.citation-grid { display: grid; gap: 8px; margin-top: 7px; }
|
||||
.citation-card { border-left: 3px solid #5b91d9; background: #f8fbff; padding: 8px 10px; min-width: 0; overflow-wrap: anywhere; }
|
||||
.citation-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
|
||||
.citation-card p { margin: 5px 0; color: #44566d; white-space: pre-wrap; }
|
||||
.citation-card summary { cursor: pointer; color: #44566d; overflow-wrap: anywhere; }
|
||||
.chat-tools { margin-top: 12px; }
|
||||
.legacy-evidence { margin-top: 10px; }
|
||||
.legacy-evidence .citation-grid { max-height: 300px; overflow-y: auto; }
|
||||
.chat-tools { margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid #e2eaf3; }
|
||||
.chat-tool { border: 1px solid #e2e8ef; border-radius: 6px; margin-top: 5px; padding: 6px 9px; }
|
||||
.chat-tool summary { cursor: pointer; list-style-position: inside; overflow-wrap: anywhere; }
|
||||
.tool-detail { margin-top: 8px; padding-left: 12px; }
|
||||
|
||||
Reference in New Issue
Block a user