Files
web/docs/superpowers/plans/2026-04-06-ai-chat-stop-button.md
T
xuyueandClaude Opus 5.5 47d4679984 chore: 合并为单仓库,统一部署流程
- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:02:12 +08:00

193 lines
5.1 KiB
Markdown

# AI Chat Stop Button 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:** Add a stop button to the AI chat panel that cancels in-flight streaming, removes the pending messages from the UI, and ensures nothing is saved to the database or submitted.
**Architecture:** When the user clicks stop, the frontend pops the unsaved user message from the UI, resets streaming state, and closes the WebSocket. The backend's existing `disconnect()` handler deletes the in-progress user message from the DB; the assistant message and Submission are never created because `complete` is never sent.
**Tech Stack:** Vue 3, TypeScript, Naive UI, WebSocket (Django Channels)
---
## File Map
| File | Change |
|---|---|
| `client/src/store/prompt.ts` | Store `_currentTaskId`; set `streaming=true` on send; add `stopPrompt()` |
| `client/src/components/ai/PromptPanel.vue` | Import `stopPrompt`; swap Send/Stop buttons based on `streaming` |
---
### Task 1: Update `store/prompt.ts`
**Files:**
- Modify: `client/src/store/prompt.ts`
- [ ] **Step 1: Store `_currentTaskId` and set it in `connectPrompt`**
In `client/src/store/prompt.ts`, after the line `let ws: WebSocket | null = null`, add:
```typescript
let _currentTaskId = 0
```
Then inside `connectPrompt(taskId: number)`, add as the very first line of the function body:
```typescript
_currentTaskId = taskId
```
Result — the start of `connectPrompt` becomes:
```typescript
export function connectPrompt(taskId: number) {
_currentTaskId = taskId
if (ws) ws.close()
// ... rest unchanged
```
- [ ] **Step 2: Set `streaming = true` immediately on send**
In `sendPrompt()`, add `streaming.value = true` as the first statement before the `push`:
```typescript
export function sendPrompt(content: string, model: string = "") {
if (!ws || ws.readyState !== WebSocket.OPEN) return
streaming.value = true
messages.value.push({ role: "user", content })
ws.send(JSON.stringify({ type: "message", content, model }))
}
```
- [ ] **Step 3: Add `stopPrompt()` function**
After `sendPrompt`, add:
```typescript
export function stopPrompt() {
// Remove the user message added to UI (not yet saved in DB)
if (
messages.value.length > 0 &&
messages.value[messages.value.length - 1].role === "user"
) {
messages.value.pop()
}
streaming.value = false
streamingContent.value = ""
// Closing triggers backend disconnect() which deletes the DB user message
if (ws) {
ws.close()
ws = null
}
// Reconnect immediately so the next message can be sent
if (_currentTaskId) {
connectPrompt(_currentTaskId)
}
}
```
- [ ] **Step 4: Verify the file looks correct**
Open `client/src/store/prompt.ts` and confirm:
- `_currentTaskId` is declared after `let ws`
- `connectPrompt` sets `_currentTaskId = taskId` as its first line
- `sendPrompt` sets `streaming.value = true` before the push
- `stopPrompt` is exported and appears between `sendPrompt` and the private `applyCode`
- [ ] **Step 5: Commit**
```bash
cd /home/xuyue/Projects/webpreview
git add client/src/store/prompt.ts
git commit -m "feat: add stopPrompt() and track currentTaskId in prompt store"
```
---
### Task 2: Add Stop button to `PromptPanel.vue`
**Files:**
- Modify: `client/src/components/ai/PromptPanel.vue`
- [ ] **Step 1: Import `stopPrompt`**
Find the import line in `<script setup>`:
```typescript
import {
messages,
streaming,
streamingContent,
sendPrompt,
} from "../../store/prompt"
```
Add `stopPrompt` to the import:
```typescript
import {
messages,
streaming,
streamingContent,
sendPrompt,
stopPrompt,
} from "../../store/prompt"
```
- [ ] **Step 2: Replace the send button with conditional Stop/Send buttons**
Find the current button block in `<template>`:
```html
<n-button
type="primary"
:loading="streaming"
:disabled="!input.trim() || streaming"
@click="send"
>
发送
</n-button>
```
Replace with:
```html
<n-button
v-if="streaming"
type="error"
@click="stopPrompt"
>
停止
</n-button>
<n-button
v-else
type="primary"
:disabled="!input.trim()"
@click="send"
>
发送
</n-button>
```
- [ ] **Step 3: Verify in browser**
Start the dev server:
```bash
cd /home/xuyue/Projects/webpreview/client
npm run start
```
Open a challenge page, send a message, confirm:
1. The **停止** button appears immediately when send is clicked (no gap)
2. Clicking **停止** removes the user message and streaming content from the UI
3. The **发送** button reappears and is usable for the next message
4. A subsequent message works normally (WS reconnected)
- [ ] **Step 4: Commit**
```bash
cd /home/xuyue/Projects/webpreview
git add client/src/components/ai/PromptPanel.vue
git commit -m "feat: show stop button during AI streaming in PromptPanel"
```