fix
This commit is contained in:
14
src/api.ts
14
src/api.ts
@@ -1,6 +1,13 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { router } from "./router"
|
import { router } from "./router"
|
||||||
import type { TutorialIn, ChallengeIn, FlagType, SubmissionOut, PromptMessage, TaskStatsOut } from "./utils/type"
|
import type {
|
||||||
|
TutorialIn,
|
||||||
|
ChallengeIn,
|
||||||
|
FlagType,
|
||||||
|
SubmissionOut,
|
||||||
|
PromptMessage,
|
||||||
|
TaskStatsOut,
|
||||||
|
} from "./utils/type"
|
||||||
import { BASE_URL, STORAGE_KEY } from "./utils/const"
|
import { BASE_URL, STORAGE_KEY } from "./utils/const"
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
@@ -227,7 +234,6 @@ export const Submission = {
|
|||||||
const res = await http.get(`/submission/stats/${taskId}`, { params })
|
const res = await http.get(`/submission/stats/${taskId}`, { params })
|
||||||
return res.data as TaskStatsOut
|
return res.data as TaskStatsOut
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Prompt = {
|
export const Prompt = {
|
||||||
@@ -240,7 +246,9 @@ export const Prompt = {
|
|||||||
|
|
||||||
async getMessages(conversationId: string): Promise<PromptMessage[]> {
|
async getMessages(conversationId: string): Promise<PromptMessage[]> {
|
||||||
return (
|
return (
|
||||||
await http.get<PromptMessage[]>(`/prompt/conversations/${conversationId}/messages/`)
|
await http.get<PromptMessage[]>(
|
||||||
|
`/prompt/conversations/${conversationId}/messages/`,
|
||||||
|
)
|
||||||
).data
|
).data
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,10 @@ async function submitStudent() {
|
|||||||
studentLoading.value = true
|
studentLoading.value = true
|
||||||
showStudentError.value = false
|
showStudentError.value = false
|
||||||
try {
|
try {
|
||||||
const data = await Account.login(selectedUsername.value, studentPassword.value)
|
const data = await Account.login(
|
||||||
|
selectedUsername.value,
|
||||||
|
studentPassword.value,
|
||||||
|
)
|
||||||
user.username = data.username
|
user.username = data.username
|
||||||
user.role = data.role
|
user.role = data.role
|
||||||
user.loaded = true
|
user.loaded = true
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
<n-button quaternary @click="download" :disabled="!showDL">下载</n-button>
|
<n-button quaternary @click="download" :disabled="!showDL">下载</n-button>
|
||||||
<n-button quaternary @click="open">全屏</n-button>
|
<n-button quaternary @click="open">全屏</n-button>
|
||||||
<n-button quaternary v-if="props.clearable" @click="clear">清空</n-button>
|
<n-button quaternary v-if="props.clearable" @click="clear">清空</n-button>
|
||||||
<n-button quaternary v-if="props.showCodeButton" @click="emits('showCode')">代码</n-button>
|
<n-button
|
||||||
|
quaternary
|
||||||
|
v-if="props.showCodeButton"
|
||||||
|
@click="emits('showCode')"
|
||||||
|
>代码</n-button
|
||||||
|
>
|
||||||
<n-button quaternary v-if="props.submissionId" @click="copyLink">
|
<n-button quaternary v-if="props.submissionId" @click="copyLink">
|
||||||
链接
|
链接
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<span>加载历史记录…</span>
|
<span>加载历史记录…</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]">
|
<div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]">
|
||||||
<div class="message-role">{{ msg.role === 'user' ? '我' : 'AI' }}</div>
|
<div class="message-role">{{ msg.role === "user" ? "我" : "AI" }}</div>
|
||||||
<div class="message-content" v-html="renderContent(msg)"></div>
|
<div class="message-content" v-html="renderContent(msg)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="streaming" class="message assistant">
|
<div v-if="streaming" class="message assistant">
|
||||||
@@ -14,7 +14,11 @@
|
|||||||
<div v-if="!streamingContent" class="typing-indicator">
|
<div v-if="!streamingContent" class="typing-indicator">
|
||||||
<span></span><span></span><span></span>
|
<span></span><span></span><span></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="message-content" v-html="renderMarkdown(streamingContent)"></div>
|
<div
|
||||||
|
v-else
|
||||||
|
class="message-content"
|
||||||
|
v-html="renderMarkdown(streamingContent)"
|
||||||
|
></div>
|
||||||
<div class="streaming-hint">AI 正在思考中…</div>
|
<div class="streaming-hint">AI 正在思考中…</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +32,12 @@
|
|||||||
@keydown.enter.exact.prevent="send"
|
@keydown.enter.exact.prevent="send"
|
||||||
/>
|
/>
|
||||||
<n-flex justify="space-between" align="center" style="margin-top: 8px">
|
<n-flex justify="space-between" align="center" style="margin-top: 8px">
|
||||||
<n-button text size="small" @click="newConversation" :disabled="streaming">
|
<n-button
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
@click="newConversation"
|
||||||
|
:disabled="streaming"
|
||||||
|
>
|
||||||
新对话
|
新对话
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
@@ -69,13 +78,46 @@ function send() {
|
|||||||
const renderer = new Renderer()
|
const renderer = new Renderer()
|
||||||
renderer.code = function ({ lang }: { text: string; lang?: string }) {
|
renderer.code = function ({ lang }: { text: string; lang?: string }) {
|
||||||
const label = lang ? lang.toUpperCase() : "CODE"
|
const label = lang ? lang.toUpperCase() : "CODE"
|
||||||
const colors: Record<string, { bg: string; fg: string; dot: string; border: string; shimmer: string }> = {
|
const colors: Record<
|
||||||
html: { bg: "#fff5f0", fg: "#e05020", dot: "#e05020", border: "#f0d0c0", shimmer: "#fff5f0, #ffeee5, #fff5f0" },
|
string,
|
||||||
css: { bg: "#f0f0ff", fg: "#6060d0", dot: "#6060d0", border: "#d0d0f0", shimmer: "#f0f0ff, #e8e8fa, #f0f0ff" },
|
{ bg: string; fg: string; dot: string; border: string; shimmer: string }
|
||||||
js: { bg: "#fffbf0", fg: "#c0960a", dot: "#c0960a", border: "#f0e0b0", shimmer: "#fffbf0, #fff5e0, #fffbf0" },
|
> = {
|
||||||
javascript: { bg: "#fffbf0", fg: "#c0960a", dot: "#c0960a", border: "#f0e0b0", shimmer: "#fffbf0, #fff5e0, #fffbf0" },
|
html: {
|
||||||
|
bg: "#fff5f0",
|
||||||
|
fg: "#e05020",
|
||||||
|
dot: "#e05020",
|
||||||
|
border: "#f0d0c0",
|
||||||
|
shimmer: "#fff5f0, #ffeee5, #fff5f0",
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
bg: "#f0f0ff",
|
||||||
|
fg: "#6060d0",
|
||||||
|
dot: "#6060d0",
|
||||||
|
border: "#d0d0f0",
|
||||||
|
shimmer: "#f0f0ff, #e8e8fa, #f0f0ff",
|
||||||
|
},
|
||||||
|
js: {
|
||||||
|
bg: "#fffbf0",
|
||||||
|
fg: "#c0960a",
|
||||||
|
dot: "#c0960a",
|
||||||
|
border: "#f0e0b0",
|
||||||
|
shimmer: "#fffbf0, #fff5e0, #fffbf0",
|
||||||
|
},
|
||||||
|
javascript: {
|
||||||
|
bg: "#fffbf0",
|
||||||
|
fg: "#c0960a",
|
||||||
|
dot: "#c0960a",
|
||||||
|
border: "#f0e0b0",
|
||||||
|
shimmer: "#fffbf0, #fff5e0, #fffbf0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const c = colors[(lang ?? "").toLowerCase()] ?? {
|
||||||
|
bg: "#f0f7ff",
|
||||||
|
fg: "#2080f0",
|
||||||
|
dot: "#2080f0",
|
||||||
|
border: "#e0eaf5",
|
||||||
|
shimmer: "#f0f7ff, #e8f4f8, #f0f7ff",
|
||||||
}
|
}
|
||||||
const c = colors[(lang ?? "").toLowerCase()] ?? { bg: "#f0f7ff", fg: "#2080f0", dot: "#2080f0", border: "#e0eaf5", shimmer: "#f0f7ff, #e8f4f8, #f0f7ff" }
|
|
||||||
return `<div class="code-placeholder" style="background: linear-gradient(90deg, ${c.shimmer}); background-size: 200% 100%; border-color: ${c.border}"><span class="code-placeholder-dot" style="background: ${c.dot}"></span><span class="code-placeholder-label" style="color: ${c.fg}; background: ${c.fg}18">${label}</span><span class="code-placeholder-text">代码已自动应用到预览区</span></div>`
|
return `<div class="code-placeholder" style="background: linear-gradient(90deg, ${c.shimmer}); background-size: 200% 100%; border-color: ${c.border}"><span class="code-placeholder-dot" style="background: ${c.dot}"></span><span class="code-placeholder-label" style="color: ${c.fg}; background: ${c.fg}18">${label}</span><span class="code-placeholder-text">代码已自动应用到预览区</span></div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,16 +130,13 @@ function renderContent(msg: { role: string; content: string }): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scroll to bottom on new messages
|
// Auto-scroll to bottom on new messages
|
||||||
watch(
|
watch([() => messages.value.length, streamingContent], () => {
|
||||||
[() => messages.value.length, streamingContent],
|
nextTick(() => {
|
||||||
() => {
|
if (messagesRef.value) {
|
||||||
nextTick(() => {
|
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
||||||
if (messagesRef.value) {
|
}
|
||||||
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
})
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -180,13 +219,22 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { background-position: -200% 0; }
|
0% {
|
||||||
100% { background-position: 200% 0; }
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 0.4; }
|
0%,
|
||||||
50% { opacity: 1; }
|
100% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
@@ -212,8 +260,16 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
0%,
|
||||||
30% { transform: translateY(-6px); opacity: 1; }
|
60%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.streaming-hint {
|
.streaming-hint {
|
||||||
@@ -237,5 +293,4 @@ watch(
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -38,15 +38,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-button
|
<n-button v-if="roleSuper" text @click="statsModal = true">
|
||||||
v-if="(roleAdmin || roleSuper) && taskTab === TASK_TYPE.Challenge && taskId > 0"
|
|
||||||
text
|
|
||||||
@click="statsModal = true"
|
|
||||||
>
|
|
||||||
<Icon :width="16" icon="lucide:bar-chart-2"></Icon>
|
<Icon :width="16" icon="lucide:bar-chart-2"></Icon>
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="authed"
|
v-if="authed"
|
||||||
text
|
text
|
||||||
@click="$router.push({ name: 'submissions', params: { page: 1 } })"
|
@click="$router.push({ name: 'submissions', params: { page: 1 } })"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,24 +16,38 @@
|
|||||||
|
|
||||||
<template v-else-if="stats">
|
<template v-else-if="stats">
|
||||||
<!-- 班级筛选 -->
|
<!-- 班级筛选 -->
|
||||||
<n-flex align="center" style="margin-bottom: 16px; flex-wrap: wrap; gap: 6px">
|
<n-flex
|
||||||
|
align="center"
|
||||||
|
style="margin-bottom: 16px; flex-wrap: wrap; gap: 6px"
|
||||||
|
>
|
||||||
<span style="color: #666; font-size: 12px">班级筛选:</span>
|
<span style="color: #666; font-size: 12px">班级筛选:</span>
|
||||||
<n-button
|
<n-button
|
||||||
size="small"
|
size="small"
|
||||||
:type="selectedClass === null ? 'primary' : 'default'"
|
:type="selectedClass === null ? 'primary' : 'default'"
|
||||||
@click="selectClass(null)"
|
@click="selectClass(null)"
|
||||||
>全部</n-button>
|
>全部</n-button
|
||||||
|
>
|
||||||
<n-button
|
<n-button
|
||||||
v-for="c in stats.classes"
|
v-for="c in stats.classes"
|
||||||
:key="c"
|
:key="c"
|
||||||
size="small"
|
size="small"
|
||||||
:type="selectedClass === c ? 'primary' : 'default'"
|
:type="selectedClass === c ? 'primary' : 'default'"
|
||||||
@click="selectClass(c)"
|
@click="selectClass(c)"
|
||||||
>{{ c }}</n-button>
|
>{{ c }}</n-button
|
||||||
|
>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<!-- 关键指标 -->
|
<!-- 关键指标 -->
|
||||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); border: 1px solid #eee; border-radius: 6px; overflow: hidden; margin-bottom: 12px">
|
<div
|
||||||
|
style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(metric, i) in metrics"
|
v-for="(metric, i) in metrics"
|
||||||
:key="metric.label"
|
:key="metric.label"
|
||||||
@@ -43,139 +57,384 @@
|
|||||||
borderRight: i < metrics.length - 1 ? '1px solid #eee' : 'none',
|
borderRight: i < metrics.length - 1 ? '1px solid #eee' : 'none',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div :style="{ fontSize: '20px', fontWeight: '700', color: metric.color }">{{ metric.value }}</div>
|
<div
|
||||||
<div style="color: #888; font-size: 11px; margin-top: 2px">{{ metric.label }}</div>
|
:style="{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: metric.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ metric.value }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #888; font-size: 11px; margin-top: 2px">
|
||||||
|
{{ metric.label }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 未提交名单(可折叠) -->
|
<!-- 未提交名单(可折叠) -->
|
||||||
<div style="border: 1px solid #eee; border-radius: 6px; margin-bottom: 8px; overflow: hidden">
|
<div
|
||||||
|
style="
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style="padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; background: #fff8f8"
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff8f8;
|
||||||
|
"
|
||||||
@click="showUnsubmitted = !showUnsubmitted"
|
@click="showUnsubmitted = !showUnsubmitted"
|
||||||
>
|
>
|
||||||
<n-flex align="center" :size="6">
|
<n-flex align="center" :size="6">
|
||||||
<span style="width: 7px; height: 7px; background: #d03050; border-radius: 50%; display: inline-block"></span>
|
<span
|
||||||
<span style="font-weight: 600; color: #d03050; font-size: 12px">未提交({{ stats.unsubmitted_count }}人)</span>
|
style="
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background: #d03050;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
<span style="font-weight: 600; color: #d03050; font-size: 12px"
|
||||||
|
>未提交({{ stats.unsubmitted_count }}人)</span
|
||||||
|
>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<Icon :icon="showUnsubmitted ? 'lucide:chevron-down' : 'lucide:chevron-right'" :width="14" style="color: #aaa" />
|
<Icon
|
||||||
|
:icon="
|
||||||
|
showUnsubmitted ? 'lucide:chevron-down' : 'lucide:chevron-right'
|
||||||
|
"
|
||||||
|
:width="14"
|
||||||
|
style="color: #aaa"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showUnsubmitted" style="padding: 10px 14px; background: #fff8f8; display: flex; flex-wrap: wrap; gap: 5px">
|
<div
|
||||||
|
v-if="showUnsubmitted"
|
||||||
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #fff8f8;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-for="u in stats.unsubmitted_users"
|
v-for="u in stats.unsubmitted_users"
|
||||||
:key="u.username"
|
:key="u.username"
|
||||||
size="small"
|
size="small"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
style="border-color: #ffd0d0; background: #fff"
|
style="border-color: #ffd0d0; background: #fff"
|
||||||
>{{ u.username }} <span style="color: #bbb; margin-left: 4px">{{ u.classname }}</span></n-tag>
|
>{{ u.username }}
|
||||||
<span v-if="!stats.unsubmitted_users.length" style="color: #aaa; font-size: 12px">暂无</span>
|
<span style="color: #bbb; margin-left: 4px">{{
|
||||||
|
u.classname
|
||||||
|
}}</span></n-tag
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!stats.unsubmitted_users.length"
|
||||||
|
style="color: #aaa; font-size: 12px"
|
||||||
|
>暂无</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 未打分名单(可折叠) -->
|
<!-- 未打分名单(可折叠) -->
|
||||||
<div style="border: 1px solid #eee; border-radius: 6px; margin-bottom: 12px; overflow: hidden">
|
<div
|
||||||
|
style="
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style="padding: 10px 14px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; background: #fff8f8"
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff8f8;
|
||||||
|
"
|
||||||
@click="showUnrated = !showUnrated"
|
@click="showUnrated = !showUnrated"
|
||||||
>
|
>
|
||||||
<n-flex align="center" :size="6">
|
<n-flex align="center" :size="6">
|
||||||
<span style="width: 7px; height: 7px; background: #d03050; border-radius: 50%; display: inline-block"></span>
|
<span
|
||||||
<span style="font-weight: 600; color: #d03050; font-size: 12px">未打分({{ stats.unrated_count }}人)</span>
|
style="
|
||||||
<span style="font-size: 11px; color: #aaa">— 还没给任何提交打过分</span>
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background: #d03050;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
<span style="font-weight: 600; color: #d03050; font-size: 12px"
|
||||||
|
>未打分({{ stats.unrated_count }}人)</span
|
||||||
|
>
|
||||||
|
<span style="font-size: 11px; color: #aaa"
|
||||||
|
>— 还没给任何提交打过分</span
|
||||||
|
>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<Icon :icon="showUnrated ? 'lucide:chevron-down' : 'lucide:chevron-right'" :width="14" style="color: #aaa" />
|
<Icon
|
||||||
|
:icon="
|
||||||
|
showUnrated ? 'lucide:chevron-down' : 'lucide:chevron-right'
|
||||||
|
"
|
||||||
|
:width="14"
|
||||||
|
style="color: #aaa"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showUnrated" style="padding: 10px 14px; background: #fff8f8; display: flex; flex-wrap: wrap; gap: 5px">
|
<div
|
||||||
|
v-if="showUnrated"
|
||||||
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #fff8f8;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-for="u in stats.unrated_users"
|
v-for="u in stats.unrated_users"
|
||||||
:key="u.username"
|
:key="u.username"
|
||||||
size="small"
|
size="small"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
style="border-color: #ffd0d0; background: #fff"
|
style="border-color: #ffd0d0; background: #fff"
|
||||||
>{{ u.username }} <span style="color: #bbb; margin-left: 4px">{{ u.classname }}</span></n-tag>
|
>{{ u.username }}
|
||||||
<span v-if="!stats.unrated_users.length" style="color: #aaa; font-size: 12px">暂无</span>
|
<span style="color: #bbb; margin-left: 4px">{{
|
||||||
|
u.classname
|
||||||
|
}}</span></n-tag
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!stats.unrated_users.length"
|
||||||
|
style="color: #aaa; font-size: 12px"
|
||||||
|
>暂无</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提交次数分布 -->
|
<!-- 提交次数分布 -->
|
||||||
<div style="margin-bottom: 12px">
|
<div style="margin-bottom: 12px">
|
||||||
<div style="font-weight: 600; font-size: 13px; margin-bottom: 8px; color: #333">
|
<div
|
||||||
|
style="
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
"
|
||||||
|
>
|
||||||
提交次数分布
|
提交次数分布
|
||||||
<span style="font-size: 11px; color: #aaa; font-weight: 400">(已提交的 {{ stats.submitted_count }} 人)</span>
|
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
||||||
|
>(已提交的 {{ stats.submitted_count }} 人)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 8px">
|
<div style="display: flex; gap: 8px">
|
||||||
<div
|
<div
|
||||||
v-for="bucket in countBuckets"
|
v-for="bucket in countBuckets"
|
||||||
:key="bucket.label"
|
:key="bucket.label"
|
||||||
:style="{ flex: 1, border: `1px solid ${bucket.borderColor}`, borderRadius: '6px', padding: '10px 8px', textAlign: 'center', background: bucket.bg }"
|
:style="{
|
||||||
|
flex: 1,
|
||||||
|
border: `1px solid ${bucket.borderColor}`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '10px 8px',
|
||||||
|
textAlign: 'center',
|
||||||
|
background: bucket.bg,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div :style="{ fontSize: '20px', fontWeight: '700', color: bucket.color }">{{ bucket.value }}</div>
|
<div
|
||||||
<div style="color: #aaa; font-size: 11px; margin-top: 2px">{{ bucket.label }}</div>
|
:style="{
|
||||||
<div style="margin-top: 8px; background: #e8e8e8; border-radius: 3px; height: 4px">
|
fontSize: '20px',
|
||||||
<div :style="{ background: bucket.color, height: '4px', borderRadius: '3px', width: bucketPct(bucket.value) }"></div>
|
fontWeight: '700',
|
||||||
|
color: bucket.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ bucket.value }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #aaa; font-size: 11px; margin-top: 2px">
|
||||||
|
{{ bucket.label }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 4px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
background: bucket.color,
|
||||||
|
height: '4px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
width: bucketPct(bucket.value),
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div style="color: #bbb; font-size: 10px; margin-top: 3px">
|
||||||
|
{{ bucketPct(bucket.value) }}
|
||||||
</div>
|
</div>
|
||||||
<div style="color: #bbb; font-size: 10px; margin-top: 3px">{{ bucketPct(bucket.value) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分数分布 -->
|
<!-- 分数分布 -->
|
||||||
<div style="margin-bottom: 12px">
|
<div style="margin-bottom: 12px">
|
||||||
<div style="font-weight: 600; font-size: 13px; margin-bottom: 8px; color: #333">
|
<div
|
||||||
|
style="
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
"
|
||||||
|
>
|
||||||
分数分布
|
分数分布
|
||||||
<span style="font-size: 11px; color: #aaa; font-weight: 400">(1–5 星,已打分提交)</span>
|
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
||||||
|
>(1–5 星,已打分提交)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: flex-end; gap: 6px; height: 80px">
|
<div
|
||||||
|
style="display: flex; align-items: flex-end; gap: 6px; height: 80px"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="bar in scoreBars"
|
v-for="bar in scoreBars"
|
||||||
:key="bar.label"
|
:key="bar.label"
|
||||||
style="flex: 1; display: flex; flex-direction: column; align-items: center"
|
style="
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div style="color: #aaa; font-size: 11px; margin-bottom: 3px">{{ bar.value }}</div>
|
<div style="color: #aaa; font-size: 11px; margin-bottom: 3px">
|
||||||
<div :style="{ background: bar.color, width: '100%', height: bar.height, borderRadius: '3px 3px 0 0', minHeight: '3px' }"></div>
|
{{ bar.value }}
|
||||||
<div style="color: #aaa; font-size: 11px; margin-top: 4px">{{ bar.label }}</div>
|
</div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
background: bar.color,
|
||||||
|
width: '100%',
|
||||||
|
height: bar.height,
|
||||||
|
borderRadius: '3px 3px 0 0',
|
||||||
|
minHeight: '3px',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div style="color: #aaa; font-size: 11px; margin-top: 4px">
|
||||||
|
{{ bar.label }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 人气提交 Top 5 -->
|
<!-- 人气提交 Top 5 -->
|
||||||
<div style="margin-bottom: 12px">
|
<div style="margin-bottom: 12px">
|
||||||
<div style="font-weight: 600; font-size: 13px; margin-bottom: 8px; color: #333">
|
<div
|
||||||
|
style="
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
"
|
||||||
|
>
|
||||||
人气提交 Top 5
|
人气提交 Top 5
|
||||||
<span style="font-size: 11px; color: #aaa; font-weight: 400">(按打分人数)</span>
|
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
||||||
|
>(按打分人数)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 5px">
|
<div style="display: flex; flex-direction: column; gap: 5px">
|
||||||
<div
|
<div
|
||||||
v-for="(sub, i) in stats.top_submissions"
|
v-for="(sub, i) in stats.top_submissions"
|
||||||
:key="sub.submission_id"
|
:key="sub.submission_id"
|
||||||
style="display: flex; align-items: center; gap: 10px; padding: 6px 10px; background: #f8f8f8; border-radius: 6px; cursor: pointer"
|
style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
@click="viewSubmission(sub.submission_id)"
|
@click="viewSubmission(sub.submission_id)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{ width: '20px', height: '20px', background: rankColor(i), borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: '700', fontSize: '11px', flexShrink: 0 }"
|
:style="{
|
||||||
>{{ i + 1 }}</div>
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: rankColor(i),
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: '11px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ i + 1 }}
|
||||||
|
</div>
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<div style="font-weight: 500; font-size: 13px">{{ sub.username }}</div>
|
<div style="font-weight: 500; font-size: 13px">
|
||||||
<div style="color: #aaa; font-size: 11px">{{ sub.score.toFixed(1) }} 分 · {{ sub.rating_count }} 人打分 · {{ sub.classname }}</div>
|
{{ sub.username }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #aaa; font-size: 11px">
|
||||||
|
{{ sub.score.toFixed(1) }} 分 · {{ sub.rating_count }} 人打分
|
||||||
|
· {{ sub.classname }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="color: #2080f0; font-size: 12px">查看 →</div>
|
<div style="color: #2080f0; font-size: 12px">查看 →</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="!stats.top_submissions.length" style="color: #aaa; font-size: 12px">暂无打分记录</span>
|
<span
|
||||||
|
v-if="!stats.top_submissions.length"
|
||||||
|
style="color: #aaa; font-size: 12px"
|
||||||
|
>暂无打分记录</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标记统计 -->
|
<!-- 标记统计 -->
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600; font-size: 13px; margin-bottom: 8px; color: #333">标记统计</div>
|
<div
|
||||||
|
style="
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
标记统计
|
||||||
|
</div>
|
||||||
<n-flex :size="8" style="flex-wrap: wrap">
|
<n-flex :size="8" style="flex-wrap: wrap">
|
||||||
<div
|
<div
|
||||||
v-for="flag in flagBadges"
|
v-for="flag in flagBadges"
|
||||||
:key="flag.label"
|
:key="flag.label"
|
||||||
:style="{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 10px', background: flag.bg, borderRadius: '4px', border: `1px solid ${flag.border}` }"
|
:style="{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '4px 10px',
|
||||||
|
background: flag.bg,
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: `1px solid ${flag.border}`,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<span :style="{ width: '7px', height: '7px', background: flag.color, borderRadius: '50%', display: 'inline-block' }"></span>
|
<span
|
||||||
<span :style="{ color: flag.color, fontSize: '12px' }">{{ flag.label }}</span>
|
:style="{
|
||||||
<span style="font-weight: 700; font-size: 13px; color: #333">{{ flag.value }}</span>
|
width: '7px',
|
||||||
|
height: '7px',
|
||||||
|
background: flag.color,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'inline-block',
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span :style="{ color: flag.color, fontSize: '12px' }">{{
|
||||||
|
flag.label
|
||||||
|
}}</span>
|
||||||
|
<span style="font-weight: 700; font-size: 13px; color: #333">{{
|
||||||
|
flag.value
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +494,11 @@ const metrics = computed(() => {
|
|||||||
return [
|
return [
|
||||||
{ label: "已提交", value: stats.value.submitted_count, color: "#18a058" },
|
{ label: "已提交", value: stats.value.submitted_count, color: "#18a058" },
|
||||||
{ label: "未提交", value: stats.value.unsubmitted_count, color: "#d03050" },
|
{ label: "未提交", value: stats.value.unsubmitted_count, color: "#d03050" },
|
||||||
{ label: "平均分", value: stats.value.average_score?.toFixed(1) ?? "—", color: "#2080f0" },
|
{
|
||||||
|
label: "平均分",
|
||||||
|
value: stats.value.average_score?.toFixed(1) ?? "—",
|
||||||
|
color: "#2080f0",
|
||||||
|
},
|
||||||
{ label: "未打分", value: stats.value.unrated_count, color: "#d03050" },
|
{ label: "未打分", value: stats.value.unrated_count, color: "#d03050" },
|
||||||
{ label: "参与排名", value: stats.value.nominated_count, color: "#f0a020" },
|
{ label: "参与排名", value: stats.value.nominated_count, color: "#f0a020" },
|
||||||
]
|
]
|
||||||
@@ -245,10 +508,34 @@ const countBuckets = computed(() => {
|
|||||||
if (!stats.value) return []
|
if (!stats.value) return []
|
||||||
const d = stats.value.submission_count_distribution
|
const d = stats.value.submission_count_distribution
|
||||||
return [
|
return [
|
||||||
{ label: "仅 1 次", value: d.count_1, color: "#888", bg: "#fafafa", borderColor: "#e0e0e0" },
|
{
|
||||||
{ label: "2 次", value: d.count_2, color: "#2080f0", bg: "#f0f7ff", borderColor: "#d0e8ff" },
|
label: "仅 1 次",
|
||||||
{ label: "3 次", value: d.count_3, color: "#18a058", bg: "#f0fff4", borderColor: "#c8e8d0" },
|
value: d.count_1,
|
||||||
{ label: "4 次+", value: d.count_4_plus, color: "#f0a020", bg: "#fffbf0", borderColor: "#ffe0a0" },
|
color: "#888",
|
||||||
|
bg: "#fafafa",
|
||||||
|
borderColor: "#e0e0e0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "2 次",
|
||||||
|
value: d.count_2,
|
||||||
|
color: "#2080f0",
|
||||||
|
bg: "#f0f7ff",
|
||||||
|
borderColor: "#d0e8ff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "3 次",
|
||||||
|
value: d.count_3,
|
||||||
|
color: "#18a058",
|
||||||
|
bg: "#f0fff4",
|
||||||
|
borderColor: "#c8e8d0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "4 次+",
|
||||||
|
value: d.count_4_plus,
|
||||||
|
color: "#f0a020",
|
||||||
|
bg: "#fffbf0",
|
||||||
|
borderColor: "#ffe0a0",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -271,10 +558,34 @@ const flagBadges = computed(() => {
|
|||||||
if (!stats.value) return []
|
if (!stats.value) return []
|
||||||
const f = stats.value.flag_stats
|
const f = stats.value.flag_stats
|
||||||
return [
|
return [
|
||||||
{ label: "值得展示", value: f.red, color: "#d03050", bg: "#fff0f0", border: "#ffd0d0" },
|
{
|
||||||
{ label: "需要讲解", value: f.blue, color: "#2080f0", bg: "#f0f7ff", border: "#c8deff" },
|
label: "值得展示",
|
||||||
{ label: "优秀作品", value: f.green, color: "#18a058", bg: "#f0fff4", border: "#b8e8c8" },
|
value: f.red,
|
||||||
{ label: "需要改进", value: f.yellow, color: "#f0a020", bg: "#fffbf0", border: "#ffe8a0" },
|
color: "#d03050",
|
||||||
|
bg: "#fff0f0",
|
||||||
|
border: "#ffd0d0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "需要讲解",
|
||||||
|
value: f.blue,
|
||||||
|
color: "#2080f0",
|
||||||
|
bg: "#f0f7ff",
|
||||||
|
border: "#c8deff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "优秀作品",
|
||||||
|
value: f.green,
|
||||||
|
color: "#18a058",
|
||||||
|
bg: "#f0fff4",
|
||||||
|
border: "#b8e8c8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "需要改进",
|
||||||
|
value: f.yellow,
|
||||||
|
color: "#f0a020",
|
||||||
|
bg: "#fffbf0",
|
||||||
|
border: "#ffe8a0",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ function flash(btn: HTMLButtonElement, done: string, original: string) {
|
|||||||
|
|
||||||
function setupCodeActions() {
|
function setupCodeActions() {
|
||||||
$content.value?.addEventListener("click", (e: MouseEvent) => {
|
$content.value?.addEventListener("click", (e: MouseEvent) => {
|
||||||
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>("[data-action]")
|
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(
|
||||||
|
"[data-action]",
|
||||||
|
)
|
||||||
if (!btn) return
|
if (!btn) return
|
||||||
const wrapper = btn.closest<HTMLElement>("[data-lang]")!
|
const wrapper = btn.closest<HTMLElement>("[data-lang]")!
|
||||||
const lang = wrapper.dataset.lang ?? "html"
|
const lang = wrapper.dataset.lang ?? "html"
|
||||||
|
|||||||
@@ -7,10 +7,18 @@
|
|||||||
@update:show="$emit('update:show', $event)"
|
@update:show="$emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-empty v-if="!loading && rounds.length === 0" description="暂无对话记录" />
|
<n-empty
|
||||||
|
v-if="!loading && rounds.length === 0"
|
||||||
|
description="暂无对话记录"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; height: 75vh"
|
style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
height: 75vh;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style="
|
style="
|
||||||
@@ -25,26 +33,46 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(round, index) in rounds"
|
v-for="(round, index) in rounds"
|
||||||
:key="index"
|
:key="index"
|
||||||
style="display: flex; gap: 10px; align-items: flex-start; cursor: pointer"
|
style="
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
@click="selectedRound = index"
|
@click="selectedRound = index"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
flexShrink: 0, width: '22px', height: '22px', borderRadius: '50%',
|
flexShrink: 0,
|
||||||
|
width: '22px',
|
||||||
|
height: '22px',
|
||||||
|
borderRadius: '50%',
|
||||||
background: selectedRound === index ? '#2080f0' : '#c2d5fb',
|
background: selectedRound === index ? '#2080f0' : '#c2d5fb',
|
||||||
color: '#fff', fontSize: '12px', fontWeight: 'bold',
|
color: '#fff',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
fontSize: '12px',
|
||||||
marginTop: '2px', transition: 'background 0.2s',
|
fontWeight: 'bold',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: '2px',
|
||||||
|
transition: 'background 0.2s',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
flex: 1, padding: '10px 14px', borderRadius: '8px',
|
flex: 1,
|
||||||
|
padding: '10px 14px',
|
||||||
|
borderRadius: '8px',
|
||||||
background: selectedRound === index ? '#e8f0fe' : '#f5f5f5',
|
background: selectedRound === index ? '#e8f0fe' : '#f5f5f5',
|
||||||
border: selectedRound === index ? '1px solid #2080f0' : '1px solid #e0e0e0',
|
border:
|
||||||
fontSize: '13px', lineHeight: '1.6', transition: 'all 0.2s',
|
selectedRound === index
|
||||||
|
? '1px solid #2080f0'
|
||||||
|
: '1px solid #e0e0e0',
|
||||||
|
fontSize: '13px',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
transition: 'all 0.2s',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ round.question }}
|
{{ round.question }}
|
||||||
@@ -60,7 +88,12 @@
|
|||||||
:srcdoc="selectedPageHtml"
|
:srcdoc="selectedPageHtml"
|
||||||
:key="selectedRound"
|
:key="selectedRound"
|
||||||
sandbox="allow-scripts"
|
sandbox="allow-scripts"
|
||||||
style="flex: 1; border: 1px solid #e0e0e0; border-radius: 6px; background: #fff"
|
style="
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<n-empty v-else description="该轮无网页代码" style="margin: auto" />
|
<n-empty v-else description="该轮无网页代码" style="margin: auto" />
|
||||||
</div>
|
</div>
|
||||||
@@ -86,10 +119,17 @@ const messages = ref<PromptMessage[]>([])
|
|||||||
const selectedRound = ref(0)
|
const selectedRound = ref(0)
|
||||||
|
|
||||||
const rounds = computed(() => {
|
const rounds = computed(() => {
|
||||||
const result: { question: string; html: string | null; css: string | null; js: string | null }[] = []
|
const result: {
|
||||||
|
question: string
|
||||||
|
html: string | null
|
||||||
|
css: string | null
|
||||||
|
js: string | null
|
||||||
|
}[] = []
|
||||||
for (const [i, msg] of messages.value.entries()) {
|
for (const [i, msg] of messages.value.entries()) {
|
||||||
if (msg.role !== "user") continue
|
if (msg.role !== "user") continue
|
||||||
let html: string | null = null, css: string | null = null, js: string | null = null
|
let html: string | null = null,
|
||||||
|
css: string | null = null,
|
||||||
|
js: string | null = null
|
||||||
for (const reply of messages.value.slice(i + 1)) {
|
for (const reply of messages.value.slice(i + 1)) {
|
||||||
if (reply.role === "user") break
|
if (reply.role === "user") break
|
||||||
if (reply.role === "assistant" && reply.code_html) {
|
if (reply.role === "assistant" && reply.code_html) {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal preset="card" :show="show" style="max-width: 60%" @update:show="$emit('update:show', $event)">
|
<n-modal
|
||||||
|
preset="card"
|
||||||
|
:show="show"
|
||||||
|
style="max-width: 60%"
|
||||||
|
@update:show="$emit('update:show', $event)"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<span>前端代码</span>
|
<span>前端代码</span>
|
||||||
<n-button tertiary @click="$emit('copy-to-editor')">复制到编辑框</n-button>
|
<n-button tertiary @click="$emit('copy-to-editor')"
|
||||||
|
>复制到编辑框</n-button
|
||||||
|
>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
<n-tabs animated type="segment">
|
<n-tabs animated type="segment">
|
||||||
|
|||||||
@@ -14,7 +14,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h } from "vue"
|
import { computed, h } from "vue"
|
||||||
import { NButton, NDataTable, NPopconfirm, NSpin, type DataTableColumn } from "naive-ui"
|
import {
|
||||||
|
NButton,
|
||||||
|
NDataTable,
|
||||||
|
NPopconfirm,
|
||||||
|
NSpin,
|
||||||
|
type DataTableColumn,
|
||||||
|
} from "naive-ui"
|
||||||
import type { SubmissionOut } from "../../utils/type"
|
import type { SubmissionOut } from "../../utils/type"
|
||||||
import { TASK_TYPE } from "../../utils/const"
|
import { TASK_TYPE } from "../../utils/const"
|
||||||
import { parseTime } from "../../utils/helper"
|
import { parseTime } from "../../utils/helper"
|
||||||
@@ -61,10 +67,14 @@ const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
|
|||||||
render: (r) => {
|
render: (r) => {
|
||||||
const myScore = r.my_score > 0 ? String(r.my_score) : "-"
|
const myScore = r.my_score > 0 ? String(r.my_score) : "-"
|
||||||
const avgScore = r.score > 0 ? r.score.toFixed(2) : "-"
|
const avgScore = r.score > 0 ? r.score.toFixed(2) : "-"
|
||||||
return h("div", { style: { display: "flex", gap: "6px", alignItems: "baseline" } }, [
|
return h(
|
||||||
h("span", avgScore),
|
"div",
|
||||||
h("span", { style: { fontSize: "11px", color: "#999" } }, myScore),
|
{ style: { display: "flex", gap: "6px", alignItems: "baseline" } },
|
||||||
])
|
[
|
||||||
|
h("span", avgScore),
|
||||||
|
h("span", { style: { fontSize: "11px", color: "#999" } }, myScore),
|
||||||
|
],
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,55 +83,77 @@ const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
|
|||||||
width: 60,
|
width: 60,
|
||||||
render: (r: SubmissionOut) => {
|
render: (r: SubmissionOut) => {
|
||||||
if (r.username !== user.username) {
|
if (r.username !== user.username) {
|
||||||
return r.nominated ? h("span", { style: { color: "#f0a020" } }, "🏅") : null
|
return r.nominated
|
||||||
|
? h("span", { style: { color: "#f0a020" } }, "🏅")
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
return h(
|
return h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
text: true,
|
||||||
title: r.nominated ? "已参与排名(点击可重新提名)" : "参与排名",
|
title: r.nominated ? "已参与排名(点击可重新提名)" : "参与排名",
|
||||||
onClick: (e: Event) => { e.stopPropagation(); emit("nominate", r) },
|
onClick: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
emit("nominate", r)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
() => (r.nominated ? "🏅" : "☆"),
|
() => (r.nominated ? "🏅" : "☆"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(isChallenge.value
|
...(isChallenge.value
|
||||||
? [{
|
? [
|
||||||
title: "提示词",
|
{
|
||||||
key: "conversation_id",
|
title: "提示词",
|
||||||
width: 70,
|
key: "conversation_id",
|
||||||
render: (r: SubmissionOut) => {
|
width: 70,
|
||||||
if (!r.conversation_id) return "-"
|
render: (r: SubmissionOut) => {
|
||||||
return h(
|
if (!r.conversation_id) return "-"
|
||||||
NButton,
|
return h(
|
||||||
{ text: true, type: "primary", onClick: (e: Event) => { e.stopPropagation(); emit("show-chain", r.conversation_id!) } },
|
NButton,
|
||||||
() => "查看",
|
{
|
||||||
)
|
text: true,
|
||||||
},
|
type: "primary",
|
||||||
} as DataTableColumn<SubmissionOut>]
|
onClick: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
emit("show-chain", r.conversation_id!)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() => "查看",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
} as DataTableColumn<SubmissionOut>,
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
...(!isChallenge.value
|
...(!isChallenge.value
|
||||||
? [{
|
? [
|
||||||
title: "操作",
|
{
|
||||||
key: "actions",
|
title: "操作",
|
||||||
width: 60,
|
key: "actions",
|
||||||
render: (r: SubmissionOut) => {
|
width: 60,
|
||||||
if (r.username !== user.username) return null
|
render: (r: SubmissionOut) => {
|
||||||
return h(
|
if (r.username !== user.username) return null
|
||||||
NPopconfirm,
|
return h(
|
||||||
{ onPositiveClick: () => emit("delete", r, props.row.id) },
|
NPopconfirm,
|
||||||
{
|
{ onPositiveClick: () => emit("delete", r, props.row.id) },
|
||||||
trigger: () => h(
|
{
|
||||||
NButton,
|
trigger: () =>
|
||||||
{ text: true, type: "error", size: "small", onClick: (e: Event) => e.stopPropagation() },
|
h(
|
||||||
() => "删除",
|
NButton,
|
||||||
),
|
{
|
||||||
default: () => "确定删除这次提交?",
|
text: true,
|
||||||
},
|
type: "error",
|
||||||
)
|
size: "small",
|
||||||
},
|
onClick: (e: Event) => e.stopPropagation(),
|
||||||
} as DataTableColumn<SubmissionOut>]
|
},
|
||||||
|
() => "删除",
|
||||||
|
),
|
||||||
|
default: () => "确定删除这次提交?",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
} as DataTableColumn<SubmissionOut>,
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,14 +13,23 @@
|
|||||||
<span style="display: flex; align-items: center; gap: 6px">
|
<span style="display: flex; align-items: center; gap: 6px">
|
||||||
<span
|
<span
|
||||||
:style="{
|
:style="{
|
||||||
display: 'inline-block', width: '10px', height: '10px',
|
display: 'inline-block',
|
||||||
borderRadius: '50%', backgroundColor: opt.color,
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: opt.color,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</span>
|
</span>
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="flag" text block type="error" @click="$emit('update:flag', null)">
|
<n-button
|
||||||
|
v-if="flag"
|
||||||
|
text
|
||||||
|
block
|
||||||
|
type="error"
|
||||||
|
@click="$emit('update:flag', null)"
|
||||||
|
>
|
||||||
清除
|
清除
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -32,7 +41,11 @@
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import type { FlagType } from "../../utils/type"
|
import type { FlagType } from "../../utils/type"
|
||||||
|
|
||||||
const FLAG_OPTIONS: { value: NonNullable<FlagType>; color: string; label: string }[] = [
|
const FLAG_OPTIONS: {
|
||||||
|
value: NonNullable<FlagType>
|
||||||
|
color: string
|
||||||
|
label: string
|
||||||
|
}[] = [
|
||||||
{ value: "red", color: "#e03030", label: "值得展示" },
|
{ value: "red", color: "#e03030", label: "值得展示" },
|
||||||
{ value: "blue", color: "#2080f0", label: "需要讲解" },
|
{ value: "blue", color: "#2080f0", label: "需要讲解" },
|
||||||
{ value: "green", color: "#18a058", label: "优秀作品" },
|
{ value: "green", color: "#18a058", label: "优秀作品" },
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<n-tab-pane name="desc" tab="挑战描述" display-directive="show">
|
<n-tab-pane name="desc" tab="挑战描述" display-directive="show">
|
||||||
<div class="markdown-body" style="padding: 12px; overflow-y: auto; height: 100%" v-html="challengeContent" />
|
<div
|
||||||
|
class="markdown-body"
|
||||||
|
style="padding: 12px; overflow-y: auto; height: 100%"
|
||||||
|
v-html="challengeContent"
|
||||||
|
/>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
||||||
<PromptPanel />
|
<PromptPanel />
|
||||||
@@ -19,11 +23,24 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #2>
|
<template #2>
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<Preview :html="html" :css="css" :js="js" show-code-button clearable @showCode="showCode = true" @clear="clearAll" />
|
<Preview
|
||||||
|
:html="html"
|
||||||
|
:css="css"
|
||||||
|
:js="js"
|
||||||
|
show-code-button
|
||||||
|
clearable
|
||||||
|
@showCode="showCode = true"
|
||||||
|
@clear="clearAll"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-split>
|
</n-split>
|
||||||
<n-modal v-model:show="showCode" preset="card" title="代码" style="width: 700px">
|
<n-modal
|
||||||
|
v-model:show="showCode"
|
||||||
|
preset="card"
|
||||||
|
title="代码"
|
||||||
|
style="width: 700px"
|
||||||
|
>
|
||||||
<n-tabs type="line">
|
<n-tabs type="line">
|
||||||
<n-tab-pane name="html" tab="HTML">
|
<n-tab-pane name="html" tab="HTML">
|
||||||
<n-code :code="html" language="html" />
|
<n-code :code="html" language="html" />
|
||||||
@@ -49,7 +66,14 @@ import Preview from "../components/Preview.vue"
|
|||||||
import { Challenge, Submission } from "../api"
|
import { Challenge, Submission } from "../api"
|
||||||
import { html, css, js } from "../store/editors"
|
import { html, css, js } from "../store/editors"
|
||||||
import { taskId } from "../store/task"
|
import { taskId } from "../store/task"
|
||||||
import { connectPrompt, disconnectPrompt, conversationId, streaming, setOnCodeComplete, loadHistory } from "../store/prompt"
|
import {
|
||||||
|
connectPrompt,
|
||||||
|
disconnectPrompt,
|
||||||
|
conversationId,
|
||||||
|
streaming,
|
||||||
|
setOnCodeComplete,
|
||||||
|
loadHistory,
|
||||||
|
} from "../store/prompt"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -71,7 +95,7 @@ async function loadChallenge() {
|
|||||||
taskId.value = data.task_ptr
|
taskId.value = data.task_ptr
|
||||||
challengeTitle.value = `#${data.display} ${data.title}`
|
challengeTitle.value = `#${data.display} ${data.title}`
|
||||||
challengeContent.value = await marked.parse(data.content, { async: true })
|
challengeContent.value = await marked.parse(data.content, { async: true })
|
||||||
loadHistory(data.task_ptr) // HTTP preload — async, non-blocking
|
loadHistory(data.task_ptr) // HTTP preload — async, non-blocking
|
||||||
connectPrompt(data.task_ptr) // WebSocket — synchronous open
|
connectPrompt(data.task_ptr) // WebSocket — synchronous open
|
||||||
setOnCodeComplete(async (code) => {
|
setOnCodeComplete(async (code) => {
|
||||||
if (!conversationId.value) return
|
if (!conversationId.value) return
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
:max="0.8"
|
:max="0.8"
|
||||||
>
|
>
|
||||||
<template #1>
|
<template #1>
|
||||||
<n-flex vertical style="height: 100%; padding-right: 10px; overflow: hidden">
|
<n-flex
|
||||||
|
vertical
|
||||||
|
style="height: 100%; padding-right: 10px; overflow: hidden"
|
||||||
|
>
|
||||||
<n-flex justify="space-between" style="flex-shrink: 0">
|
<n-flex justify="space-between" style="flex-shrink: 0">
|
||||||
<n-button secondary @click="() => goHome($router, taskTab, step)">
|
<n-button secondary @click="() => goHome($router, taskTab, step)">
|
||||||
返回首页
|
返回首页
|
||||||
@@ -21,14 +24,23 @@
|
|||||||
:options="flagFilterOptions"
|
:options="flagFilterOptions"
|
||||||
@update:value="handleFlagSelect"
|
@update:value="handleFlagSelect"
|
||||||
/>
|
/>
|
||||||
<n-input style="width: 120px" v-model:value="query.username" clearable />
|
<n-input
|
||||||
|
style="width: 120px"
|
||||||
|
v-model:value="query.username"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
<n-pagination
|
<n-pagination
|
||||||
v-model:page="query.page"
|
v-model:page="query.page"
|
||||||
:page-size="10"
|
:page-size="10"
|
||||||
:item-count="count"
|
:item-count="count"
|
||||||
simple
|
simple
|
||||||
/>
|
/>
|
||||||
<n-button secondary style="padding: 0 10px" title="刷新" @click="init">
|
<n-button
|
||||||
|
secondary
|
||||||
|
style="padding: 0 10px"
|
||||||
|
title="刷新"
|
||||||
|
@click="init"
|
||||||
|
>
|
||||||
<Icon :width="16" icon="lucide:refresh-cw" />
|
<Icon :width="16" icon="lucide:refresh-cw" />
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
@@ -109,7 +121,9 @@ const data = ref<SubmissionOut[]>([])
|
|||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
page: Number(route.params.page),
|
page: Number(route.params.page),
|
||||||
username: (Array.isArray(route.query.username) ? "" : (route.query.username ?? "")) as string,
|
username: (Array.isArray(route.query.username)
|
||||||
|
? ""
|
||||||
|
: (route.query.username ?? "")) as string,
|
||||||
flag: null as string | null,
|
flag: null as string | null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -145,7 +159,10 @@ const flagFilterOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function handleFlagSelect(value: string | null) {
|
function handleFlagSelect(value: string | null) {
|
||||||
if (value === "_clear_all") { clearAllFlags(); return }
|
if (value === "_clear_all") {
|
||||||
|
clearAllFlags()
|
||||||
|
return
|
||||||
|
}
|
||||||
query.flag = value
|
query.flag = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,10 +233,14 @@ const columns: DataTableColumn<SubmissionOut>[] = [
|
|||||||
render: (row) => {
|
render: (row) => {
|
||||||
const myScore = row.my_score > 0 ? String(row.my_score) : "-"
|
const myScore = row.my_score > 0 ? String(row.my_score) : "-"
|
||||||
const avgScore = row.score > 0 ? row.score.toFixed(2) : "-"
|
const avgScore = row.score > 0 ? row.score.toFixed(2) : "-"
|
||||||
return h("div", { style: { display: "flex", gap: "6px", alignItems: "baseline" } }, [
|
return h(
|
||||||
h("span", avgScore),
|
"div",
|
||||||
h("span", { style: { fontSize: "11px", color: "#999" } }, myScore),
|
{ style: { display: "flex", gap: "6px", alignItems: "baseline" } },
|
||||||
])
|
[
|
||||||
|
h("span", avgScore),
|
||||||
|
h("span", { style: { fontSize: "11px", color: "#999" } }, myScore),
|
||||||
|
],
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -251,7 +272,11 @@ async function handleExpand(keys: (string | number)[]) {
|
|||||||
async function handleDelete(row: SubmissionOut, parentId: string) {
|
async function handleDelete(row: SubmissionOut, parentId: string) {
|
||||||
await Submission.delete(row.id)
|
await Submission.delete(row.id)
|
||||||
const items = expandedData.get(parentId)
|
const items = expandedData.get(parentId)
|
||||||
if (items) expandedData.set(parentId, items.filter((d) => d.id !== row.id))
|
if (items)
|
||||||
|
expandedData.set(
|
||||||
|
parentId,
|
||||||
|
items.filter((d) => d.id !== row.id),
|
||||||
|
)
|
||||||
if (submission.value.id === row.id) submission.value.id = ""
|
if (submission.value.id === row.id) submission.value.id = ""
|
||||||
const res = await Submission.list(query)
|
const res = await Submission.list(query)
|
||||||
data.value = res.items
|
data.value = res.items
|
||||||
@@ -292,7 +317,10 @@ async function handleNominateChild(row: SubmissionOut, parentId: string) {
|
|||||||
await Submission.nominate(row.id)
|
await Submission.nominate(row.id)
|
||||||
const items = expandedData.get(parentId)
|
const items = expandedData.get(parentId)
|
||||||
if (items) {
|
if (items) {
|
||||||
expandedData.set(parentId, items.map((d) => ({ ...d, nominated: d.id === row.id })))
|
expandedData.set(
|
||||||
|
parentId,
|
||||||
|
items.map((d) => ({ ...d, nominated: d.id === row.id })),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
data.value = data.value.map((d) => {
|
data.value = data.value.map((d) => {
|
||||||
if (d.username === user.username && d.task_id === row.task_id) {
|
if (d.username === user.username && d.task_id === row.task_id) {
|
||||||
@@ -316,18 +344,46 @@ function copyToEditor() {
|
|||||||
goHome(router, submission.value.task_type, submission.value.task_display)
|
goHome(router, submission.value.task_type, submission.value.task_display)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => query.page, (v) => { init(); router.push({ params: { page: v } }) })
|
watch(
|
||||||
watchDebounced(() => query.username, () => { query.page = 1; init() }, { debounce: 500, maxWait: 1000 })
|
() => query.page,
|
||||||
watch(() => query.flag, () => { query.page = 1; init() })
|
(v) => {
|
||||||
|
init()
|
||||||
|
router.push({ params: { page: v } })
|
||||||
|
},
|
||||||
|
)
|
||||||
|
watchDebounced(
|
||||||
|
() => query.username,
|
||||||
|
() => {
|
||||||
|
query.page = 1
|
||||||
|
init()
|
||||||
|
},
|
||||||
|
{ debounce: 500, maxWait: 1000 },
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => query.flag,
|
||||||
|
() => {
|
||||||
|
query.page = 1
|
||||||
|
init()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
submission.value = {
|
submission.value = {
|
||||||
id: "", userid: 0, username: "",
|
id: "",
|
||||||
task_id: 0, task_display: 0, task_title: "",
|
userid: 0,
|
||||||
|
username: "",
|
||||||
|
task_id: 0,
|
||||||
|
task_display: 0,
|
||||||
|
task_title: "",
|
||||||
task_type: TASK_TYPE.Tutorial,
|
task_type: TASK_TYPE.Tutorial,
|
||||||
score: 0, my_score: 0, html: "", css: "", js: "",
|
score: 0,
|
||||||
created: new Date(), modified: new Date(),
|
my_score: 0,
|
||||||
|
html: "",
|
||||||
|
css: "",
|
||||||
|
js: "",
|
||||||
|
created: new Date(),
|
||||||
|
modified: new Date(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ export const streaming = ref(false)
|
|||||||
export const historyLoading = ref(false)
|
export const historyLoading = ref(false)
|
||||||
let _historyLoadId = 0
|
let _historyLoadId = 0
|
||||||
export const streamingContent = ref("")
|
export const streamingContent = ref("")
|
||||||
let _onCodeComplete: ((code: { html: string | null; css: string | null; js: string | null }) => void) | null = null
|
let _onCodeComplete:
|
||||||
|
| ((code: {
|
||||||
|
html: string | null
|
||||||
|
css: string | null
|
||||||
|
js: string | null
|
||||||
|
}) => void)
|
||||||
|
| null = null
|
||||||
|
|
||||||
export function setOnCodeComplete(fn: typeof _onCodeComplete) {
|
export function setOnCodeComplete(fn: typeof _onCodeComplete) {
|
||||||
_onCodeComplete = fn
|
_onCodeComplete = fn
|
||||||
@@ -90,8 +96,8 @@ export function connectPrompt(taskId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function disconnectPrompt() {
|
export function disconnectPrompt() {
|
||||||
_historyLoadId++ // cancel any in-flight loadHistory
|
_historyLoadId++ // cancel any in-flight loadHistory
|
||||||
historyLoading.value = false // reset here; finally block won't (loadId mismatch)
|
historyLoading.value = false // reset here; finally block won't (loadId mismatch)
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close()
|
ws.close()
|
||||||
ws = null
|
ws = null
|
||||||
@@ -109,17 +115,27 @@ export async function loadHistory(taskId: number) {
|
|||||||
historyLoading.value = true
|
historyLoading.value = true
|
||||||
try {
|
try {
|
||||||
const convs = await Prompt.listConversations(taskId)
|
const convs = await Prompt.listConversations(taskId)
|
||||||
console.log("[loadHistory] convs:", convs.map((c: any) => ({ id: c.id, is_active: c.is_active, message_count: c.message_count, username: c.username })), "user.username:", user.username)
|
console.log(
|
||||||
if (loadId !== _historyLoadId) return // navigated away, abort
|
"[loadHistory] convs:",
|
||||||
|
convs.map((c: any) => ({
|
||||||
|
id: c.id,
|
||||||
|
is_active: c.is_active,
|
||||||
|
message_count: c.message_count,
|
||||||
|
username: c.username,
|
||||||
|
})),
|
||||||
|
"user.username:",
|
||||||
|
user.username,
|
||||||
|
)
|
||||||
|
if (loadId !== _historyLoadId) return // navigated away, abort
|
||||||
const active = convs.find(
|
const active = convs.find(
|
||||||
(c: { is_active: boolean; message_count: number; username: string }) =>
|
(c: { is_active: boolean; message_count: number; username: string }) =>
|
||||||
c.is_active && c.message_count > 0 && c.username === user.username
|
c.is_active && c.message_count > 0 && c.username === user.username,
|
||||||
)
|
)
|
||||||
console.log("[loadHistory] active:", active)
|
console.log("[loadHistory] active:", active)
|
||||||
if (!active) return
|
if (!active) return
|
||||||
const raw: RawMessage[] = await Prompt.getMessages(active.id)
|
const raw: RawMessage[] = await Prompt.getMessages(active.id)
|
||||||
console.log("[loadHistory] raw messages:", raw.length)
|
console.log("[loadHistory] raw messages:", raw.length)
|
||||||
if (loadId !== _historyLoadId) return // navigated away, abort
|
if (loadId !== _historyLoadId) return // navigated away, abort
|
||||||
// Only apply if nothing has arrived via WebSocket yet
|
// Only apply if nothing has arrived via WebSocket yet
|
||||||
if (messages.value.length > 0) return
|
if (messages.value.length > 0) return
|
||||||
conversationId.value = active.id
|
conversationId.value = active.id
|
||||||
@@ -157,7 +173,11 @@ export function newConversation() {
|
|||||||
ws.send(JSON.stringify({ type: "new_conversation" }))
|
ws.send(JSON.stringify({ type: "new_conversation" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCode(code: { html: string | null; css: string | null; js: string | null }) {
|
function applyCode(code: {
|
||||||
|
html: string | null
|
||||||
|
css: string | null
|
||||||
|
js: string | null
|
||||||
|
}) {
|
||||||
if (code.html !== null) html.value = code.html
|
if (code.html !== null) html.value = code.html
|
||||||
if (code.css !== null) css.value = code.css
|
if (code.css !== null) css.value = code.css
|
||||||
if (code.js !== null) js.value = code.js
|
if (code.js !== null) js.value = code.js
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export const ADMIN_URL = import.meta.env.PUBLIC_ADMIN_URL
|
|||||||
|
|
||||||
export const BASE_URL = import.meta.env.PUBLIC_BASE_URL
|
export const BASE_URL = import.meta.env.PUBLIC_BASE_URL
|
||||||
|
|
||||||
export const WS_BASE_URL = import.meta.env.PUBLIC_WS_URL || `ws://${window.location.host}`
|
export const WS_BASE_URL =
|
||||||
|
import.meta.env.PUBLIC_WS_URL || `ws://${window.location.host}`
|
||||||
|
|
||||||
export enum TASK_TYPE {
|
export enum TASK_TYPE {
|
||||||
Tutorial = "tutorial",
|
Tutorial = "tutorial",
|
||||||
|
|||||||
Reference in New Issue
Block a user