# Momentry Studio 整合計劃 **日期**:2026-10-27 **目標**:將 Search Session API 整合到 Momentry Studio 前端 **預計工作量**:2-3 小時(Studio 團隊) **前置條件**:✅ Search Session API 已實施並部署 --- ## 📋 整合步驟 ### Phase 1:前端基礎設施(30 分鐘) #### 1.1 創建 API Service 層 **文件**:`src/api/searchSession.ts`(新建) ```typescript // src/api/searchSession.ts import type { SearchSession, SearchMessage } from '@/types/search'; const API_BASE = import.meta.env.VITE_MARKBASE_URL || 'http://localhost:11438'; interface ApiResponse { success: boolean; data?: T; error?: string; code?: string; } export class SearchSessionApi { private token: string; constructor(token: string) { this.token = token; } private async request( endpoint: string, options: RequestInit = {} ): Promise> { const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { const error = await response.json(); return { success: false, error: error.error || 'Request failed', code: error.code, }; } const data = await response.json(); return { success: true, data }; } // Session CRUD async listSessions(limit = 30, offset = 0): Promise> { return this.request(`/api/v2/search-sessions?limit=${limit}&offset=${offset}`); } async createSession(title: string, mode: string): Promise> { return this.request('/api/v2/search-sessions', { method: 'POST', body: JSON.stringify({ title, mode }), }); } async updateSession(id: string, title: string): Promise> { return this.request(`/api/v2/search-sessions/${id}`, { method: 'PUT', body: JSON.stringify({ title }), }); } async deleteSession(id: string): Promise> { return this.request(`/api/v2/search-sessions/${id}`, { method: 'DELETE', }); } // Messages async listMessages(sessionId: string): Promise> { return this.request(`/api/v2/search-sessions/${sessionId}/messages`); } async createMessage( sessionId: string, role: 'user' | 'assistant', content: string, mode: string, results?: string ): Promise> { return this.request(`/api/v2/search-sessions/${sessionId}/messages`, { method: 'POST', body: JSON.stringify({ role, content, mode, results: results || '' }), }); } async updateMessage( sessionId: string, messageId: string, updates: Partial> ): Promise> { return this.request(`/api/v2/search-sessions/${sessionId}/messages/${messageId}`, { method: 'PATCH', body: JSON.stringify(updates), }); } } ``` --- #### 1.2 定義 TypeScript 類型 **文件**:`src/types/search.ts`(新建) ```typescript // src/types/search.ts export interface SearchSession { id: string; title: string; mode: 'keyword' | 'visual' | 'semantic'; message_count: number; created_at: string; updated_at: string; } export interface SearchMessage { id: string; role: 'user' | 'assistant'; content: string; results: string; // JSON string mode: 'keyword' | 'visual' | 'semantic'; created_at: string; } export interface SearchSessionList { sessions: SearchSession[]; total: number; } export interface SearchConversation { session: SearchSession; messages: SearchMessage[]; } ``` --- ### Phase 2:Composable 層(40 分鐘) #### 2.1 創建 useSearchSession Composable **文件**:`src/composables/useSearchSession.ts`(新建) ```typescript // src/composables/useSearchSession.ts import { ref, computed, onMounted } from 'vue'; import { SearchSessionApi } from '@/api/searchSession'; import type { SearchSession, SearchMessage } from '@/types/search'; export function useSearchSession() { const token = localStorage.getItem('markbase_token') || ''; const api = new SearchSessionApi(token); // State const sessions = ref([]); const currentSession = ref(null); const messages = ref([]); const isLoading = ref(false); const error = ref(null); // Computed const hasCurrentSession = computed(() => currentSession.value !== null); const totalSessions = computed(() => sessions.value.length); // Actions async function loadSessions() { isLoading.value = true; error.value = null; const response = await api.listSessions(); if (response.success && response.data) { sessions.value = response.data.sessions; } else { error.value = response.error || 'Failed to load sessions'; } isLoading.value = false; } async function createNewSession(title: string, mode: 'keyword' | 'visual' | 'semantic' = 'keyword') { isLoading.value = true; error.value = null; const response = await api.createSession(title, mode); if (response.success && response.data) { sessions.value.unshift(response.data.session); currentSession.value = response.data.session; messages.value = []; } else { error.value = response.error || 'Failed to create session'; } isLoading.value = false; } async function loadSession(sessionId: string) { isLoading.value = true; error.value = null; // Load session details const session = sessions.value.find(s => s.id === sessionId); if (session) { currentSession.value = session; } // Load messages const response = await api.listMessages(sessionId); if (response.success && response.data) { messages.value = response.data.messages; } else { error.value = response.error || 'Failed to load messages'; } isLoading.value = false; } async function sendMessage(content: string, mode: 'keyword' | 'visual' | 'semantic' = 'keyword') { if (!currentSession.value) { error.value = 'No active session'; return; } isLoading.value = true; error.value = null; // Add user message const userMsgResponse = await api.createMessage( currentSession.value.id, 'user', content, mode ); if (!userMsgResponse.success) { error.value = userMsgResponse.error || 'Failed to send message'; isLoading.value = false; return; } if (userMsgResponse.data) { messages.value.push(userMsgResponse.data.message); } // TODO: Call search backend and get results // For now, simulate assistant response const assistantMsgResponse = await api.createMessage( currentSession.value.id, 'assistant', 'Search results will appear here...', mode, JSON.stringify([{ file: 'example.txt', score: 0.95 }]) ); if (assistantMsgResponse.success && assistantMsgResponse.data) { messages.value.push(assistantMsgResponse.data.message); } isLoading.value = false; } async function updateSessionTitle(sessionId: string, title: string) { const response = await api.updateSession(sessionId, title); if (response.success) { const session = sessions.value.find(s => s.id === sessionId); if (session) { session.title = title; } } else { error.value = response.error || 'Failed to update session'; } } async function deleteSession(sessionId: string) { const response = await api.deleteSession(sessionId); if (response.success) { sessions.value = sessions.value.filter(s => s.id !== sessionId); if (currentSession.value?.id === sessionId) { currentSession.value = null; messages.value = []; } } else { error.value = response.error || 'Failed to delete session'; } } // Auto-load sessions on mount onMounted(() => { loadSessions(); }); return { // State sessions, currentSession, messages, isLoading, error, // Computed hasCurrentSession, totalSessions, // Actions loadSessions, createNewSession, loadSession, sendMessage, updateSessionTitle, deleteSession, }; } ``` --- ### Phase 3:UI 組件(60 分鐘) #### 3.1 創建 SessionList 組件 **文件**:`src/components/search/SessionList.vue`(新建) ```vue ``` --- #### 3.2 創建 SessionItem 組件 **文件**:`src/components/search/SessionItem.vue`(新建) ```vue ``` --- #### 3.3 創建 ChatArea 組件 **文件**:`src/components/search/ChatArea.vue`(新建) ```vue ``` --- #### 3.4 創建 MessageItem 組件 **文件**:`src/components/search/MessageItem.vue`(新建) ```vue ``` --- ### Phase 4:視圖整合(30 分鐘) #### 4.1 修改 SearchView.vue **文件**:`src/views/SearchView.vue`(修改) ```vue ``` --- ### Phase 5:移除舊代碼(20 分鐘) #### 5.1 移除 search_history 相關代碼 **文件清單**: 1. **src-tauri/src/db.rs**: - 移除 `search_history` 表相關函數 - 移除 `SearchHistoryItem` 結構體 2. **src-tauri/src/proxy.rs**: - 移除 `search-history` handler 3. **src-tauri/src/main.rs**: - 移除 `search-history` command 註冊 **示例**: ```rust // 移除前 #[tauri::command] fn get_search_history() -> Vec { // ... } // 移除後 // (刪除整個函數) ``` --- ### Phase 6:測試與驗證(30 分鐘) #### 6.1 功能測試清單 | 測試項目 | 預期結果 | 狀態 | |---------|---------|------| | 創建新 Session | Session 出現在列表中 | ⏳ 待測 | | 切換 Session | 對話記錄正確載入 | ⏳ 待測 | | 發送訊息 | 訊息出現在對話中 | ⏳ 待測 | | 更新 Session 標題 | 標題即時更新 | ⏳ 待測 | | 刪除 Session | Session 從列表移除 | ⏳ 待測 | | 自動清理 | 24 小時後清理未命名 Session | ⏳ 待測 | | 用戶隔離 | 不同用戶看不到對方數據 | ⏳ 待測 | #### 6.2 性能測試 | 測試項目 | 目標 | 狀態 | |---------|------|------| | 載入 30 個 Sessions | < 500ms | ⏳ 待測 | | 載入 100 條訊息 | < 1s | ⏳ 待測 | | 創建新 Session | < 100ms | ⏳ 待測 | | 發送訊息 | < 200ms | ⏳ 待測 | --- ## 📝 移交清單 ### 給 Studio 團隊 **必須完成**: - [ ] 創建所有新文件(API、composable、components) - [ ] 修改 SearchView.vue - [ ] 移除舊的 search_history 代碼 - [ ] 更新環境變數(VITE_MARKBASE_URL) - [ ] 測試所有功能 **可選優化**: - [ ] 添加離線緩存 - [ ] 添加搜索結果高亮 - [ ] 添加拖拽排序 Sessions - [ ] 添加 Session 分享功能 --- ## 🔗 相關資源 **API 文檔**: - `/Users/accusys/markbase/docs/SEARCH_SESSION_API_REPORT.md` - `/Users/accusys/momentry_studio/docs/MARKBASE_SEARCH_SESSION_API.md` **API 端點**: - `http://localhost:11438/api/v2/search-sessions` - Token: 從 `/api/v2/admin/login` 獲取 **測試帳號**: - Username: `demo` - Password: `demo123` --- ## ⚠️ 注意事項 1. **Token 管理**: - Token 存儲在 localStorage - Token 過期時需要重新登錄 2. **錯誤處理**: - API 返回 `{ success: false, error: "..." }` 格式 - 需要統一處理錯誤提示 3. **性能考慮**: - Sessions 列表默認 30 條 - 需要實現滾動載入(無限滾動) 4. **用戶隔離**: - API 自動根據 Token 過濾 user_id - 前端不需要手動傳遞 user_id --- ## 🎯 成功標準 **功能完整性**: - ✅ 所有 7 個 API 端點可用 - ✅ Session CRUD 功能正常 - ✅ 訊息發送/接收功能正常 **性能達標**: - ✅ API 響應時間 < 100ms - ✅ UI 流暢度 > 60fps **用戶體驗**: - ✅ 直觀的 UI 操作 - ✅ 即時反饋 - ✅ 錯誤提示友好 --- **整合計劃完成,Studio 團隊可開始實施。** **預計完成時間**:2-3 小時 **聯繫方式**:如有問題請參考 API 文檔或聯繫 MarkBase 團隊