feat: Day 14-17 - Frontend Chat Interface completed

Frontend:
- Create MessageList component with streaming animation
- Create MessageInput component with @knowledge base support
- Create ModelSelector component (DeepSeek/Qwen/Gemini)
- Implement conversationApi with SSE streaming
- Update AgentChatPage integrate all components
- Add Markdown rendering (react-markdown + remark-gfm)
- Add code highlighting (react-syntax-highlighter)
- Add vite-env.d.ts for environment variables

Features:
- Real-time streaming output with cursor animation
- Markdown and code block rendering
- Model switching (DeepSeek-V3, Qwen3-72B, Gemini Pro)
- @Knowledge base selector (UI ready)
- Auto-scroll to bottom
- Shift+Enter for new line, Enter to send
- Beautiful message bubble design

Build: Frontend build successfully (7.94s, 1.9MB)

New Files:
- components/chat/MessageList.tsx (170 lines)
- components/chat/MessageList.css (150 lines)
- components/chat/MessageInput.tsx (145 lines)
- components/chat/MessageInput.css (60 lines)
- components/chat/ModelSelector.tsx (110 lines)
- components/chat/ModelSelector.css (35 lines)
- api/conversationApi.ts (170 lines)
- src/vite-env.d.ts (9 lines)

Total: ~850 lines of new code
This commit is contained in:
AI Clinical Dev Team
2025-10-10 20:52:30 +08:00
parent 8bd2b4fc54
commit 84bf1c86ab
11 changed files with 2939 additions and 119 deletions

View File

@@ -0,0 +1,169 @@
import request from './request';
import type { ModelType } from '../components/chat/ModelSelector';
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
}
export interface Message {
id: string;
conversationId: string;
role: 'user' | 'assistant';
content: string;
model?: string;
metadata?: any;
tokens?: number;
createdAt: string;
}
export interface Conversation {
id: string;
userId: string;
projectId: string;
agentId: string;
title: string;
modelName: string;
messageCount: number;
totalTokens: number;
metadata?: any;
createdAt: string;
updatedAt: string;
project?: {
id: string;
name: string;
background?: string;
researchType?: string;
};
messages?: Message[];
}
export interface CreateConversationData {
projectId: string;
agentId: string;
title?: string;
}
export interface SendMessageData {
conversationId: string;
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
}
export interface SendMessageResponse {
userMessage: Message;
assistantMessage: Message;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
// 创建对话
export const createConversation = (data: CreateConversationData) => {
return request.post<ApiResponse<Conversation>>('/conversations', data);
};
// 获取对话列表
export const getConversations = (projectId?: string) => {
const params = projectId ? { projectId } : {};
return request.get<ApiResponse<Conversation[]>>('/conversations', { params });
};
// 获取对话详情
export const getConversationById = (id: string) => {
return request.get<ApiResponse<Conversation>>(`/conversations/${id}`);
};
// 发送消息(非流式)
export const sendMessage = (data: SendMessageData) => {
return request.post<ApiResponse<SendMessageResponse>>('/conversations/message', data);
};
// 发送消息(流式输出)
export const sendMessageStream = async (
data: SendMessageData,
onChunk: (content: string) => void,
onComplete: () => void,
onError: (error: Error) => void
) => {
try {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/conversations/message/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Response body is null');
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || !trimmedLine.startsWith('data:')) {
continue;
}
const dataStr = trimmedLine.slice(5).trim();
if (dataStr === '[DONE]') {
onComplete();
return;
}
try {
const chunk = JSON.parse(dataStr);
if (chunk.content) {
onChunk(chunk.content);
}
} catch (parseError) {
console.error('Failed to parse SSE data:', parseError);
}
}
}
onComplete();
} catch (error) {
console.error('Stream error:', error);
onError(error as Error);
}
};
// 删除对话
export const deleteConversation = (id: string) => {
return request.delete<ApiResponse>(`/conversations/${id}`);
};
export default {
createConversation,
getConversations,
getConversationById,
sendMessage,
sendMessageStream,
deleteConversation,
};