Commit 50f02701 authored by David Yang's avatar David Yang

feat: 增加审计回答 Word 下载

parent 4657579b
...@@ -17,7 +17,6 @@ import { ...@@ -17,7 +17,6 @@ import {
Empty, Empty,
Flex, Flex,
InputNumber, InputNumber,
message,
Modal, Modal,
Progress, Progress,
Space, Space,
...@@ -26,6 +25,7 @@ import { ...@@ -26,6 +25,7 @@ import {
Tooltip, Tooltip,
Typography, Typography,
Upload, Upload,
message,
} from 'antd'; } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
...@@ -36,6 +36,7 @@ import { ...@@ -36,6 +36,7 @@ import {
exportAuditExcel, exportAuditExcel,
exportAuditMarkdown, exportAuditMarkdown,
exportAuditWord, exportAuditWord,
exportGroundedAuditWord,
filterAuditRecords, filterAuditRecords,
} from './export'; } from './export';
...@@ -401,6 +402,43 @@ export function AuditChecklistResult({ result, aiContent = '' }) { ...@@ -401,6 +402,43 @@ export function AuditChecklistResult({ result, aiContent = '' }) {
); );
} }
export function AuditWordDownload({
records = [],
aiContent = '',
question = '',
title = '审计知识库问答',
}) {
const { styles } = useAuditStyles();
const [loading, setLoading] = useState(false);
const handleDownload = async () => {
setLoading(true);
try {
await exportGroundedAuditWord({ records, aiContent, question, title });
} catch (error) {
message.error(`Word 导出失败:${error.message || '未知错误'}`);
} finally {
setLoading(false);
}
};
return (
<div className={styles.exportBar}>
<Typography.Text type="secondary">
知识库原文 {records.length} 条
</Typography.Text>
<Button
type="primary"
icon={<FileWordOutlined />}
loading={loading}
onClick={handleDownload}
>
下载 Word
</Button>
</div>
);
}
export function AuditKnowledgeImportDialog({ export function AuditKnowledgeImportDialog({
open, open,
existingRecords, existingRecords,
......
...@@ -283,6 +283,13 @@ function createWordStylesXml() { ...@@ -283,6 +283,13 @@ function createWordStylesXml() {
</w:styles>`; </w:styles>`;
} }
function wrapWordDocument(body) {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body.join(
'',
)}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="720" w:footer="720" w:gutter="0"/><w:cols w:space="720"/><w:docGrid w:linePitch="360"/></w:sectPr></w:body></w:document>`;
}
function createWordDocumentXml(result, aiContent, threshold) { function createWordDocumentXml(result, aiContent, threshold) {
const summary = parseAuditSummary(aiContent, result.project); const summary = parseAuditSummary(aiContent, result.project);
const groups = groupAuditRecords(filterAuditRecords(result, threshold)); const groups = groupAuditRecords(filterAuditRecords(result, threshold));
...@@ -395,13 +402,10 @@ function createWordDocumentXml(result, aiContent, threshold) { ...@@ -395,13 +402,10 @@ function createWordDocumentXml(result, aiContent, threshold) {
}); });
}); });
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> return wrapWordDocument(body);
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body.join(
'',
)}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="720" w:footer="720" w:gutter="0"/><w:cols w:space="720"/><w:docGrid w:linePitch="360"/></w:sectPr></w:body></w:document>`;
} }
export async function createAuditWordBlob(result, aiContent, threshold) { async function createWordBlob(documentXml) {
const zip = new JSZip(); const zip = new JSZip();
zip.file( zip.file(
'[Content_Types].xml', '[Content_Types].xml',
...@@ -414,10 +418,7 @@ export async function createAuditWordBlob(result, aiContent, threshold) { ...@@ -414,10 +418,7 @@ export async function createAuditWordBlob(result, aiContent, threshold) {
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`,
); );
const word = zip.folder('word'); const word = zip.folder('word');
word.file( word.file('document.xml', documentXml);
'document.xml',
createWordDocumentXml(result, aiContent, threshold),
);
word.file('styles.xml', createWordStylesXml()); word.file('styles.xml', createWordStylesXml());
word word
.folder('_rels') .folder('_rels')
...@@ -432,9 +433,102 @@ export async function createAuditWordBlob(result, aiContent, threshold) { ...@@ -432,9 +433,102 @@ export async function createAuditWordBlob(result, aiContent, threshold) {
}); });
} }
export async function createAuditWordBlob(result, aiContent, threshold) {
return createWordBlob(createWordDocumentXml(result, aiContent, threshold));
}
export async function exportAuditWord(result, aiContent, threshold) { export async function exportAuditWord(result, aiContent, threshold) {
downloadBlob( downloadBlob(
await createAuditWordBlob(result, aiContent, threshold), await createAuditWordBlob(result, aiContent, threshold),
`${safeFileName(result.project)}.docx`, `${safeFileName(result.project)}.docx`,
); );
} }
function createGroundedAuditWordDocumentXml({
title,
question,
aiContent,
records = [],
}) {
const body = [
wordParagraph(wordRun(title || '审计知识库问答'), {
style: 'Title',
alignment: 'center',
}),
wordParagraph(
wordRun(`生成时间:${formatWordDate()}`, {
size: '18',
color: '808080',
}),
{ alignment: 'right' },
),
wordParagraph(wordRun('用户问题', { bold: true }), {
style: 'Heading1',
}),
wordParagraph(wordRun(question || '(未记录)'), { firstLine: '420' }),
wordParagraph(wordRun('智能体回答', { bold: true }), {
style: 'Heading1',
}),
wordParagraph(wordRun(stripMarkdown(aiContent) || '(无回答内容)'), {
firstLine: '420',
}),
wordParagraph(wordRun('知识库原文依据', { bold: true }), {
style: 'Heading1',
}),
];
if (!records.length) {
body.push(wordParagraph(wordRun('(当前知识库没有可用风险点)')));
return wrapWordDocument(body);
}
records.forEach((record, index) => {
const heading =
record.riskPointCode ||
record.level2Project ||
record.riskDescription ||
`风险点 ${index + 1}`;
const details = [
['风险点编号', record.riskPointCode],
['业务板块', record.businessSegment],
['一级项目', record.level1Project],
['二级项目', record.level2Project],
['具体风险点描述', record.riskDescription],
['法规依据', record.regulationBasis],
['风险点来源', record.riskSource],
['风险等级', record.riskLevel],
['来源文件名', record.sourceFileName],
['来源行号', record.sourceRow],
['文件批次', record.sourceBatch],
['备注', record.remark],
];
body.push(
wordParagraph(wordRun(`${index + 1}. ${heading}`, { bold: true }), {
style: 'Heading2',
}),
...details
.filter(
([, value]) => value !== undefined && value !== null && value !== '',
)
.map(([label, value]) =>
wordParagraph(
[
wordRun(`${label}:`, { bold: true, size: '21' }),
wordRun(String(value), { size: '21' }),
],
{ leftIndent: '283' },
),
),
);
});
return wrapWordDocument(body);
}
export async function exportGroundedAuditWord(options) {
downloadBlob(
await createWordBlob(createGroundedAuditWordDocumentXml(options)),
`${safeFileName(options.title || '审计知识库问答')}.docx`,
);
}
...@@ -48,6 +48,7 @@ import { ...@@ -48,6 +48,7 @@ import {
AuditChecklistResult, AuditChecklistResult,
AuditKnowledgeBar, AuditKnowledgeBar,
AuditKnowledgeImportDialog, AuditKnowledgeImportDialog,
AuditWordDownload,
} from '../../features/audit/components'; } from '../../features/audit/components';
import { import {
buildGroundedAuditKnowledgePrompt, buildGroundedAuditKnowledgePrompt,
...@@ -681,6 +682,7 @@ export default forwardRef((props, ref) => { ...@@ -681,6 +682,7 @@ export default forwardRef((props, ref) => {
const abortController = useRef(null); const abortController = useRef(null);
const auditRecordsRef = useRef([]); const auditRecordsRef = useRef([]);
const pendingAuditResultRef = useRef(null); const pendingAuditResultRef = useRef(null);
const pendingAuditExportRef = useRef(null);
const promptGroups = useMemo(() => { const promptGroups = useMemo(() => {
return [ return [
buildTopicPromptGroup(configuredTopics), buildTopicPromptGroup(configuredTopics),
...@@ -740,6 +742,10 @@ export default forwardRef((props, ref) => { ...@@ -740,6 +742,10 @@ export default forwardRef((props, ref) => {
originMessage?.auditResult || originMessage?.auditResult ||
pendingAuditResultRef.current || pendingAuditResultRef.current ||
undefined, undefined,
auditExport:
originMessage?.auditExport ||
pendingAuditExportRef.current ||
undefined,
tool_calls: undefined, tool_calls: undefined,
}; };
} }
...@@ -793,6 +799,10 @@ export default forwardRef((props, ref) => { ...@@ -793,6 +799,10 @@ export default forwardRef((props, ref) => {
originMessage?.auditResult || originMessage?.auditResult ||
pendingAuditResultRef.current || pendingAuditResultRef.current ||
undefined, undefined,
auditExport:
originMessage?.auditExport ||
pendingAuditExportRef.current ||
undefined,
tool_calls: toolCalls, tool_calls: toolCalls,
tools: toolCalls, tools: toolCalls,
}; };
...@@ -987,6 +997,14 @@ export default forwardRef((props, ref) => { ...@@ -987,6 +997,14 @@ export default forwardRef((props, ref) => {
if (usesStrictAuditKnowledge) { if (usesStrictAuditKnowledge) {
const records = await loadAuditRecords(); const records = await loadAuditRecords();
pendingAuditExportRef.current = {
question: message.content,
title:
conversationAgent?.key === 'audit-checklist' ||
topic?.name === AUDIT_TOPIC.name
? '审计检查清单'
: '审计知识库问答',
};
auditKnowledgePrompt = buildGroundedAuditKnowledgePrompt( auditKnowledgePrompt = buildGroundedAuditKnowledgePrompt(
records, records,
message.content, message.content,
...@@ -1006,6 +1024,7 @@ export default forwardRef((props, ref) => { ...@@ -1006,6 +1024,7 @@ export default forwardRef((props, ref) => {
const controller = new AbortController(); const controller = new AbortController();
onStream(controller); onStream(controller);
pendingAuditResultRef.current = null; pendingAuditResultRef.current = null;
pendingAuditExportRef.current = null;
onUpdate({ onUpdate({
data: 'LOADING', data: 'LOADING',
...@@ -1021,6 +1040,7 @@ export default forwardRef((props, ref) => { ...@@ -1021,6 +1040,7 @@ export default forwardRef((props, ref) => {
: await getTools(); : await getTools();
const sanitizeLlmMessage = ({ const sanitizeLlmMessage = ({
auditResult, auditResult,
auditExport,
files, files,
loading, loading,
settings, settings,
...@@ -1287,6 +1307,33 @@ export default forwardRef((props, ref) => { ...@@ -1287,6 +1307,33 @@ export default forwardRef((props, ref) => {
} }
if (item.message.role === 'assistant') { if (item.message.role === 'assistant') {
let previousQuestion = '';
for (
let messageIndex = index - 1;
messageIndex >= 0;
messageIndex -= 1
) {
if (messages[messageIndex]?.message?.role === 'user') {
previousQuestion = messages[messageIndex].message.content || '';
break;
}
}
const usesAuditWordExport =
conversationAgent?.key === 'knowledge-copilot' ||
conversationAgent?.key === 'audit-checklist' ||
conversationTopic?.name === AUDIT_TOPIC.name;
const auditExport =
item.message.auditExport ||
(usesAuditWordExport && previousQuestion
? {
question: previousQuestion,
title:
conversationAgent?.key === 'audit-checklist' ||
conversationTopic?.name === AUDIT_TOPIC.name
? '审计检查清单'
: '审计知识库问答',
}
: null);
messageItem.classNames = { messageItem.classNames = {
content: `${item.status === 'loading' ? styles.loadingMessage : ''} ${ content: `${item.status === 'loading' ? styles.loadingMessage : ''} ${
styles.assistantBubble styles.assistantBubble
...@@ -1417,6 +1464,14 @@ export default forwardRef((props, ref) => { ...@@ -1417,6 +1464,14 @@ export default forwardRef((props, ref) => {
aiContent={item.message.content} aiContent={item.message.content}
/> />
)} )}
{item.status === 'success' && auditExport && (
<AuditWordDownload
records={auditRecords}
aiContent={item.message.content}
question={auditExport.question}
title={auditExport.title}
/>
)}
</Flex> </Flex>
); );
}; };
...@@ -1456,7 +1511,7 @@ export default forwardRef((props, ref) => { ...@@ -1456,7 +1511,7 @@ export default forwardRef((props, ref) => {
return messageItem; return messageItem;
}); });
}, [messages]); }, [auditRecords, conversationAgent, conversationTopic, messages]);
useEffect(() => { useEffect(() => {
abortController?.current?.abort('cancelled'); abortController?.current?.abort('cancelled');
...@@ -1472,10 +1527,10 @@ export default forwardRef((props, ref) => { ...@@ -1472,10 +1527,10 @@ export default forwardRef((props, ref) => {
}, [conversation]); }, [conversation]);
useEffect(() => { useEffect(() => {
if (!conversation && userInfo) { if (userInfo) {
loadAuditRecords().catch(() => {}); loadAuditRecords().catch(() => {});
} }
}, [conversation, loadAuditRecords, userInfo]); }, [loadAuditRecords, userInfo]);
useEffect(() => { useEffect(() => {
if (conversation) { if (conversation) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment