fix 文档管理模块 & 法规对话模块

This commit is contained in:
wangwei
2026-05-20 23:34:08 +08:00
parent c22b03dc07
commit b065d55c86
39 changed files with 1671 additions and 540 deletions
@@ -0,0 +1,58 @@
import React, { useRef } from 'react';
import { useTheme } from '../../contexts';
import type { RetrievalData } from '../../types';
interface CitedAnswerProps {
text: string;
sources: RetrievalData[];
onCiteClick: (index: number) => void;
}
export const CitedAnswer: React.FC<CitedAnswerProps> = ({ text, sources, onCiteClick }) => {
const { theme } = useTheme();
if (!text) return null;
// Split on [N] patterns, preserving delimiters
const parts = text.split(/(\[\d+\])/g);
return (
<span style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>
{parts.map((part, i) => {
const match = part.match(/^\[(\d+)\]$/);
if (!match) return <React.Fragment key={i}>{part}</React.Fragment>;
const idx = parseInt(match[1], 10);
const source = sources[idx - 1];
return (
<sup
key={i}
title={source ? `${source.file} · ${source.clause}` : `引用 ${idx}`}
onClick={() => onCiteClick(idx)}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 18,
height: 18,
padding: '0 4px',
marginLeft: 1,
fontSize: 10,
fontWeight: 700,
background: theme.gradientAccent,
color: '#fff',
borderRadius: 4,
cursor: source ? 'pointer' : 'default',
verticalAlign: 'super',
lineHeight: 1,
userSelect: 'none',
}}
>
{idx}
</sup>
);
})}
</span>
);
};
+51 -5
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { useTheme } from '../../contexts';
import type { ChatMessage, RetrievalData } from '../../types';
import { getQuickQuestions, ragChat } from '../../api/rag';
import { CitedAnswer } from './CitedAnswer';
const ragQuickQuestionsDefault = [
'电动自行车上路需要什么条件?',
@@ -24,6 +25,8 @@ export const RagChatPage: React.FC = () => {
const [showClearConfirm, setShowClearConfirm] = useState<boolean>(false);
const [selectedRetrieval, setSelectedRetrieval] = useState<RetrievalData | null>(null);
const [quickQuestions, setQuickQuestions] = useState<string[]>(ragQuickQuestionsDefault);
const [filterRegulationType, setFilterRegulationType] = useState<string>('');
const [highlightedSourceIdx, setHighlightedSourceIdx] = useState<number | null>(null);
function nextMessageId() {
const currentId = nextMessageIdRef.current;
@@ -55,8 +58,10 @@ export const RagChatPage: React.FC = () => {
setInput('');
setLoading(true);
setRetrievals([]);
setHighlightedSourceIdx(null);
let currentResponse = '';
const activeFilters = filterRegulationType.trim() || undefined;
void ragChat(
text,
@@ -112,7 +117,8 @@ export const RagChatPage: React.FC = () => {
},
() => {
setLoading(false);
}
},
activeFilters
);
};
@@ -130,8 +136,10 @@ export const RagChatPage: React.FC = () => {
setLoading(true);
setMessages((prev) => [...prev.slice(0, -1)]);
setRetrievals([]);
setHighlightedSourceIdx(null);
let currentResponse = '';
const activeFilters = filterRegulationType.trim() || undefined;
void ragChat(
lastUserMsg.content,
@@ -185,7 +193,8 @@ export const RagChatPage: React.FC = () => {
},
() => {
setLoading(false);
}
},
activeFilters
);
};
@@ -267,7 +276,17 @@ export const RagChatPage: React.FC = () => {
whiteSpace: 'pre-wrap',
border: msg.role === 'assistant' ? `1px solid ${theme.border}` : 'none',
}}>
{msg.content}
{msg.role === 'assistant' ? (
<CitedAnswer
text={msg.content}
sources={retrievals}
onCiteClick={(idx) => {
setHighlightedSourceIdx(idx);
const el = document.getElementById(`source-${idx}`);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
/>
) : msg.content}
{msg.role === 'assistant' && msg.retrievalIds && msg.retrievalIds.length > 0 && (
<div style={{
marginTop: 10,
@@ -331,6 +350,31 @@ export const RagChatPage: React.FC = () => {
background: theme.bg,
borderTop: `1px solid ${theme.border}`,
}}>
<div style={{
display: 'flex',
gap: 8,
marginBottom: 10,
alignItems: 'center',
}}>
<span className="mono" style={{ fontSize: 11, color: theme.text3, whiteSpace: 'nowrap' }}></span>
<input
value={filterRegulationType}
onChange={(e) => setFilterRegulationType(e.target.value)}
placeholder="如: GB / UN-ECE / IATF(留空不过滤)"
style={{
flex: 1,
maxWidth: 280,
padding: '5px 10px',
fontSize: 12,
background: theme.bgHover,
border: `1px solid ${theme.border}`,
borderRadius: 6,
color: theme.text,
outline: 'none',
}}
/>
</div>
<div style={{
display: 'flex',
gap: 8,
@@ -468,14 +512,16 @@ export const RagChatPage: React.FC = () => {
{retrievals.map((r, i) => (
<div
key={r.id}
id={`source-${i + 1}`}
onClick={() => setSelectedRetrieval(r)}
style={{
padding: 16,
background: theme.bgHover,
background: highlightedSourceIdx === i + 1 ? theme.bgElevated : theme.bgHover,
borderRadius: 10,
border: `1px solid ${theme.border}`,
border: `1px solid ${highlightedSourceIdx === i + 1 ? theme.accent : theme.border}`,
cursor: 'pointer',
position: 'relative',
transition: 'border-color 0.2s, background 0.2s',
}}
>
<div style={{