fix AI prompt chain
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -31,7 +31,6 @@ declare module 'vue' {
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
@@ -40,7 +39,6 @@ declare module 'vue' {
|
||||
NRate: typeof import('naive-ui')['NRate']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSplit: typeof import('naive-ui')['NSplit']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<n-flex>
|
||||
<n-button quaternary @click="download" :disabled="!showDL">下载</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.showCodeButton" @click="emits('showCode')">查看代码</n-button>
|
||||
<n-button quaternary v-if="props.submissionId" @click="copyLink">
|
||||
复制链接
|
||||
</n-button>
|
||||
@@ -35,10 +37,12 @@ interface Props {
|
||||
css: string
|
||||
js: string
|
||||
submissionId?: string
|
||||
showCodeButton?: boolean
|
||||
clearable?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(["afterScore", "showCode"])
|
||||
const emits = defineEmits(["afterScore", "showCode", "clear"])
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
@@ -101,6 +105,10 @@ function open() {
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
emits("clear")
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
copy(`${document.location.origin}/submission/${props.submissionId}`)
|
||||
message.success("该提交的链接已复制")
|
||||
|
||||
@@ -61,8 +61,7 @@ const renderer = new Renderer()
|
||||
renderer.code = function ({ text, lang }: { text: string; lang?: string }) {
|
||||
const escape = (s: string) =>
|
||||
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)
|
||||
const label = lang ? `查看代码(${escape(lang)})` : "查看代码"
|
||||
return `<details class="code-block"><summary>${label}</summary><pre><code class="hljs${lang ? ` language-${escape(lang)}` : ""}">${escape(text)}</code></pre></details>`
|
||||
return `<pre><code class="hljs${lang ? ` language-${escape(lang)}` : ""}">${text}</code></pre>`
|
||||
}
|
||||
|
||||
function renderMarkdown(text: string): string {
|
||||
@@ -137,29 +136,4 @@ watch(
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.message-content :deep(details.code-block) {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.message-content :deep(details.code-block summary) {
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-content :deep(details.code-block[open] summary) {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.message-content :deep(details.code-block pre) {
|
||||
margin: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,18 +19,7 @@
|
||||
</template>
|
||||
<template #2>
|
||||
<div class="right-panel">
|
||||
<Preview :html="html" :css="css" :js="js" />
|
||||
<n-flex class="toolbar" align="center" :size="8">
|
||||
<n-button secondary @click="showCode = true">查看代码</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!conversationId"
|
||||
:loading="submitLoading"
|
||||
@click="submit"
|
||||
>
|
||||
提交作品
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<Preview :html="html" :css="css" :js="js" show-code-button clearable @showCode="showCode = true" @clear="clearAll" />
|
||||
</div>
|
||||
</template>
|
||||
</n-split>
|
||||
@@ -52,7 +41,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { marked } from "marked"
|
||||
import PromptPanel from "../components/PromptPanel.vue"
|
||||
@@ -60,18 +48,16 @@ import Preview from "../components/Preview.vue"
|
||||
import { Challenge, Submission } from "../api"
|
||||
import { html, css, js } from "../store/editors"
|
||||
import { taskId } from "../store/task"
|
||||
import { connectPrompt, disconnectPrompt, conversationId, streaming, onCodeComplete } from "../store/prompt"
|
||||
import { connectPrompt, disconnectPrompt, conversationId, streaming, setOnCodeComplete } from "../store/prompt"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const leftSize = ref(0.4)
|
||||
const activeTab = ref("desc")
|
||||
const challengeTitle = ref("")
|
||||
const challengeContent = ref("")
|
||||
const showCode = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
watch(streaming, (val) => {
|
||||
if (val) activeTab.value = "chat"
|
||||
@@ -84,7 +70,7 @@ async function loadChallenge() {
|
||||
challengeTitle.value = `#${data.display} ${data.title}`
|
||||
challengeContent.value = await marked.parse(data.content, { async: true })
|
||||
connectPrompt(data.task_ptr)
|
||||
onCodeComplete = async (code) => {
|
||||
setOnCodeComplete(async (code) => {
|
||||
if (!conversationId.value) return
|
||||
try {
|
||||
await Submission.create(taskId.value, {
|
||||
@@ -96,7 +82,13 @@ async function loadChallenge() {
|
||||
} catch {
|
||||
// 静默失败,不打扰用户
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
html.value = ""
|
||||
css.value = ""
|
||||
js.value = ""
|
||||
}
|
||||
|
||||
function back() {
|
||||
@@ -104,24 +96,6 @@ function back() {
|
||||
router.push({ name: "home-challenge-list" })
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!conversationId.value) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await Submission.create(taskId.value, {
|
||||
html: html.value,
|
||||
css: css.value,
|
||||
js: js.value,
|
||||
conversationId: conversationId.value,
|
||||
})
|
||||
message.success("提交成功")
|
||||
} catch {
|
||||
message.error("提交失败")
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadChallenge)
|
||||
onUnmounted(disconnectPrompt)
|
||||
</script>
|
||||
@@ -153,10 +127,4 @@ onUnmounted(disconnectPrompt)
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,22 +63,58 @@
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="chainModal" preset="card" title="Prompt 思维链" style="max-width: 60%; max-height: 80vh">
|
||||
<n-modal v-model:show="chainModal" preset="card" title="提示词" style="width: 90vw; max-width: 1400px">
|
||||
<n-spin :show="chainLoading">
|
||||
<div v-for="msg in chainMessages" :key="msg.id" style="margin-bottom: 16px">
|
||||
<div :style="{ fontWeight: 'bold', fontSize: '12px', marginBottom: '4px', color: msg.role === 'user' ? '#2080f0' : '#18a058' }">
|
||||
{{ msg.role === "user" ? "学生" : "AI" }}
|
||||
<n-empty v-if="!chainLoading && chainRounds.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 #e0e0e0; display: flex; flex-direction: column; gap: 8px">
|
||||
<div
|
||||
v-for="(round, index) in chainRounds"
|
||||
: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 ? '#2080f0' : '#c2d5fb',
|
||||
color: '#fff', 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 ? '#e8f0fe' : '#f5f5f5',
|
||||
border: selectedRound === index ? '1px solid #2080f0' : '1px solid #e0e0e0',
|
||||
fontSize: '13px', lineHeight: '1.6', transition: 'all 0.2s',
|
||||
}">
|
||||
{{ round.question }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧:对应网页预览 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px">
|
||||
<div style="font-weight: bold; font-size: 13px; color: #555">
|
||||
第 {{ selectedRound + 1 }} 轮网页
|
||||
</div>
|
||||
<iframe
|
||||
v-if="selectedPageHtml"
|
||||
:srcdoc="selectedPageHtml"
|
||||
:key="selectedRound"
|
||||
sandbox="allow-scripts"
|
||||
style="flex: 1; border: 1px solid #e0e0e0; border-radius: 6px; background: #fff"
|
||||
/>
|
||||
<n-empty v-else description="该轮无网页代码" style="margin: auto" />
|
||||
</div>
|
||||
<div v-html="renderMarkdown(msg.content)" style="font-size: 14px; line-height: 1.6" />
|
||||
</div>
|
||||
<n-empty v-if="!chainLoading && chainMessages.length === 0" description="暂无对话记录" />
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NButton, type DataTableColumn } from "naive-ui"
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from "vue"
|
||||
import { marked } from "marked"
|
||||
import { Submission, Prompt } from "../api"
|
||||
import type { SubmissionOut } from "../utils/type"
|
||||
import { parseTime } from "../utils/helper"
|
||||
@@ -109,23 +145,51 @@ const js = computed(() => submission.value.js)
|
||||
|
||||
const codeModal = ref(false)
|
||||
const chainModal = ref(false)
|
||||
const chainMessages = ref<{ id: number; role: string; content: string }[]>([])
|
||||
const chainMessages = ref<{ id: number; role: string; content: string; code_html: string | null; code_css: string | null; code_js: string | null }[]>([])
|
||||
const chainLoading = ref(false)
|
||||
const selectedRound = ref(0)
|
||||
|
||||
const chainRounds = computed(() => {
|
||||
const messages = chainMessages.value
|
||||
const rounds: { question: string; html: string | null; css: string | null; js: string | null }[] = []
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (messages[i].role !== "user") continue
|
||||
let html = null, css = null, js = null
|
||||
for (let j = i + 1; j < messages.length; j++) {
|
||||
if (messages[j].role === "user") break
|
||||
if (messages[j].role === "assistant" && messages[j].code_html) {
|
||||
html = messages[j].code_html
|
||||
css = messages[j].code_css
|
||||
js = messages[j].code_js
|
||||
break
|
||||
}
|
||||
}
|
||||
rounds.push({ question: messages[i].content, html, css, js })
|
||||
}
|
||||
return rounds
|
||||
})
|
||||
|
||||
const selectedPageHtml = computed(() => {
|
||||
const round = chainRounds.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">${style}</head><body>${round.html}${script}</body></html>`
|
||||
})
|
||||
|
||||
async function showChain(conversationId: string) {
|
||||
chainLoading.value = true
|
||||
chainModal.value = true
|
||||
selectedRound.value = 0
|
||||
try {
|
||||
chainMessages.value = await Prompt.getMessages(conversationId)
|
||||
const last = chainRounds.value.length - 1
|
||||
if (last >= 0) selectedRound.value = last
|
||||
} finally {
|
||||
chainLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function renderMarkdown(text: string): string {
|
||||
return marked.parse(text, { async: false }) as string
|
||||
}
|
||||
|
||||
const columns: DataTableColumn<SubmissionOut>[] = [
|
||||
{
|
||||
title: "时间",
|
||||
@@ -161,7 +225,7 @@ const columns: DataTableColumn<SubmissionOut>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "思维链",
|
||||
title: "提示词",
|
||||
key: "conversation_id",
|
||||
width: 70,
|
||||
render: (row) => {
|
||||
|
||||
@@ -14,7 +14,11 @@ export const conversationId = ref<string>("")
|
||||
export const connected = ref(false)
|
||||
export const streaming = ref(false)
|
||||
export const streamingContent = ref("")
|
||||
export 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) {
|
||||
_onCodeComplete = fn
|
||||
}
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
|
||||
@@ -55,8 +59,8 @@ export function connectPrompt(taskId: number) {
|
||||
// Apply code to editors
|
||||
if (data.code) {
|
||||
applyCode(data.code)
|
||||
if (onCodeComplete) {
|
||||
onCodeComplete(data.code)
|
||||
if (_onCodeComplete) {
|
||||
_onCodeComplete(data.code)
|
||||
}
|
||||
}
|
||||
} else if (data.type === "error") {
|
||||
@@ -84,7 +88,7 @@ export function disconnectPrompt() {
|
||||
connected.value = false
|
||||
streaming.value = false
|
||||
streamingContent.value = ""
|
||||
onCodeComplete = null
|
||||
_onCodeComplete = null
|
||||
}
|
||||
|
||||
export function sendPrompt(content: string) {
|
||||
|
||||
Reference in New Issue
Block a user