Compare commits
5 Commits
83537face1
...
ecce21aaaf
| Author | SHA1 | Date | |
|---|---|---|---|
| ecce21aaaf | |||
| 951e53c1dd | |||
| f26b06877c | |||
| 1744c405a5 | |||
| 5f95b88914 |
@@ -181,7 +181,6 @@ export const Submission = {
|
|||||||
score_min?: number
|
score_min?: number
|
||||||
score_max_exclusive?: number
|
score_max_exclusive?: number
|
||||||
score_lt_threshold?: number
|
score_lt_threshold?: number
|
||||||
nominated?: boolean
|
|
||||||
ordering?: string
|
ordering?: string
|
||||||
grouped?: boolean
|
grouped?: boolean
|
||||||
}) {
|
}) {
|
||||||
@@ -223,11 +222,6 @@ export const Submission = {
|
|||||||
return res.data as { cleared: number }
|
return res.data as { cleared: number }
|
||||||
},
|
},
|
||||||
|
|
||||||
async nominate(id: string) {
|
|
||||||
const res = await http.put(`/submission/${id}/nominate`)
|
|
||||||
return res.data as { nominated: boolean }
|
|
||||||
},
|
|
||||||
|
|
||||||
async getStats(taskId: number, classname?: string): Promise<TaskStatsOut> {
|
async getStats(taskId: number, classname?: string): Promise<TaskStatsOut> {
|
||||||
const params: Record<string, string | number> = {}
|
const params: Record<string, string | number> = {}
|
||||||
if (classname) params.classname = classname
|
if (classname) params.classname = classname
|
||||||
|
|||||||
139
src/components/ExternalAIPanel.vue
Normal file
139
src/components/ExternalAIPanel.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="external-panel">
|
||||||
|
<div class="content-area">
|
||||||
|
<div class="field-label">提示词</div>
|
||||||
|
<n-input
|
||||||
|
v-model:value="promptText"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||||
|
placeholder="粘贴你发给外部 AI 的提示词..."
|
||||||
|
/>
|
||||||
|
<div class="field-label" style="margin-top: 12px">AI 代码</div>
|
||||||
|
<n-input
|
||||||
|
v-model:value="rawCode"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 6, maxRows: 16 }"
|
||||||
|
placeholder="粘贴外部 AI 返回的完整 HTML 代码..."
|
||||||
|
/>
|
||||||
|
<div v-if="splitResult" class="split-result">
|
||||||
|
<n-tag size="small" type="success">HTML · {{ splitResult.html.length }} 字符</n-tag>
|
||||||
|
<n-tag size="small" type="info">CSS · {{ splitResult.css.length }} 字符</n-tag>
|
||||||
|
<n-tag size="small" type="warning">JS · {{ splitResult.js.length }} 字符</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-bar">
|
||||||
|
<n-button :disabled="!rawCode.trim()" @click="applyPreview">应用预览</n-button>
|
||||||
|
<n-button type="primary" :disabled="!splitResult" :loading="submitting" @click="submit">
|
||||||
|
提交
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { useMessage } from "naive-ui"
|
||||||
|
import { html, css, js } from "../store/editors"
|
||||||
|
import { Submission } from "../api"
|
||||||
|
|
||||||
|
const props = defineProps<{ taskId: number }>()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const promptText = ref("")
|
||||||
|
const rawCode = ref("")
|
||||||
|
const splitResult = ref<{ html: string; css: string; js: string } | null>(null)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
watch(rawCode, () => {
|
||||||
|
splitResult.value = null
|
||||||
|
})
|
||||||
|
|
||||||
|
function splitHtml(raw: string): { html: string; css: string; js: string } {
|
||||||
|
let result = raw
|
||||||
|
const cssBlocks: string[] = []
|
||||||
|
const jsBlocks: string[] = []
|
||||||
|
|
||||||
|
result = result.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (_, content) => {
|
||||||
|
cssBlocks.push(content.trim())
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
result = result.replace(
|
||||||
|
/<script(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi,
|
||||||
|
(_, content) => {
|
||||||
|
jsBlocks.push(content.trim())
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
html: result.trim(),
|
||||||
|
css: cssBlocks.join("\n\n"),
|
||||||
|
js: jsBlocks.join("\n\n"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPreview() {
|
||||||
|
const result = splitHtml(rawCode.value)
|
||||||
|
splitResult.value = result
|
||||||
|
html.value = result.html
|
||||||
|
css.value = result.css
|
||||||
|
js.value = result.js
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!splitResult.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await Submission.create(props.taskId, {
|
||||||
|
html: splitResult.value.html,
|
||||||
|
css: splitResult.value.css,
|
||||||
|
js: splitResult.value.js,
|
||||||
|
})
|
||||||
|
message.success("提交成功")
|
||||||
|
promptText.value = ""
|
||||||
|
rawCode.value = ""
|
||||||
|
splitResult.value = null
|
||||||
|
} catch {
|
||||||
|
message.error("提交失败,请重试")
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.external-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-result {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -279,72 +279,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 人气提交 Top 5 -->
|
|
||||||
<div style="margin-bottom: 12px">
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
人气提交 Top 5
|
|
||||||
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
|
||||||
>(按打分人数)</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 5px">
|
|
||||||
<div
|
|
||||||
v-for="(sub, i) in stats.top_submissions"
|
|
||||||
:key="sub.submission_id"
|
|
||||||
:style="{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '10px',
|
|
||||||
padding: '6px 10px',
|
|
||||||
background: rankBg(i),
|
|
||||||
borderRadius: '6px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}"
|
|
||||||
@click="viewSubmission(sub.submission_id)"
|
|
||||||
>
|
|
||||||
<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,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ i + 1 }}
|
|
||||||
</div>
|
|
||||||
<div style="flex: 1">
|
|
||||||
<div style="font-weight: 500; font-size: 13px">
|
|
||||||
{{ displayName(sub.username, sub.classname) }}
|
|
||||||
</div>
|
|
||||||
<div style="color: #aaa; font-size: 11px">
|
|
||||||
{{ sub.score.toFixed(1) }} 分 ·
|
|
||||||
{{ sub.rating_count }} 人打分
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="color: #2080f0; font-size: 12px">查看 →</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="!stats.top_submissions.length"
|
|
||||||
style="color: #aaa; font-size: 12px"
|
|
||||||
>暂无打分记录</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 标记统计 -->
|
<!-- 标记统计 -->
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -436,20 +370,6 @@ function viewSubmission(id: string) {
|
|||||||
window.open(href, "_blank")
|
window.open(href, "_blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
function rankColor(i: number) {
|
|
||||||
return (
|
|
||||||
(["#f0a020", "#909090", "#cd7f32", "#8899aa", "#7a8fa0"] as const)[i] ??
|
|
||||||
"#aaa"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function rankBg(i: number) {
|
|
||||||
return (
|
|
||||||
(["#fffbef", "#f8f8f8", "#fdf5ee", "#f2f5f8", "#eef2f5"] as const)[i] ??
|
|
||||||
"#f8f8f8"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function bucketPct(value: number) {
|
function bucketPct(value: number) {
|
||||||
const total = stats.value?.submitted_count ?? 0
|
const total = stats.value?.submitted_count ?? 0
|
||||||
if (!total) return "0%"
|
if (!total) return "0%"
|
||||||
@@ -467,7 +387,6 @@ const metrics = computed(() => {
|
|||||||
color: "#2080f0",
|
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" },
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -77,15 +77,6 @@ const menu = computed(() => [
|
|||||||
width: 20,
|
width: 20,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "排名榜",
|
|
||||||
key: "ranking",
|
|
||||||
icon: () =>
|
|
||||||
h(Icon, {
|
|
||||||
icon: "streamline-emojis:sunglasses",
|
|
||||||
width: 20,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "退出账号",
|
label: "退出账号",
|
||||||
key: "logout",
|
key: "logout",
|
||||||
@@ -117,9 +108,6 @@ function clickMenu(name: string) {
|
|||||||
query: { username: user.username },
|
query: { username: user.username },
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "ranking":
|
|
||||||
router.push({ name: "ranking" })
|
|
||||||
break
|
|
||||||
case "logout":
|
case "logout":
|
||||||
handleLogout()
|
handleLogout()
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const emit = defineEmits<{
|
|||||||
select: [id: string]
|
select: [id: string]
|
||||||
delete: [row: SubmissionOut, parentId: string]
|
delete: [row: SubmissionOut, parentId: string]
|
||||||
"show-chain": [conversationId: string]
|
"show-chain": [conversationId: string]
|
||||||
nominate: [row: SubmissionOut]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge)
|
const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge)
|
||||||
@@ -77,30 +76,6 @@ const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "排名",
|
|
||||||
key: "nominated",
|
|
||||||
width: 60,
|
|
||||||
render: (r: SubmissionOut) => {
|
|
||||||
if (r.username !== user.username) {
|
|
||||||
return r.nominated
|
|
||||||
? h("span", { style: { color: "#f0a020" } }, "🏅")
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
return h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
title: r.nominated ? "已参与排名(点击可重新提名)" : "参与排名",
|
|
||||||
onClick: (e: Event) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
emit("nominate", r)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
() => (r.nominated ? "🏅" : "☆"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(isChallenge.value
|
...(isChallenge.value
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
||||||
<PromptPanel />
|
<PromptPanel />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="external" tab="手动提交" display-directive="show">
|
||||||
|
<ExternalAIPanel :task-id="taskId" />
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
<div class="challenge-content">
|
<div class="challenge-content">
|
||||||
@@ -72,6 +75,7 @@ import { useMessage } from "naive-ui"
|
|||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { marked } from "marked"
|
import { marked } from "marked"
|
||||||
import PromptPanel from "../components/PromptPanel.vue"
|
import PromptPanel from "../components/PromptPanel.vue"
|
||||||
|
import ExternalAIPanel from "../components/ExternalAIPanel.vue"
|
||||||
import Preview from "../components/Preview.vue"
|
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"
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const columns: DataTableColumn<SubmissionOut>[] = [
|
|||||||
onSelect: (id) => getSubmissionByID(id),
|
onSelect: (id) => getSubmissionByID(id),
|
||||||
onDelete: (r, parentId) => handleDelete(r, parentId),
|
onDelete: (r, parentId) => handleDelete(r, parentId),
|
||||||
"onShow-chain": (id) => showChain(id),
|
"onShow-chain": (id) => showChain(id),
|
||||||
onNominate: (r) => handleNominateChild(r, row.id),
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -313,23 +312,6 @@ async function getSubmissionByID(id: string) {
|
|||||||
submission.value = await Submission.get(id)
|
submission.value = await Submission.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleNominateChild(row: SubmissionOut, parentId: string) {
|
|
||||||
await Submission.nominate(row.id)
|
|
||||||
const items = expandedData.get(parentId)
|
|
||||||
if (items) {
|
|
||||||
expandedData.set(
|
|
||||||
parentId,
|
|
||||||
items.map((d) => ({ ...d, nominated: d.id === row.id })),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
data.value = data.value.map((d) => {
|
|
||||||
if (d.username === user.username && d.task_id === row.task_id) {
|
|
||||||
d.nominated = d.id === row.id
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterScore() {
|
function afterScore() {
|
||||||
data.value = data.value.map((d) => {
|
data.value = data.value.map((d) => {
|
||||||
if (d.id === submission.value.id) d.my_score = submission.value.my_score
|
if (d.id === submission.value.id) d.my_score = submission.value.my_score
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export interface SubmissionOut {
|
|||||||
my_score: number
|
my_score: number
|
||||||
conversation_id?: string
|
conversation_id?: string
|
||||||
flag?: FlagType
|
flag?: FlagType
|
||||||
nominated: boolean
|
|
||||||
submit_count: number
|
submit_count: number
|
||||||
created: Date
|
created: Date
|
||||||
modified: Date
|
modified: Date
|
||||||
@@ -107,14 +106,6 @@ export interface UserTag {
|
|||||||
classname: string
|
classname: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TopSubmission {
|
|
||||||
submission_id: string
|
|
||||||
username: string
|
|
||||||
classname: string
|
|
||||||
score: number
|
|
||||||
rating_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubmissionCountBucket {
|
export interface SubmissionCountBucket {
|
||||||
count_1: number
|
count_1: number
|
||||||
count_2: number
|
count_2: number
|
||||||
@@ -142,12 +133,10 @@ export interface TaskStatsOut {
|
|||||||
unsubmitted_count: number
|
unsubmitted_count: number
|
||||||
average_score: number | null
|
average_score: number | null
|
||||||
unrated_count: number
|
unrated_count: number
|
||||||
nominated_count: number
|
|
||||||
unsubmitted_users: UserTag[]
|
unsubmitted_users: UserTag[]
|
||||||
unrated_users: UserTag[]
|
unrated_users: UserTag[]
|
||||||
submission_count_distribution: SubmissionCountBucket
|
submission_count_distribution: SubmissionCountBucket
|
||||||
score_distribution: ScoreBucket
|
score_distribution: ScoreBucket
|
||||||
top_submissions: TopSubmission[]
|
|
||||||
flag_stats: FlagStats
|
flag_stats: FlagStats
|
||||||
classes: string[]
|
classes: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user