import { useCopy } from "@/hooks/use-copy"; import { ToolUIPart } from "ai"; import { callCodeRunWorker } from "lib/code-runner/call-worker"; import { CodeRunnerResult, LogEntry, } from "lib/code-runner/code-runner.interface"; import { cn, isString, toAny } from "lib/utils"; import { AlertTriangleIcon, CheckIcon, ChevronRight, CopyIcon, Loader, Percent, PlayIcon, } from "lucide-react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { safe } from "ts-safe"; import { CodeBlock } from "ui/CodeBlock"; import { Skeleton } from "ui/skeleton"; import { TextShimmer } from "ui/text-shimmer"; export const CodeExecutor = memo(function CodeExecutor({ part, onResult, type, }: { part: ToolUIPart; onResult?: (result?: any) => void; type: "javascript" | "python"; }) { const isRun = useRef(false); const { copy, copied } = useCopy(); const [isExecuting, setIsExecuting] = useState(false); const lastStartedAt = useRef(Date.now()); const [realtimeLogs, setRealtimeLogs] = useState< (CodeRunnerResult["logs"][number] & { time: number })[] >([]); const codeResultContainerRef = useRef(null); const runCode = useCallback( async (code: string, type: "javascript" | "python") => { lastStartedAt.current = Date.now(); const result = await callCodeRunWorker(type, { code, timeout: 30000, onLog: (log) => { setRealtimeLogs((prev) => [...prev, { ...log, time: Date.now() }]); }, }); return result; }, [], ); const menualToolCall = useCallback( async (code: string) => { const result = await runCode(code, type); const logstring = JSON.stringify(result.logs); onResult?.({ ...toAny({ ...result, logs: logstring.length > 5000 ? [ { type: "info", args: [ { type: "data", value: "Log output exceeded storage limit (10KB). Full output was displayed to user but truncated for server storage.", }, ], }, ] : result.logs, }), guide: "Execution finished. Provide: 1) Main results/outputs 2) Key insights or findings 3) Error explanations if any. Don't repeat code or raw logs - interpret and summarize for the user.", }); }, [onResult], ); const isRunning = useMemo(() => { return isExecuting || part.state.startsWith("input"); }, [isExecuting, part.state]); const scrollToCode = useCallback(() => { codeResultContainerRef.current?.scrollTo({ top: codeResultContainerRef.current.scrollHeight, behavior: "smooth", }); }, []); const result = useMemo(() => { if (part.state.startsWith("input")) return null; return part.output as CodeRunnerResult; }, [part]); const logs = useMemo(() => { const error = result?.error; const logs: (LogEntry & { time?: number })[] = realtimeLogs.length ? realtimeLogs : (result?.logs ?? []); if (error) { logs.push({ type: "error", args: [{ type: "data", value: error }], time: lastStartedAt.current, }); } return logs.map((log, i) => { return (
{new Date(toAny(log).time || Date.now()).toISOString()}
{log.type == "error" ? ( ) : log.type == "warn" ? ( ) : ( )}
{log.args.map((arg, i) => { if (arg.type == "image") { /* eslint-disable-next-line @next/next/no-img-element */ return Code output; } return ( {isString(arg?.value) ? arg.value.toString() : JSON.stringify(arg.value ?? arg)} ); })}
); }); }, [part, realtimeLogs]); const reExecute = useCallback(async () => { if (isExecuting) return; setIsExecuting(true); setRealtimeLogs([ { type: "log", args: [{ type: "data", value: "Re-executing code..." }], time: Date.now(), }, ]); const code = toAny(part.input)?.code; safe(() => runCode(code, type)).watch(() => setIsExecuting(false)); }, [part.input, isExecuting]); const header = useMemo(() => { if (isRunning) return ( <> Generating Code... ); return ( <> {result?.error ? ( <> ERROR ) : (
{type == "javascript" ? "JS" : type == "python" ? "PY" : ">_"}
)} ); }, [part.state, result, isRunning]); const fallback = useMemo(() => { return ; }, []); const logContainer = useMemo(() => { if (!logs.length) return null; return (
{isRunning ? ( ) : (
)} better-chatbot
{logs} {isRunning && (
|
)}
); }, [logs, isRunning]); useEffect(() => { if ( onResult && part.input && part.state == "input-available" && !isRun.current ) { isRun.current = true; menualToolCall(toAny(part.input)?.code); } }, [part.state, !!onResult]); useEffect(() => { if (isRunning) { const closeKey = setInterval(scrollToCode, 300); return () => clearInterval(closeKey); } else if (part.state.startsWith("output") && isRun.current) { scrollToCode(); } }, [isRunning]); return (
{header}
{part.state.startsWith("output") && ( <>
Run
copy(toAny(part.input)?.code ?? "")} > {copied ? ( ) : ( )} Copy
)}
{logContainer}
); }); function CodeFallback() { return (
); }