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:
@@ -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';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user