Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
"author": "DreamTeam Mobile <contact@dreamteam-mobile.com>",
"license": "UNLICENSED",
"dependencies": {
"@ruby/wasm-wasi": "^2.8.1",
"jspdf": "^2.5.2",
"prismjs": "^1.29.0",
"pyodide": "^0.29.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"wasmoon": "^1.16.0",
"zustand": "^5.0.11"
},
"devDependencies": {
Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/LanguageSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ describe('LanguageSelector', () => {
expect(select.value).toBe('javascript');
});

it('renders all 15 supported languages', () => {
it('renders all 16 supported languages', () => {
const { container } = render(<LanguageSelector />);
const options = container.querySelectorAll('option');
expect(options).toHaveLength(15);
expect(options).toHaveLength(16);
});

it('displays human-readable labels', () => {
const { getByText } = render(<LanguageSelector />);
// Executable languages get a ▶ prefix
// Executable languages get a ▶ prefix (JS, TS, Python, C, C++, Go, Ruby, Lua)
expect(getByText(/JavaScript/)).toBeInTheDocument();
expect(getByText(/TypeScript/)).toBeInTheDocument();
expect(getByText('Python')).toBeInTheDocument();
expect(getByText('C++')).toBeInTheDocument();
expect(getByText(/Python/)).toBeInTheDocument();
expect(getByText(/C\+\+/)).toBeInTheDocument();
expect(getByText('C#')).toBeInTheDocument();
});

Expand Down
1 change: 1 addition & 0 deletions src/components/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-go';
import 'prismjs/components/prism-rust';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-lua';
import 'prismjs/components/prism-swift';
import 'prismjs/components/prism-scala';
import 'prismjs/components/prism-markup';
Expand Down
13 changes: 11 additions & 2 deletions src/components/CodeEditor/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEditorStore } from '../../stores/editorStore';
import { codeTemplates } from '../../services/code-editor-logic';
import { isExecutable } from '../../services/code-executor';
import { isExecutable, isWasmLanguage, preloadRuntime } from '../../services/code-executor';

const LANGUAGES = Object.keys(codeTemplates);

Expand All @@ -16,6 +16,7 @@ const LANGUAGE_LABELS: Record<string, string> = {
go: 'Go',
rust: 'Rust',
ruby: 'Ruby',
lua: 'Lua',
swift: 'Swift',
scala: 'Scala',
php: 'PHP',
Expand All @@ -26,11 +27,19 @@ export default function LanguageSelector() {
const language = useEditorStore((s) => s.language);
const setLanguage = useEditorStore((s) => s.setLanguage);

const handleChange = (newLang: string) => {
setLanguage(newLang);
// Start preloading WASM runtime in background when user switches
if (isWasmLanguage(newLang)) {
preloadRuntime(newLang);
}
};

return (
<div id="languageSelector">
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
onChange={(e) => handleChange(e.target.value)}
aria-label="Select language"
>
{LANGUAGES.map((lang) => (
Expand Down
39 changes: 33 additions & 6 deletions src/components/CodeEditor/OutputPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
import { useRef, useEffect } from 'react';
import { useExecutionStore } from '../../stores/executionStore';
import { useEditorStore } from '../../stores/editorStore';
import { isExecutable } from '../../services/code-executor';
import { useRuntimeStore } from '../../stores/runtimeStore';
import { isExecutable, isWasmLanguage } from '../../services/code-executor';

const LANGUAGE_NAMES: Record<string, string> = {
python: 'Python',
c: 'C',
cpp: 'C++',
go: 'Go',
ruby: 'Ruby',
lua: 'Lua',
};

export default function OutputPanel() {
const isRunning = useExecutionStore((s) => s.isRunning);
const output = useExecutionStore((s) => s.output);
const panelExpanded = useExecutionStore((s) => s.panelExpanded);
const togglePanel = useExecutionStore((s) => s.togglePanel);
const language = useEditorStore((s) => s.language);
const runtimeInfo = useRuntimeStore((s) => s.getRuntime(language === 'c' ? 'cpp' : language));
const outputRef = useRef<HTMLPreElement>(null);

const isWasm = isWasmLanguage(language);
const isLoading = isWasm && runtimeInfo.status === 'loading';

// Auto-scroll to bottom when output changes
useEffect(() => {
if (outputRef.current && panelExpanded) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, [output, panelExpanded]);

// Hide when language doesn't support execution, or no output and not running
if (!isExecutable(language) || (!output && !isRunning)) return null;
// Hide when language doesn't support execution, or no output/loading/running
if (!isExecutable(language) || (!output && !isRunning && !isLoading)) return null;

const hasError = output && output.exitCode !== 0;
const hasOutput = output && (output.stdout || output.stderr);
const langName = LANGUAGE_NAMES[language] || language;

return (
<div className={`output-panel${panelExpanded ? ' expanded' : ' collapsed'}`}>
<div className="output-header" onClick={togglePanel}>
<div className="output-header-left">
{isRunning ? (
{isLoading ? (
<span className="output-spinner" />
) : isRunning ? (
<span className="output-spinner" />
) : output ? (
<span className={`output-status-icon ${hasError ? 'error' : 'success'}`}>
Expand All @@ -39,7 +56,12 @@ export default function OutputPanel() {
{output && (
<span className="output-duration">{output.duration}ms</span>
)}
{isRunning && (
{isLoading && (
<span className="output-running-label">
Downloading {langName} runtime... {runtimeInfo.progress > 0 ? `${runtimeInfo.progress}%` : ''}
</span>
)}
{isRunning && !isLoading && (
<span className="output-running-label">Running...</span>
)}
</div>
Expand All @@ -60,7 +82,12 @@ export default function OutputPanel() {
</div>
{panelExpanded && (
<pre className="output-body" ref={outputRef}>
{isRunning && !hasOutput && (
{isLoading && !hasOutput && (
<span className="output-placeholder">
Loading {langName} runtime... {runtimeInfo.progress > 0 ? `${runtimeInfo.progress}%` : 'please wait'}
</span>
)}
{isRunning && !hasOutput && !isLoading && (
<span className="output-placeholder">Executing...</span>
)}
{output?.stdout && (
Expand Down
54 changes: 53 additions & 1 deletion src/components/TabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useUIStore } from '../stores/uiStore';
import { useEditorStore } from '../stores/editorStore';
import { useExecutionStore } from '../stores/executionStore';
import { isExecutable } from '../services/code-executor';
import { useRuntimeStore } from '../stores/runtimeStore';
import { isExecutable, isWasmLanguage } from '../services/code-executor';
import LanguageSelector from './CodeEditor/LanguageSelector';

export default function TabBar() {
Expand All @@ -11,8 +12,18 @@ export default function TabBar() {
const isRunning = useExecutionStore((s) => s.isRunning);
const runCode = useExecutionStore((s) => s.runCode);
const cancelCode = useExecutionStore((s) => s.cancelCode);
const runtimeInfo = useRuntimeStore((s) => s.getRuntime(language === 'c' ? 'cpp' : language));

const showRunButton = activeTab === 'code' && isExecutable(language);
const isWasm = isWasmLanguage(language);
const isLoading = isWasm && runtimeInfo.status === 'loading';
const hasError = isWasm && runtimeInfo.status === 'error';
const progress = runtimeInfo.progress;

// SVG progress ring params
const radius = 6;
const circumference = 2 * Math.PI * radius;
const strokeOffset = circumference - (progress / 100) * circumference;

return (
<div id="tabBarWrapper">
Expand Down Expand Up @@ -43,6 +54,47 @@ export default function TabBar() {
<rect x="3" y="3" width="10" height="10" rx="1" />
</svg>
</button>
) : isLoading ? (
<button
className="icon-btn run-btn loading"
disabled
title={`Loading runtime... ${progress}%`}
aria-label={`Loading runtime ${progress}%`}
>
<svg width="16" height="16" viewBox="0 0 16 16">
{/* Background circle */}
<circle
cx="8" cy="8" r={radius}
fill="none"
stroke="var(--text-secondary)"
strokeWidth="2"
opacity="0.3"
/>
{/* Progress arc */}
<circle
cx="8" cy="8" r={radius}
fill="none"
stroke="var(--accent)"
strokeWidth="2"
strokeDasharray={circumference}
strokeDashoffset={progress > 0 ? strokeOffset : 0}
strokeLinecap="round"
transform="rotate(-90 8 8)"
className={progress === 0 ? 'progress-ring-indeterminate' : ''}
/>
</svg>
</button>
) : hasError ? (
<button
className="icon-btn run-btn error"
onClick={() => runCode?.()}
title={`Runtime error — click to retry: ${runtimeInfo.error ?? ''}`}
aria-label="Retry loading runtime"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="var(--danger)">
<path d="M8 1l7 14H1L8 1zm0 5v4m0 2v1" fill="none" stroke="var(--danger)" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</button>
) : (
<button
className="icon-btn run-btn"
Expand Down
Loading