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
123 changes: 123 additions & 0 deletions packages/app/src/amicode/inspector/device-inspector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { For, Show, createMemo } from "solid-js"
import type { createInspectorBridge } from "./inspector-bridge"

type Props = { bridge: ReturnType<typeof createInspectorBridge> }

function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null
}

export function DeviceInspector(props: Props) {
const devices = () => Array.from(props.bridge.devices().entries())
const active = createMemo(() => {
const id = props.bridge.activeDevice()
if (id && props.bridge.devices().has(id)) return { id, data: props.bridge.devices().get(id)! }
const first = devices()[0]
return first ? { id: first[0], data: first[1] } : undefined
})

const refresh = (device: string) => {
window.parent.postMessage({ source: "amicode", kind: "device:refresh", device }, "*")
// also try direct parent (when not in deck pane iframe indirection the host still listens)
window.postMessage({ source: "amicode", kind: "device:refresh", device }, "*")
}

return (
<div class="flex flex-col gap-3 p-3" data-component="device-inspector">
<div class="flex items-center justify-between">
<div class="text-12-medium">Device Inspector</div>
<Show when={devices().length > 1}>
<select
class="text-12-regular border border-border-weaker-base rounded px-1 py-0.5 bg-background-base max-w-[140px] truncate"
value={active()?.id ?? ""}
onChange={(e) => props.bridge.setActiveDevice(e.currentTarget.value)}
>
<For each={devices()}>{([id]) => <option value={id}>{id}</option>}</For>
</select>
</Show>
</div>

<Show when={!active()} fallback={
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<div class="text-11-medium truncate">{active()!.id}</div>
<button class="ml-auto text-11-regular border rounded px-2 py-0.5" onClick={() => refresh(active()!.id)}>
Refresh
</button>
</div>

<Show when={isRecord(active()!.data.status) && (active()!.data.status as Record<string, unknown>).driveLines}>
{(dl) => (
<div>
<div class="text-11-medium text-text-faint">Drive lines</div>
<div class="flex flex-wrap gap-1 mt-1">
<For each={(active()!.data.status as { driveLines: { id: string; online: boolean }[] }).driveLines}>
{(d) => (
<span class="text-11-regular border rounded-full px-2 py-0.5" data-online={d.online}>
{d.id} {d.online ? "●" : "○"}
</span>
)}
</For>
</div>
</div>
)}
</Show>

<Show when={isRecord(active()!.data.status) && (active()!.data.status as Record<string, unknown>).qubits}>
<div>
<div class="text-11-medium text-text-faint">Qubits</div>
<div class="flex flex-wrap gap-1 mt-1">
<For each={(active()!.data.status as { qubits: { qubit: string; status: string }[] }).qubits}>
{(q) => <span class="text-11-regular border rounded px-2 py-0.5">{q.qubit}: {q.status}</span>}
</For>
</div>
</div>
</Show>

<Show when={isRecord(active()!.data.status) && (active()!.data.status as Record<string, unknown>).metrics}>
<div>
<div class="text-11-medium text-text-faint">Metrics</div>
<For each={Object.entries((active()!.data.status as { metrics: Record<string, { value: number; ageSeconds: number; status: string }> }).metrics)}>
{([k, m]) => (
<div class="flex justify-between text-11-regular border-b border-border-weaker-base py-1">
<span>{k}</span>
<span class="font-mono">
{m.value.toFixed(4)} · {m.status} · {Math.round(m.ageSeconds)}s ago
</span>
</div>
)}
</For>
</div>
</Show>

<Show when={isRecord(active()!.data.status) && (active()!.data.status as Record<string, unknown>).calibrationParams}>
<div>
<div class="text-11-medium text-text-faint">Calibration params</div>
<div class="text-11-regular font-mono whitespace-pre-wrap break-all border rounded p-2 bg-[var(--v2-background-bg-subtle)]">
{JSON.stringify((active()!.data.status as { calibrationParams: unknown }).calibrationParams, null, 2)}
</div>
</div>
</Show>

<Show when={active()!.data.actions.length > 0}>
<div>
<div class="text-11-medium text-text-faint">Recommended actions</div>
<For each={active()!.data.actions as { node: string; action: string; locked?: boolean; reason?: string }[]}>
{(a) => (
<div class="border rounded p-2 mt-1 text-11-regular" data-locked={a.locked}>
<div class="font-mono">{a.node} → {a.action} {a.locked ? "🔒" : ""}</div>
<Show when={a.reason}>
<div class="text-text-weak">{a.reason}</div>
</Show>
</div>
)}
</For>
</div>
</Show>
</div>
}>
<div class="text-12-regular text-text-weak py-8 text-center">No device — configure one in settings.</div>
</Show>
</div>
)
}
122 changes: 122 additions & 0 deletions packages/app/src/amicode/inspector/inspector-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { createSignal, onCleanup } from "solid-js"

export type RunIteration = { runId: string; iter: number; objective: number; inf_pr: number; inf_du: number }
export type RunPulseMeta = { runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string }
export type RunPulse = { runId: string; iter: number; dt: number; values: number[][] }
export type RunCompletion = { runId: string; fidelity: number; iterations: number; status: string }
export type RunBridgeMessage =
| { type: "run:iteration"; runId: string; iter: number; objective: number; inf_pr: number; inf_du: number }
| { type: "run:pulse-meta"; runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string }
| { type: "run:pulse"; runId: string; iter: number; dt: number; values: number[][] }
| { type: "run:completion"; runId: string; fidelity: number; iterations: number; status: string }
| { type: "run:activate"; runId: string }
| { type: "run:timing"; runId: string; elapsed: number }
| { type: "run:label"; runId: string; label: string }

export type DeviceBridgeMessage =
| { type: "device:status"; device: string; status: unknown }
| { type: "device:actions"; device: string; actions: unknown[] }
| { type: "device:activate"; device: string }

export type InspectorMessage = RunBridgeMessage | DeviceBridgeMessage

type RunState = {
label?: string
iterations: RunIteration[]
pulseMeta?: RunPulseMeta
pulses: RunPulse[]
completion?: RunCompletion
timing?: number
}

export function createInspectorBridge() {
const [runs, setRuns] = createSignal<Map<string, RunState>>(new Map())
const [activeRunId, setActiveRunId] = createSignal<string | undefined>(undefined)
const [devices, setDevices] = createSignal<Map<string, { status: unknown; actions: unknown[] }>>(new Map())
const [activeDevice, setActiveDevice] = createSignal<string | undefined>(undefined)

const onMessage = (e: MessageEvent) => {
const d = e.data as InspectorMessage & { source?: string }
if (!d || d.source !== "amicode") return
switch (d.type) {
case "run:iteration": {
setRuns((m) => {
const n = new Map(m)
const s = n.get(d.runId) ?? { iterations: [], pulses: [] }
n.set(d.runId, { ...s, iterations: [...s.iterations, { runId: d.runId, iter: d.iter, objective: d.objective, inf_pr: d.inf_pr, inf_du: d.inf_du }] })
return n
})
if (!activeRunId()) setActiveRunId(d.runId)
break
}
case "run:pulse-meta":
setRuns((m) => {
const n = new Map(m)
const s = n.get(d.runId) ?? { iterations: [], pulses: [] }
n.set(d.runId, { ...s, pulseMeta: d })
return n
})
break
case "run:pulse":
setRuns((m) => {
const n = new Map(m)
const s = n.get(d.runId) ?? { iterations: [], pulses: [] }
n.set(d.runId, { ...s, pulses: [...s.pulses.slice(-200), d] })
return n
})
break
case "run:completion":
setRuns((m) => {
const n = new Map(m)
const s = n.get(d.runId) ?? { iterations: [], pulses: [] }
n.set(d.runId, { ...s, completion: d })
return n
})
break
case "run:activate":
setActiveRunId(d.runId)
break
case "run:label":
setRuns((m) => {
const n = new Map(m)
const s = n.get(d.runId) ?? { iterations: [], pulses: [] }
n.set(d.runId, { ...s, label: d.label })
return n
})
break
case "run:timing":
setRuns((m) => {
const n = new Map(m)
const s = n.get(d.runId) ?? { iterations: [], pulses: [] }
n.set(d.runId, { ...s, timing: d.elapsed })
return n
})
break
case "device:status":
setDevices((m) => {
const n = new Map(m)
const cur = n.get(d.device) ?? { status: undefined, actions: [] }
n.set(d.device, { ...cur, status: d.status })
return n
})
if (!activeDevice()) setActiveDevice(d.device)
break
case "device:actions":
setDevices((m) => {
const n = new Map(m)
const cur = n.get(d.device) ?? { status: undefined, actions: [] }
n.set(d.device, { ...cur, actions: d.actions })
return n
})
break
case "device:activate":
setActiveDevice(d.device)
break
}
}

window.addEventListener("message", onMessage)
onCleanup(() => window.removeEventListener("message", onMessage))

return { runs, activeRunId, setActiveRunId, devices, activeDevice, setActiveDevice }
}
108 changes: 108 additions & 0 deletions packages/app/src/amicode/inspector/run-inspector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { For, Show, createMemo } from "solid-js"
import type { createInspectorBridge } from "./inspector-bridge"

type Props = { bridge: ReturnType<typeof createInspectorBridge> }

// Minimal pulse sparkline — renders correctly at 320px column width and responds
// to resize via viewBox (no fixed pixel width). Each drive is a polyline.
function PulseChart(props: { values: number[][]; bounds?: [number, number][] }) {
const paths = createMemo(() => {
const v = props.values
if (v.length === 0 || v[0].length === 0) return []
return v.map((drive, idx) => {
const b = props.bounds?.[idx]
const lo = b?.[0] ?? Math.min(...drive)
const hi = b?.[1] ?? Math.max(...drive)
const range = hi - lo || 1
const pts = drive.map((val, i) => {
const x = (i / (drive.length - 1)) * 100
const y = 50 - ((val - lo) / range) * 40 - 5
return `${x},${y}`
})
return { d: `M ${pts.join(" L ")}`, color: idx % 2 === 0 ? "var(--amico-accent, #eab308)" : "var(--amico-accent-2, #38bdf8)" }
})
})
return (
<svg viewBox="0 0 100 50" class="w-full h-[120px] bg-[var(--v2-background-bg-subtle)] rounded-md" preserveAspectRatio="none">
<For each={paths()}>{(p) => <path d={p.d} fill="none" stroke={p.color} stroke-width={1.2} vector-effect="non-scaling-stroke" />}</For>
</svg>
)
}

export function RunInspector(props: Props) {
const runs = () => Array.from(props.bridge.runs().entries())
const active = createMemo(() => {
const id = props.bridge.activeRunId()
if (id && props.bridge.runs().has(id)) return { id, state: props.bridge.runs().get(id)! }
const first = runs()[0]
return first ? { id: first[0], state: first[1] } : undefined
})
const latestIter = createMemo(() => active()?.state.iterations.at(-1))
const latestPulse = createMemo(() => active()?.state.pulses.at(-1))

return (
<div class="flex flex-col gap-3 p-3" data-component="run-inspector">
<div class="flex items-center justify-between">
<div class="text-12-medium">Run Inspector</div>
<Show when={runs().length > 1}>
<select
class="text-12-regular border border-border-weaker-base rounded px-1 py-0.5 bg-background-base max-w-[140px] truncate"
value={active()?.id ?? ""}
onChange={(e) => props.bridge.setActiveRunId(e.currentTarget.value)}
>
<For each={runs()}>{([id, s]) => <option value={id}>{s.label ?? id}</option>}</For>
</select>
</Show>
</div>

<Show when={!active()} fallback={
<div class="flex flex-col gap-3">
<div class="text-11-regular text-text-weak truncate" title={active()!.id}>
{active()!.state.label ?? active()!.id}
</div>
<Show when={active()!.state.pulseMeta}>
{(meta) => (
<div class="text-11-regular text-text-weak">
{meta().drives} drive(s) · {meta().knots} knots · {meta().labels.join(", ")}
</div>
)}
</Show>
<Show when={latestPulse()}>{(p) => <PulseChart values={p().values} bounds={active()!.state.pulseMeta?.bounds} />}</Show>
<Show when={!latestPulse() && active()!.state.pulseMeta}>
<div class="text-11-regular text-text-weak">Waiting for pulse data…</div>
</Show>
<div class="grid grid-cols-3 gap-2 text-11-regular">
<div>
<div class="text-text-faint">iter</div>
<div class="text-13-medium">{latestIter()?.iter ?? "—"}</div>
</div>
<div>
<div class="text-text-faint">objective</div>
<div class="font-mono text-11-regular">{latestIter() ? latestIter()!.objective.toExponential(2) : "—"}</div>
</div>
<div>
<div class="text-text-faint">inf</div>
<div class="font-mono text-11-regular">{latestIter() ? `${latestIter()!.inf_pr.toExponential(1)}/${latestIter()!.inf_du.toExponential(1)}` : "—"}</div>
</div>
</div>
<Show when={active()!.state.completion}>
{(c) => (
<div class="rounded-md border border-border-weaker-base p-2 text-11-regular">
<div class="text-text-faint">completion</div>
<div>
{c().status} · F={c().fidelity.toFixed(5)} · {c().iterations} iters
<Show when={active()!.state.timing}> · {active()!.state.timing!.toFixed(1)}s</Show>
</div>
</div>
)}
</Show>
<Show when={!active()!.state.completion && latestIter()}>
<div class="text-11-regular text-text-weak">running · iter {latestIter()!.iter}</div>
</Show>
</div>
}>
<div class="text-12-regular text-text-weak py-8 text-center">No solve yet — launch one from the chat.</div>
</Show>
</div>
)
}
Loading