From c6d2e17476b049a1f3bed00c37bc6b6c0aa87fdf Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 28 Sep 2025 10:51:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0AI=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- package-lock.json | 78 ++++++++++++++++--- package.json | 3 + src/composables/analyse.ts | 53 ------------- src/composables/analysis.ts | 150 ++++++++++++++++++++++++++++++++++++ src/desktop/Content.vue | 42 +++++----- src/main.ts | 2 + src/templates.ts | 2 +- 8 files changed, 250 insertions(+), 82 deletions(-) delete mode 100644 src/composables/analyse.ts create mode 100644 src/composables/analysis.ts diff --git a/.env b/.env index 9bb1551..6da22fa 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ PUBLIC_JUDGE0API_URL=https://judge0api.xuyue.cc PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=2e801f7d6efdcc99 -PUBLIC_CODEAPI_URL=https://code.xuyue.cc/api +PUBLIC_CODEAPI_URL=http://localhost:8080 PUBLIC_PYVIZ_URL=https://pyviz.xuyue.cc \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eeb2bca..eed4006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,9 @@ "copy-text-to-clipboard": "^3.2.1", "fflate": "^0.8.2", "file-saver": "^2.0.5", + "highlight.js": "^11.11.1", + "marked": "^16.3.0", + "marked-highlight": "^2.2.2", "naive-ui": "^2.43.1", "normalize.css": "^8.0.1", "query-string": "^9.3.0", @@ -441,6 +444,7 @@ "integrity": "sha512-1/yyJJfZo4hqMsL3WQQmMDYFp0L/znHqjHrYE6NKsiKhkBEwEwSVMk1M5QoRu2EcRL1acW5AJf7WJyKFfPZ//Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.5.4", "@rspack/lite-tapable": "~1.0.1", @@ -667,6 +671,7 @@ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -1077,6 +1082,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1103,6 +1109,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1203,6 +1210,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -1294,6 +1302,7 @@ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -1376,6 +1385,7 @@ "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" @@ -1397,6 +1407,7 @@ "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -1812,9 +1823,10 @@ } }, "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" } @@ -1909,6 +1921,28 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", + "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/marked-highlight": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.2.tgz", + "integrity": "sha512-KlHOP31DatbtPPXPaI8nx1KTrG3EW0Z5zewCwpUj65swbtKOTStteK3sNAjBqV75Pgo3fNEVNHeptg18mDuWgw==", + "license": "MIT", + "peerDependencies": { + "marked": ">=4 <17" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2308,6 +2342,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2383,6 +2418,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", @@ -2480,6 +2516,7 @@ "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -2887,6 +2924,7 @@ "resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.7.tgz", "integrity": "sha512-1/yyJJfZo4hqMsL3WQQmMDYFp0L/znHqjHrYE6NKsiKhkBEwEwSVMk1M5QoRu2EcRL1acW5AJf7WJyKFfPZ//Q==", "dev": true, + "peer": true, "requires": { "@rspack/core": "1.5.4", "@rspack/lite-tapable": "~1.0.1", @@ -3018,6 +3056,7 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, + "peer": true, "requires": { "tslib": "^2.8.0" } @@ -3373,7 +3412,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-import-phases": { "version": "1.0.4", @@ -3387,6 +3427,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3446,6 +3487,7 @@ "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.0.tgz", "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -3499,6 +3541,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "peer": true, "requires": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -3558,6 +3601,7 @@ "version": "0.15.14", "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "peer": true, "requires": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" @@ -3578,7 +3622,8 @@ "date-fns": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "peer": true }, "date-fns-tz": { "version": "3.2.0", @@ -3843,9 +3888,9 @@ } }, "highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==" + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" }, "jest-worker": { "version": "27.5.1", @@ -3916,6 +3961,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "marked": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", + "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", + "peer": true + }, + "marked-highlight": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.2.tgz", + "integrity": "sha512-KlHOP31DatbtPPXPaI8nx1KTrG3EW0Z5zewCwpUj65swbtKOTStteK3sNAjBqV75Pgo3fNEVNHeptg18mDuWgw==", + "requires": {} + }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4165,7 +4222,8 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true + "devOptional": true, + "peer": true }, "undici-types": { "version": "7.8.0", @@ -4203,6 +4261,7 @@ "version": "3.5.21", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", + "peer": true, "requires": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", @@ -4267,6 +4326,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 3025fad..eee7b27 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "copy-text-to-clipboard": "^3.2.1", "fflate": "^0.8.2", "file-saver": "^2.0.5", + "highlight.js": "^11.11.1", + "marked": "^16.3.0", + "marked-highlight": "^2.2.2", "naive-ui": "^2.43.1", "normalize.css": "^8.0.1", "query-string": "^9.3.0", diff --git a/src/composables/analyse.ts b/src/composables/analyse.ts deleted file mode 100644 index ca7e8cc..0000000 --- a/src/composables/analyse.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { computed, reactive } from "vue" -import { Status } from "../types" -import { output, status } from "./code" - -export const analyse = reactive({ - line: -1, - message: "", -}) - -export const showAnalyse = computed( - () => ![Status.Accepted, Status.NotStarted].includes(status.value), -) - -function findError(line: string, language = "python") { - const python: any = { - "EOFError: EOF when reading a line": "需要在输入框填写输入信息", - "SyntaxError: invalid character in identifier": - "可能是单词拼写错误,可能是括号、引号写成中文的了", - "SyntaxError: invalid syntax": "语法错误,不合法的语法", - "SyntaxError: EOL while scanning string literal": - "可能是这一行最后一个符号是中文的,或者引号、括号不匹配", - "NameError: name '(.*?)' is not defined": (name: string) => - `命名错误,${name} 不知道是什么东西`, - "IndentationError: expected an indented block": "缩进错误:这一行需要缩进", - 'TypeError: can only concatenate str \\(not "(.*?)"\\) to str': - "文字和数字不能相加", - } - const c: any = {} - const regex = { c, python }[language] - let message = "" - for (let r in regex) { - const err = line.match(r) - if (err) { - if (typeof regex[r] === "function") { - message = regex[r](err[1]) - } else { - message = regex[r] - } - break - } - } - return message -} - -export function analyzeError() { - const line = output.value.match(/File "script.py", line (\d+)/) - if (line) { - analyse.line = parseInt(line[1]) - } - const lines = output.value.split("\n") - const lastLine = lines[lines.length - 1] - analyse.message = findError(lastLine) -} diff --git a/src/composables/analysis.ts b/src/composables/analysis.ts new file mode 100644 index 0000000..c6ca8d1 --- /dev/null +++ b/src/composables/analysis.ts @@ -0,0 +1,150 @@ +import { computed, ref } from "vue" +import { Status } from "../types" +import { output, status, code } from "./code" + +export const analysis = ref("") +export const loading = ref(false) + +export async function getAIAnalysis() { + analysis.value = "" + // 使用 streaming 流式方式 fetch /ai/analysis 接口,传入 code 和 error_info + const baseUrl = import.meta.env.PUBLIC_CODEAPI_URL + loading.value = true + try { + const response = await fetch(`${baseUrl}/ai`, { + method: "POST", + body: JSON.stringify({ + code: code.value, + language: code.language, + error_info: output.value, + }), + }) + const reader = response.body?.getReader() + if (!reader) return + + const decoder = new TextDecoder() + let buffer = "" + let eventLines: string[] = [] + let currentEvent: string | null = null + + const flushEvent = () => { + if (eventLines.length === 0) return false + const raw = eventLines.join("\n") + eventLines = [] + const event = currentEvent ?? "message" + currentEvent = null + + if (!raw) return false + + let payload: unknown + try { + payload = JSON.parse(raw) + } catch (error) { + // eslint-disable-next-line no-console + console.error("无法解析 SSE 数据", error, raw) + return false + } + + const data = (payload as { data?: string }).data ?? "" + const message = (payload as { message?: string }).message ?? "" + + if (event === "chunk") { + appendContent(data) + return false + } + + if (event === "error") { + if (loading.value) { + loading.value = false + } + if (message) { + appendContent(`\n[错误] ${message}`) + } + return true + } + + if (event === "done") { + if (loading.value) { + loading.value = false + } + return true + } + + appendContent(data || message) + return false + } + + const processLine = (line: string) => { + if (line === "") { + return flushEvent() + } + + if (line.startsWith("event:")) { + currentEvent = line.slice(6).trimStart() + return false + } + + if (!line.startsWith("data:")) return false + + let value = line.slice(5) + if (value.startsWith(" ")) { + value = value.slice(1) + } + eventLines.push(value) + return false + } + + const processBuffer = (final = false) => { + const lines = buffer.split("\n") + if (!final) { + buffer = lines.pop() ?? "" + } else { + buffer = "" + } + + for (const line of lines) { + const shouldStop = processLine(line) + if (shouldStop) { + return true + } + } + + if (final) { + return processLine("") + } + + return false + } + const appendContent = (segment: string) => { + if (!segment) return + analysis.value += segment + if (loading.value) { + loading.value = false + } + } + + while (true) { + const { done, value } = (await reader.read()) as ReadableStreamReadResult< + Uint8Array + > + if (done) break + + buffer += decoder.decode(value, { stream: true }) + if (processBuffer()) { + return + } + } + + if (processBuffer(true)) { + return + } + } finally { + if (loading.value) { + loading.value = false + } + } +} + +export const showAnalysis = computed( + () => ![Status.Accepted, Status.NotStarted].includes(status.value), +) diff --git a/src/desktop/Content.vue b/src/desktop/Content.vue index df248d1..662e981 100644 --- a/src/desktop/Content.vue +++ b/src/desktop/Content.vue @@ -2,10 +2,16 @@ import copyTextToClipboard from "copy-text-to-clipboard" import { useMessage } from "naive-ui" import { computed, watch, useTemplateRef } from "vue" +import { marked } from "marked" // @ts-ignore import * as Sk from "skulpt" import CodeEditor from "../components/CodeEditor.vue" -import { analyse, analyzeError, showAnalyse } from "../composables/analyse" +import { + analysis, + loading, + getAIAnalysis, + showAnalysis, +} from "../composables/analysis" import { clearInput, code, @@ -55,7 +61,7 @@ function runSkulptTurtle() { Sk.TurtleGraphics = { target: canvas, width: canvas.clientWidth, - height: canvas.clientHeight + height: canvas.clientHeight, } Sk.misceval .asyncToPromise(function () { @@ -134,26 +140,19 @@ watch(turtleRunId, () => runSkulptTurtle()) 运行成功 - 运行失败 - + 运行失败 + - - - {{ analyse.message }} - + +
+
@@ -174,4 +173,11 @@ watch(turtleRunId, () => runSkulptTurtle()) width: 100%; height: 100%; } + +.analysisPanel { + width: 400px; + min-height: 60px; + max-height: 300px; + overflow: auto; +} diff --git a/src/main.ts b/src/main.ts index e059752..4eb1494 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,7 @@ import { NTabPane, NTabs, NTag, + NSpin, create, } from "naive-ui" import "normalize.css" @@ -45,6 +46,7 @@ const naive = create({ NTabs, NTabPane, NDropdown, + NSpin, ], }) diff --git a/src/templates.ts b/src/templates.ts index 82c3cde..172ef79 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -17,7 +17,7 @@ for i in range(4): turtle.done()` -export const languageToId = { +export const languageToId: { [key in string]: number } = { c: 50, cpp: 54, java: 62,