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:
@@ -95,6 +95,9 @@ vite.config.*.timestamp-*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ exec nginx -g 'daemon off;'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -218,6 +218,9 @@ http {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -49,3 +49,6 @@ export default apiClient;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -248,3 +248,6 @@ export async function logout(): Promise<void> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,3 +14,6 @@ export * from './api';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,3 +38,6 @@ export async function fetchUserModules(): Promise<string[]> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -118,3 +118,6 @@ const ModulePermissionModal: React.FC<ModulePermissionModalProps> = ({
|
||||
|
||||
export default ModulePermissionModal;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,3 +29,6 @@ const AdminModule: React.FC = () => {
|
||||
|
||||
export default AdminModule;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -194,3 +194,6 @@ export const TENANT_TYPE_NAMES: Record<TenantType, string> = {
|
||||
PUBLIC: '公共',
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,3 +78,6 @@ export default AgentCard;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 输入框 */}
|
||||
|
||||
@@ -8,3 +8,6 @@ export { ChatWorkspace } from './ChatWorkspace';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -172,3 +172,6 @@ export const BRAND_COLORS = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -210,3 +210,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -565,6 +565,9 @@ export default FulltextDetailDrawer;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -158,6 +158,9 @@ export const useAssets = (activeTab: AssetTabType) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,6 +148,9 @@ export const useRecentTasks = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -347,6 +347,9 @@ export default DropnaDialog;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -432,6 +432,9 @@ export default MetricTimePanel;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -318,6 +318,9 @@ export default PivotPanel;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,9 @@ export function useSessionStatus({
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -110,6 +110,9 @@ export interface DataStats {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,9 @@ export type AssetTabType = 'all' | 'processed' | 'raw';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -297,3 +297,6 @@ export default KnowledgePage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,3 +52,6 @@ export interface BatchTemplate {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -130,3 +130,6 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -50,3 +50,6 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,6 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -63,3 +63,6 @@ export default function Header({ onUpload }: HeaderProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -117,3 +117,6 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,3 +45,6 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,3 +80,6 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,6 @@ export { default as TaskDetail } from './TaskDetail';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -291,3 +291,6 @@ export default function Dashboard() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -240,3 +240,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -341,3 +341,6 @@ export default TenantListPage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -250,3 +250,6 @@ export async function fetchModuleList(): Promise<ModuleInfo[]> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -469,3 +469,6 @@ export default AIStreamChat;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -169,3 +169,6 @@ export default ConversationList;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,3 +21,6 @@ export type {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -313,3 +313,6 @@ export default useAIStream;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -242,3 +242,6 @@ export default useConversations;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -277,3 +277,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -213,3 +213,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -150,3 +150,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ export { default as Placeholder } from './Placeholder';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3
frontend-v2/src/vite-env.d.ts
vendored
3
frontend-v2/src/vite-env.d.ts
vendored
@@ -41,6 +41,9 @@ interface ImportMeta {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user