feat(aia): Integrate PromptService for 10 AI agents

Features:
- Migrate 10 agent prompts from hardcoded to database
- Add grayscale preview support (DRAFT/ACTIVE distribution)
- Implement 3-tier fallback (DB -> Cache -> Hardcoded)
- Add version management and rollback capability

Files changed:
- backend/scripts/migrate-aia-prompts.ts (new migration script)
- backend/src/common/prompt/prompt.fallbacks.ts (add AIA fallbacks)
- backend/src/modules/aia/services/agentService.ts (integrate PromptService)
- backend/src/modules/aia/services/conversationService.ts (pass userId)
- backend/src/modules/aia/types/index.ts (fix AgentStage type)

Documentation:
- docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-18-Prompt管理系统集成.md
- docs/02-通用能力层/00-通用能力层清单.md (add FileCard, Prompt management)
- docs/00-系统总体设计/00-系统当前状态与开发指南.md (update to v3.6)

Prompt codes:
- AIA_SCIENTIFIC_QUESTION, AIA_PICO_ANALYSIS, AIA_TOPIC_EVALUATION
- AIA_OUTCOME_DESIGN, AIA_CRF_DESIGN, AIA_SAMPLE_SIZE
- AIA_PROTOCOL_WRITING, AIA_METHODOLOGY_REVIEW
- AIA_PAPER_POLISH, AIA_PAPER_TRANSLATE

Tested: Migration script executed, all 10 prompts inserted successfully
This commit is contained in:
2026-01-18 15:48:53 +08:00
parent 66255368b7
commit 57fdc6ef00
290 changed files with 2950 additions and 106 deletions

View File

@@ -95,6 +95,9 @@ vite.config.*.timestamp-*

View File

@@ -62,6 +62,9 @@ exec nginx -g 'daemon off;'

View File

@@ -218,6 +218,9 @@ http {

View File

@@ -49,3 +49,6 @@ export default apiClient;

View File

@@ -248,3 +248,6 @@ export async function logout(): Promise<void> {

View File

@@ -14,3 +14,6 @@ export * from './api';

View File

@@ -38,3 +38,6 @@ export async function fetchUserModules(): Promise<string[]> {

View File

@@ -118,3 +118,6 @@ const ModulePermissionModal: React.FC<ModulePermissionModalProps> = ({
export default ModulePermissionModal;

View File

@@ -29,3 +29,6 @@ const AdminModule: React.FC = () => {
export default AdminModule;

View File

@@ -194,3 +194,6 @@ export const TENANT_TYPE_NAMES: Record<TenantType, string> = {
PUBLIC: '公共',
};

View File

@@ -78,3 +78,6 @@ export default AgentCard;

View File

@@ -17,8 +17,12 @@ import {
Download,
Paperclip,
Lightbulb,
Pencil,
Trash2,
Check,
} from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import { FileCard } from '@ant-design/x';
import { useAIStream } from '@/shared/components/Chat';
import { ThinkingBlock } from '@/shared/components/Chat';
import { getAccessToken } from '@/framework/auth/api';
@@ -26,6 +30,33 @@ import { AGENTS, AGENT_PROMPTS, BRAND_COLORS } from '../constants';
import type { AgentConfig, Conversation, Message } from '../types';
import '../styles/chat-workspace.css';
/**
* 根据文件扩展名获取 FileCard 的 icon 类型
*/
const getFileIcon = (filename: string): string => {
const ext = filename.split('.').pop()?.toLowerCase() || '';
const iconMap: Record<string, string> = {
pdf: 'pdf',
doc: 'word',
docx: 'word',
xls: 'excel',
xlsx: 'excel',
ppt: 'ppt',
pptx: 'ppt',
txt: 'default',
md: 'markdown',
jpg: 'image',
jpeg: 'image',
png: 'image',
gif: 'image',
mp4: 'video',
mp3: 'audio',
zip: 'zip',
rar: 'zip',
};
return iconMap[ext] || 'default';
};
interface ChatWorkspaceProps {
agent: AgentConfig;
initialQuery?: string;
@@ -55,8 +86,13 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(true);
const [attachments, setAttachments] = useState<Array<{id: string; name: string; size: number; uploading: boolean}>>([]);
const [isUploading, setIsUploading] = useState(false);
const [editingConvId, setEditingConvId] = useState<string | null>(null);
const [editingTitle, setEditingTitle] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const AgentIcon = getIcon(agent.icon);
const themeColor = BRAND_COLORS[agent.theme];
@@ -126,12 +162,80 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
}
}, [agent.id, isCreatingConversation]);
// 初始化:自动创建对话
// 初始化:加载对话列表
useEffect(() => {
if (!currentConversationId && !isCreatingConversation) {
createConversation();
}
}, [currentConversationId, isCreatingConversation, createConversation]);
const loadConversations = async () => {
try {
const token = getAccessToken();
const response = await fetch(`/api/v1/aia/conversations?agentId=${agent.id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const result = await response.json();
if (result.data?.conversations && result.data.conversations.length > 0) {
setConversations(result.data.conversations.map((c: any) => ({
id: c.id,
agentId: c.agentId,
title: c.title,
createdAt: new Date(c.createdAt),
updatedAt: new Date(c.updatedAt),
})));
// 选择最近的对话
setCurrentConversationId(result.data.conversations[0].id);
} else {
// 没有历史对话,创建新对话
createConversation();
}
} else {
// 加载失败,创建新对话
createConversation();
}
} catch (error) {
console.error('[ChatWorkspace] 加载对话列表失败:', error);
createConversation();
}
};
loadConversations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent.id]);
// 加载对话的历史消息
useEffect(() => {
if (!currentConversationId) return;
const loadMessages = async () => {
try {
const token = getAccessToken();
const response = await fetch(`/api/v1/aia/conversations/${currentConversationId}/messages`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const result = await response.json();
if (result.data?.messages) {
setMessages(result.data.messages.map((m: any) => ({
id: m.id,
role: m.role,
content: m.content,
thinking: m.thinkingContent,
attachments: m.attachmentDetails, // 使用附件详情
createdAt: new Date(m.createdAt),
})));
}
}
} catch (error) {
console.error('[ChatWorkspace] 加载历史消息失败:', error);
}
};
loadMessages();
}, [currentConversationId]);
// 滚动到底部
const scrollToBottom = useCallback(() => {
@@ -170,11 +274,17 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
}
}
// 添加用户消息
// 获取已上传的附件信息(用于显示)
const uploadedAttachments = attachments
.filter(a => !a.uploading && !a.id.startsWith('temp-'))
.map(a => ({ id: a.id, filename: a.name, size: a.size }));
// 添加用户消息(包含附件信息)
const userMessage: Message = {
id: `user-${Date.now()}`,
role: 'user',
content,
attachments: uploadedAttachments.length > 0 ? uploadedAttachments : undefined,
createdAt: new Date(),
};
setMessages(prev => [...prev, userMessage]);
@@ -196,25 +306,53 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
};
setMessages(prev => [...prev, aiMessage]);
// 获取已上传的附件 ID排除正在上传的
const uploadedAttachmentIds = attachments
.filter(a => !a.uploading && !a.id.startsWith('temp-'))
.map(a => a.id);
// 调用流式API
const result = await sendStreamMessage(content, {
conversationId: convId,
agentId: agent.id,
enableDeepThinking: deepThinkingEnabled,
attachmentIds: uploadedAttachmentIds.length > 0 ? uploadedAttachmentIds : undefined,
});
// 更新对话标题
// 发送成功后清空附件
if (uploadedAttachmentIds.length > 0) {
setAttachments([]);
}
// 更新对话标题(首次发消息时自动命名)
if (conversations.find(c => c.id === convId)?.title === '新对话') {
const title = content.length > 20 ? content.slice(0, 20) + '...' : content;
const newTitle = content.length > 20 ? content.slice(0, 20) + '...' : content;
// 更新前端状态
setConversations(prev =>
prev.map(c =>
c.id === convId
? { ...c, title, updatedAt: new Date() }
? { ...c, title: newTitle, updatedAt: new Date() }
: c
)
);
// 同步保存到后端数据库
try {
const token = getAccessToken();
await fetch(`/api/v1/aia/conversations/${convId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ title: newTitle }),
});
} catch (error) {
console.error('[ChatWorkspace] 保存对话标题失败:', error);
}
}
}, [inputValue, isStreaming, currentConversationId, agent.id, deepThinkingEnabled, conversations, sendStreamMessage, createConversation]);
}, [inputValue, isStreaming, currentConversationId, agent.id, deepThinkingEnabled, conversations, sendStreamMessage, createConversation, attachments]);
// 处理键盘事件
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -261,6 +399,165 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
setDeepThinkingEnabled(prev => !prev);
}, []);
// 触发文件选择
const handleAttachmentClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
// 处理文件上传
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
// 确保有对话 ID
let convId = currentConversationId;
if (!convId) {
convId = await createConversation();
if (!convId) {
console.error('[ChatWorkspace] 创建对话失败,无法上传附件');
return;
}
}
const file = files[0];
const allowedTypes = ['.pdf', '.docx', '.doc', '.txt', '.xlsx'];
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
if (!allowedTypes.includes(ext)) {
alert(`不支持的文件类型: ${ext}\n支持的类型: ${allowedTypes.join(', ')}`);
return;
}
if (file.size > 20 * 1024 * 1024) {
alert('文件大小不能超过 20MB');
return;
}
// 添加到附件列表(上传中状态)
const tempId = `temp-${Date.now()}`;
setAttachments(prev => [...prev, { id: tempId, name: file.name, size: file.size, uploading: true }]);
setIsUploading(true);
try {
const token = getAccessToken();
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`/api/v1/aia/conversations/${convId}/attachments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status}`);
}
const result = await response.json();
// 更新附件状态
setAttachments(prev =>
prev.map(a => a.id === tempId
? { id: result.data.id, name: file.name, size: file.size, uploading: false }
: a
)
);
} catch (error) {
console.error('[ChatWorkspace] 上传附件失败:', error);
// 移除失败的附件
setAttachments(prev => prev.filter(a => a.id !== tempId));
alert('附件上传失败,请重试');
} finally {
setIsUploading(false);
// 清空 input允许重复选择同一文件
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
}, [currentConversationId, createConversation]);
// 移除附件
const removeAttachment = useCallback((attachmentId: string) => {
setAttachments(prev => prev.filter(a => a.id !== attachmentId));
}, []);
// 开始编辑对话标题
const startEditConversation = useCallback((convId: string, currentTitle: string, e: React.MouseEvent) => {
e.stopPropagation();
setEditingConvId(convId);
setEditingTitle(currentTitle);
}, []);
// 保存对话标题
const saveConversationTitle = useCallback(async (convId: string) => {
if (!editingTitle.trim()) {
setEditingConvId(null);
return;
}
try {
const token = getAccessToken();
const response = await fetch(`/api/v1/aia/conversations/${convId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ title: editingTitle.trim() }),
});
if (response.ok) {
setConversations(prev =>
prev.map(c => c.id === convId ? { ...c, title: editingTitle.trim() } : c)
);
}
} catch (error) {
console.error('[ChatWorkspace] 更新对话标题失败:', error);
} finally {
setEditingConvId(null);
}
}, [editingTitle]);
// 删除对话
const deleteConversation = useCallback(async (convId: string, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm('确定删除这个对话吗?')) {
return;
}
try {
const token = getAccessToken();
const response = await fetch(`/api/v1/aia/conversations/${convId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
setConversations(prev => prev.filter(c => c.id !== convId));
// 如果删除的是当前对话,切换到其他对话或创建新对话
if (currentConversationId === convId) {
const remaining = conversations.filter(c => c.id !== convId);
if (remaining.length > 0) {
setCurrentConversationId(remaining[0].id);
setMessages([]);
} else {
setCurrentConversationId(null);
setMessages([]);
createConversation();
}
}
}
} catch (error) {
console.error('[ChatWorkspace] 删除对话失败:', error);
}
}, [currentConversationId, conversations, createConversation]);
return (
<div className="chat-workspace">
{/* 移动端遮罩 */}
@@ -292,22 +589,64 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
{/* 历史记录 */}
<div className="sidebar-history">
<div className="history-group">
<div className="history-label">Today</div>
<div className="history-label"></div>
{conversations.map(conv => (
<button
<div
key={conv.id}
className={`history-item ${currentConversationId === conv.id ? 'active' : ''}`}
onClick={() => {
if (editingConvId) return; // 编辑中不切换
if (currentConversationId !== conv.id) {
setCurrentConversationId(conv.id);
setMessages([]); // 清空消息,后续可加载历史消息
setMessages([]);
setInputValue('');
}
setSidebarOpen(false);
}}
>
{conv.title}
</button>
{editingConvId === conv.id ? (
<div className="history-item-edit">
<input
type="text"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') saveConversationTitle(conv.id);
if (e.key === 'Escape') setEditingConvId(null);
}}
onClick={(e) => e.stopPropagation()}
autoFocus
className="history-edit-input"
/>
<button
className="history-edit-save"
onClick={(e) => { e.stopPropagation(); saveConversationTitle(conv.id); }}
>
<Check size={14} />
</button>
</div>
) : (
<>
<span className="history-item-title">{conv.title}</span>
<div className="history-item-actions">
<button
className="history-action-btn"
onClick={(e) => startEditConversation(conv.id, conv.title, e)}
title="编辑名称"
>
<Pencil size={12} />
</button>
<button
className="history-action-btn delete"
onClick={(e) => deleteConversation(conv.id, e)}
title="删除对话"
>
<Trash2 size={12} />
</button>
</div>
</>
)}
</div>
))}
</div>
</div>
@@ -393,16 +732,33 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
/>
)}
{/* 消息内容 */}
<div className={`message-bubble ${msg.role}`}>
{msg.content}
{msg.role === 'assistant' && isStreaming && index === messages.length - 1 && (
<span className="typing-cursor"></span>
)}
</div>
{/* 用户消息的附件显示(使用 FileCard类似 DeepSeek */}
{msg.role === 'user' && msg.attachments && msg.attachments.length > 0 && (
<div className="message-file-cards">
{msg.attachments.map(att => (
<FileCard
key={att.id}
name={att.filename}
byte={att.size}
icon={getFileIcon(att.filename) as any}
size="small"
/>
))}
</div>
)}
{/* 消息内容(有内容时才显示气泡) */}
{(msg.content || (msg.role === 'assistant' && isStreaming && index === messages.length - 1)) && (
<div className={`message-bubble ${msg.role}`}>
{msg.content}
{msg.role === 'assistant' && isStreaming && index === messages.length - 1 && (
<span className="typing-cursor"></span>
)}
</div>
)}
</div>
{msg.role === 'user' && (
{msg.role === 'user' && (msg.content || (msg.attachments && msg.attachments.length > 0)) && (
<div className="message-avatar user-avatar">U</div>
)}
</div>
@@ -413,18 +769,62 @@ export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
{/* 输入区域(靠下) */}
<div className="workspace-input-area">
{/* 深度思考按钮 */}
<div className="input-toolbar">
<button
className={`deep-thinking-btn ${deepThinkingEnabled ? 'active' : ''}`}
onClick={toggleDeepThinking}
>
<Lightbulb size={14} />
</button>
<button className="attachment-btn">
<Paperclip size={16} />
</button>
{/* 工具栏:深度思考 + 附件按钮 + 已上传附件(同一行) */}
<div className="input-toolbar-row">
<div className="toolbar-buttons">
<button
className={`deep-thinking-btn ${deepThinkingEnabled ? 'active' : ''}`}
onClick={toggleDeepThinking}
>
<Lightbulb size={14} />
</button>
<button
className={`attachment-btn ${attachments.length > 0 ? 'has-attachments' : ''}`}
onClick={handleAttachmentClick}
disabled={isUploading}
title="添加附件PDF、Word、TXT、Excel"
>
<Paperclip size={16} />
{attachments.length > 0 && (
<span className="attachment-count">{attachments.length}</span>
)}
</button>
</div>
{/* 已上传附件(紧凑显示,与按钮同行) */}
{attachments.length > 0 && (
<div className="toolbar-attachments">
{attachments.map(att => (
<div key={att.id} className={`toolbar-attachment-chip ${att.uploading ? 'uploading' : ''}`}>
<span className="chip-icon">{getFileIcon(att.name) === 'pdf' ? '📄' : getFileIcon(att.name) === 'word' ? '📝' : getFileIcon(att.name) === 'excel' ? '📊' : '📎'}</span>
<span className="chip-name" title={att.name}>
{att.name.length > 15 ? att.name.slice(0, 12) + '...' + att.name.slice(-4) : att.name}
</span>
{att.uploading ? (
<span className="chip-loading"></span>
) : (
<button
className="chip-remove"
onClick={() => removeAttachment(att.id)}
title="移除附件"
>
<X size={12} />
</button>
)}
</div>
))}
</div>
)}
{/* 隐藏的文件输入 */}
<input
ref={fileInputRef}
type="file"
accept=".pdf,.docx,.doc,.txt,.xlsx"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
</div>
{/* 输入框 */}

View File

@@ -8,3 +8,6 @@ export { ChatWorkspace } from './ChatWorkspace';

View File

@@ -172,3 +172,6 @@ export const BRAND_COLORS = {

View File

@@ -210,3 +210,6 @@

View File

@@ -152,6 +152,10 @@
background: none;
border: none;
border-left: 2px solid transparent;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border-radius: 8px;
font-size: 12px;
color: #64748b;
@@ -174,6 +178,85 @@
border-left-color: #4F6EF2;
}
/* 历史记录标题 */
.history-item-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
/* 历史记录操作按钮 */
.history-item-actions {
display: none;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.history-item:hover .history-item-actions {
display: flex;
}
.history-action-btn {
padding: 4px;
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.history-action-btn:hover {
background-color: #e2e8f0;
color: #475569;
}
.history-action-btn.delete:hover {
background-color: #fee2e2;
color: #ef4444;
}
/* 历史记录编辑模式 */
.history-item-edit {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
}
.history-edit-input {
flex: 1;
padding: 4px 8px;
border: 1px solid #4F6EF2;
border-radius: 4px;
font-size: 12px;
outline: none;
background: white;
}
.history-edit-save {
padding: 4px;
background: #4F6EF2;
border: none;
color: white;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.history-edit-save:hover {
background: #3b5fe0;
}
/* 用户信息 */
.sidebar-user {
padding: 16px;
@@ -495,7 +578,103 @@
border-top: 1px solid #f1f5f9;
}
/* 工具栏 */
/* 工具栏(新版:按钮+附件同一行) */
.input-toolbar-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: #e2e8f0 transparent;
padding-bottom: 4px;
}
.input-toolbar-row::-webkit-scrollbar {
height: 4px;
}
.input-toolbar-row::-webkit-scrollbar-track {
background: transparent;
}
.input-toolbar-row::-webkit-scrollbar-thumb {
background-color: #e2e8f0;
border-radius: 2px;
}
.toolbar-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* 工具栏中的附件列表 */
.toolbar-attachments {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* 紧凑型附件卡片 */
.toolbar-attachment-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 12px;
color: #475569;
white-space: nowrap;
transition: all 0.2s;
}
.toolbar-attachment-chip:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
.toolbar-attachment-chip.uploading {
opacity: 0.7;
}
.toolbar-attachment-chip .chip-icon {
font-size: 14px;
}
.toolbar-attachment-chip .chip-name {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-attachment-chip .chip-loading {
font-size: 12px;
}
.toolbar-attachment-chip .chip-remove {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.toolbar-attachment-chip .chip-remove:hover {
background: #e2e8f0;
color: #ef4444;
}
/* 旧版工具栏(保留兼容) */
.input-toolbar {
display: flex;
align-items: center;
@@ -544,6 +723,91 @@
color: #64748b;
}
.attachment-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.attachment-btn.has-attachments {
color: #4F6EF2;
position: relative;
}
.attachment-count {
position: absolute;
top: -2px;
right: -2px;
background-color: #4F6EF2;
color: white;
font-size: 10px;
min-width: 14px;
height: 14px;
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
}
/* 附件预览区域 */
.attachments-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 16px;
max-width: 1024px;
margin: 0 auto;
}
.attachment-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background-color: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 12px;
color: #475569;
}
.attachment-chip.uploading {
opacity: 0.6;
}
.attachment-name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.uploading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.remove-attachment {
padding: 2px;
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.remove-attachment:hover {
background-color: #fee2e2;
color: #ef4444;
}
/* 输入框 */
.input-box {
position: relative;
@@ -642,3 +906,43 @@
background-color: #94a3b8;
}
/* === 消息中的附件显示 === */
.message-attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
/* FileCard 在消息中的显示样式 */
.message-file-cards {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
max-width: 320px;
}
.message-item.user .message-file-cards {
margin-left: auto;
}
/* 自定义 FileCard 样式以适配对话场景 */
.message-file-cards :global(.ant-file-card) {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 附件预览区域的 FileCard.List 样式 */
.attachments-preview :global(.ant-file-card-list) {
gap: 8px;
}
.attachments-preview :global(.ant-file-card) {
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
}

View File

@@ -48,6 +48,15 @@ export interface Conversation {
updatedAt: Date;
}
/**
* 消息附件(简化版,用于消息显示)
*/
export interface MessageAttachment {
id: string;
filename: string;
size: number;
}
/**
* 消息
*/
@@ -56,8 +65,11 @@ export interface Message {
role: 'user' | 'assistant';
content: string;
thinking?: string;
attachments?: MessageAttachment[]; // 用户消息的附件
createdAt: Date;
}

View File

@@ -565,6 +565,9 @@ export default FulltextDetailDrawer;

View File

@@ -158,6 +158,9 @@ export const useAssets = (activeTab: AssetTabType) => {

View File

@@ -148,6 +148,9 @@ export const useRecentTasks = () => {

View File

@@ -347,6 +347,9 @@ export default DropnaDialog;

View File

@@ -432,6 +432,9 @@ export default MetricTimePanel;

View File

@@ -318,6 +318,9 @@ export default PivotPanel;

View File

@@ -118,6 +118,9 @@ export function useSessionStatus({

View File

@@ -110,6 +110,9 @@ export interface DataStats {

View File

@@ -106,6 +106,9 @@ export type AssetTabType = 'all' | 'processed' | 'raw';

View File

@@ -297,3 +297,6 @@ export default KnowledgePage;

View File

@@ -52,3 +52,6 @@ export interface BatchTemplate {

View File

@@ -130,3 +130,6 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A

View File

@@ -50,3 +50,6 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti

View File

@@ -73,3 +73,6 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC

View File

@@ -63,3 +63,6 @@ export default function Header({ onUpload }: HeaderProps) {

View File

@@ -117,3 +117,6 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) {

View File

@@ -45,3 +45,6 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }:

View File

@@ -80,3 +80,6 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }:

View File

@@ -22,3 +22,6 @@ export { default as TaskDetail } from './TaskDetail';

View File

@@ -291,3 +291,6 @@ export default function Dashboard() {

View File

@@ -240,3 +240,6 @@

View File

@@ -341,3 +341,6 @@ export default TenantListPage;

View File

@@ -250,3 +250,6 @@ export async function fetchModuleList(): Promise<ModuleInfo[]> {

View File

@@ -469,3 +469,6 @@ export default AIStreamChat;

View File

@@ -169,3 +169,6 @@ export default ConversationList;

View File

@@ -21,3 +21,6 @@ export type {

View File

@@ -313,3 +313,6 @@ export default useAIStream;

View File

@@ -242,3 +242,6 @@ export default useConversations;

View File

@@ -61,6 +61,9 @@ export { default as Placeholder } from './Placeholder';

View File

@@ -41,6 +41,9 @@ interface ImportMeta {