fix
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled

This commit is contained in:
2026-05-09 01:30:36 -06:00
parent 53a0759507
commit f7817d8c26
6 changed files with 95 additions and 124 deletions

View File

@@ -84,8 +84,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from "vue" import { ref, computed, watch, nextTick } from "vue"
import { marked } from "marked"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { renderMarkdown } from "../../utils/markdown"
import { import {
messages, messages,
streaming, streaming,
@@ -110,10 +110,6 @@ const displayStreamingContent = computed(() =>
streamingContent.value.replace(/^\[READY\]\n?/, "") streamingContent.value.replace(/^\[READY\]\n?/, "")
) )
function renderMarkdown(text: string): string {
return marked.parse(text) as string
}
function send() { function send() {
const text = draftPrompt.value.trim() const text = draftPrompt.value.trim()
if (!text || streaming.value) return if (!text || streaming.value) return

View File

@@ -75,17 +75,36 @@
{{ item.source === "manual" ? "手动提交" : "AI 对话" }} {{ item.source === "manual" ? "手动提交" : "AI 对话" }}
</n-tag> </n-tag>
</n-flex> </n-flex>
<n-tag <n-flex align="center" :wrap="false" :size="4">
v-if="selectedAssistantMessageId === item.assistant_message_id" <n-tag
size="small" v-if="selectedAssistantMessageId === item.assistant_message_id"
type="success" size="small"
:bordered="false" type="success"
> :bordered="false"
正在预览 >
</n-tag> 正在预览
<n-text depth="3"> </n-tag>
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }} <n-text depth="3">
</n-text> {{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
</n-text>
<n-tooltip placement="top">
<template #trigger>
<n-button
quaternary
circle
size="small"
:loading="deletingId === item.assistant_message_id"
:disabled="deletingId !== null"
@click="deleteItem(item, $event)"
>
<template #icon>
<Icon icon="lucide:trash-2" />
</template>
</n-button>
</template>
删除这条历史对话
</n-tooltip>
</n-flex>
</n-flex> </n-flex>
<div <div
class="prompt-markdown markdown-body" class="prompt-markdown markdown-body"
@@ -112,7 +131,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from "vue" import { onMounted, ref, watch } from "vue"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { marked } from "marked" import { useMessage } from "naive-ui"
import { renderMarkdown } from "../../utils/markdown"
import { Prompt } from "../../api" import { Prompt } from "../../api"
import type { PromptHistoryItem } from "../../utils/type" import type { PromptHistoryItem } from "../../utils/type"
import { parseTime } from "../../utils/helper" import { parseTime } from "../../utils/helper"
@@ -127,6 +147,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
select: [code: { html: string; css: string; js: string }] select: [code: { html: string; css: string; js: string }]
deleted: [assistantMessageId: number]
}>() }>()
type HistoryViewItem = PromptHistoryItem & { type HistoryViewItem = PromptHistoryItem & {
@@ -137,7 +158,11 @@ type HistoryViewItem = PromptHistoryItem & {
const items = ref<HistoryViewItem[]>([]) const items = ref<HistoryViewItem[]>([])
const loading = ref(false) const loading = ref(false)
const selectedAssistantMessageId = ref<number | null>(null) const selectedAssistantMessageId = ref<number | null>(null)
const deletingId = ref<number | null>(null)
let loadedTaskId = 0 let loadedTaskId = 0
let pendingRefresh = false
const naiveMessage = useMessage()
function toViewItem(item: PromptHistoryItem): HistoryViewItem { function toViewItem(item: PromptHistoryItem): HistoryViewItem {
const html = item.code_html ?? "" const html = item.code_html ?? ""
@@ -155,8 +180,25 @@ function toViewItem(item: PromptHistoryItem): HistoryViewItem {
} }
} }
function renderMarkdown(text: string): string { async function deleteItem(item: HistoryViewItem, e: Event) {
return marked.parse(text) as string e.stopPropagation()
if (deletingId.value !== null) return
deletingId.value = item.assistant_message_id
try {
await Prompt.deleteMessagePair(item.assistant_message_id)
items.value = items.value.filter(
(i) => i.assistant_message_id !== item.assistant_message_id,
)
if (selectedAssistantMessageId.value === item.assistant_message_id) {
selectedAssistantMessageId.value = null
}
emit("deleted", item.assistant_message_id)
naiveMessage.success("已删除")
} catch {
naiveMessage.error("删除失败,请重试")
} finally {
deletingId.value = null
}
} }
function selectItem(item: HistoryViewItem) { function selectItem(item: HistoryViewItem) {
@@ -194,7 +236,14 @@ async function load(force = true) {
watch( watch(
() => [props.active, props.taskId] as const, () => [props.active, props.taskId] as const,
([active]) => { ([active]) => {
if (active) load(false) if (active) {
if (pendingRefresh) {
pendingRefresh = false
load(true)
} else {
load(false)
}
}
}, },
) )
@@ -202,6 +251,7 @@ watch(
() => props.refreshKey, () => props.refreshKey,
() => { () => {
if (props.active) load(true) if (props.active) load(true)
else pendingRefresh = true
}, },
) )
@@ -243,6 +293,7 @@ onMounted(() => {
overflow: hidden; overflow: hidden;
} }
.history-card.is-selected { .history-card.is-selected {
--n-color: #f7fffa; --n-color: #f7fffa;
box-shadow: 0 10px 24px rgba(24, 160, 88, 0.14); box-shadow: 0 10px 24px rgba(24, 160, 88, 0.14);

View File

@@ -100,7 +100,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick, computed } from "vue" import { ref, watch, nextTick, computed } from "vue"
import { useStorage } from "@vueuse/core" import { useStorage } from "@vueuse/core"
import { marked, Renderer } from "marked"
import { useMessage } from "naive-ui" import { useMessage } from "naive-ui"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import GuidancePanel from "./GuidancePanel.vue" import GuidancePanel from "./GuidancePanel.vue"
@@ -113,8 +112,12 @@ import {
sendPrompt, sendPrompt,
stopPrompt, stopPrompt,
currentTaskId, currentTaskId,
removeMessagePair,
} from "../../store/prompt" } from "../../store/prompt"
import { Prompt } from "../../api" import { Prompt } from "../../api"
import { renderMarkdown } from "../../utils/markdown"
const emit = defineEmits<{ deleted: [] }>()
const input = ref("") const input = ref("")
const messagesRef = ref<HTMLElement>() const messagesRef = ref<HTMLElement>()
@@ -151,11 +154,9 @@ const pairs = computed(() => {
async function deletePair(assistantMsgId: number) { async function deletePair(assistantMsgId: number) {
try { try {
await Prompt.deleteMessagePair(assistantMsgId) await Prompt.deleteMessagePair(assistantMsgId)
const msgIdx = messages.value.findIndex((m) => m.id === assistantMsgId) removeMessagePair(assistantMsgId)
if (msgIdx >= 1) {
messages.value.splice(msgIdx - 1, 2)
}
naiveMessage.success("已删除") naiveMessage.success("已删除")
emit("deleted")
} catch { } catch {
naiveMessage.error("删除失败,请重试") naiveMessage.error("删除失败,请重试")
} }
@@ -179,56 +180,6 @@ function onGuidanceGenerate(finalPrompt: string) {
input.value = "" input.value = ""
} }
const renderer = new Renderer()
renderer.code = function ({ lang }: { text: string; lang?: string }) {
const label = lang ? lang.toUpperCase() : "CODE"
const colors: Record<
string,
{ bg: string; fg: string; dot: string; border: string; shimmer: string }
> = {
html: {
bg: "#f0fff4",
fg: "#18a058",
dot: "#18a058",
border: "#b8e8cc",
shimmer: "#f0fff4, #e0f7ea, #f0fff4",
},
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",
}
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>`
}
function renderMarkdown(text: string): string {
return marked.parse(text, { renderer }) as string
}
function renderContent(msg: { role: string; content: string }): string { function renderContent(msg: { role: string; content: string }): string {
return renderMarkdown(msg.content) return renderMarkdown(msg.content)
} }
@@ -289,47 +240,6 @@ watch([() => messages.value.length, streamingContent], () => {
font-size: 13px; font-size: 13px;
} }
.message-content :deep(.code-placeholder) {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
margin: 8px 0;
background: linear-gradient(90deg, #f0f7ff, #e8f4f8, #f0f7ff);
background-size: 200% 100%;
animation: shimmer 2s ease-in-out infinite;
border-radius: 6px;
border: 1px solid #e0eaf5;
}
.message-content :deep(.code-placeholder-dot) {
width: 8px;
height: 8px;
border-radius: 50%;
background: #2080f0;
animation: pulse 1.5s ease-in-out infinite;
}
.message-content :deep(.code-placeholder-label) {
font-size: 11px;
font-weight: 600;
padding: 1px 6px;
border-radius: 3px;
}
.message-content :deep(.code-placeholder-text) {
font-size: 12px;
color: #888;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes pulse { @keyframes pulse {
0%, 0%,

View File

@@ -49,7 +49,7 @@
</div> </div>
</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 @deleted="historyRefreshKey++" />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="external" tab="手动提交" display-directive="show"> <n-tab-pane name="external" tab="手动提交" display-directive="show">
<ExternalAIPanel :task-id="taskId" @submitted="historyRefreshKey++" /> <ExternalAIPanel :task-id="taskId" @submitted="historyRefreshKey++" />
@@ -61,6 +61,7 @@
:asset-base-url="assetBaseUrl" :asset-base-url="assetBaseUrl"
:refresh-key="historyRefreshKey" :refresh-key="historyRefreshKey"
@select="previewHistoryItem" @select="previewHistoryItem"
@deleted="removeMessagePair"
/> />
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
@@ -139,6 +140,7 @@ import {
disconnectPrompt, disconnectPrompt,
streaming, streaming,
setOnCodeComplete, setOnCodeComplete,
removeMessagePair,
} from "../store/prompt" } from "../store/prompt"
const route = useRoute() const route = useRoute()
@@ -193,14 +195,17 @@ async function loadChallenge() {
const display = Number(route.params.display) const display = Number(route.params.display)
taskTab.value = TASK_TYPE.Challenge taskTab.value = TASK_TYPE.Challenge
challengeDisplay.value = display challengeDisplay.value = display
const data = await Challenge.get(display) const [data, fetchedAssets] = await Promise.all([
Challenge.get(display),
TaskAssets.listChallenge(display),
])
taskId.value = data.task_ptr taskId.value = data.task_ptr
challengeAuthor.value = data.author_name ?? "" challengeAuthor.value = data.author_name ?? ""
challengeContent.value = await marked.parse(data.content, { challengeContent.value = await marked.parse(data.content, {
async: true, async: true,
renderer: challengeRenderer, renderer: challengeRenderer,
} as MarkedOptions) } as MarkedOptions)
assets.value = await TaskAssets.listChallenge(display) assets.value = fetchedAssets
if (!authed.value) return if (!authed.value) return
connectPrompt(data.task_ptr) connectPrompt(data.task_ptr)
setOnCodeComplete(async (code, messageId) => { setOnCodeComplete(async (code, messageId) => {

View File

@@ -28,10 +28,8 @@ export function setOnCodeComplete(fn: typeof _onCodeComplete) {
} }
let ws: WebSocket | null = null let ws: WebSocket | null = null
let _currentTaskId = 0
export function connectPrompt(taskId: number) { export function connectPrompt(taskId: number) {
_currentTaskId = taskId
currentTaskId.value = taskId currentTaskId.value = taskId
if (ws) ws.close() if (ws) ws.close()
@@ -102,7 +100,6 @@ export function disconnectPrompt() {
streaming.value = false streaming.value = false
streamingContent.value = "" streamingContent.value = ""
currentTaskId.value = 0 currentTaskId.value = 0
_currentTaskId = 0
_onCodeComplete = null _onCodeComplete = null
} }
@@ -122,8 +119,15 @@ export function stopPrompt() {
} }
streaming.value = false streaming.value = false
streamingContent.value = "" streamingContent.value = ""
if (_currentTaskId) { if (currentTaskId.value) {
connectPrompt(_currentTaskId) connectPrompt(currentTaskId.value)
}
}
export function removeMessagePair(assistantMsgId: number) {
const idx = messages.value.findIndex((m) => m.id === assistantMsgId)
if (idx >= 1) {
messages.value.splice(idx - 1, 2)
} }
} }

5
src/utils/markdown.ts Normal file
View File

@@ -0,0 +1,5 @@
import { marked } from "marked"
export function renderMarkdown(text: string): string {
return marked.parse(text) as string
}