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:
169
frontend/src/api/conversationApi.ts
Normal file
169
frontend/src/api/conversationApi.ts
Normal 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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user