@@ -119,6 +136,57 @@ const nums = computed(() => {
return rounds.value.map((r) => (isGuide(r) ? "" : String(++n)))
})
+// 层级走势:每轮 AI 对话 / 外部 AI 一根柱子,两轮以上、有标过层级才显示
+const trend = computed(() =>
+ rounds.value.flatMap((r, i) => (isGuide(r) ? [] : [{ i, level: r.prompt_level }])),
+)
+const showTrend = computed(() => trend.value.length > 1 && trend.value.some((b) => b.level))
+
+// 和前面某一轮一字不差的提问(手改那一轮的正文都一样,不算)
+const repeats = computed(() => {
+ const seen = new Set()
+ return rounds.value.map((r) => {
+ if (isGuide(r) || r.source === "edit") return false
+ const key = r.question.replace(/\s+/g, " ").trim()
+ const dup = seen.has(key)
+ seen.add(key)
+ return dup
+ })
+})
+
+// 这一轮的代码比上一版改了多少行:只按行内容数,不管顺序
+function codeLines(r: PromptRound) {
+ return [r.html, r.css, r.js]
+ .join("\n")
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean)
+}
+
+function lineDiff(before: string[], after: string[]) {
+ const left = new Map()
+ for (const l of before) left.set(l, (left.get(l) ?? 0) + 1)
+ let added = 0
+ for (const l of after) {
+ const n = left.get(l) ?? 0
+ if (n) left.set(l, n - 1)
+ else added++
+ }
+ const removed = before.length - (after.length - added)
+ return added || removed ? `+${added} −${removed} 行` : "代码没变"
+}
+
+const changes = computed(() => {
+ let prev: string[] | null = null
+ return rounds.value.map((r) => {
+ if (isGuide(r) || !r.html) return ""
+ const cur = codeLines(r)
+ const text = prev ? lineDiff(prev, cur) : ""
+ prev = cur
+ return text
+ })
+})
+
const doc = computed(() => {
const r = rounds.value[selected.value]
if (!r?.html) return ""
@@ -163,6 +231,13 @@ async function load() {
}
}
+function select(i: number) {
+ selected.value = i
+ nextTick(() =>
+ document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" }),
+ )
+}
+
// 在捕获阶段接管按键:弹框开着时,批改页的 Esc / J / K、批改面板的 1–5 / P 都不能触发
function onKey(e: KeyboardEvent) {
if (!props.show) return
@@ -176,10 +251,7 @@ function onKey(e: KeyboardEvent) {
e.preventDefault()
let i = selected.value + step
while (rounds.value[i] && isGuide(rounds.value[i]!)) i += step
- if (rounds.value[i]) selected.value = i
- nextTick(() =>
- document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" }),
- )
+ if (rounds.value[i]) select(i)
}
watch(
@@ -233,7 +305,57 @@ onUnmounted(() => window.removeEventListener("keydown", onKey, true))
height: 72vh;
}
+.left {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.trend {
+ display: flex;
+ align-items: flex-end;
+ gap: 10px;
+ padding: 0 6px 6px 2px;
+ border-bottom: 1px solid var(--wp-line);
+}
+
+.bars {
+ flex: 1;
+ height: 38px;
+ display: flex;
+ align-items: flex-end;
+ gap: 3px;
+}
+
+.bar {
+ flex: 1;
+ max-width: 22px;
+ padding: 0;
+ border: 0;
+ border-radius: 2px 2px 0 0;
+ background: var(--wp-faint);
+ opacity: 0.55;
+ cursor: pointer;
+}
+
+.bar.none {
+ background: var(--wp-line);
+ opacity: 1;
+}
+
+.bar:hover,
+.bar.on {
+ opacity: 1;
+}
+
+.bar.on {
+ background: var(--wp-brand);
+}
+
.rounds {
+ flex: 1;
+ min-height: 0;
margin: 0;
padding: 0 6px 0 0;
list-style: none;
@@ -280,6 +402,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKey, true))
.src,
.level,
+.repeat,
.final {
padding: 0 6px;
border-radius: 4px;
@@ -321,6 +444,11 @@ onUnmounted(() => window.removeEventListener("keydown", onKey, true))
color: var(--wp-muted);
}
+.repeat {
+ color: var(--wp-warn);
+ background: var(--wp-warn-soft);
+}
+
.level {
color: var(--wp-ink);
background: var(--wp-paper);
diff --git a/client/src/components/task/TaskStatsModal.vue b/client/src/components/task/TaskStatsModal.vue
index 91fd143..aa2f4b0 100644
--- a/client/src/components/task/TaskStatsModal.vue
+++ b/client/src/components/task/TaskStatsModal.vue
@@ -130,6 +130,34 @@
还没有打过分的作品
+
+
+
+
+ 怎么问的 AI
+ 每人最新一版 · {{ stats.prompts.with_chain }} 份有提示词记录
+
+
+
+
{{ stats.prompts.avg_rounds }}平均几轮
+
{{ pct(stats.prompts.one_round, stats.prompts.with_chain) }}一轮就交
+
{{ pct(stats.prompts.external, stats.prompts.with_chain) }}最后一版用外部 AI
+
{{ pct(stats.prompts.used_guidance, stats.prompts.with_chain) }}问过怎么写提示词
+
+
+
+ {{ levelLabel(i + 1) }}
+
+
+
+ {{ n }}
+
+
+
+
+ {{ stats.submitted_count ? "都是直接交的代码,没有提示词记录" : "还没有人交" }}
+
+