Skip to content
4 changes: 2 additions & 2 deletions ui/components/Artifacts/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export default function ArtifactContent({
return <HtmlPreview content={activeArtifactData.content} language={activeArtifactData.language || ''} />;
}

if (showLogs) {
return activeArtifactData && <Logs entries={logEntries[activeArtifactData.id] || []} />;
if (showLogs && activeArtifactData) {
return <Logs entries={logEntries[activeArtifactData.id] || []} />;
}

return activeArtifact && renderArtifact(artifacts.find(a => a.id === activeArtifact)!);
Expand Down
57 changes: 11 additions & 46 deletions ui/components/Artifacts/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,15 @@ interface OutputEntry {
type: 'log' | 'info' | 'warning' | 'error' | 'success' | 'output'
}

export default function Logs() {
interface LogsProps {
entries: OutputEntry[];
}

export default function Logs({ entries }: LogsProps) {
const outputRef = useRef<HTMLDivElement>(null)
const [entries, setEntries] = useState<OutputEntry[]>([])
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true)
const [isMinimized, setIsMinimized] = useState(false)

const addEntry = useCallback((content: string, type: OutputEntry['type'] = 'output') => {
setEntries(prevEntries => [...prevEntries, {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
content,
type,
}])
}, [])

useEffect(() => {
if (outputRef.current && isScrolledToBottom) {
outputRef.current.scrollTop = outputRef.current.scrollHeight
Expand All @@ -39,39 +33,6 @@ export default function Logs() {
setIsScrolledToBottom(scrollHeight - scrollTop === clientHeight)
}, [])

// Simulating entries for demonstration
useEffect(() => {
const types: OutputEntry['type'][] = ['log', 'info', 'warning', 'error', 'success', 'output']
const messages = [
"Initializing quantum neural network...",
"Syncing with decentralized cloud nodes...",
"Optimizing AI-driven microservices...",
"Establishing secure blockchain connection...",
"Deploying edge computing resources...",
"Received request: GET /api/v3/quantum-data",
"Error: Temporal anomaly detected in data stream",
"Successfully processed 1 million records in 0.1 seconds",
"> npm run future",
"$ next quantum-dev",
"- Quantum server ready on 0.0.0.0:3000, multiverse url: http://localhost:3000"
]

const addRandomEntry = () => {
const randomType = types[Math.floor(Math.random() * types.length)]
const randomMessage = messages[Math.floor(Math.random() * messages.length)]
addEntry(randomMessage, randomType)
}

// Add initial entries
for (let i = 0; i < 5; i++) {
addRandomEntry()
}

const timer = setInterval(addRandomEntry, 2000)

return () => clearInterval(timer)
}, [addEntry])

const typeStyles = {
error: 'bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30',
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-300 border-yellow-500/30',
Expand Down Expand Up @@ -104,8 +65,12 @@ export default function Logs() {
>
<div className="font-mono text-sm space-y-3">
<AnimatePresence>
{entries.map((entry) => (
<LogEntry key={entry.id} entry={entry} typeStyles={typeStyles} />
{entries.map((entry, index) => (
<LogEntry
key={entry.id || `entry-${index}`}
entry={entry}
typeStyles={typeStyles}
/>
))}
</AnimatePresence>
</div>
Expand Down
68 changes: 55 additions & 13 deletions ui/components/Artifacts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ArtifactActions from './Actions'
import ArtifactSidebar from './Sidebar'
import Code from './Code'
import TextPreview from './TextPreview'
import usePyodide from '@/hooks/usePyodide'

interface ArtifactContent {
id: string;
Expand Down Expand Up @@ -74,6 +75,7 @@ export default function Artifacts({
const [logEntries, setLogEntries] = useState<{[key: string]: OutputEntry[]}>({});
const [copied, setCopied] = useState(false);
const { resolvedTheme } = useTheme();
const { pyodide } = usePyodide();

const activeArtifactData = artifacts.find(a => a.id === activeArtifact);

Expand All @@ -90,20 +92,60 @@ export default function Artifacts({
}
};

const runCode = () => {
const runCode = async () => {
if (activeArtifactData?.type === 'code') {
onRun(activeArtifactData.id, activeArtifactData.content);
setLogEntries(prev => ({
...prev,
[activeArtifactData.id]: [
...(prev[activeArtifactData.id] || []),
{
timestamp: new Date().toISOString(),
content: `Running ${activeArtifactData.name}...`,
type: 'info'
}
]
}));
if (activeArtifactData.language === 'python' && pyodide) {
try {
// Capture console output
let output = '';
const originalConsoleLog = console.log;
console.log = (...args) => {
output += args.join(' ') + '\n';
originalConsoleLog.apply(console, args);
};

const result = await pyodide.runPython(activeArtifactData.content);

// Restore original console.log
console.log = originalConsoleLog;

setLogEntries(prev => ({
...prev,
[activeArtifactData.id]: [
...(prev[activeArtifactData.id] || []),
{
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
content: output.trim(),
type: 'output'
},
// Only add the result if it's not undefined
...(result !== undefined ? [{
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
content: `Result: ${result}`,
type: 'output'
}] : [])
]
}));
} catch (error) {
setLogEntries(prev => ({
...prev,
[activeArtifactData.id]: [
...(prev[activeArtifactData.id] || []),
{
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
content: `Error: ${error.message}`,
type: 'error'
}
]
}));
}
} else {
// Handle other languages or call the original onRun function
onRun(activeArtifactData.id, activeArtifactData.content);
}
setShowLogs(true);
}
};
Expand Down
39 changes: 39 additions & 0 deletions ui/hooks/usePyodide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useState, useEffect } from "react";

declare global {
interface Window {
loadPyodide?: () => Promise<any>;
}
}

const PYODIDE_VERSION = "0.25.0";

export default function usePyodide() {
const [pyodide, setPyodide] = useState<any>(null);

useEffect(() => {
const loadPyodide = async () => {
if (typeof window !== 'undefined' && !window.loadPyodide) {
const script = document.createElement('script');
script.src = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.js`;
script.async = true;
script.onload = async () => {
const loadedPyodide = await (window as any).loadPyodide({
indexURL: `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`,
});
setPyodide(loadedPyodide);
};
document.body.appendChild(script);
} else if (typeof window !== 'undefined' && window.loadPyodide) {
const loadedPyodide = await (window as any).loadPyodide({
indexURL: `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`,
});
setPyodide(loadedPyodide);
}
};

loadPyodide();
}, []);

return { pyodide };
}