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

26 KiB
Raw Blame History

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(新建)

// 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(新建)

// 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(新建)

// 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(新建)

<!-- 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(新建)

<!-- 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(新建)

<!-- 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(新建)

<!-- 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(修改)

<!-- 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 註冊

示例:

// 移除前
#[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 團隊