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
|
`viewer.nvim.control`. Samo ponowne włączenie synchronizacji nie uzbraja
|
||||||
sterowania klawiaturą bez jawnego `control: true`.
|
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
|
## Postęp zajęć i raport
|
||||||
|
|
||||||
| Metoda i endpoint | Znaczenie |
|
| Metoda i endpoint | Znaczenie |
|
||||||
|
|||||||
@@ -1991,7 +1991,6 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
phase.steps.map((step) => ({ phase, step })),
|
phase.steps.map((step) => ({ phase, step })),
|
||||||
);
|
);
|
||||||
const storageKey = `${config.storage_key ?? asset.label}-uml-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 focusStorageKey = `${config.storage_key ?? asset.label}-navigation-level`;
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -2001,10 +2000,6 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
const [svgMarkup, setSvgMarkup] = useState("");
|
const [svgMarkup, setSvgMarkup] = useState("");
|
||||||
const [svgFailed, setSvgFailed] = useState(false);
|
const [svgFailed, setSvgFailed] = useState(false);
|
||||||
const [revealRevision, setRevealRevision] = useState(0);
|
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(() =>
|
const [stepIndex, setStepIndex] = useState(() =>
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -2026,10 +2021,6 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
() => localStorage.setItem(storageKey, String(stepIndex)),
|
() => localStorage.setItem(storageKey, String(stepIndex)),
|
||||||
[stepIndex, storageKey],
|
[stepIndex, storageKey],
|
||||||
);
|
);
|
||||||
useEffect(
|
|
||||||
() => localStorage.setItem(viewStorageKey, evidenceView),
|
|
||||||
[evidenceView, viewStorageKey],
|
|
||||||
);
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => localStorage.setItem(focusStorageKey, focusLevel),
|
() => localStorage.setItem(focusStorageKey, focusLevel),
|
||||||
[focusLevel, focusStorageKey],
|
[focusLevel, focusStorageKey],
|
||||||
@@ -2405,53 +2396,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
}, [active, entries, focusLevel, onSetStatus, progress]);
|
}, [active, entries, focusLevel, onSetStatus, progress]);
|
||||||
if (!active) return null;
|
if (!active) return null;
|
||||||
const { phase, step } = active;
|
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 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -2523,243 +2468,6 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user