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

7.1 KiB
Raw Permalink Blame History

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:

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:

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:

    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
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:

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:

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
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:

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:

<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):

<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

git add client/src/components/submissions/ChainModal.vue
git commit -m "feat: show AI/manual source indicator on each chain round"