添加AI分析
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
2025-09-28 10:51:04 +08:00
parent 0f0312529b
commit c6d2e17476
8 changed files with 250 additions and 82 deletions

View File

@@ -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)
}

150
src/composables/analysis.ts Normal file
View File

@@ -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<ArrayBuffer>
>
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),
)

View File

@@ -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())
<n-tag v-if="status === Status.Accepted" type="success">
运行成功
</n-tag>
<n-tag v-if="showAnalyse" type="warning">运行失败</n-tag>
<n-popover
v-if="showAnalyse && code.language === 'python'"
trigger="click"
>
<n-tag v-if="showAnalysis" type="warning">运行失败</n-tag>
<n-popover v-if="showAnalysis" trigger="click">
<template #trigger>
<n-button quaternary type="error" @click="analyzeError">
<n-button quaternary type="error" @click="getAIAnalysis">
推测原因
</n-button>
</template>
<template #header v-if="analyse.line > 0">
错误在第
<n-tag type="error">
<b>{{ analyse.line }}</b>
</n-tag>
</template>
<span v-if="analyse.message">
{{ analyse.message }}
</span>
<n-spin :show="loading">
<div
class="analysisPanel"
v-html="marked.parse(analysis)"
></div>
</n-spin>
</n-popover>
</template>
</CodeEditor>
@@ -174,4 +173,11 @@ watch(turtleRunId, () => runSkulptTurtle())
width: 100%;
height: 100%;
}
.analysisPanel {
width: 400px;
min-height: 60px;
max-height: 300px;
overflow: auto;
}
</style>

View File

@@ -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,
],
})

View File

@@ -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,