add delete button
This commit is contained in:
@@ -160,12 +160,14 @@ export const Submission = {
|
|||||||
js?: string
|
js?: string
|
||||||
prompt?: string
|
prompt?: string
|
||||||
},
|
},
|
||||||
|
messageId?: number,
|
||||||
) {
|
) {
|
||||||
const { prompt, ...rest } = code
|
const { prompt, ...rest } = code
|
||||||
const data = {
|
const data = {
|
||||||
task_id: taskId,
|
task_id: taskId,
|
||||||
...rest,
|
...rest,
|
||||||
prompt: prompt || null,
|
prompt: prompt || null,
|
||||||
|
message_id: messageId ?? null,
|
||||||
}
|
}
|
||||||
const res = await http.post("/submission/", data)
|
const res = await http.post("/submission/", data)
|
||||||
return res.data
|
return res.data
|
||||||
@@ -260,6 +262,13 @@ export const Prompt = {
|
|||||||
if (!convs.length) return []
|
if (!convs.length) return []
|
||||||
return this.getMessages(convs[0].id)
|
return this.getMessages(convs[0].id)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deleteMessagePair(
|
||||||
|
messageId: number,
|
||||||
|
): Promise<{ deleted: boolean; submission_deleted: boolean }> {
|
||||||
|
const res = await http.delete(`/prompt/messages/${messageId}/pair`)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Helper = {
|
export const Helper = {
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt-panel">
|
<div class="prompt-panel">
|
||||||
<div class="messages" ref="messagesRef">
|
<div class="messages" ref="messagesRef">
|
||||||
<div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]">
|
<div
|
||||||
<div class="message-role">{{ msg.role === "user" ? "我" : "AI" }}</div>
|
v-for="(pair, pi) in pairs"
|
||||||
<div class="message-content" v-html="renderContent(msg)"></div>
|
:key="pair.assistantMsg?.id ?? 'user-' + pair.index"
|
||||||
|
class="message-pair"
|
||||||
|
:class="{ 'has-delete': pair.assistantMsg?.id && !streaming }"
|
||||||
|
>
|
||||||
|
<!-- Delete button: only shown on hover when pair has assistant id and not streaming -->
|
||||||
|
<button
|
||||||
|
v-if="pair.assistantMsg?.id && !streaming"
|
||||||
|
class="pair-delete-btn"
|
||||||
|
@click="deletePair(pair.assistantMsg!.id!)"
|
||||||
|
title="删除这轮对话及关联提交"
|
||||||
|
>
|
||||||
|
<Icon icon="lucide:trash-2" :width="14" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- User message -->
|
||||||
|
<div class="message user">
|
||||||
|
<div class="message-role">我</div>
|
||||||
|
<div class="message-content" v-html="renderContent(pair.userMsg)"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Assistant message -->
|
||||||
|
<div v-if="pair.assistantMsg" class="message assistant">
|
||||||
|
<div class="message-role">AI</div>
|
||||||
|
<div class="message-content" v-html="renderContent(pair.assistantMsg)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Streaming indicator -->
|
||||||
<div v-if="streaming" class="message assistant">
|
<div v-if="streaming" class="message assistant">
|
||||||
<div class="message-role">AI</div>
|
<div class="message-role">AI</div>
|
||||||
<div v-if="!streamingContent" class="typing-indicator">
|
<div v-if="!streamingContent" class="typing-indicator">
|
||||||
@@ -51,9 +77,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } 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 { marked, Renderer } from "marked"
|
||||||
|
import { useMessage } from "naive-ui"
|
||||||
|
import { Icon } from "@iconify/vue"
|
||||||
import {
|
import {
|
||||||
messages,
|
messages,
|
||||||
streaming,
|
streaming,
|
||||||
@@ -62,9 +90,11 @@ import {
|
|||||||
sendPrompt,
|
sendPrompt,
|
||||||
stopPrompt,
|
stopPrompt,
|
||||||
} from "../../store/prompt"
|
} from "../../store/prompt"
|
||||||
|
import { Prompt } from "../../api"
|
||||||
|
|
||||||
const input = ref("")
|
const input = ref("")
|
||||||
const messagesRef = ref<HTMLElement>()
|
const messagesRef = ref<HTMLElement>()
|
||||||
|
const naiveMessage = useMessage()
|
||||||
|
|
||||||
const modelOptions = [
|
const modelOptions = [
|
||||||
{ label: "豆包", value: "doubao-seed-2-0-lite-260215" },
|
{ label: "豆包", value: "doubao-seed-2-0-lite-260215" },
|
||||||
@@ -72,6 +102,40 @@ const modelOptions = [
|
|||||||
]
|
]
|
||||||
const selectedModel = useStorage("prompt-model", "deepseek-chat")
|
const selectedModel = useStorage("prompt-model", "deepseek-chat")
|
||||||
|
|
||||||
|
// Group messages into user+assistant pairs
|
||||||
|
const pairs = computed(() => {
|
||||||
|
const result: Array<{
|
||||||
|
userMsg: { role: string; content: string; id?: number }
|
||||||
|
assistantMsg: { role: string; content: string; id?: number; code?: any } | null
|
||||||
|
index: number
|
||||||
|
}> = []
|
||||||
|
const msgs = messages.value
|
||||||
|
let i = 0
|
||||||
|
while (i < msgs.length) {
|
||||||
|
if (msgs[i].role === "user") {
|
||||||
|
const assistantMsg = msgs[i + 1]?.role === "assistant" ? msgs[i + 1] : null
|
||||||
|
result.push({ userMsg: msgs[i], assistantMsg, index: i })
|
||||||
|
i += assistantMsg ? 2 : 1
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
async function deletePair(assistantMsgId: number) {
|
||||||
|
try {
|
||||||
|
await Prompt.deleteMessagePair(assistantMsgId)
|
||||||
|
const msgIdx = messages.value.findIndex((m) => m.id === assistantMsgId)
|
||||||
|
if (msgIdx >= 1) {
|
||||||
|
messages.value.splice(msgIdx - 1, 2)
|
||||||
|
}
|
||||||
|
naiveMessage.success("已删除")
|
||||||
|
} catch {
|
||||||
|
naiveMessage.error("删除失败,请重试")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
const text = input.value.trim()
|
const text = input.value.trim()
|
||||||
if (!text || streaming.value) return
|
if (!text || streaming.value) return
|
||||||
@@ -133,7 +197,6 @@ function renderContent(msg: { role: string; content: string }): string {
|
|||||||
return renderMarkdown(msg.content)
|
return renderMarkdown(msg.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scroll to bottom on new messages
|
|
||||||
watch([() => messages.value.length, streamingContent], () => {
|
watch([() => messages.value.length, streamingContent], () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (messagesRef.value) {
|
if (messagesRef.value) {
|
||||||
@@ -289,4 +352,38 @@ watch([() => messages.value.length, streamingContent], () => {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-top: 1px solid #e0e0e0;
|
border-top: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-pair {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-pair .message {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pair-delete-btn {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pair-delete-btn:hover {
|
||||||
|
color: #e03e3e;
|
||||||
|
border-color: #e03e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-pair.has-delete:hover .pair-delete-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -183,13 +183,17 @@ async function loadChallenge() {
|
|||||||
assets.value = await TaskAssets.listChallenge(display)
|
assets.value = await TaskAssets.listChallenge(display)
|
||||||
if (!authed.value) return
|
if (!authed.value) return
|
||||||
connectPrompt(data.task_ptr)
|
connectPrompt(data.task_ptr)
|
||||||
setOnCodeComplete(async (code) => {
|
setOnCodeComplete(async (code, messageId) => {
|
||||||
try {
|
try {
|
||||||
await Submission.create(taskId.value, {
|
await Submission.create(
|
||||||
|
taskId.value,
|
||||||
|
{
|
||||||
html: code.html ?? "",
|
html: code.html ?? "",
|
||||||
css: code.css ?? "",
|
css: code.css ?? "",
|
||||||
js: code.js ?? "",
|
js: code.js ?? "",
|
||||||
})
|
},
|
||||||
|
messageId,
|
||||||
|
)
|
||||||
message.success("已自动提交本次对话生成的代码")
|
message.success("已自动提交本次对话生成的代码")
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败,不打扰用户
|
// 静默失败,不打扰用户
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { html, css, js } from "./editors"
|
|||||||
export interface PromptMessage {
|
export interface PromptMessage {
|
||||||
role: "user" | "assistant"
|
role: "user" | "assistant"
|
||||||
content: string
|
content: string
|
||||||
|
id?: number // assistant message backend pk (for deletion)
|
||||||
code?: { html: string | null; css: string | null; js: string | null }
|
code?: { html: string | null; css: string | null; js: string | null }
|
||||||
created?: string
|
created?: string
|
||||||
}
|
}
|
||||||
@@ -15,11 +16,10 @@ export const connected = ref(false)
|
|||||||
export const streaming = ref(false)
|
export const streaming = ref(false)
|
||||||
export const streamingContent = ref("")
|
export const streamingContent = ref("")
|
||||||
let _onCodeComplete:
|
let _onCodeComplete:
|
||||||
| ((code: {
|
| ((
|
||||||
html: string | null
|
code: { html: string | null; css: string | null; js: string | null },
|
||||||
css: string | null
|
messageId: number
|
||||||
js: string | null
|
) => void)
|
||||||
}) => void)
|
|
||||||
| null = null
|
| null = null
|
||||||
|
|
||||||
export function setOnCodeComplete(fn: typeof _onCodeComplete) {
|
export function setOnCodeComplete(fn: typeof _onCodeComplete) {
|
||||||
@@ -45,14 +45,10 @@ export function connectPrompt(taskId: number) {
|
|||||||
if (data.type === "init") {
|
if (data.type === "init") {
|
||||||
streaming.value = false
|
streaming.value = false
|
||||||
streamingContent.value = ""
|
streamingContent.value = ""
|
||||||
// Skip overwriting messages if HTTP preload already loaded this conversation.
|
|
||||||
// If conversation_id differs (e.g. after "新对话"), always overwrite.
|
|
||||||
const alreadyLoaded = conversationId.value === data.conversation_id
|
const alreadyLoaded = conversationId.value === data.conversation_id
|
||||||
conversationId.value = data.conversation_id
|
conversationId.value = data.conversation_id
|
||||||
if (!alreadyLoaded) {
|
if (!alreadyLoaded) {
|
||||||
messages.value = data.messages || []
|
messages.value = data.messages || []
|
||||||
// Apply code from last assistant message if exists
|
|
||||||
// (skipped when HTTP preload already loaded and applied)
|
|
||||||
const lastAssistant = [...messages.value]
|
const lastAssistant = [...messages.value]
|
||||||
.reverse()
|
.reverse()
|
||||||
.find((m) => m.role === "assistant" && m.code)
|
.find((m) => m.role === "assistant" && m.code)
|
||||||
@@ -65,18 +61,17 @@ export function connectPrompt(taskId: number) {
|
|||||||
streamingContent.value += data.content
|
streamingContent.value += data.content
|
||||||
} else if (data.type === "complete") {
|
} else if (data.type === "complete") {
|
||||||
streaming.value = false
|
streaming.value = false
|
||||||
// Push the full assistant message
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: streamingContent.value,
|
content: streamingContent.value,
|
||||||
|
id: data.message_id,
|
||||||
code: data.code,
|
code: data.code,
|
||||||
})
|
})
|
||||||
streamingContent.value = ""
|
streamingContent.value = ""
|
||||||
// Apply code to editors
|
|
||||||
if (data.code) {
|
if (data.code) {
|
||||||
applyCode(data.code)
|
applyCode(data.code)
|
||||||
if (_onCodeComplete) {
|
if (_onCodeComplete) {
|
||||||
_onCodeComplete(data.code)
|
_onCodeComplete(data.code, data.message_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (data.type === "error") {
|
} else if (data.type === "error") {
|
||||||
@@ -115,7 +110,6 @@ export function sendPrompt(content: string, model: string = "") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stopPrompt() {
|
export function stopPrompt() {
|
||||||
// Remove the user message added to UI (not yet saved in DB)
|
|
||||||
if (
|
if (
|
||||||
messages.value.length > 0 &&
|
messages.value.length > 0 &&
|
||||||
messages.value[messages.value.length - 1].role === "user"
|
messages.value[messages.value.length - 1].role === "user"
|
||||||
@@ -124,7 +118,6 @@ export function stopPrompt() {
|
|||||||
}
|
}
|
||||||
streaming.value = false
|
streaming.value = false
|
||||||
streamingContent.value = ""
|
streamingContent.value = ""
|
||||||
// connectPrompt closes old WS (triggering backend disconnect cleanup) then reconnects
|
|
||||||
if (_currentTaskId) {
|
if (_currentTaskId) {
|
||||||
connectPrompt(_currentTaskId)
|
connectPrompt(_currentTaskId)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user