diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 618e617..eee69b1 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from 'react'; export interface Settings { cchUrl: string; cchApiKeyConfigured: boolean; + metasoApiKeyConfigured: boolean; apiFormat: 'claude' | 'openai'; defaultModel: string; defaultTools: string[]; @@ -51,9 +52,10 @@ export interface Model { const defaultSettings: Settings = { cchUrl: 'http://localhost:13500', cchApiKeyConfigured: false, + metasoApiKeyConfigured: false, apiFormat: 'claude', defaultModel: 'claude-sonnet-4-5-20250929', - defaultTools: ['web_search', 'web_fetch'], + defaultTools: ['web_search', 'web_fetch', 'mita_search', 'mita_reader'], systemPrompt: '', temperature: '0.7', theme: 'light', @@ -88,7 +90,7 @@ export function useSettings() { }, []); // 更新设置 - const updateSettings = useCallback(async (updates: Partial) => { + const updateSettings = useCallback(async (updates: Partial) => { try { setSaving(true); setError(null); diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index 9020254..eb677ce 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -4,7 +4,7 @@ import { useState, useCallback, useRef } from 'react'; import { executePythonInPyodide, type LoadingCallback } from '@/services/tools/pyodideRunner'; export interface StreamMessage { - type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'pyodide_execution_required' | 'done' | 'error'; + type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'tool_search_images' | 'pyodide_execution_required' | 'tool_used' | 'done' | 'error'; content?: string; id?: string; name?: string; @@ -19,6 +19,22 @@ export interface StreamMessage { success?: boolean; result?: string; images?: string[]; + // 搜索到的图片 + searchImages?: SearchImageData[]; + // 工具使用相关 + toolName?: string; + usedTools?: string[]; +} + +// 搜索图片数据类型 +export interface SearchImageData { + title: string; + imageUrl: string; + width: number; + height: number; + score: string; + position: number; + sourceUrl?: string; } export interface ChatMessage { @@ -32,10 +48,14 @@ export interface ChatMessage { outputTokens?: number; // 工具执行产生的图片 images?: string[]; + // 搜索到的图片 + searchImages?: SearchImageData[]; // 用户上传的图片(Base64) uploadedImages?: string[]; // 用户上传的文档 uploadedDocuments?: UploadedDocument[]; + // 使用的工具列表 + usedTools?: string[]; // Pyodide 加载状态 pyodideStatus?: { stage: 'loading' | 'ready' | 'error'; @@ -67,6 +87,29 @@ async function saveMessageImages(messageId: string, images: string[]): Promise { + if (!messageId || !searchImages || searchImages.length === 0) return; + + try { + const response = await fetch(`/api/messages/${messageId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ searchImages }), + }); + + if (!response.ok) { + console.error('Failed to save search images:', await response.text()); + } + } catch (error) { + console.error('Error saving search images:', error); + } +} + /** * 将文件转换为 Base64 */ @@ -159,6 +202,8 @@ export function useStreamChat() { const abortControllerRef = useRef(null); // 临时存储 Pyodide 执行产生的图片,等待 messageId const pendingImagesRef = useRef([]); + // 临时存储搜索到的图片,等待 messageId + const pendingSearchImagesRef = useRef([]); // 发送消息 const sendMessage = useCallback(async (options: { @@ -371,6 +416,47 @@ export function useStreamChat() { return updated; }); } + } else if (event.type === 'tool_search_images') { + // 处理图片搜索结果 + if (event.searchImages && event.searchImages.length > 0) { + // 存储到临时变量,等待 messageId 后保存到数据库 + pendingSearchImagesRef.current = [ + ...pendingSearchImagesRef.current, + ...event.searchImages, + ]; + // 更新 UI + setMessages((prev) => { + const updated = [...prev]; + const lastIndex = updated.length - 1; + if (updated[lastIndex]?.role === 'assistant') { + const existingSearchImages = updated[lastIndex].searchImages || []; + updated[lastIndex] = { + ...updated[lastIndex], + searchImages: [...existingSearchImages, ...event.searchImages!], + }; + } + return updated; + }); + } + } else if (event.type === 'tool_used') { + // 实时工具使用事件 + if (event.toolName) { + setMessages((prev) => { + const updated = [...prev]; + const lastIndex = updated.length - 1; + if (updated[lastIndex]?.role === 'assistant') { + const existingTools = updated[lastIndex].usedTools || []; + // 避免重复添加 + if (!existingTools.includes(event.toolName!)) { + updated[lastIndex] = { + ...updated[lastIndex], + usedTools: [...existingTools, event.toolName!], + }; + } + } + return updated; + }); + } } else if (event.type === 'pyodide_execution_required') { // 需要在浏览器端执行 Python 图形代码 const code = event.code || ''; @@ -444,16 +530,25 @@ export function useStreamChat() { pendingImagesRef.current = []; // 清空临时存储 } + // 如果有待保存的搜索图片,保存到数据库 + if (event.messageId && pendingSearchImagesRef.current.length > 0) { + saveMessageSearchImages(event.messageId, pendingSearchImagesRef.current); + pendingSearchImagesRef.current = []; // 清空临时存储 + } + setMessages((prev) => { const updated = [...prev]; const lastIndex = updated.length - 1; if (updated[lastIndex]?.role === 'assistant') { + // 如果 done 事件包含 usedTools,使用它(保证完整性) + const finalUsedTools = event.usedTools || updated[lastIndex].usedTools; updated[lastIndex] = { ...updated[lastIndex], id: event.messageId || updated[lastIndex].id, status: 'completed', inputTokens: event.inputTokens, outputTokens: event.outputTokens, + usedTools: finalUsedTools, }; } return updated;