chore: 将前端移动到 client/ 目录,准备合并为单仓库

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 16:01:25 +08:00
1 parent 1f6935e31e
commit d1c6b8c201
79 files changed

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
import { useStorage } from "@vueuse/core"
import { STORAGE_KEY } from "../utils/const"
const defaultHTML = ``
const defaultCSS = ``
export const html = useStorage(STORAGE_KEY.HTML, defaultHTML)
export const css = useStorage(STORAGE_KEY.CSS, defaultCSS)
export const js = useStorage(STORAGE_KEY.JS, "")
export const tab = useStorage(STORAGE_KEY.TAB, "html")
export const size = useStorage(STORAGE_KEY.FONTSIZE, 24)
export function reset(lang: "html" | "css" | "js") {
if (lang === "html") {
html.value = defaultHTML
} else if (lang === "css") {
css.value = defaultCSS
} else {
js.value = ""
}
}
+111
View File
@@ -0,0 +1,111 @@
import { ref } from "vue"
import { WS_BASE_URL } from "../utils/const"
export interface GuidanceMessage {
role: "user" | "assistant"
content: string
id?: number
}
export const messages = ref<GuidanceMessage[]>([])
export const connected = ref(false)
export const streaming = ref(false)
export const streamingContent = ref("")
export const isReady = ref(false)
export const isOpen = ref(false)
export const initialPrompt = ref("")
let ws: WebSocket | null = null
let _taskId = 0
function setupHandlers(socket: WebSocket) {
socket.onopen = () => {
connected.value = true
if (initialPrompt.value) {
sendToSocket(initialPrompt.value)
}
}
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === "init") {
streaming.value = false
streamingContent.value = ""
} else if (data.type === "stream") {
streaming.value = true
streamingContent.value += data.content
} else if (data.type === "complete") {
streaming.value = false
const content = streamingContent.value.replace(/^\[READY\]\n?/, "")
messages.value.push({ role: "assistant", content, id: data.message_id })
streamingContent.value = ""
if (data.is_ready) isReady.value = true
} else if (data.type === "error") {
streaming.value = false
streamingContent.value = ""
messages.value.push({ role: "assistant", content: data.content })
}
}
socket.onclose = () => {
connected.value = false
}
}
function sendToSocket(content: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
streaming.value = true
messages.value.push({ role: "user", content })
ws.send(JSON.stringify({ type: "message", content }))
}
export function openGuidance(taskId: number, prompt: string) {
if (!taskId) return
_taskId = taskId
initialPrompt.value = prompt
messages.value = []
isReady.value = false
streamingContent.value = ""
isOpen.value = true
if (ws) ws.close()
ws = new WebSocket(`${WS_BASE_URL}/ws/guidance/${taskId}/`)
setupHandlers(ws)
}
export function sendGuidance(content: string) {
sendToSocket(content)
}
export function stopGuidance() {
if (
messages.value.length > 0 &&
messages.value[messages.value.length - 1].role === "user"
) {
messages.value.pop()
}
streaming.value = false
streamingContent.value = ""
if (_taskId) {
if (ws) ws.close()
ws = new WebSocket(`${WS_BASE_URL}/ws/guidance/${_taskId}/`)
initialPrompt.value = ""
setupHandlers(ws)
}
}
export function closeGuidance() {
if (ws) {
ws.close()
ws = null
}
isOpen.value = false
messages.value = []
isReady.value = false
connected.value = false
streaming.value = false
streamingContent.value = ""
initialPrompt.value = ""
_taskId = 0
}
+3
View File
@@ -0,0 +1,3 @@
import { ref } from "vue"
export const loginModal = ref(false)
+5
View File
@@ -0,0 +1,5 @@
// client/src/store/panel.ts
import { ref } from "vue"
export const show = ref(true)
export const panelSize = ref(2 / 5)
+149
View File
@@ -0,0 +1,149 @@
import { ref } from "vue"
import { WS_BASE_URL } from "../utils/const"
import { html, css, js } from "./editors"
export interface PromptMessage {
role: "user" | "assistant"
content: string
id?: number // assistant message backend pk (for deletion)
code?: { html: string | null; css: string | null; js: string | null }
submitted?: boolean // whether this assistant message's code has been submitted
created?: string
}
export const messages = ref<PromptMessage[]>([])
export const conversationId = ref<string>("")
export const connected = ref(false)
export const streaming = ref(false)
export const streamingContent = ref("")
export const currentTaskId = ref(0)
let _onCodeComplete:
| ((
code: { html: string | null; css: string | null; js: string | null },
messageId: number
) => void)
| null = null
export function setOnCodeComplete(fn: typeof _onCodeComplete) {
_onCodeComplete = fn
}
let ws: WebSocket | null = null
export function connectPrompt(taskId: number) {
currentTaskId.value = taskId
if (ws) ws.close()
ws = new WebSocket(`${WS_BASE_URL}/ws/prompt/${taskId}/`)
ws.onopen = () => {
connected.value = true
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === "init") {
streaming.value = false
streamingContent.value = ""
const alreadyLoaded = conversationId.value === data.conversation_id
conversationId.value = data.conversation_id
if (!alreadyLoaded) {
messages.value = data.messages || []
const lastAssistant = [...messages.value]
.reverse()
.find((m) => m.role === "assistant" && m.code)
if (lastAssistant?.code) {
applyCode(lastAssistant.code)
}
}
} else if (data.type === "stream") {
streaming.value = true
streamingContent.value += data.content
} else if (data.type === "complete") {
streaming.value = false
messages.value.push({
role: "assistant",
content: streamingContent.value,
id: data.message_id,
code: data.code,
submitted: false,
})
streamingContent.value = ""
if (data.code) {
applyCode(data.code)
if (_onCodeComplete) {
_onCodeComplete(data.code, data.message_id)
}
}
} else if (data.type === "error") {
streaming.value = false
streamingContent.value = ""
messages.value.push({
role: "assistant",
content: data.content,
})
}
}
ws.onclose = () => {
connected.value = false
}
}
export function disconnectPrompt() {
if (ws) {
ws.close()
ws = null
}
messages.value = []
conversationId.value = ""
connected.value = false
streaming.value = false
streamingContent.value = ""
currentTaskId.value = 0
_onCodeComplete = null
}
export function sendPrompt(content: string, model: string = "") {
if (!ws || ws.readyState !== WebSocket.OPEN) return
streaming.value = true
messages.value.push({ role: "user", content })
ws.send(JSON.stringify({ type: "message", content, model }))
}
export function stopPrompt() {
if (
messages.value.length > 0 &&
messages.value[messages.value.length - 1].role === "user"
) {
messages.value.pop()
}
streaming.value = false
streamingContent.value = ""
if (currentTaskId.value) {
connectPrompt(currentTaskId.value)
}
}
export function markMessageSubmitted(assistantMsgId: number) {
const msg = messages.value.find((m) => m.id === assistantMsgId)
if (msg) msg.submitted = true
}
export function removeMessagePair(assistantMsgId: number) {
const idx = messages.value.findIndex((m) => m.id === assistantMsgId)
if (idx >= 1) {
messages.value.splice(idx - 1, 2)
}
}
function applyCode(code: {
html: string | null
css: string | null
js: string | null
}) {
if (code.html !== null) html.value = code.html
if (code.css !== null) css.value = code.css
if (code.js !== null) js.value = code.js
}
+20
View File
@@ -0,0 +1,20 @@
import { ref } from "vue"
import type { SubmissionAll } from "../utils/type"
import { TASK_TYPE } from "../utils/const"
export const submission = ref<SubmissionAll>({
id: "",
userid: 0,
username: "",
task_id: 0,
task_display: 0, // 这是序号
task_title: "",
task_type: TASK_TYPE.Tutorial,
score: 0.0,
my_score: 0,
html: "",
css: "",
js: "",
created: new Date(),
modified: new Date(),
})
+12
View File
@@ -0,0 +1,12 @@
import { ref } from "vue"
import { useStorage } from "@vueuse/core"
import { TASK_TYPE } from "../utils/const"
const currentTask = window.location.pathname.startsWith("/challenge")
? TASK_TYPE.Challenge
: TASK_TYPE.Tutorial
export const taskTab = ref(currentTask)
export const taskId = ref(0)
export const challengeDisplay = useStorage("challenge-display", 0)
export const assetBaseUrl = ref("")
+34
View File
@@ -0,0 +1,34 @@
import { useStorage } from "@vueuse/core"
import { ref } from "vue"
import { Tutorial } from "../api"
export const step = useStorage("tutorial-step", 1)
export const tutorialIds = ref<number[]>([])
export async function loadTutorials(): Promise<void> {
tutorialIds.value = await Tutorial.listDisplay()
if (tutorialIds.value.length && !tutorialIds.value.includes(step.value)) {
step.value = tutorialIds.value[0] as number
}
}
export function prevDisabled(): boolean {
const i = tutorialIds.value.indexOf(step.value)
return i <= 0
}
export function nextDisabled(): boolean {
const i = tutorialIds.value.indexOf(step.value)
return i === -1 || i === tutorialIds.value.length - 1
}
export function prev(): void {
const i = tutorialIds.value.indexOf(step.value)
if (i > 0) step.value = tutorialIds.value[i - 1] as number
}
export function next(): void {
const i = tutorialIds.value.indexOf(step.value)
if (i !== -1 && i < tutorialIds.value.length - 1)
step.value = tutorialIds.value[i + 1] as number
}
+18
View File
@@ -0,0 +1,18 @@
import { computed, reactive } from "vue"
import { Role } from "../utils/type"
export const user = reactive({
loaded: false,
username: "",
role: Role.Normal,
})
export const authed = computed(() => !!user.username)
export const roleNormal = computed(
() => authed.value && user.role === Role.Normal,
)
export const roleAdmin = computed(
() => authed.value && user.role === Role.Admin,
)
export const roleSuper = computed(
() => authed.value && user.role === Role.Super,
)