- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件 - 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow: 构建前端上传到 build/,再在服务器上 git pull 并重建容器 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
Load Chat History on Challenge Entry — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: When a user opens a challenge's AI chat tab, their previous conversation messages load immediately via HTTP API before the WebSocket connects.
Architecture: Call Prompt.listConversations(taskId) + Prompt.getMessages(id) in ChallengeHome.vue alongside connectPrompt(). The HTTP response populates the store immediately; the WebSocket init handler skips overwriting messages if the conversation ID already matches (preventing stale overwrite without breaking "新对话"). A cancellation token prevents in-flight HTTP responses from writing to the store after the user navigates away.
Tech Stack: Vue 3, TypeScript, Naive UI (n-spin), existing Prompt API client
File Map
| File | Change |
|---|---|
client/src/utils/type.ts |
Add created?: string to PromptMessage interface |
client/src/store/prompt.ts |
Add historyLoading ref, cancellation token, loadHistory() function; update init handler and disconnectPrompt |
client/src/pages/ChallengeHome.vue |
Call loadHistory() alongside connectPrompt() in loadChallenge() |
client/src/components/PromptPanel.vue |
Import historyLoading; show n-spin while loading |
Task 1: Add created to PromptMessage in utils/type.ts
Files:
- Modify:
client/src/utils/type.ts
The PromptMessage interface represents the raw HTTP response shape from
GET /prompt/conversations/{id}/messages/. The backend already returns a created field
but the frontend type is missing it, which would cause a TypeScript error when mapping
the HTTP response in loadHistory.
-
Step 1: Add
createdfieldIn
client/src/utils/type.ts, change:export interface PromptMessage { id: number role: string content: string code_html: string | null code_css: string | null code_js: string | null }to:
export interface PromptMessage { id: number role: string content: string code_html: string | null code_css: string | null code_js: string | null created?: string } -
Step 2: Verify TypeScript compiles
cd /home/xuyue/Projects/webpreview/client && npx tsc --noEmitExpected: no new errors
-
Step 3: Commit
cd /home/xuyue/Projects/webpreview/client git add src/utils/type.ts git commit -m "fix: add created field to PromptMessage type"
Task 2: Add historyLoading and loadHistory to prompt.ts
Files:
- Modify:
client/src/store/prompt.ts
Key design decisions:
-
Use a
_historyLoadIdcounter as a cancellation token:disconnectPromptincrements it, andloadHistorychecks it before writing to the store — preventing in-flight responses from corrupting state after the user navigates away. -
Filter conversations by
conv.username === user.usernameso admin/super users only load their own chat history (the API returns all users' conversations for non-normal roles). -
No new local type needed:
Prompt.getMessagesalready returnsPromptMessage[](fromutils/type.ts) which now includescreated. -
Step 1: Add import for
userstore andPromptMessagetypeAt the top of
client/src/store/prompt.ts, add to the existing imports:import { Prompt } from "../api" import type { PromptMessage as RawMessage } from "../utils/type" import { user } from "./user"Note:
RawMessageis an alias to avoid collision with the store's ownPromptMessageinterface (which uses the nestedcode: {html, css, js}shape). -
Step 2: Add
historyLoadingexport and cancellation tokenAfter the existing
export const streaming = ref(false)line, add:export const historyLoading = ref(false) let _historyLoadId = 0 -
Step 3: Add
loadHistoryfunctionAdd this function after
disconnectPrompt:export async function loadHistory(taskId: number) { const loadId = ++_historyLoadId historyLoading.value = true try { const convs = await Prompt.listConversations(taskId) if (loadId !== _historyLoadId) return // navigated away, abort const active = convs.find( (c: { is_active: boolean; message_count: number; username: string }) => c.is_active && c.message_count > 0 && c.username === user.username ) if (!active) return const raw: RawMessage[] = await Prompt.getMessages(active.id) if (loadId !== _historyLoadId) return // navigated away, abort conversationId.value = active.id messages.value = raw.map((m) => ({ role: m.role as "user" | "assistant", content: m.content, code: m.role === "assistant" ? { html: m.code_html, css: m.code_css, js: m.code_js } : undefined, created: m.created, })) } catch { // 静默失败,不影响 WebSocket 正常流程 } finally { if (loadId === _historyLoadId) historyLoading.value = false } } -
Step 4: Fix
disconnectPromptto resethistoryLoadingand cancel in-flight loadsFind
disconnectPrompt. Add two lines at the top of the function body:export function disconnectPrompt() { _historyLoadId++ // cancel any in-flight loadHistory historyLoading.value = false if (ws) { ws.close() ws = null } messages.value = [] conversationId.value = "" connected.value = false streaming.value = false streamingContent.value = "" _onCodeComplete = null } -
Step 5: Fix the
inithandler guardFind the
if (data.type === "init")block insideconnectPrompt. Replace the whole block:if (data.type === "init") { // Skip overwriting messages if HTTP preload already loaded this conversation. // If conversation_id differs (e.g. after "新对话"), always overwrite. const alreadyLoaded = conversationId.value === data.conversation_id conversationId.value = data.conversation_id if (!alreadyLoaded) { messages.value = data.messages || [] } // Apply code from last assistant message if exists const lastAssistant = [...messages.value] .reverse() .find((m) => m.role === "assistant" && m.code) if (lastAssistant?.code) { applyCode(lastAssistant.code) } } -
Step 6: Verify TypeScript compiles
cd /home/xuyue/Projects/webpreview/client && npx tsc --noEmitExpected: no errors in
src/store/prompt.ts -
Step 7: Commit
cd /home/xuyue/Projects/webpreview/client git add src/store/prompt.ts git commit -m "feat: add loadHistory for HTTP-based chat history preload"
Task 3: Call loadHistory in ChallengeHome.vue
Files:
-
Modify:
client/src/pages/ChallengeHome.vue -
Step 1: Add
loadHistoryto the importIn the existing prompt import line, add
loadHistory:import { connectPrompt, disconnectPrompt, conversationId, streaming, setOnCodeComplete, loadHistory, } from "../store/prompt" -
Step 2: Call
loadHistoryalongsideconnectPromptinloadChallengeIn
loadChallenge(), aftertaskId.value = data.task_ptr, find the line:connectPrompt(data.task_ptr)Replace with:
loadHistory(data.task_ptr) // HTTP preload — async, non-blocking connectPrompt(data.task_ptr) // WebSocket — synchronous openBoth fire without
awaitso they run concurrently. HTTP is typically faster than WebSocket handshake + backend DB query, so the user usually sees history before theinitmessage arrives. Theinitguard handles deduplication if WS wins the race. -
Step 3: Verify TypeScript compiles
cd /home/xuyue/Projects/webpreview/client && npx tsc --noEmitExpected: no errors
-
Step 4: Commit
cd /home/xuyue/Projects/webpreview/client git add src/pages/ChallengeHome.vue git commit -m "feat: preload chat history via HTTP on challenge entry"
Task 4: Show loading indicator in PromptPanel.vue
Files:
-
Modify:
client/src/components/PromptPanel.vue -
Step 1: Import
historyLoadingIn the script block, add
historyLoadingto the existing import:import { messages, streaming, streamingContent, sendPrompt, newConversation, historyLoading, } from "../store/prompt" -
Step 2: Add loading indicator to template
Inside
<div class="messages" ref="messagesRef">, add as first child:<div class="messages" ref="messagesRef"> <div v-if="historyLoading" class="history-loading"> <n-spin size="small" /> <span>加载历史记录…</span> </div> <div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]"> -
Step 3: Add loading style
In
<style scoped>, add:.history-loading { display: flex; align-items: center; gap: 8px; padding: 12px; color: #aaa; font-size: 13px; justify-content: center; } -
Step 4: Verify TypeScript compiles
cd /home/xuyue/Projects/webpreview/client && npx tsc --noEmitExpected: no errors
-
Step 5: Manual smoke test
- Start Django:
uv run manage.py runserver(fromapi/) - Start dev server:
npm run start(fromclient/) - Log in as a user who has previous AI chat on a challenge
- Open that challenge → switch to "AI 对话" tab → previous messages appear (with brief loading spinner)
- Navigate away and back → same result
- Open a challenge with no prior AI chat → empty chat, no spinner
- During a chat, click "新对话" → messages clear correctly
- Start Django:
-
Step 6: Commit
cd /home/xuyue/Projects/webpreview/client git add src/components/PromptPanel.vue git commit -m "feat: show loading indicator while chat history preloads"