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>
|
||||
<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>
|
||||
<ol class="chain">
|
||||
<li v-for="(r, i) in chain" :key="i">
|
||||
<n-flex :size="6" align="center">
|
||||
<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>
|
||||
<div class="q">{{ r.question }}</div>
|
||||
</li>
|
||||
@@ -135,6 +140,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@@ -143,7 +155,8 @@ import { useMessage } from "naive-ui"
|
||||
import { Icon } from "../../icons"
|
||||
import { Showcase, Submission } from "../../api"
|
||||
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 {
|
||||
AwardManageOut,
|
||||
FlagType,
|
||||
@@ -181,6 +194,7 @@ const sub = ref<SubmissionAll | null>(null)
|
||||
const versions = ref<SubmissionOut[]>([])
|
||||
const chain = ref<PromptRound[]>([])
|
||||
const rating = ref(false)
|
||||
const showChain = ref(false)
|
||||
|
||||
let awardsCache: Promise<AwardManageOut[]> | null = null
|
||||
const awards = ref<AwardManageOut[]>([])
|
||||
@@ -419,6 +433,14 @@ h4 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chain-head {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chain-head h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.delete {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -2,232 +2,81 @@
|
||||
<n-modal
|
||||
:show="show"
|
||||
preset="card"
|
||||
style="width: 90vw; max-width: 1400px"
|
||||
@update:show="$emit('update:show', $event)"
|
||||
style="width: 92vw; max-width: 1280px"
|
||||
:bordered="false"
|
||||
@update:show="emit('update:show', $event)"
|
||||
>
|
||||
<template #header>
|
||||
<n-flex justify="start" align="center">
|
||||
<span>提示词</span>
|
||||
<n-tooltip placement="bottom-start">
|
||||
<template #trigger>
|
||||
<Icon icon="lucide:info" :width="20" style="color: var(--wp-faint)" />
|
||||
</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>
|
||||
<div class="head">
|
||||
<b>提示词</b>
|
||||
<span v-if="rounds.length" class="muted">{{ rounds.length }} 轮</span>
|
||||
<span v-if="title" class="muted title">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<n-spin :show="loading">
|
||||
<n-empty
|
||||
v-if="!loading && rounds.length === 0"
|
||||
description="暂无对话记录"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
height: 75vh;
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<template #header-extra>
|
||||
<n-popover trigger="click" placement="bottom-end">
|
||||
<template #trigger>
|
||||
<n-button size="small" quaternary style="margin-right: 8px">
|
||||
<template #icon><Icon icon="lucide:info" /></template>
|
||||
L1–L6 是什么
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="levels">
|
||||
<div class="levels-note">按布鲁姆认知层级给每次提问打的标签,越高越需要思考:</div>
|
||||
<div v-for="(l, n) in PROMPT_LEVELS" :key="n">
|
||||
<b>L{{ n }} {{ l.name }}</b>:{{ l.desc }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px">
|
||||
<div style="font-weight: bold; font-size: 13px; color: var(--wp-ink)">
|
||||
第 {{ selectedRound + 1 }} 轮网页
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</n-spin>
|
||||
@@ -235,157 +84,301 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { NPopconfirm, NButton, useMessage } from "naive-ui"
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from "vue"
|
||||
import { Icon } from "../../icons"
|
||||
import { Showcase, Submission } from "../../api"
|
||||
import { renderMarkdown } from "../../utils/markdown"
|
||||
import { sourceLabel } from "../../utils/helper"
|
||||
import { storageShim } from "../../utils/previewStorage"
|
||||
import { Prompt, Submission } from "../../api"
|
||||
import { levelLabel, PROMPT_LEVELS, sourceLabel } from "../../utils/helper"
|
||||
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
||||
import type { PromptRound } from "../../utils/type"
|
||||
import { user, roleSuper } from "../../store/user"
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 一份提交是怎么一轮一轮问出来的:左边每轮的提问,右边这一轮生成的网页
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
submissionId: string
|
||||
username?: string
|
||||
// 例如“张三 · 个人名片”
|
||||
title?: string
|
||||
// 题目素材的地址,网页里的图片要用
|
||||
assetBaseUrl?: string
|
||||
// 作品展里的作品走作品展的接口(不用是老师或作者本人)
|
||||
fromShowcase?: boolean
|
||||
}>()
|
||||
|
||||
const canDelete = computed(
|
||||
() =>
|
||||
roleSuper.value || (!!props.username && props.username === user.username),
|
||||
)
|
||||
|
||||
defineEmits<{ "update:show": [value: boolean] }>()
|
||||
const emit = defineEmits<{ "update:show": [value: boolean] }>()
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRound = ref(0)
|
||||
const expandedRounds = ref<Set<number>>(new Set())
|
||||
type ChainRound = Omit<PromptRound, "source"> & {
|
||||
source: string | null
|
||||
assistantMsgId: number | null
|
||||
}
|
||||
const rounds = ref<ChainRound[]>([])
|
||||
const rounds = ref<PromptRound[]>([])
|
||||
const selected = ref(0)
|
||||
const expanded = ref(new Set<number>())
|
||||
|
||||
async function deleteRound(index: number) {
|
||||
const round = rounds.value[index]
|
||||
if (!round.assistantMsgId) return
|
||||
try {
|
||||
await Prompt.deleteMessagePair(round.assistantMsgId)
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.detail ?? "删除失败,请重试")
|
||||
return
|
||||
}
|
||||
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>`
|
||||
const doc = computed(() => {
|
||||
const r = rounds.value[selected.value]
|
||||
if (!r?.html) return ""
|
||||
return buildPreviewDocument({
|
||||
html: r.html,
|
||||
css: r.css ?? "",
|
||||
js: r.js ?? "",
|
||||
assetBaseUrl: props.assetBaseUrl,
|
||||
})
|
||||
})
|
||||
|
||||
function isPromptLong(text: string): boolean {
|
||||
return text.length > 220 || text.split(/\r?\n/).length > 4
|
||||
function isLong(text: string) {
|
||||
return text.length > 200 || text.split(/\r?\n/).length > 4
|
||||
}
|
||||
|
||||
function isExpanded(index: number): boolean {
|
||||
return expandedRounds.value.has(index)
|
||||
function toggle(i: number) {
|
||||
const next = new Set(expanded.value)
|
||||
if (!next.delete(i)) next.add(i)
|
||||
expanded.value = next
|
||||
}
|
||||
|
||||
function toggleExpanded(index: number) {
|
||||
const next = new Set(expandedRounds.value)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
next.add(index)
|
||||
}
|
||||
expandedRounds.value = next
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
if (!props.submissionId) return
|
||||
let seq = 0
|
||||
async function load() {
|
||||
const s = ++seq
|
||||
loading.value = true
|
||||
rounds.value = []
|
||||
selectedRound.value = 0
|
||||
expandedRounds.value = new Set()
|
||||
expanded.value = new Set()
|
||||
try {
|
||||
const data = await Submission.getPromptChain(props.submissionId)
|
||||
rounds.value = data.map((round) => ({
|
||||
...round,
|
||||
source: round.source ?? null,
|
||||
assistantMsgId: round.assistant_msg_id ?? null,
|
||||
}))
|
||||
const last = rounds.value.length - 1
|
||||
if (last >= 0) selectedRound.value = last
|
||||
const data = props.fromShowcase
|
||||
? await Showcase.getPromptChain(props.submissionId)
|
||||
: await Submission.getPromptChain(props.submissionId)
|
||||
if (s !== seq) return
|
||||
rounds.value = data
|
||||
// 默认看交的那一版
|
||||
selected.value = Math.max(0, data.length - 1)
|
||||
await nextTick()
|
||||
document.querySelector(".rounds .round.on")?.scrollIntoView({ block: "nearest" })
|
||||
} catch {
|
||||
rounds.value = []
|
||||
if (s === seq) rounds.value = []
|
||||
} 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(
|
||||
() => [props.show, props.submissionId] as const,
|
||||
([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>
|
||||
|
||||
<style scoped>
|
||||
.prompt-markdown {
|
||||
flex: 1;
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prompt-markdown.is-collapsed {
|
||||
position: relative;
|
||||
max-height: 126px;
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.prompt-markdown :deep(p),
|
||||
.prompt-markdown :deep(ul),
|
||||
.prompt-markdown :deep(ol),
|
||||
.prompt-markdown :deep(blockquote),
|
||||
.prompt-markdown :deep(pre) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
.muted {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--wp-muted);
|
||||
}
|
||||
|
||||
.prompt-markdown :deep(:last-child) {
|
||||
margin-bottom: 0;
|
||||
.levels {
|
||||
font-size: 13px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.prompt-markdown :deep(pre) {
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
.levels-note {
|
||||
color: var(--wp-muted);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.prompt-markdown :deep(code) {
|
||||
white-space: pre-wrap;
|
||||
.num {
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -102,7 +102,8 @@
|
||||
v-if="current"
|
||||
v-model:show="showChain"
|
||||
: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>
|
||||
|
||||
|
||||
@@ -23,3 +23,17 @@ export function sourceLabel(source: string | null | undefined) {
|
||||
if (source === "manual") 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