feat: 重新设计提示词链弹框,批改面板也能逐轮看网页
- 左边每轮提问:来源(AI 对话 / 手改 / 外部 AI)、等级写成“L3 应用”,标出交的这一版和没有网页的轮次; 右边是这一轮生成的网页,↑ ↓ 切换 - 预览改用 buildPreviewDocument:题目素材能加载,暗色下也保持白底 - 批改面板加“逐轮看网页”;弹框开着时接管按键,批改页的 Esc / J / K 和面板的 1–5 / P 不会被触发 - 查看器里不再能删除对话轮次(学生在做题页的对话和历史里删) - 预留作品展的接口参数(fromShowcase) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ad9be2b30
commit
3fb23dc3f3
4 files changed
+359
-329
No files matched your search
@@ -120,13 +120,18 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h4>提示词 <span class="muted" v-if="chain.length">· {{ chain.length }} 轮</span></h4>
|
<n-flex justify="space-between" align="center" class="chain-head">
|
||||||
|
<h4>提示词 <span class="muted" v-if="chain.length">· {{ chain.length }} 轮</span></h4>
|
||||||
|
<n-button v-if="chain.length" size="tiny" quaternary @click="showChain = true">
|
||||||
|
逐轮看网页
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
<div v-if="!chain.length" class="muted">这份提交没有关联的提示词</div>
|
<div v-if="!chain.length" class="muted">这份提交没有关联的提示词</div>
|
||||||
<ol class="chain">
|
<ol class="chain">
|
||||||
<li v-for="(r, i) in chain" :key="i">
|
<li v-for="(r, i) in chain" :key="i">
|
||||||
<n-flex :size="6" align="center">
|
<n-flex :size="6" align="center">
|
||||||
<span class="src">{{ sourceLabel(r.source) }}</span>
|
<span class="src">{{ sourceLabel(r.source) }}</span>
|
||||||
<span v-if="r.prompt_level" class="level">L{{ r.prompt_level }}</span>
|
<span v-if="r.prompt_level" class="level">{{ levelLabel(r.prompt_level) }}</span>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<div class="q">{{ r.question }}</div>
|
<div class="q">{{ r.question }}</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -135,6 +140,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
|
<ChainModal
|
||||||
|
v-if="sub"
|
||||||
|
v-model:show="showChain"
|
||||||
|
:submission-id="sub.id"
|
||||||
|
:title="`${sub.username} · ${sub.task_title}`"
|
||||||
|
:asset-base-url="`/media/tasks/${sub.task_type}/${sub.task_display}/`"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -143,7 +155,8 @@ import { useMessage } from "naive-ui"
|
|||||||
import { Icon } from "../../icons"
|
import { Icon } from "../../icons"
|
||||||
import { Showcase, Submission } from "../../api"
|
import { Showcase, Submission } from "../../api"
|
||||||
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
||||||
import { parseTime, sourceLabel } from "../../utils/helper"
|
import { levelLabel, parseTime, sourceLabel } from "../../utils/helper"
|
||||||
|
import ChainModal from "../submissions/ChainModal.vue"
|
||||||
import type {
|
import type {
|
||||||
AwardManageOut,
|
AwardManageOut,
|
||||||
FlagType,
|
FlagType,
|
||||||
@@ -181,6 +194,7 @@ const sub = ref<SubmissionAll | null>(null)
|
|||||||
const versions = ref<SubmissionOut[]>([])
|
const versions = ref<SubmissionOut[]>([])
|
||||||
const chain = ref<PromptRound[]>([])
|
const chain = ref<PromptRound[]>([])
|
||||||
const rating = ref(false)
|
const rating = ref(false)
|
||||||
|
const showChain = ref(false)
|
||||||
|
|
||||||
let awardsCache: Promise<AwardManageOut[]> | null = null
|
let awardsCache: Promise<AwardManageOut[]> | null = null
|
||||||
const awards = ref<AwardManageOut[]>([])
|
const awards = ref<AwardManageOut[]>([])
|
||||||
@@ -419,6 +433,14 @@ h4 {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chain-head {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-head h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.delete {
|
.delete {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,232 +2,81 @@
|
|||||||
<n-modal
|
<n-modal
|
||||||
:show="show"
|
:show="show"
|
||||||
preset="card"
|
preset="card"
|
||||||
style="width: 90vw; max-width: 1400px"
|
style="width: 92vw; max-width: 1280px"
|
||||||
@update:show="$emit('update:show', $event)"
|
:bordered="false"
|
||||||
|
@update:show="emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-flex justify="start" align="center">
|
<div class="head">
|
||||||
<span>提示词</span>
|
<b>提示词</b>
|
||||||
<n-tooltip placement="bottom-start">
|
<span v-if="rounds.length" class="muted">{{ rounds.length }} 轮</span>
|
||||||
<template #trigger>
|
<span v-if="title" class="muted title">{{ title }}</span>
|
||||||
<Icon icon="lucide:info" :width="20" style="color: var(--wp-faint)" />
|
</div>
|
||||||
</template>
|
|
||||||
<div style="line-height: 2">
|
|
||||||
<div>
|
|
||||||
<span :style="{ color: levelColors[1], fontWeight: 'bold' }">
|
|
||||||
L1
|
|
||||||
</span>
|
|
||||||
— 记忆:复述或识别知识点
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span :style="{ color: levelColors[2], fontWeight: 'bold' }">
|
|
||||||
L2
|
|
||||||
</span>
|
|
||||||
— 理解:用自己的话解释概念
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span :style="{ color: levelColors[3], fontWeight: 'bold' }">
|
|
||||||
L3
|
|
||||||
</span>
|
|
||||||
— 应用:将知识用于新情境
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span :style="{ color: levelColors[4], fontWeight: 'bold' }">
|
|
||||||
L4
|
|
||||||
</span>
|
|
||||||
— 分析:拆解结构、找出规律
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span :style="{ color: levelColors[5], fontWeight: 'bold' }">
|
|
||||||
L5
|
|
||||||
</span>
|
|
||||||
— 评价:基于标准作出判断
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span :style="{ color: levelColors[6], fontWeight: 'bold' }">
|
|
||||||
L6
|
|
||||||
</span>
|
|
||||||
— 创造:整合信息产出新成果
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-tooltip>
|
|
||||||
</n-flex>
|
|
||||||
</template>
|
</template>
|
||||||
<n-spin :show="loading">
|
<template #header-extra>
|
||||||
<n-empty
|
<n-popover trigger="click" placement="bottom-end">
|
||||||
v-if="!loading && rounds.length === 0"
|
<template #trigger>
|
||||||
description="暂无对话记录"
|
<n-button size="small" quaternary style="margin-right: 8px">
|
||||||
/>
|
<template #icon><Icon icon="lucide:info" /></template>
|
||||||
<div
|
L1–L6 是什么
|
||||||
v-else
|
</n-button>
|
||||||
style="
|
</template>
|
||||||
display: grid;
|
<div class="levels">
|
||||||
grid-template-columns: 1fr 1fr;
|
<div class="levels-note">按布鲁姆认知层级给每次提问打的标签,越高越需要思考:</div>
|
||||||
gap: 16px;
|
<div v-for="(l, n) in PROMPT_LEVELS" :key="n">
|
||||||
height: 75vh;
|
<b>L{{ n }} {{ l.name }}</b>:{{ l.desc }}
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 8px;
|
|
||||||
border-right: 1px solid var(--wp-line);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(round, index) in rounds"
|
|
||||||
:key="index"
|
|
||||||
style="
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: flex-start;
|
|
||||||
cursor: pointer;
|
|
||||||
"
|
|
||||||
@click="selectedRound = index"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
flexShrink: 0,
|
|
||||||
width: '22px',
|
|
||||||
height: '22px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: selectedRound === index ? 'var(--wp-css)' : 'var(--wp-css-soft)',
|
|
||||||
color: 'var(--wp-surface)',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginTop: '2px',
|
|
||||||
transition: 'background 0.2s',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ index + 1 }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
flex: 1,
|
|
||||||
padding: '10px 14px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
background: selectedRound === index ? 'var(--wp-css-soft)' : 'var(--wp-paper)',
|
|
||||||
border:
|
|
||||||
selectedRound === index
|
|
||||||
? '1px solid var(--wp-css)'
|
|
||||||
: '1px solid var(--wp-line)',
|
|
||||||
fontSize: '13px',
|
|
||||||
lineHeight: '1.6',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="prompt-markdown markdown-body"
|
|
||||||
:class="{
|
|
||||||
'is-collapsed':
|
|
||||||
isPromptLong(round.question) && !isExpanded(index),
|
|
||||||
}"
|
|
||||||
v-html="renderMarkdown(round.question)"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="round.source"
|
|
||||||
:style="{
|
|
||||||
fontSize: '10px',
|
|
||||||
padding: '1px 5px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
background:
|
|
||||||
round.source === 'conversation' ? 'var(--wp-css-soft)' : 'var(--wp-line)',
|
|
||||||
color:
|
|
||||||
round.source === 'conversation' ? 'var(--wp-css)' : 'var(--wp-muted)',
|
|
||||||
fontWeight: 500,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}"
|
|
||||||
>{{ sourceLabel(round.source) }}</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="round.prompt_level"
|
|
||||||
:style="{
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: levelColors[round.prompt_level],
|
|
||||||
}"
|
|
||||||
>L{{ round.prompt_level }}</span
|
|
||||||
>
|
|
||||||
<n-button
|
|
||||||
v-if="isPromptLong(round.question)"
|
|
||||||
text
|
|
||||||
size="tiny"
|
|
||||||
type="primary"
|
|
||||||
@click.stop="toggleExpanded(index)"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
:icon="
|
|
||||||
isExpanded(index)
|
|
||||||
? 'lucide:chevron-up'
|
|
||||||
: 'lucide:chevron-down'
|
|
||||||
"
|
|
||||||
:width="12"
|
|
||||||
/>
|
|
||||||
{{ isExpanded(index) ? "收起" : "展开" }}
|
|
||||||
</n-button>
|
|
||||||
<n-popconfirm
|
|
||||||
v-if="round.assistantMsgId && canDelete"
|
|
||||||
:show-icon="false"
|
|
||||||
@positive-click="deleteRound(index)"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
|
||||||
text
|
|
||||||
size="tiny"
|
|
||||||
type="error"
|
|
||||||
style="margin-left: 2px"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon icon="lucide:trash-2" :width="12" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
确定删除这一轮?
|
|
||||||
</n-popconfirm>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 8px">
|
</n-popover>
|
||||||
<div style="font-weight: bold; font-size: 13px; color: var(--wp-ink)">
|
</template>
|
||||||
第 {{ selectedRound + 1 }} 轮网页
|
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<n-empty
|
||||||
|
v-if="!loading && !rounds.length"
|
||||||
|
description="这份提交没有关联的提示词"
|
||||||
|
style="padding: 80px 0"
|
||||||
|
/>
|
||||||
|
<div v-else class="body">
|
||||||
|
<ol class="rounds">
|
||||||
|
<li
|
||||||
|
v-for="(r, i) in rounds"
|
||||||
|
:key="i"
|
||||||
|
:class="['round', { on: i === selected }]"
|
||||||
|
@click="selected = i"
|
||||||
|
>
|
||||||
|
<div class="round-head">
|
||||||
|
<span class="num">{{ i + 1 }}</span>
|
||||||
|
<span :class="['src', `src-${r.source || 'conversation'}`]">
|
||||||
|
{{ sourceLabel(r.source) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="r.prompt_level" class="level">{{ levelLabel(r.prompt_level) }}</span>
|
||||||
|
<span v-if="i === rounds.length - 1" class="final">交的这一版</span>
|
||||||
|
<span v-else-if="!r.html" class="muted no-code">没有网页</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="['q', 'markdown-body', { clamp: isLong(r.question) && !expanded.has(i) }]"
|
||||||
|
v-html="renderMarkdown(r.question)"
|
||||||
|
></div>
|
||||||
|
<button v-if="isLong(r.question)" class="more" @click.stop="toggle(i)">
|
||||||
|
{{ expanded.has(i) ? "收起" : "展开全部" }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-head">
|
||||||
|
<b>第 {{ selected + 1 }} 轮生成的网页</b>
|
||||||
|
<span class="muted">↑ ↓ 切换</span>
|
||||||
|
</div>
|
||||||
|
<div class="frame">
|
||||||
|
<iframe
|
||||||
|
v-if="doc"
|
||||||
|
:key="selected"
|
||||||
|
:sandbox="PREVIEW_SANDBOX"
|
||||||
|
:srcdoc="doc"
|
||||||
|
></iframe>
|
||||||
|
<n-empty v-else description="这一轮没有生成网页" style="margin: auto" />
|
||||||
</div>
|
</div>
|
||||||
<iframe
|
|
||||||
v-if="selectedPageHtml"
|
|
||||||
:srcdoc="selectedPageHtml"
|
|
||||||
:key="selectedRound"
|
|
||||||
sandbox="allow-scripts"
|
|
||||||
style="
|
|
||||||
flex: 1;
|
|
||||||
border: 1px solid var(--wp-line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--wp-surface);
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<n-empty v-else description="该轮无网页代码" style="margin: auto" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
@@ -235,157 +84,301 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, nextTick, onUnmounted, ref, watch } from "vue"
|
||||||
import { NPopconfirm, NButton, useMessage } from "naive-ui"
|
|
||||||
import { Icon } from "../../icons"
|
import { Icon } from "../../icons"
|
||||||
|
import { Showcase, Submission } from "../../api"
|
||||||
import { renderMarkdown } from "../../utils/markdown"
|
import { renderMarkdown } from "../../utils/markdown"
|
||||||
import { sourceLabel } from "../../utils/helper"
|
import { levelLabel, PROMPT_LEVELS, sourceLabel } from "../../utils/helper"
|
||||||
import { storageShim } from "../../utils/previewStorage"
|
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
||||||
import { Prompt, Submission } from "../../api"
|
|
||||||
import type { PromptRound } from "../../utils/type"
|
import type { PromptRound } from "../../utils/type"
|
||||||
import { user, roleSuper } from "../../store/user"
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
|
// 一份提交是怎么一轮一轮问出来的:左边每轮的提问,右边这一轮生成的网页
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
submissionId: string
|
submissionId: string
|
||||||
username?: string
|
// 例如“张三 · 个人名片”
|
||||||
|
title?: string
|
||||||
|
// 题目素材的地址,网页里的图片要用
|
||||||
|
assetBaseUrl?: string
|
||||||
|
// 作品展里的作品走作品展的接口(不用是老师或作者本人)
|
||||||
|
fromShowcase?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
const emit = defineEmits<{ "update:show": [value: boolean] }>()
|
||||||
const canDelete = computed(
|
|
||||||
() =>
|
|
||||||
roleSuper.value || (!!props.username && props.username === user.username),
|
|
||||||
)
|
|
||||||
|
|
||||||
defineEmits<{ "update:show": [value: boolean] }>()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const selectedRound = ref(0)
|
const rounds = ref<PromptRound[]>([])
|
||||||
const expandedRounds = ref<Set<number>>(new Set())
|
const selected = ref(0)
|
||||||
type ChainRound = Omit<PromptRound, "source"> & {
|
const expanded = ref(new Set<number>())
|
||||||
source: string | null
|
|
||||||
assistantMsgId: number | null
|
|
||||||
}
|
|
||||||
const rounds = ref<ChainRound[]>([])
|
|
||||||
|
|
||||||
async function deleteRound(index: number) {
|
const doc = computed(() => {
|
||||||
const round = rounds.value[index]
|
const r = rounds.value[selected.value]
|
||||||
if (!round.assistantMsgId) return
|
if (!r?.html) return ""
|
||||||
try {
|
return buildPreviewDocument({
|
||||||
await Prompt.deleteMessagePair(round.assistantMsgId)
|
html: r.html,
|
||||||
} catch (error: any) {
|
css: r.css ?? "",
|
||||||
message.error(error.response?.data?.detail ?? "删除失败,请重试")
|
js: r.js ?? "",
|
||||||
return
|
assetBaseUrl: props.assetBaseUrl,
|
||||||
}
|
})
|
||||||
await loadMessages()
|
|
||||||
if (selectedRound.value >= rounds.value.length) {
|
|
||||||
selectedRound.value = Math.max(0, rounds.value.length - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelColors: Record<number, string> = {
|
|
||||||
1: "var(--wp-faint)",
|
|
||||||
2: "var(--wp-brand)",
|
|
||||||
3: "var(--wp-css)",
|
|
||||||
4: "#c47ab8",
|
|
||||||
5: "var(--wp-warn)",
|
|
||||||
6: "var(--wp-bad)",
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedPageHtml = computed(() => {
|
|
||||||
const round = rounds.value[selectedRound.value]
|
|
||||||
if (!round?.html) return null
|
|
||||||
const style = round.css ? `<style>${round.css}</style>` : ""
|
|
||||||
const script = round.js ? `<script>${round.js}<\/script>` : ""
|
|
||||||
return `<!DOCTYPE html><html><head><meta charset="utf-8">${storageShim()}${style}</head><body>${round.html}${script}</body></html>`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function isPromptLong(text: string): boolean {
|
function isLong(text: string) {
|
||||||
return text.length > 220 || text.split(/\r?\n/).length > 4
|
return text.length > 200 || text.split(/\r?\n/).length > 4
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExpanded(index: number): boolean {
|
function toggle(i: number) {
|
||||||
return expandedRounds.value.has(index)
|
const next = new Set(expanded.value)
|
||||||
|
if (!next.delete(i)) next.add(i)
|
||||||
|
expanded.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleExpanded(index: number) {
|
let seq = 0
|
||||||
const next = new Set(expandedRounds.value)
|
async function load() {
|
||||||
if (next.has(index)) {
|
const s = ++seq
|
||||||
next.delete(index)
|
|
||||||
} else {
|
|
||||||
next.add(index)
|
|
||||||
}
|
|
||||||
expandedRounds.value = next
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMessages() {
|
|
||||||
if (!props.submissionId) return
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
rounds.value = []
|
rounds.value = []
|
||||||
selectedRound.value = 0
|
expanded.value = new Set()
|
||||||
expandedRounds.value = new Set()
|
|
||||||
try {
|
try {
|
||||||
const data = await Submission.getPromptChain(props.submissionId)
|
const data = props.fromShowcase
|
||||||
rounds.value = data.map((round) => ({
|
? await Showcase.getPromptChain(props.submissionId)
|
||||||
...round,
|
: await Submission.getPromptChain(props.submissionId)
|
||||||
source: round.source ?? null,
|
if (s !== seq) return
|
||||||
assistantMsgId: round.assistant_msg_id ?? null,
|
rounds.value = data
|
||||||
}))
|
// 默认看交的那一版
|
||||||
const last = rounds.value.length - 1
|
selected.value = Math.max(0, data.length - 1)
|
||||||
if (last >= 0) selectedRound.value = last
|
await nextTick()
|
||||||
|
document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" })
|
||||||
} catch {
|
} catch {
|
||||||
rounds.value = []
|
if (s === seq) rounds.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (s === seq) loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在捕获阶段接管按键:弹框开着时,批改页的 Esc / J / K、批改面板的 1–5 / P 都不能触发
|
||||||
|
function onKey(e: KeyboardEvent) {
|
||||||
|
if (!props.show) return
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
emit("update:show", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0
|
||||||
|
if (!step || !rounds.value.length) return
|
||||||
|
e.preventDefault()
|
||||||
|
selected.value = Math.min(rounds.value.length - 1, Math.max(0, selected.value + step))
|
||||||
|
nextTick(() =>
|
||||||
|
document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.show, props.submissionId] as const,
|
() => [props.show, props.submissionId] as const,
|
||||||
([visible]) => {
|
([visible]) => {
|
||||||
if (visible) loadMessages()
|
if (visible) {
|
||||||
|
load()
|
||||||
|
window.addEventListener("keydown", onKey, true)
|
||||||
|
} else {
|
||||||
|
window.removeEventListener("keydown", onKey, true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
onUnmounted(() => window.removeEventListener("keydown", onKey, true))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.prompt-markdown {
|
.head {
|
||||||
flex: 1;
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background: transparent;
|
|
||||||
color: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown.is-collapsed {
|
.title {
|
||||||
position: relative;
|
white-space: nowrap;
|
||||||
max-height: 126px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(p),
|
.muted {
|
||||||
.prompt-markdown :deep(ul),
|
font-size: 12px;
|
||||||
.prompt-markdown :deep(ol),
|
font-weight: 400;
|
||||||
.prompt-markdown :deep(blockquote),
|
color: var(--wp-muted);
|
||||||
.prompt-markdown :deep(pre) {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(:last-child) {
|
.levels {
|
||||||
margin-bottom: 0;
|
font-size: 13px;
|
||||||
|
line-height: 1.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(pre) {
|
.levels-note {
|
||||||
padding: 8px;
|
color: var(--wp-muted);
|
||||||
overflow-x: auto;
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 2fr) 3fr;
|
||||||
|
gap: 16px;
|
||||||
|
height: 72vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounds {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 6px 0 0;
|
||||||
|
list-style: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--wp-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--wp-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round:hover {
|
||||||
|
border-color: var(--wp-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.round.on {
|
||||||
|
border-color: var(--wp-brand);
|
||||||
|
background: var(--wp-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(code) {
|
.num {
|
||||||
white-space: pre-wrap;
|
min-width: 18px;
|
||||||
|
font: 600 12px var(--wp-font-mono);
|
||||||
|
color: var(--wp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.round.on .num {
|
||||||
|
color: var(--wp-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.src,
|
||||||
|
.level,
|
||||||
|
.final {
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.src {
|
||||||
|
color: var(--wp-css);
|
||||||
|
background: var(--wp-css-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.src-edit {
|
||||||
|
color: var(--wp-js);
|
||||||
|
background: var(--wp-js-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.src-manual {
|
||||||
|
color: var(--wp-html);
|
||||||
|
background: var(--wp-html-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
color: var(--wp-ink);
|
||||||
|
background: var(--wp-paper);
|
||||||
|
border: 1px solid var(--wp-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.final {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--wp-brand);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-code {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--wp-ink);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.q.clamp {
|
||||||
|
max-height: 84px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-mask-image: linear-gradient(#000 55%, transparent);
|
||||||
|
mask-image: linear-gradient(#000 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(p),
|
||||||
|
.q :deep(ul),
|
||||||
|
.q :deep(ol),
|
||||||
|
.q :deep(pre),
|
||||||
|
.q :deep(blockquote) {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(pre) {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(code) {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--wp-brand);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid var(--wp-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame iframe {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -102,7 +102,8 @@
|
|||||||
v-if="current"
|
v-if="current"
|
||||||
v-model:show="showChain"
|
v-model:show="showChain"
|
||||||
:submission-id="current.id"
|
:submission-id="current.id"
|
||||||
:username="current.username"
|
:title="`${current.task_display}. ${current.task_title}`"
|
||||||
|
:asset-base-url="`/media/tasks/${current.task_type}/${current.task_display}/`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -23,3 +23,17 @@ export function sourceLabel(source: string | null | undefined) {
|
|||||||
if (source === "manual") return "外部 AI"
|
if (source === "manual") return "外部 AI"
|
||||||
return "AI 对话"
|
return "AI 对话"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提问的 Bloom 等级(services/classifier.ts 后台打的标签)
|
||||||
|
export const PROMPT_LEVELS: Record<number, { name: string; desc: string }> = {
|
||||||
|
1: { name: "记忆", desc: "复述或识别知识点" },
|
||||||
|
2: { name: "理解", desc: "用自己的话解释概念" },
|
||||||
|
3: { name: "应用", desc: "把知识用到新情境" },
|
||||||
|
4: { name: "分析", desc: "拆解结构、找出规律" },
|
||||||
|
5: { name: "评价", desc: "按标准作出判断" },
|
||||||
|
6: { name: "创造", desc: "整合信息做出新东西" },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function levelLabel(level: number | null | undefined) {
|
||||||
|
return level ? `L${level} ${PROMPT_LEVELS[level]?.name ?? ""}`.trim() : ""
|
||||||
|
}
|
||||||
Reference in new issue
Block a user