refactor: remove legacy session recovery UI
This commit is contained in:
+2
-2
@@ -93,6 +93,6 @@ uv run pytest -q
|
|||||||
|
|
||||||
正式回答上方的工具过程现在先整体折叠,每次调用再单独折叠;引用移至回答最后并整体折叠。会话页面移除了全局“后台进度”卡片。`get_scores` 改为按项目和评分项分页,每次最多一个项目、六项评分,并提供后续偏移量;长结果不再以空泛的截断提示替代全部评分。新检索调用会在运行记录中保存最多六条有界原文片段;回答引用对全部工具结果去重,不再只保存前 20 条。
|
正式回答上方的工具过程现在先整体折叠,每次调用再单独折叠;引用移至回答最后并整体折叠。会话页面移除了全局“后台进度”卡片。`get_scores` 改为按项目和评分项分页,每次最多一个项目、六项评分,并提供后续偏移量;长结果不再以空泛的截断提示替代全部评分。新检索调用会在运行记录中保存最多六条有界原文片段;回答引用对全部工具结果去重,不再只保存前 20 条。
|
||||||
|
|
||||||
业务数据库 6 号会话的旧回答原先只有前 20 条引用。已在备份 `data/backups/wisegrade-pre-chat-citation-20260927.dump`(权限 `600`,不提交)后,用当前项目知识库中逐字匹配的片段补入 14 条来源引用,包括综合报告第 48 页;旧记录的三个 `get_scores` 截断调用附上明确标注的**当前数据库重查结果**,不宣称模型当时读过这些值。旧 `search_project_material` 调用仅持久化了引用数量,查询文本和每次调用的六条原文当时未保存,无法精确还原调用归属;工具详情会展示本轮已核验的相关原文,并明确标明不能归属到这一次调用,也可跳转到回答末尾的引用。新调用保存实际每次找到的片段。
|
本地业务数据库 6 号会话曾为定位问题补入来源引用和当前评分重查值;用户计划自行删除该会话,界面不保留针对这条旧记录的提示或特殊展示。操作前备份保存在 `data/backups/wisegrade-pre-chat-citation-20260927.dump`(权限 `600`,不提交)。新调用会保存实际每次找到的片段。
|
||||||
|
|
||||||
隔离 PostgreSQL 测试库执行 `pytest -q -m 'not integration'`:35 通过、2 个集成测试未运行。前端 `npm run build` 通过,Chromium `npm run test:e2e`:3 通过。API 与 Web 镜像已重新构建并以 `docker compose up -d --wait api web` 更新,Web 真实会话 6 的引用第 48 页、默认折叠、旧工具详情中的 14 条已核验来源片段及无后台进度卡片已用浏览器核对;新评分工具对该会话范围返回六项,序列化后 6,820 字符。用真实项目检索“视觉检测 Otsu 光照鲁棒性 第48页”返回六条带原文与链接的结果,其中包含第 48 页。前端产物仍有 Vite 大块警告。
|
隔离 PostgreSQL 测试库执行 `pytest -q -m 'not integration'`:35 通过、2 个集成测试未运行。前端 `npm run build` 通过,Chromium `npm run test:e2e`:3 通过。API 与 Web 镜像已重新构建并以 `docker compose up -d --wait api web` 更新;新评分工具对限定范围返回六项,序列化后 6,820 字符。用真实项目检索“视觉检测 Otsu 光照鲁棒性 第48页”返回六条带原文与链接的结果,其中包含第 48 页。前端产物仍有 Vite 大块警告。
|
||||||
|
|||||||
@@ -100,15 +100,15 @@ function CitationCards({ items }: { items: any[] }) {
|
|||||||
{safeUrl(item.image_url) && <a href={safeUrl(item.image_url)} target="_blank" rel="noopener noreferrer">查看图像</a>}</Space></div>;
|
{safeUrl(item.image_url) && <a href={safeUrl(item.image_url)} target="_blank" rel="noopener noreferrer">查看图像</a>}</Space></div>;
|
||||||
})}</div>;
|
})}</div>;
|
||||||
}
|
}
|
||||||
function Citations({ items, messageId }: { items: any[]; messageId: number }) {
|
function Citations({ items }: { items: any[] }) {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const unique = (items || []).filter(item => { if (!item || typeof item !== 'object') return false;
|
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]);
|
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 (seen.has(key)) return false; seen.add(key); return true; });
|
||||||
if (!unique.length) return null;
|
if (!unique.length) return null;
|
||||||
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>;
|
return <section className="chat-citations" aria-label="引用资料"><details className="chat-citation-group"><summary><Text strong>引用 · {unique.length}</Text></summary><CitationCards items={unique} /></details></section>;
|
||||||
}
|
}
|
||||||
function Tools({ calls, messageId, legacyEvidence = [] }: { calls: any[]; messageId?: number; legacyEvidence?: any[] }) {
|
function Tools({ calls }: { calls: any[] }) {
|
||||||
if (!calls?.length) return null;
|
if (!calls?.length) return null;
|
||||||
const active = calls.filter(call => call.status === 'running').length;
|
const active = calls.filter(call => call.status === 'running').length;
|
||||||
const failed = calls.filter(call => call.status === 'failed').length;
|
const failed = calls.filter(call => call.status === 'failed').length;
|
||||||
@@ -118,10 +118,7 @@ function Tools({ calls, messageId, legacyEvidence = [] }: { calls: any[]; messag
|
|||||||
<Tag color={statusColor(call.status || 'completed')}>{call.status === 'running' ? '进行中' : call.status === 'failed' ? '失败' : '成功'}</Tag>
|
<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>
|
<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>
|
<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>
|
<Text strong>{call.error ? '错误' : '调用结果'}</Text><pre>{call.error || (typeof call.result === 'string' ? call.result : JSON.stringify(call.result ?? '运行中', null, 2))}</pre></div>
|
||||||
{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>;
|
</details>)}</div></details></section>;
|
||||||
}
|
}
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
@@ -229,7 +226,7 @@ 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>
|
<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>}
|
{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' ? '教师' : '智评'}>
|
</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' ? <><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>}
|
{item.role === 'assistant' ? <><Tools calls={item.tool_calls} /><Answer text={item.content} /><Citations items={item.citations} /></> : <Paragraph className="chat-content">{item.content}</Paragraph>}
|
||||||
</Card>)}
|
</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>}>
|
{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>)}
|
<Tools calls={run.tool_calls} />{run.error && <Alert type="error" message={run.error} />}</Card>)}
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ body { margin: 0; }
|
|||||||
.citation-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
|
.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 p { margin: 5px 0; color: #44566d; white-space: pre-wrap; }
|
||||||
.citation-card summary { cursor: pointer; color: #44566d; overflow-wrap: anywhere; }
|
.citation-card summary { cursor: pointer; color: #44566d; overflow-wrap: anywhere; }
|
||||||
.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-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 { 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; }
|
.chat-tool summary { cursor: pointer; list-style-position: inside; overflow-wrap: anywhere; }
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ async function mockApi(page: Page) {
|
|||||||
tool_calls: [{ id: 'call-one', name: 'search_project_material', status: 'completed', arguments: { project_id: 1 },
|
tool_calls: [{ id: 'call-one', name: 'search_project_material', status: 'completed', arguments: { project_id: 1 },
|
||||||
summary: '找到 1 条引用', result: { found: true, citations: [{ page: 48, quote: '第48页原文片段' }] } },
|
summary: '找到 1 条引用', result: { found: true, citations: [{ page: 48, quote: '第48页原文片段' }] } },
|
||||||
{ id: 'call-two', name: 'get_scores', status: 'completed', arguments: { batch_id: 3, criterion_offset: 0 },
|
{ id: 'call-two', name: 'get_scores', status: 'completed', arguments: { batch_id: 3, criterion_offset: 0 },
|
||||||
summary: '返回 1 项评分', result: { next_criterion_offset: 6, results: [{ rows: [{ criterion_id: 'C1', final_score: 8 }] }] } },
|
summary: '返回 1 项评分', result: { next_criterion_offset: 6, results: [{ rows: [{ criterion_id: 'C1', final_score: 8 }] }] } }] }],
|
||||||
{ id: 'call-legacy', name: 'search_project_material', status: 'completed', arguments: { project_id: 1 },
|
|
||||||
summary: '找到 6 条引用', result: { found: true, citations: { count: 6 } } }] }],
|
|
||||||
actions: [], runs: [{ id: 1, status: 'completed', tool_calls: [] }] },
|
actions: [], runs: [{ id: 1, status: 'completed', tool_calls: [] }] },
|
||||||
};
|
};
|
||||||
let nextId = 8;
|
let nextId = 8;
|
||||||
@@ -105,12 +103,10 @@ test('markdown, citations, tool folding and narrow layout are safe', async ({ pa
|
|||||||
await expect(page.locator('.tool-detail').first()).toContainText('第48页原文片段');
|
await expect(page.locator('.tool-detail').first()).toContainText('第48页原文片段');
|
||||||
await page.locator('.chat-tool').nth(1).locator('summary').click();
|
await page.locator('.chat-tool').nth(1).locator('summary').click();
|
||||||
await expect(page.locator('.chat-tool').nth(1).locator('.tool-detail')).toContainText('C1');
|
await expect(page.locator('.chat-tool').nth(1).locator('.tool-detail')).toContainText('C1');
|
||||||
await page.locator('.chat-tool').nth(2).locator('summary').click();
|
await page.locator('.chat-citation-group > summary').click();
|
||||||
await expect(page.locator('.chat-tool').nth(2).locator('.legacy-evidence')).toContainText('第48页原文片段');
|
|
||||||
await page.getByRole('button', { name: '查看本轮已核验原文' }).click();
|
|
||||||
await expect(page.locator('.chat-citation-group')).toHaveAttribute('open', '');
|
await expect(page.locator('.chat-citation-group')).toHaveAttribute('open', '');
|
||||||
await expect(page.getByText('资料.pdf')).toBeVisible();
|
await expect(page.getByText('资料.pdf')).toBeVisible();
|
||||||
await expect(page.getByText('第 48 页').last()).toBeVisible();
|
await expect(page.getByText('第 48 页')).toBeVisible();
|
||||||
const positions = await page.evaluate(() => {
|
const positions = await page.evaluate(() => {
|
||||||
const card = document.querySelector('.chat-assistant .ant-card-body');
|
const card = document.querySelector('.chat-assistant .ant-card-body');
|
||||||
return ['.chat-tools', '.chat-markdown', '.chat-citations'].map(selector =>
|
return ['.chat-tools', '.chat-markdown', '.chat-citations'].map(selector =>
|
||||||
|
|||||||
Reference in New Issue
Block a user