Files
momentry_studio/docs/STUDIO_INTEGRATION_PLAN.md
Momentry Studio fe91afff98 feat: integrate MarkBase Search Session API into Studio
- Created useSearchSession composable for session management
- Added 7 API endpoints in api/index.ts
- Updated SearchView.vue to use sessions instead of history
- Session sidebar with create, rename, delete functionality
- Auto-save messages to current session after each search
- Auto-create default session on first load
- Updated CSS for session active state and new session button
2026-10-27 05:06:03 +08:00

1216 lines
26 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<T> {
success: boolean;
data?: T;
error?: string;
code?: string;
}
export class SearchSessionApi {
private token: string;
constructor(token: string) {
this.token = token;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
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<ApiResponse<{ sessions: SearchSession[]; total: number }>> {
return this.request(`/api/v2/search-sessions?limit=${limit}&offset=${offset}`);
}
async createSession(title: string, mode: string): Promise<ApiResponse<{ session: SearchSession }>> {
return this.request('/api/v2/search-sessions', {
method: 'POST',
body: JSON.stringify({ title, mode }),
});
}
async updateSession(id: string, title: string): Promise<ApiResponse<void>> {
return this.request(`/api/v2/search-sessions/${id}`, {
method: 'PUT',
body: JSON.stringify({ title }),
});
}
async deleteSession(id: string): Promise<ApiResponse<void>> {
return this.request(`/api/v2/search-sessions/${id}`, {
method: 'DELETE',
});
}
// Messages
async listMessages(sessionId: string): Promise<ApiResponse<{ messages: SearchMessage[] }>> {
return this.request(`/api/v2/search-sessions/${sessionId}/messages`);
}
async createMessage(
sessionId: string,
role: 'user' | 'assistant',
content: string,
mode: string,
results?: string
): Promise<ApiResponse<{ message: SearchMessage }>> {
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<Pick<SearchMessage, 'role' | 'content' | 'results'>>
): Promise<ApiResponse<void>> {
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<SearchSession[]>([]);
const currentSession = ref<SearchSession | null>(null);
const messages = ref<SearchMessage[]>([]);
const isLoading = ref(false);
const error = ref<string | null>(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
<!-- src/components/search/SessionList.vue -->
<template>
<div class="session-list">
<div class="session-header">
<h3>搜索歷史</h3>
<button
class="new-session-btn"
@click="$emit('create-new')"
:disabled="isLoading"
>
+ 新搜尋
</button>
</div>
<div class="sessions" v-if="sessions.length > 0">
<SessionItem
v-for="session in sessions"
:key="session.id"
:session="session"
:is-active="currentSession?.id === session.id"
@click="$emit('select', session.id)"
@update="handleUpdate(session.id, $event)"
@delete="handleDelete(session.id)"
/>
</div>
<div class="empty-state" v-else>
<p>尚無搜索記錄</p>
<button @click="$emit('create-new')">開始第一次搜尋</button>
</div>
<div class="loading" v-if="isLoading">
<span>載入中...</span>
</div>
<div class="error" v-if="error">
<span>{{ error }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import SessionItem from './SessionItem.vue';
import type { SearchSession } from '@/types/search';
defineProps<{
sessions: SearchSession[];
currentSession: SearchSession | null;
isLoading: boolean;
error: string | null;
}>();
const emit = defineEmits<{
(e: 'create-new'): void;
(e: 'select', sessionId: string): void;
(e: 'update', sessionId: string, title: string): void;
(e: 'delete', sessionId: string): void;
}>();
function handleUpdate(sessionId: string, title: string) {
emit('update', sessionId, title);
}
function handleDelete(sessionId: string) {
if (confirm('確定要刪除此搜尋記錄?')) {
emit('delete', sessionId);
}
}
</script>
<style scoped>
.session-list {
width: 280px;
height: 100%;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
background: #fafafa;
}
.session-header {
padding: 16px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.session-header h3 {
margin: 0;
font-size: 16px;
}
.new-session-btn {
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.new-session-btn:hover {
background: #40a9ff;
}
.sessions {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
}
.empty-state button {
margin-top: 12px;
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.loading, .error {
padding: 12px;
text-align: center;
}
.error {
color: #f5222d;
}
</style>
```
---
#### 3.2 創建 SessionItem 組件
**文件**:`src/components/search/SessionItem.vue`(新建)
```vue
<!-- src/components/search/SessionItem.vue -->
<template>
<div
class="session-item"
:class="{ active: isActive }"
@click="$emit('click')"
>
<div class="session-title" v-if="!isEditing">
{{ session.title }}
</div>
<input
v-else
v-model="editTitle"
class="session-title-input"
@blur="handleSave"
@keyup.enter="handleSave"
@keyup.esc="isEditing = false"
ref="titleInput"
/>
<div class="session-meta">
<span class="mode-badge">{{ modeText }}</span>
<span class="message-count">{{ session.message_count }} 則訊息</span>
</div>
<div class="session-actions" v-if="isActive">
<button class="action-btn" @click.stop="startEdit" title="重新命名">
✏️
</button>
<button class="action-btn" @click.stop="$emit('delete')" title="刪除">
🗑️
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import type { SearchSession } from '@/types/search';
const props = defineProps<{
session: SearchSession;
isActive: boolean;
}>();
const emit = defineEmits<{
(e: 'click'): void;
(e: 'update', title: string): void;
(e: 'delete'): void;
}>();
const isEditing = ref(false);
const editTitle = ref('');
const titleInput = ref<HTMLInputElement | null>(null);
const modeText = computed(() => {
const modeMap = {
keyword: '關鍵字',
visual: '視覺搜尋',
semantic: '語意搜尋',
};
return modeMap[props.session.mode] || props.session.mode;
});
function startEdit() {
editTitle.value = props.session.title;
isEditing.value = true;
nextTick(() => {
titleInput.value?.focus();
titleInput.value?.select();
});
}
function handleSave() {
if (editTitle.value.trim() && editTitle.value !== props.session.title) {
emit('update', editTitle.value.trim());
}
isEditing.value = false;
}
</script>
<style scoped>
.session-item {
padding: 12px;
margin-bottom: 8px;
background: white;
border-radius: 6px;
cursor: pointer;
border: 1px solid #e0e0e0;
transition: all 0.2s;
position: relative;
}
.session-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.session-item.active {
border-color: #1890ff;
background: #e6f7ff;
}
.session-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.session-title-input {
width: 100%;
font-size: 14px;
font-weight: 500;
border: 1px solid #1890ff;
border-radius: 4px;
padding: 4px 8px;
margin-bottom: 8px;
}
.session-meta {
display: flex;
gap: 8px;
font-size: 12px;
color: #999;
}
.mode-badge {
padding: 2px 6px;
background: #f0f0f0;
border-radius: 4px;
font-size: 11px;
}
.session-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
}
.action-btn {
padding: 4px 8px;
background: transparent;
border: none;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
}
.action-btn:hover {
opacity: 1;
}
</style>
```
---
#### 3.3 創建 ChatArea 組件
**文件**:`src/components/search/ChatArea.vue`(新建)
```vue
<!-- src/components/search/ChatArea.vue -->
<template>
<div class="chat-area">
<div class="chat-header" v-if="currentSession">
<h2>{{ currentSession.title }}</h2>
<span class="mode-badge">{{ modeText }}</span>
</div>
<div class="messages" ref="messagesContainer">
<MessageItem
v-for="message in messages"
:key="message.id"
:message="message"
/>
<div class="empty-chat" v-if="messages.length === 0">
<p>開始新的搜尋對話</p>
</div>
</div>
<div class="input-area">
<select v-model="searchMode" class="mode-selector">
<option value="keyword">關鍵字搜尋</option>
<option value="visual">視覺搜尋</option>
<option value="semantic">語意搜尋</option>
</select>
<input
v-model="inputText"
class="chat-input"
placeholder="輸入搜尋內容..."
@keyup.enter="handleSend"
:disabled="isLoading || !currentSession"
/>
<button
class="send-btn"
@click="handleSend"
:disabled="isLoading || !currentSession || !inputText.trim()"
>
發送
</button>
</div>
<div class="loading-overlay" v-if="isLoading">
<span>搜尋中...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue';
import MessageItem from './MessageItem.vue';
import type { SearchSession, SearchMessage } from '@/types/search';
const props = defineProps<{
currentSession: SearchSession | null;
messages: SearchMessage[];
isLoading: boolean;
}>();
const emit = defineEmits<{
(e: 'send', content: string, mode: string): void;
}>();
const inputText = ref('');
const searchMode = ref<'keyword' | 'visual' | 'semantic'>('keyword');
const messagesContainer = ref<HTMLElement | null>(null);
const modeText = computed(() => {
const modeMap = {
keyword: '關鍵字',
visual: '視覺搜尋',
semantic: '語意搜尋',
};
return modeMap[props.currentSession?.mode || 'keyword'] || '關鍵字';
});
function handleSend() {
if (!inputText.value.trim()) return;
emit('send', inputText.value.trim(), searchMode.value);
inputText.value = '';
}
// Auto-scroll to bottom when new messages arrive
watch(
() => props.messages.length,
() => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
});
}
);
</script>
<style scoped>
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background: white;
}
.chat-header {
padding: 16px 24px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h2 {
margin: 0;
font-size: 18px;
}
.mode-badge {
padding: 4px 8px;
background: #f0f0f0;
border-radius: 4px;
font-size: 12px;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.empty-chat {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #999;
}
.input-area {
padding: 16px 24px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 12px;
align-items: center;
}
.mode-selector {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
background: white;
}
.chat-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.chat-input:focus {
outline: none;
border-color: #1890ff;
}
.send-btn {
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.send-btn:hover:not(:disabled) {
background: #40a9ff;
}
.send-btn:disabled {
background: #d9d9d9;
cursor: not-allowed;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
color: #1890ff;
}
</style>
```
---
#### 3.4 創建 MessageItem 組件
**文件**:`src/components/search/MessageItem.vue`(新建)
```vue
<!-- src/components/search/MessageItem.vue -->
<template>
<div class="message-item" :class="message.role">
<div class="message-avatar">
{{ message.role === 'user' ? '👤' : '🤖' }}
</div>
<div class="message-content">
<div class="message-header">
<span class="role-label">{{ message.role === 'user' ? '用戶' : '助手' }}</span>
<span class="timestamp">{{ formatTime(message.created_at) }}</span>
</div>
<div class="message-text">
{{ message.content }}
</div>
<div class="message-results" v-if="message.results">
<div class="results-header">搜索結果:</div>
<div class="results-content">
<ResultItem
v-for="(result, index) in parseResults(message.results)"
:key="index"
:result="result"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import ResultItem from './ResultItem.vue';
import type { SearchMessage } from '@/types/search';
const props = defineProps<{
message: SearchMessage;
}>();
function formatTime(timestamp: string): string {
const date = new Date(timestamp);
return date.toLocaleTimeString('zh-TW', {
hour: '2-digit',
minute: '2-digit',
});
}
function parseResults(resultsJson: string): Array<{ file: string; score: number }> {
try {
return JSON.parse(resultsJson);
} catch {
return [];
}
}
</script>
<style scoped>
.message-item {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.message-item.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
flex-shrink: 0;
}
.message-content {
flex: 1;
max-width: 80%;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: #999;
}
.message-text {
padding: 12px 16px;
background: #f5f5f5;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
}
.message-item.user .message-text {
background: #1890ff;
color: white;
}
.message-results {
margin-top: 12px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.results-header {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.results-content {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
```
---
### Phase 4:視圖整合(30 分鐘)
#### 4.1 修改 SearchView.vue
**文件**:`src/views/SearchView.vue`(修改)
```vue
<!-- src/views/SearchView.vue -->
<template>
<div class="search-view">
<SessionList
:sessions="sessions"
:current-session="currentSession"
:is-loading="isLoading"
:error="error"
@create-new="handleCreateNew"
@select="handleSelectSession"
@update="handleUpdateSession"
@delete="handleDeleteSession"
/>
<ChatArea
:current-session="currentSession"
:messages="messages"
:is-loading="isLoading"
@send="handleSendMessage"
/>
</div>
</template>
<script setup lang="ts">
import { useSearchSession } from '@/composables/useSearchSession';
import SessionList from '@/components/search/SessionList.vue';
import ChatArea from '@/components/search/ChatArea.vue';
const {
sessions,
currentSession,
messages,
isLoading,
error,
createNewSession,
loadSession,
sendMessage,
updateSessionTitle,
deleteSession,
} = useSearchSession();
async function handleCreateNew() {
const title = `搜尋 ${new Date().toLocaleDateString('zh-TW')}`;
await createNewSession(title, 'keyword');
}
async function handleSelectSession(sessionId: string) {
await loadSession(sessionId);
}
async function handleSendMessage(content: string, mode: string) {
await sendMessage(content, mode as 'keyword' | 'visual' | 'semantic');
}
async function handleUpdateSession(sessionId: string, title: string) {
await updateSessionTitle(sessionId, title);
}
async function handleDeleteSession(sessionId: string) {
await deleteSession(sessionId);
}
</script>
<style scoped>
.search-view {
display: flex;
height: 100vh;
background: #fafafa;
}
</style>
```
---
### 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<SearchHistoryItem> {
// ...
}
// 移除後
// (刪除整個函數)
```
---
### 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 團隊