Files
web/docs/superpowers/plans/2026-04-16-chain-source-indicator.md
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

268 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
# Chain Source Indicator 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:** In the ChainModal prompt chain, show a small tag on each round indicating whether it originated from AI dialogue or a manual submission.
**Architecture:** The `Message.source` field already exists on the backend ("conversation" or "manual") but is not serialized. We expose it through the existing `list_messages` endpoint, propagate it through the frontend type, and render a tag badge in `ChainModal.vue` alongside the existing L1–L6 level badge.
**Tech Stack:** Django-ninja (Python), Vue 3, TypeScript, Naive UI
---
## File Map
| File | Change |
|------|--------|
| `api/prompt/schemas.py` | Add `source: str` to `MessageOut` |
| `api/prompt/api.py` | Add `"source": m.source` to `list_messages` response |
| `client/src/utils/type.ts` | Add `source?: string` to `PromptMessage` |
| `client/src/components/submissions/ChainModal.vue` | Carry `source` through rounds, render tag |
---
### Task 1: Expose `source` from the backend messages endpoint
**Files:**
- Modify: `api/prompt/schemas.py`
- Modify: `api/prompt/api.py`
- [ ] **Step 1: Add `source` to `MessageOut` schema**
Open `api/prompt/schemas.py`. Change:
```python
class MessageOut(Schema):
id: int
role: str
content: str
code_html: Optional[str] = None
code_css: Optional[str] = None
code_js: Optional[str] = None
prompt_level: Optional[int] = None
created: str
```
to:
```python
class MessageOut(Schema):
id: int
role: str
source: str
content: str
code_html: Optional[str] = None
code_css: Optional[str] = None
code_js: Optional[str] = None
prompt_level: Optional[int] = None
created: str
```
- [ ] **Step 2: Include `source` in the `list_messages` response dict**
Open `api/prompt/api.py`. In `list_messages`, the return list comprehension currently builds dicts manually. Add `"source": m.source`:
```python
return [
{
"id": m.id,
"role": m.role,
"source": m.source,
"content": m.content,
"code_html": m.code_html,
"code_css": m.code_css,
"code_js": m.code_js,
"prompt_level": m.prompt_level,
"created": m.created.isoformat(),
}
for m in messages
]
```
- [ ] **Step 3: Verify the API returns `source` manually**
With the dev server running (`uv run manage.py runserver` from `api/`), open the Django-ninja interactive docs at `http://localhost:8000/api/docs` and call the messages endpoint. Confirm the response includes `"source": "conversation"` or `"source": "manual"` on each message object.
- [ ] **Step 4: Commit**
```bash
git add api/prompt/schemas.py api/prompt/api.py
git commit -m "feat: expose message source field in messages API"
```
---
### Task 2: Add `source` to the frontend type
**Files:**
- Modify: `client/src/utils/type.ts`
- [ ] **Step 1: Add `source` to `PromptMessage`**
Open `client/src/utils/type.ts`. Change:
```typescript
export interface PromptMessage {
id: number
role: string
content: string
code_html: string | null
code_css: string | null
code_js: string | null
prompt_level?: number | null
created: string
}
```
to:
```typescript
export interface PromptMessage {
id: number
role: string
source?: string
content: string
code_html: string | null
code_css: string | null
code_js: string | null
prompt_level?: number | null
created: string
}
```
- [ ] **Step 2: Commit**
```bash
git add client/src/utils/type.ts
git commit -m "feat: add source field to PromptMessage type"
```
---
### Task 3: Render source indicator in ChainModal
**Files:**
- Modify: `client/src/components/submissions/ChainModal.vue`
- [ ] **Step 1: Carry `source` through the rounds computed property**
In `ChainModal.vue`, the `rounds` computed property builds objects with `{ question, prompt_level, html, css, js }`. Add `source` from the user message:
```typescript
const rounds = computed(() => {
const result: {
question: string
source: string | null
prompt_level: number | null
html: string | null
css: string | null
js: string | null
}[] = []
for (const [i, msg] of messages.value.entries()) {
if (msg.role !== "user") continue
let html: string | null = null,
css: string | null = null,
js: string | null = null
for (const reply of messages.value.slice(i + 1)) {
if (reply.role === "user") break
if (reply.role === "assistant" && reply.code_html) {
html = reply.code_html
css = reply.code_css
js = reply.code_js
break
}
}
result.push({
question: msg.content,
source: msg.source ?? null,
prompt_level: msg.prompt_level ?? null,
html,
css,
js,
})
}
return result
})
```
- [ ] **Step 2: Add the source tag to the round card template**
Locate the `<div>` that renders the question and the L-level badge in the round card. It currently looks like:
```html
<div
style="
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
"
>
<span>{{ round.question }}</span>
<span
v-if="round.prompt_level"
:style="{
flexShrink: 0,
fontSize: '11px',
fontWeight: 'bold',
color: levelColors[round.prompt_level],
marginTop: '2px',
}"
>L{{ round.prompt_level }}</span
>
</div>
```
Replace it with (source tag appears to the right of the L-level badge, in a small flex column):
```html
<div
style="
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
"
>
<span>{{ round.question }}</span>
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex-shrink: 0">
<span
v-if="round.prompt_level"
:style="{
fontSize: '11px',
fontWeight: 'bold',
color: levelColors[round.prompt_level],
marginTop: '2px',
}"
>L{{ round.prompt_level }}</span
>
<span
v-if="round.source"
:style="{
fontSize: '10px',
padding: '1px 5px',
borderRadius: '4px',
background: round.source === 'conversation' ? '#e8f0fe' : '#f0f0f0',
color: round.source === 'conversation' ? '#2060c0' : '#888',
fontWeight: 500,
whiteSpace: 'nowrap',
}"
>{{ round.source === "conversation" ? "AI" : "手动" }}</span>
</div>
</div>
```
- [ ] **Step 3: Verify visually in the browser**
Start the frontend dev server (`npm run start` from `client/`). Open the Submissions page, expand a row, click 查看 to open ChainModal. Confirm:
- Rounds from AI chat show a blue `AI` badge
- Rounds from manual submissions show a gray `手动` badge
- Rounds with no `source` (old data) show no badge and don't break
- [ ] **Step 4: Commit**
```bash
git add client/src/components/submissions/ChainModal.vue
git commit -m "feat: show AI/manual source indicator on each chain round"
```