diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e1a998..f8a6bff 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/katex": "^0.16.7", + "@types/node": "^26.2.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", @@ -1287,6 +1288,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -2279,6 +2290,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index de4e14b..68d010a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/katex": "^0.16.7", + "@types/node": "^26.2.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", diff --git a/frontend/src/lib/actions/renderMath.ts b/frontend/src/lib/actions/renderMath.ts index 24e93d9..167fe84 100644 --- a/frontend/src/lib/actions/renderMath.ts +++ b/frontend/src/lib/actions/renderMath.ts @@ -5,7 +5,7 @@ export async function autoRenderMath(node: HTMLElement) { if (!browser) return; try { - const renderMathInElement = (await import('katex/dist/contrib/auto-render')).default; + const renderMathInElement = (await import('katex/contrib/auto-render')).default; await import('katex/dist/katex.min.css'); // 1. First, handle the standard delimiters diff --git a/frontend/src/lib/components/CircuitEditor.svelte b/frontend/src/lib/components/CircuitEditor.svelte index 75b74c2..3bc166c 100644 --- a/frontend/src/lib/components/CircuitEditor.svelte +++ b/frontend/src/lib/components/CircuitEditor.svelte @@ -127,6 +127,7 @@ $effect(() => { if (simApi && ready && storageKey) { const interval = setInterval(() => { + if (!simApi) return; const currentText = simApi.exportCircuit(); const saved = localStorage.getItem(storageKey); if (currentText && currentText !== saved && currentText.trim().length > 10) { diff --git a/frontend/src/lib/services/ble-deployer.ts b/frontend/src/lib/services/ble-deployer.ts index a53eec0..b31cef3 100644 --- a/frontend/src/lib/services/ble-deployer.ts +++ b/frontend/src/lib/services/ble-deployer.ts @@ -8,6 +8,98 @@ import { type DeployProgress, type BLEACKResponse } from '$types/deployer'; +/* Ambient declarations for Web Bluetooth API (not in TS lib.dom.d.ts). */ +/* Minimal subset needed by BLEHardwareDeployer. */ +/* Wrapped in declare global because the file is a module (has imports) */ +/* and we need these types to be globally visible. */ +declare global { + interface Navigator { + readonly bluetooth: Bluetooth; + } + + interface Bluetooth { + requestDevice(options: RequestDeviceOptions): Promise; + } + + interface RequestDeviceOptions { + filters?: BluetoothLEScanFilter[]; + optionalServices?: BluetoothServiceUUID[]; + acceptAllDevices?: boolean; + } + + interface BluetoothLEScanFilter { + name?: string; + namePrefix?: string; + services?: BluetoothServiceUUID[]; + manufacturerId?: number; + } + + type BluetoothServiceUUID = string | number; + + interface BluetoothDevice { + readonly id: string; + readonly name?: string; + readonly gatt?: BluetoothRemoteGATTServer; + addEventListener( + type: 'gattserverdisconnected', + listener: (this: BluetoothDevice, ev: Event) => void + ): void; + removeEventListener( + type: 'gattserverdisconnected', + listener: (this: BluetoothDevice, ev: Event) => void + ): void; + } + + interface BluetoothRemoteGATTServer { + readonly connected: boolean; + readonly device: BluetoothDevice; + readonly mtu: number; + connect(): Promise; + disconnect(): void; + requestMTU(size: number): Promise; + getPrimaryService(service: BluetoothServiceUUID): Promise; + getPrimaryServices(service?: BluetoothServiceUUID): Promise; + } + + interface BluetoothRemoteGATTService { + readonly device: BluetoothDevice; + readonly uuid: string; + readonly isPrimary: boolean; + getCharacteristic(characteristic: BluetoothServiceUUID): Promise; + getCharacteristics(characteristic?: BluetoothServiceUUID): Promise; + } + + interface BluetoothRemoteGATTCharacteristic extends EventTarget { + readonly service: BluetoothRemoteGATTService; + readonly uuid: string; + readonly properties: BluetoothCharacteristicProperties; + readonly value: DataView | null; + readValue(): Promise; + writeValue(value: BufferSource): Promise; + writeValueWithResponse(value: BufferSource): Promise; + writeValueWithoutResponse(value: BufferSource): Promise; + startNotifications(): Promise; + stopNotifications(): Promise; + addEventListener( + type: 'characteristicvaluechanged', + listener: (this: BluetoothRemoteGATTCharacteristic, ev: Event) => void + ): void; + removeEventListener( + type: 'characteristicvaluechanged', + listener: (this: BluetoothRemoteGATTCharacteristic, ev: Event) => void + ): void; + } + + interface BluetoothCharacteristicProperties { + readonly read: boolean; + readonly write: boolean; + readonly notify: boolean; + readonly indicate: boolean; + readonly writeWithoutResponse: boolean; + readonly broadcast: boolean; + } +} + let _ackNotificationCount = 0; function logAck(tag: string, msg: string) { console.log(`[BLE-ACK#${++_ackNotificationCount}] ${tag}: ${msg}`); diff --git a/frontend/src/lib/types/lesson.ts b/frontend/src/lib/types/lesson.ts index e87f4c6..66a45bf 100644 --- a/frontend/src/lib/types/lesson.ts +++ b/frontend/src/lib/types/lesson.ts @@ -22,6 +22,7 @@ export interface LessonContent { initial_python: string; initial_circuit: string; initial_flowchart?: any; + expected_flowchart?: string; initial_quiz: string; initial_code_arduino: string; velxio_circuit: string; diff --git a/frontend/src/routes/lesson/[slug]/+page.svelte b/frontend/src/routes/lesson/[slug]/+page.svelte index 0f356d7..e643e91 100644 --- a/frontend/src/routes/lesson/[slug]/+page.svelte +++ b/frontend/src/routes/lesson/[slug]/+page.svelte @@ -186,7 +186,7 @@ import QuizQuestionView from './QuizQuestionView.svelte'; {#if pageData.lesson} - {#key pageData.lesson.filename} + {#key mgr.slug}
@@ -343,7 +342,7 @@ import QuizQuestionView from './QuizQuestionView.svelte';
{/if} - {#if !mgr.data?.active_tabs?.length || mgr.data.active_tabs.includes('c') || mgr.data.active_tabs.includes('python')} + {#if mgr.data && (!mgr.data.active_tabs?.length || mgr.data.active_tabs.includes('c') || mgr.data.active_tabs.includes('python'))}
(null); flowchartTab = $state(null); - get slug() { return get(page).params.slug; } + get slug(): string { return get(page).params.slug ?? ''; } isVelxio = $derived(this.data?.active_tabs?.includes('velxio') ?? false); isFlowchart = $derived(this.data?.active_tabs?.includes('flowchart') ?? false); @@ -379,7 +379,7 @@ export class LessonManager { this.activeTab = 'output'; try { const circuitText = this.circuitEditor.getCircuitText(); - const res = evaluateCircuitSubmission(simApi, circuitText, this.isHybrid, this.data, () => this.checkAllPassed()); + const res = evaluateCircuitSubmission(simApi, circuitText, this.isHybrid ?? false, this.data, () => this.checkAllPassed()); if (res.error) { Object.assign(this.circuitOut, { error: res.error, success: false, loading: false }); return;