fix
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
{{ item.source === "manual" ? "手动提交" : "AI 对话" }}
|
{{ item.source === "manual" ? "手动提交" : "AI 对话" }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
<n-flex align="center" :wrap="false" :size="4">
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="selectedAssistantMessageId === item.assistant_message_id"
|
v-if="selectedAssistantMessageId === item.assistant_message_id"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -86,6 +87,24 @@
|
|||||||
<n-text depth="3">
|
<n-text depth="3">
|
||||||
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
|
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
|
||||||
</n-text>
|
</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);
|
||||||
|
|||||||
@@ -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%,
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
5
src/utils/markdown.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { marked } from "marked"
|
||||||
|
|
||||||
|
export function renderMarkdown(text: string): string {
|
||||||
|
return marked.parse(text) as string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user