feat: keep UML focused on state navigation
This commit is contained in:
@@ -99,6 +99,14 @@ Wyłączenie `viewer.nvim.sync` automatycznie wyłącza również
|
||||
`viewer.nvim.control`. Samo ponowne włączenie synchronizacji nie uzbraja
|
||||
sterowania klawiaturą bez jawnego `control: true`.
|
||||
|
||||
### Podział odpowiedzialności widoków
|
||||
|
||||
Interaktywny diagram UML pokazuje przepływ i służy do wyboru kroku. Nie
|
||||
renderuje pod SVG osobnych paneli kodu, terminala, rejestrów, pamięci ani ramki
|
||||
stosu. Te dane prezentuje `Nvim view`, ustawiany przez snapshot wybranego
|
||||
kroku. Snapshoty pozostają w JSON/API, mimo że nie są powielane wizualnie pod
|
||||
diagramem.
|
||||
|
||||
## Postęp zajęć i raport
|
||||
|
||||
| Metoda i endpoint | Znaczenie |
|
||||
|
||||
@@ -1991,7 +1991,6 @@ function SnapshotInteractiveUmlSequence({
|
||||
phase.steps.map((step) => ({ phase, step })),
|
||||
);
|
||||
const storageKey = `${config.storage_key ?? asset.label}-uml-step`;
|
||||
const viewStorageKey = `${config.storage_key ?? asset.label}-evidence-view`;
|
||||
const focusStorageKey = `${config.storage_key ?? asset.label}-navigation-level`;
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
@@ -2001,10 +2000,6 @@ function SnapshotInteractiveUmlSequence({
|
||||
const [svgMarkup, setSvgMarkup] = useState("");
|
||||
const [svgFailed, setSvgFailed] = useState(false);
|
||||
const [revealRevision, setRevealRevision] = useState(0);
|
||||
const [approvalBusy, setApprovalBusy] = useState(false);
|
||||
const [evidenceView, setEvidenceView] = useState<"code" | "terminal">(() =>
|
||||
localStorage.getItem(viewStorageKey) === "terminal" ? "terminal" : "code",
|
||||
);
|
||||
const [stepIndex, setStepIndex] = useState(() =>
|
||||
Math.max(
|
||||
0,
|
||||
@@ -2026,10 +2021,6 @@ function SnapshotInteractiveUmlSequence({
|
||||
() => localStorage.setItem(storageKey, String(stepIndex)),
|
||||
[stepIndex, storageKey],
|
||||
);
|
||||
useEffect(
|
||||
() => localStorage.setItem(viewStorageKey, evidenceView),
|
||||
[evidenceView, viewStorageKey],
|
||||
);
|
||||
useEffect(
|
||||
() => localStorage.setItem(focusStorageKey, focusLevel),
|
||||
[focusLevel, focusStorageKey],
|
||||
@@ -2405,53 +2396,7 @@ function SnapshotInteractiveUmlSequence({
|
||||
}, [active, entries, focusLevel, onSetStatus, progress]);
|
||||
if (!active) return null;
|
||||
const { phase, step } = active;
|
||||
const snapshot = step.snapshot;
|
||||
const completed = Boolean(
|
||||
step.progress_id &&
|
||||
progress?.items?.[step.progress_id]?.status === "approved",
|
||||
);
|
||||
const phaseEntries = entries.filter((entry) => entry.phase.id === phase.id);
|
||||
const arena = snapshot.memory.arena;
|
||||
const arenaPercent = arena?.offset == null
|
||||
? 0
|
||||
: Math.min(100, (arena.offset / arena.size_bytes) * 100);
|
||||
const terminalView = config.terminal_view;
|
||||
const visibleEvidenceView =
|
||||
evidenceView === "terminal" && terminalView?.href ? "terminal" : "code";
|
||||
const frameFields = [...snapshot.frame.fields].sort(
|
||||
(left, right) =>
|
||||
Number.parseInt(left.address, 16) - Number.parseInt(right.address, 16),
|
||||
);
|
||||
const frameStart = Number.parseInt(snapshot.frame.sp, 16);
|
||||
const frameEnd = Number.parseInt(snapshot.frame.fp, 16);
|
||||
const frameSegments: Array<
|
||||
| { kind: "gap"; address: number; sizeBytes: number }
|
||||
| { kind: "field"; field: UmlSnapshotFrameField; sizeBytes: number }
|
||||
> = [];
|
||||
let frameCursor = frameStart;
|
||||
if (Number.isFinite(frameStart) && Number.isFinite(frameEnd)) {
|
||||
frameFields.forEach((field) => {
|
||||
const address = Number.parseInt(field.address, 16);
|
||||
const sizeBytes = field.size_bytes ?? 4;
|
||||
if (!Number.isFinite(address) || address < frameStart || address >= frameEnd) return;
|
||||
if (address > frameCursor) {
|
||||
frameSegments.push({
|
||||
kind: "gap",
|
||||
address: frameCursor,
|
||||
sizeBytes: address - frameCursor,
|
||||
});
|
||||
}
|
||||
frameSegments.push({ kind: "field", field, sizeBytes });
|
||||
frameCursor = Math.max(frameCursor, address + sizeBytes);
|
||||
});
|
||||
if (frameCursor < frameEnd) {
|
||||
frameSegments.push({
|
||||
kind: "gap",
|
||||
address: frameCursor,
|
||||
sizeBytes: frameEnd - frameCursor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -2523,243 +2468,6 @@ function SnapshotInteractiveUmlSequence({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="uml-evidence-view" aria-label="Dowód dla wybranego kroku">
|
||||
<header>
|
||||
<div>
|
||||
<small>
|
||||
{visibleEvidenceView === "terminal"
|
||||
? "DOWÓD REFERENCYJNY · TERMDEBUG"
|
||||
: `DOWÓD · KROK ${String(step.number).padStart(2, "0")}`}
|
||||
</small>
|
||||
<strong>
|
||||
{visibleEvidenceView === "terminal"
|
||||
? terminalView?.title ?? "Termdebug"
|
||||
: step.label}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="uml-evidence-tabs" role="tablist" aria-label="Rodzaj dowodu">
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={visibleEvidenceView === "code"}
|
||||
onClick={() => setEvidenceView("code")}
|
||||
>
|
||||
Kod
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={visibleEvidenceView === "terminal"}
|
||||
disabled={!terminalView?.href}
|
||||
title={terminalView?.href ? undefined : "Brak widoku terminala dla tej figury"}
|
||||
onClick={() => setEvidenceView("terminal")}
|
||||
>
|
||||
Terminal
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{visibleEvidenceView === "terminal" && terminalView?.href ? (
|
||||
<figure className="uml-terminal-view" role="tabpanel">
|
||||
<img src={terminalView.href} alt={terminalView.alt} />
|
||||
{(terminalView.title || terminalView.caption) && (
|
||||
<figcaption>
|
||||
{terminalView.title && <strong>{terminalView.title}</strong>}
|
||||
{terminalView.caption && <span>{terminalView.caption}</span>}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
) : (
|
||||
<div className="uml-code-view" role="tabpanel">
|
||||
<section>
|
||||
<header>
|
||||
<span>C</span>
|
||||
<code>{snapshot.code?.source_ref ?? step.code_ref ?? "źródło"}</code>
|
||||
</header>
|
||||
<pre><code>{snapshot.code?.source ?? step.label}</code></pre>
|
||||
</section>
|
||||
<section>
|
||||
<header>
|
||||
<span>RV32</span>
|
||||
<code>pc = {snapshot.pc}</code>
|
||||
</header>
|
||||
<pre><code>{snapshot.code?.assembly ?? step.code_ref ?? snapshot.pc}</code></pre>
|
||||
</section>
|
||||
{snapshot.code?.note && <p>{snapshot.code.note}</p>}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<aside className="uml-snapshot-panel" aria-live="polite">
|
||||
<header>
|
||||
<div>
|
||||
<span>{phase.label} · STAN HAZARD3</span>
|
||||
<strong>
|
||||
<code>{String(step.number).padStart(2, "0")}</code> {step.label}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="uml-snapshot-cursor">
|
||||
<button
|
||||
aria-label="Poprzedni krok"
|
||||
disabled={stepIndex === 0}
|
||||
onClick={() => selectIndex(stepIndex - 1)}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<output>{stepIndex + 1}/{entries.length}</output>
|
||||
<button
|
||||
aria-label="Następny krok"
|
||||
disabled={stepIndex === entries.length - 1}
|
||||
onClick={() => selectIndex(stepIndex + 1)}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<p className="uml-snapshot-description">{step.description}</p>
|
||||
|
||||
<div className="uml-state-grid">
|
||||
<section className="uml-snapshot-registers">
|
||||
<h4>Rejestry</h4>
|
||||
<dl>
|
||||
<div className="primary"><dt>pc</dt><dd>{snapshot.pc}</dd></div>
|
||||
{snapshot.registers.map((register) => (
|
||||
<div key={register.name} title={register.note}>
|
||||
<dt>{register.name}</dt>
|
||||
<dd>{register.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="uml-snapshot-variables">
|
||||
<h4>Zmienne · {snapshot.frame.name}</h4>
|
||||
<dl>
|
||||
{snapshot.frame.fields.map((field) => (
|
||||
<div
|
||||
key={`${field.name}-${field.address}`}
|
||||
data-state={field.state}
|
||||
title={field.note}
|
||||
>
|
||||
<dt>{field.name}<small>{field.address}</small></dt>
|
||||
<dd>{field.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="uml-snapshot-memory">
|
||||
<h4>Pamięć statyczna · .bss</h4>
|
||||
{arena && (
|
||||
<div className="uml-arena-snapshot">
|
||||
<div>
|
||||
<span style={{ width: `${arenaPercent}%` }} />
|
||||
{arena.offset != null && <i style={{ left: `${arenaPercent}%` }} />}
|
||||
</div>
|
||||
<p>
|
||||
<code>{arena.base}</code>
|
||||
<strong>offset {arena.offset ?? "—"}/{arena.size_bytes}</strong>
|
||||
<code>{arena.cursor}</code>
|
||||
</p>
|
||||
<code className="uml-arena-bytes">{arena.bytes}</code>
|
||||
</div>
|
||||
)}
|
||||
<dl>
|
||||
{snapshot.memory.items.map((item) => (
|
||||
<div key={`${item.name}-${item.address}`} title={item.note}>
|
||||
<dt>{item.name} <small>{item.address}</small></dt>
|
||||
<dd>{item.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="uml-snapshot-stack">
|
||||
<header>
|
||||
<h4>Ramka stosu · {snapshot.frame.name} · {snapshot.frame.size_bytes} B</h4>
|
||||
<p>
|
||||
<code>sp {snapshot.frame.sp}</code>
|
||||
<code>s0/fp {snapshot.frame.fp}</code>
|
||||
{(snapshot.frame.stack_top || config.snapshot_context?.stack_top) && (
|
||||
<code>
|
||||
top {snapshot.frame.stack_top ?? config.snapshot_context?.stack_top}
|
||||
</code>
|
||||
)}
|
||||
</p>
|
||||
</header>
|
||||
<div className="uml-stack-axis" aria-hidden="true">
|
||||
<span>niższe adresy</span><span>stos rośnie w dół ←</span><span>wyższe adresy</span>
|
||||
</div>
|
||||
<div className="uml-stack-lane">
|
||||
<strong className="uml-stack-boundary"><small>sp</small>{snapshot.frame.sp}</strong>
|
||||
{frameSegments.map((segment, index) =>
|
||||
segment.kind === "gap" ? (
|
||||
<span
|
||||
className="uml-stack-gap"
|
||||
key={`gap-${segment.address}-${index}`}
|
||||
style={{ flexGrow: Math.max(1, segment.sizeBytes / 4) }}
|
||||
>
|
||||
<strong>{segment.sizeBytes} B</strong>
|
||||
<small>ABI / inne sloty</small>
|
||||
</span>
|
||||
) : (
|
||||
<article
|
||||
key={`${segment.field.name}-${segment.field.address}`}
|
||||
data-state={segment.field.state}
|
||||
title={segment.field.note}
|
||||
style={{ flexGrow: Math.max(1, segment.sizeBytes / 4) }}
|
||||
>
|
||||
<strong>{segment.field.name}</strong>
|
||||
<code>{segment.field.value}</code>
|
||||
<small>{segment.field.address}</small>
|
||||
</article>
|
||||
),
|
||||
)}
|
||||
<strong className="uml-stack-boundary"><small>s0/fp</small>{snapshot.frame.fp}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="uml-snapshot-observation">
|
||||
<h4>Opis stanu</h4>
|
||||
<p>{snapshot.description}</p>
|
||||
<small>
|
||||
{snapshot.target ?? config.snapshot_context?.target} ·{" "}
|
||||
{snapshot.captured_at ?? config.snapshot_context?.captured_at}
|
||||
{snapshot.captured_with || config.snapshot_context?.captured_with
|
||||
? ` · ${snapshot.captured_with ?? config.snapshot_context?.captured_with}`
|
||||
: ""}
|
||||
</small>
|
||||
</section>
|
||||
</aside>
|
||||
{step.progress_id && (
|
||||
<footer className="uml-approval">
|
||||
<span>
|
||||
{completed
|
||||
? "Ten krok jest zatwierdzony i zapisany w JSON-ie zajęć."
|
||||
: "Kursor wybiera strzałkę; zatwierdzenie zapisuje krok i timestamp."}
|
||||
</span>
|
||||
<button
|
||||
disabled={!progress || approvalBusy}
|
||||
onClick={async () => {
|
||||
if (!step.progress_id || !progress) return;
|
||||
setApprovalBusy(true);
|
||||
try {
|
||||
await onSetStatus(
|
||||
step.progress_id,
|
||||
completed ? "pending" : "approved",
|
||||
);
|
||||
} finally {
|
||||
setApprovalBusy(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{approvalBusy
|
||||
? "Zapisywanie…"
|
||||
: completed
|
||||
? "Cofnij zatwierdzenie"
|
||||
: "Zatwierdź krok"}
|
||||
</button>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user