diff --git a/package-lock.json b/package-lock.json index 76f48dd..f20859d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chatgemini", - "version": "0.5.4", + "version": "0.5.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chatgemini", - "version": "0.5.4", + "version": "0.5.5", "dependencies": { "@fancyapps/ui": "^5.0.33", "@fingerprintjs/fingerprintjs": "^4.2.2", diff --git a/package.json b/package.json index e32b670..61a1f0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chatgemini", - "version": "0.5.4", + "version": "0.5.5", "homepage": ".", "private": true, "dependencies": { diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 5ac9c6e..5a64589 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -13,26 +13,34 @@ import userDebounce from "../helpers/userDebounce"; import { Point } from "unist"; import { isObjectEqual } from "../helpers/isObjectEqual"; import { getPythonResult } from "../helpers/getPythonResult"; -import { loadPyodide } from "pyodide"; +import { PyodideInterface } from "pyodide"; import { getPythonRuntime } from "../helpers/getPythonRuntime"; interface MarkdownProps { readonly className?: string; readonly typingEffect: string; + readonly pythonRuntime: PyodideInterface | null; + readonly onPythonRuntimeCreated: (pyodide: PyodideInterface) => void; readonly children: string; } +const TraceLog = "😈 [TRACE]"; +const DebugLog = "πŸš€ [DEBUG]"; +const ErrorLog = "🀬 [ERROR]"; +const PythonScriptDisplayName = "script.py"; const RunnerResultPlaceholder = ` -😈 [Info] η»“ζžœιœ€δ»₯ print θΎ“ε‡Ί -πŸš€ [Info] ε°θ―•ζ‰§θ‘Œ Python θ„šζœ¬... -`; +${DebugLog} η»“ζžœιœ€θ°ƒη”¨ print 打印 +${DebugLog} ε°θ―•ζ‰§θ‘Œ Python θ„šζœ¬...`; export const Markdown = (props: MarkdownProps) => { - const { className, typingEffect, children } = props; + const { + className, + typingEffect, + pythonRuntime, + onPythonRuntimeCreated, + children, + } = props; - const [pythonRuntime, setPythonRuntime] = useState | null>(null); const [pythonResult, setPythonResult] = useState<{ result: string; startPos: Point | null; @@ -54,10 +62,34 @@ export const Markdown = (props: MarkdownProps) => { ); const handleRunnerResult = (x: string) => - setPythonResult((prev) => ({ - ...prev, - result: `${prev.result.replace(RunnerResultPlaceholder, "")}\n${x}`, - })); + setPythonResult((prev) => { + let result = prev.result.replace(RunnerResultPlaceholder, ""); + if (result.includes(TraceLog)) { + result = result + .split("\n") + .filter((x) => !x.includes(TraceLog)) + .join("\n"); + } + return { ...prev, result: `${result}\n${x}` }; + }); + + const handleRunnerImporting = (x: string, err: boolean) => + setPythonResult((prev) => { + let { result } = prev; + if (err) { + result += `\n${ErrorLog} ${x}`; + } else { + result += `\n${TraceLog} ${x}`; + } + return { ...prev, result }; + }); + + const handleJobFinished = () => + setPythonResult((prev) => { + let { result } = prev; + result += `\n$`; + return { ...prev, result }; + }); const handleRunPython = userDebounce( async ( @@ -66,24 +98,28 @@ export const Markdown = (props: MarkdownProps) => { code: string, currentTarget: EventTarget ) => { - let runtime: ReturnType | null; - if (pythonRuntime) { - runtime = pythonRuntime; - } else { - runtime = getPythonRuntime( - `${window.location.pathname}pyodide/`, - handleRunnerResult, - handleRunnerResult - ); - setPythonRuntime(runtime); - } (currentTarget as HTMLButtonElement).disabled = true; setPythonResult({ - result: `$ python3 script.py${RunnerResultPlaceholder}`, + result: `$ python3 ${PythonScriptDisplayName}${RunnerResultPlaceholder}`, startPos, endPos, }); - await getPythonResult(runtime, code, handleRunnerResult); + let runtime = pythonRuntime; + if (!runtime) { + runtime = await getPythonRuntime( + `${window.location.pathname}pyodide/` + ); + onPythonRuntimeCreated(runtime); + } + await getPythonResult( + runtime, + code, + handleRunnerResult, + handleRunnerResult, + handleRunnerImporting, + handleRunnerResult, + handleJobFinished + ); (currentTarget as HTMLButtonElement).disabled = false; }, 300 diff --git a/src/helpers/getPythonResult.tsx b/src/helpers/getPythonResult.tsx index 2fe79a4..07a8a41 100644 --- a/src/helpers/getPythonResult.tsx +++ b/src/helpers/getPythonResult.tsx @@ -1,9 +1,13 @@ -import { loadPyodide } from "pyodide"; +import { PyodideInterface } from "pyodide"; export const getPythonResult = async ( - pyodide: ReturnType, + pyodide: PyodideInterface, code: string, - onException: (x: string) => void + onStdout: (x: string) => void, + onStderr: (x: string) => void, + onImporting: (x: string, err: boolean) => void, + onException: (x: string) => void, + onJobFinished: () => void ) => { const availablePackages = [ { keyword: "numpy", package: "numpy" }, @@ -23,6 +27,8 @@ export const getPythonResult = async ( { keyword: "hashlib", package: "hashlib" }, ]; try { + pyodide.setStdout({ batched: onStdout }); + pyodide.setStderr({ batched: onStderr }); const matchedPackages = availablePackages .filter( ({ keyword }) => @@ -31,18 +37,19 @@ export const getPythonResult = async ( ) .map(({ package: pkg }) => pkg); if (!!matchedPackages.length) { - await (await pyodide).loadPackage(matchedPackages); + await pyodide.loadPackage(matchedPackages, { + errorCallback: (x) => onImporting(x, true), + messageCallback: (x) => onImporting(x, false), + }); } - await ( - await pyodide - ).runPythonAsync(` -from js import prompt -def input(p): - return prompt(p) -__builtins__.input = input -`); - await (await pyodide).runPythonAsync(code); + await pyodide.runPythonAsync(code); } catch (e) { - onException(`${e}`); + let err = String(e); + if (err.endsWith("\n")) { + err = err.slice(0, -1); + } + onException(err); + } finally { + onJobFinished(); } }; diff --git a/src/helpers/getPythonRuntime.tsx b/src/helpers/getPythonRuntime.tsx index fcccf7e..736a4e5 100644 --- a/src/helpers/getPythonRuntime.tsx +++ b/src/helpers/getPythonRuntime.tsx @@ -1,13 +1,15 @@ import { loadPyodide } from "pyodide"; -export const getPythonRuntime = ( - repoURL: string, - onStdout: (x: string) => void, - onStderr: (x: string) => void -) => - loadPyodide({ +export const getPythonRuntime = async (repoURL: string) => { + const pyodide = await loadPyodide({ indexURL: repoURL, - stdout: onStdout, - stderr: onStderr, homedir: "/home/user", }); + await pyodide.runPythonAsync(` +from js import prompt +def input(p): + return prompt(p) +__builtins__.input = input +`); + return pyodide; +}; diff --git a/src/views/Chat.tsx b/src/views/Chat.tsx index c16fc08..f012159 100644 --- a/src/views/Chat.tsx +++ b/src/views/Chat.tsx @@ -18,6 +18,7 @@ import { ImageView } from "../components/ImageView"; import { sendUserConfirm } from "../helpers/sendUserConfirm"; import { sendUserAlert } from "../helpers/sendUserAlert"; import { RouterComponentProps } from "../config/router"; +import { PyodideInterface } from "pyodide"; const RefreshPlaceholder = "ι‡ζ–°η”ŸζˆδΈ­..."; const FallbackIfIdInvalid = @@ -42,15 +43,20 @@ const Chat = (props: RouterComponentProps) => { const [attachmentsURL, setAttachmentsURL] = useState< Record >({}); + const [pythonRuntime, setPythonRuntime] = useState( + null + ); + + const handlePythonRuntimeCreated = (pyodide: PyodideInterface) => + setPythonRuntime(pyodide); const scrollToBottom = useCallback( - (force: boolean = false) => { + (force: boolean = false) => (ai.busy || force) && - mainSectionRef?.scrollTo({ - top: mainSectionRef.scrollHeight, - behavior: "smooth", - }); - }, + mainSectionRef?.scrollTo({ + top: mainSectionRef.scrollHeight, + behavior: "smooth", + }), [ai, mainSectionRef] ); @@ -251,7 +257,13 @@ const Chat = (props: RouterComponentProps) => { !!data.length ? attachmentPostscriptHtml : "" } > - {`${parts}${ + {`${parts}${ !!data.length ? attachmentPostscriptHtml : "" }`}