feat: add approvable interactive UML stages
This commit is contained in:
+266
-16
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import "./react.css";
|
import "./react.css";
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ type UmlStage = {
|
|||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
code_ref?: string;
|
code_ref?: string;
|
||||||
|
progress_id?: string;
|
||||||
|
svg_label?: string;
|
||||||
};
|
};
|
||||||
type InteractiveAsset = {
|
type InteractiveAsset = {
|
||||||
kind: "allocator-memory-flow" | "uml-sequence";
|
kind: "allocator-memory-flow" | "uml-sequence";
|
||||||
@@ -47,6 +49,7 @@ type Task = {
|
|||||||
title: string;
|
title: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
criterion: string;
|
criterion: string;
|
||||||
|
conclusion_html: string;
|
||||||
prompt_html?: string;
|
prompt_html?: string;
|
||||||
flow: FlowBlock[];
|
flow: FlowBlock[];
|
||||||
};
|
};
|
||||||
@@ -66,6 +69,7 @@ type Progress = {
|
|||||||
items: Record<string, { status: Status }>;
|
items: Record<string, { status: Status }>;
|
||||||
summary: { total: number; counts: Record<Status, number> };
|
summary: { total: number; counts: Record<Status, number> };
|
||||||
};
|
};
|
||||||
|
type SetProgressStatus = (id: string, status: Status) => Promise<void>;
|
||||||
type CardModel = {
|
type CardModel = {
|
||||||
schema: string;
|
schema: string;
|
||||||
card: Record<string, string>;
|
card: Record<string, string>;
|
||||||
@@ -396,12 +400,20 @@ function InteractiveMemoryFlow({
|
|||||||
function InteractiveUmlSequence({
|
function InteractiveUmlSequence({
|
||||||
config,
|
config,
|
||||||
asset,
|
asset,
|
||||||
|
progress,
|
||||||
|
onSetStatus,
|
||||||
}: {
|
}: {
|
||||||
config: InteractiveAsset;
|
config: InteractiveAsset;
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
|
progress: Progress | null;
|
||||||
|
onSetStatus: SetProgressStatus;
|
||||||
}) {
|
}) {
|
||||||
const stages = config.stages ?? [];
|
const stages = config.stages ?? [];
|
||||||
const storageKey = `${config.storage_key ?? asset.label}-uml-stage`;
|
const storageKey = `${config.storage_key ?? asset.label}-uml-stage`;
|
||||||
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [svgMarkup, setSvgMarkup] = useState("");
|
||||||
|
const [svgFailed, setSvgFailed] = useState(false);
|
||||||
|
const [approvalBusy, setApprovalBusy] = useState(false);
|
||||||
const [stageIndex, setStageIndex] = useState(() =>
|
const [stageIndex, setStageIndex] = useState(() =>
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -415,6 +427,156 @@ function InteractiveUmlSequence({
|
|||||||
() => localStorage.setItem(storageKey, String(stageIndex)),
|
() => localStorage.setItem(storageKey, String(stageIndex)),
|
||||||
[stageIndex, storageKey],
|
[stageIndex, storageKey],
|
||||||
);
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
let current = true;
|
||||||
|
setSvgFailed(false);
|
||||||
|
fetch(asset.href, { cache: "no-store" })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((markup) => {
|
||||||
|
const document = new DOMParser().parseFromString(
|
||||||
|
markup,
|
||||||
|
"image/svg+xml",
|
||||||
|
);
|
||||||
|
const svg = document.documentElement;
|
||||||
|
if (svg.nodeName.toLowerCase() !== "svg" || document.querySelector("parsererror")) {
|
||||||
|
throw new Error("Niepoprawny SVG");
|
||||||
|
}
|
||||||
|
document.querySelectorAll("script, foreignObject").forEach((node) => node.remove());
|
||||||
|
svg.removeAttribute("width");
|
||||||
|
svg.removeAttribute("height");
|
||||||
|
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
if (current) setSvgMarkup(svg.outerHTML);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (current) setSvgFailed(true);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
current = false;
|
||||||
|
};
|
||||||
|
}, [asset.href]);
|
||||||
|
useEffect(() => {
|
||||||
|
const svg = canvasRef.current?.querySelector("svg");
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
svg.setAttribute("role", "img");
|
||||||
|
svg.setAttribute("aria-label", asset.alt);
|
||||||
|
svg
|
||||||
|
.querySelectorAll<SVGRectElement>("[data-uml-stage]")
|
||||||
|
.forEach((rect) => {
|
||||||
|
rect.classList.remove(
|
||||||
|
"uml-stage-region",
|
||||||
|
"uml-stage-region-bg",
|
||||||
|
"uml-stage-region-outline",
|
||||||
|
"uml-stage-region-active",
|
||||||
|
"uml-stage-region-completed",
|
||||||
|
);
|
||||||
|
rect.removeAttribute("data-uml-stage");
|
||||||
|
rect.removeAttribute("data-uml-stage-label");
|
||||||
|
rect.removeAttribute("role");
|
||||||
|
rect.removeAttribute("tabindex");
|
||||||
|
rect.removeAttribute("aria-label");
|
||||||
|
});
|
||||||
|
|
||||||
|
const texts = Array.from(svg.querySelectorAll<SVGTextElement>("text"));
|
||||||
|
const rects = Array.from(svg.querySelectorAll<SVGRectElement>("rect"));
|
||||||
|
stages.forEach((item, index) => {
|
||||||
|
if (!item.svg_label) return;
|
||||||
|
const label = texts.find((text) =>
|
||||||
|
(text.textContent ?? "").trim().startsWith(item.svg_label!),
|
||||||
|
);
|
||||||
|
const labelY = Number(label?.getAttribute("y"));
|
||||||
|
if (!label || !Number.isFinite(labelY)) return;
|
||||||
|
|
||||||
|
const candidates = rects
|
||||||
|
.map((rect) => {
|
||||||
|
const y = Number(rect.getAttribute("y"));
|
||||||
|
const height = Number(rect.getAttribute("height"));
|
||||||
|
const width = Number(rect.getAttribute("width"));
|
||||||
|
const style = rect.getAttribute("style") ?? "";
|
||||||
|
const strokeWidth = Number(
|
||||||
|
style.match(/stroke-width:\s*([\d.]+)/)?.[1] ??
|
||||||
|
rect.getAttribute("stroke-width") ??
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return { rect, y, height, width, strokeWidth, area: width * height };
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
({ y, height, width, strokeWidth }) =>
|
||||||
|
Number.isFinite(y) &&
|
||||||
|
Number.isFinite(height) &&
|
||||||
|
Number.isFinite(width) &&
|
||||||
|
strokeWidth >= 2 &&
|
||||||
|
y <= labelY &&
|
||||||
|
y + height >= labelY,
|
||||||
|
)
|
||||||
|
.sort((left, right) => left.area - right.area);
|
||||||
|
const region = candidates[0];
|
||||||
|
if (!region) return;
|
||||||
|
|
||||||
|
const sameRegion = candidates.filter(
|
||||||
|
({ y, height, width }) =>
|
||||||
|
Math.abs(y - region.y) < 0.1 &&
|
||||||
|
Math.abs(height - region.height) < 0.1 &&
|
||||||
|
Math.abs(width - region.width) < 0.1,
|
||||||
|
);
|
||||||
|
const isCompleted = Boolean(
|
||||||
|
item.progress_id &&
|
||||||
|
progress?.items?.[item.progress_id]?.status === "completed",
|
||||||
|
);
|
||||||
|
sameRegion.forEach(({ rect }, duplicateIndex) => {
|
||||||
|
const isBackground = rect.getAttribute("fill") !== "none";
|
||||||
|
rect.dataset.umlStage = String(index);
|
||||||
|
rect.dataset.umlStageLabel = item.label;
|
||||||
|
rect.classList.add(
|
||||||
|
"uml-stage-region",
|
||||||
|
isBackground ? "uml-stage-region-bg" : "uml-stage-region-outline",
|
||||||
|
);
|
||||||
|
rect.classList.toggle("uml-stage-region-active", index === stageIndex);
|
||||||
|
rect.classList.toggle("uml-stage-region-completed", isCompleted);
|
||||||
|
if (duplicateIndex === 0) {
|
||||||
|
rect.setAttribute("role", "button");
|
||||||
|
rect.setAttribute("tabindex", "0");
|
||||||
|
rect.setAttribute("aria-label", `Pokaż etap ${item.label}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [asset.alt, progress, stageIndex, stages, svgMarkup]);
|
||||||
|
|
||||||
|
const selectStageAtPoint = (clientX: number, clientY: number) => {
|
||||||
|
const svg = canvasRef.current?.querySelector("svg");
|
||||||
|
if (!svg) return;
|
||||||
|
const hit = Array.from(
|
||||||
|
svg.querySelectorAll<SVGRectElement>(".uml-stage-region-bg[data-uml-stage]"),
|
||||||
|
)
|
||||||
|
.filter((rect) => {
|
||||||
|
const bounds = rect.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
clientX >= bounds.left &&
|
||||||
|
clientX <= bounds.right &&
|
||||||
|
clientY >= bounds.top &&
|
||||||
|
clientY <= bounds.bottom
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((left, right) => {
|
||||||
|
const a = left.getBoundingClientRect();
|
||||||
|
const b = right.getBoundingClientRect();
|
||||||
|
return a.width * a.height - b.width * b.height;
|
||||||
|
})[0];
|
||||||
|
const next = Number(hit?.dataset.umlStage);
|
||||||
|
if (Number.isInteger(next)) setStageIndex(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSvgKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
const target = event.target as SVGRectElement;
|
||||||
|
const next = Number(target.dataset?.umlStage);
|
||||||
|
if (!Number.isInteger(next)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
setStageIndex(next);
|
||||||
|
};
|
||||||
const stage = stages[stageIndex];
|
const stage = stages[stageIndex];
|
||||||
return (
|
return (
|
||||||
<div className="uml-react">
|
<div className="uml-react">
|
||||||
@@ -428,21 +590,45 @@ function InteractiveUmlSequence({
|
|||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
aria-pressed={stageIndex === index}
|
aria-pressed={stageIndex === index}
|
||||||
|
data-completed={
|
||||||
|
item.progress_id &&
|
||||||
|
progress?.items?.[item.progress_id]?.status === "completed"
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}
|
||||||
onClick={() => setStageIndex(index)}
|
onClick={() => setStageIndex(index)}
|
||||||
>
|
>
|
||||||
|
{item.progress_id &&
|
||||||
|
progress?.items?.[item.progress_id]?.status === "completed" && (
|
||||||
|
<span aria-hidden="true">✓ </span>
|
||||||
|
)}
|
||||||
{item.label}
|
{item.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<a
|
<div
|
||||||
|
ref={canvasRef}
|
||||||
className="uml-canvas"
|
className="uml-canvas"
|
||||||
href={asset.href}
|
onClick={(event) => selectStageAtPoint(event.clientX, event.clientY)}
|
||||||
target="_blank"
|
onKeyDown={handleSvgKeyDown}
|
||||||
rel="noopener"
|
|
||||||
>
|
>
|
||||||
<img src={asset.href} alt={asset.alt} />
|
{svgMarkup ? (
|
||||||
</a>
|
<div
|
||||||
|
className="uml-svg-host"
|
||||||
|
// The SVG is a generated, same-origin PlantUML asset; scripts and
|
||||||
|
// foreignObject nodes are removed before it reaches the DOM.
|
||||||
|
dangerouslySetInnerHTML={{ __html: svgMarkup }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img src={asset.href} alt={asset.alt} />
|
||||||
|
)}
|
||||||
|
{svgFailed && (
|
||||||
|
<span className="uml-inline-warning">
|
||||||
|
Widok statyczny — nie udało się wczytać interaktywnego SVG.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{stage && (
|
{stage && (
|
||||||
<aside className="uml-stage">
|
<aside className="uml-stage">
|
||||||
<strong>{stage.label}</strong>
|
<strong>{stage.label}</strong>
|
||||||
@@ -450,11 +636,53 @@ function InteractiveUmlSequence({
|
|||||||
{stage.code_ref && <code>{stage.code_ref}</code>}
|
{stage.code_ref && <code>{stage.code_ref}</code>}
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
{stage?.progress_id && (
|
||||||
|
<footer className="uml-approval">
|
||||||
|
<span>
|
||||||
|
{progress?.items?.[stage.progress_id]?.status === "completed"
|
||||||
|
? "Ten stan jest zatwierdzony i zapisany w JSON-ie zajęć."
|
||||||
|
: "Kursor wybiera stan; zatwierdzenie zapisuje go w JSON-ie zajęć."}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
disabled={!progress || approvalBusy}
|
||||||
|
onClick={async () => {
|
||||||
|
if (!stage.progress_id || !progress) return;
|
||||||
|
const completed =
|
||||||
|
progress.items?.[stage.progress_id]?.status === "completed";
|
||||||
|
setApprovalBusy(true);
|
||||||
|
try {
|
||||||
|
await onSetStatus(
|
||||||
|
stage.progress_id,
|
||||||
|
completed ? "not_started" : "completed",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setApprovalBusy(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{approvalBusy
|
||||||
|
? "Zapisywanie…"
|
||||||
|
: progress?.items?.[stage.progress_id]?.status === "completed"
|
||||||
|
? "Cofnij zatwierdzenie"
|
||||||
|
: "Zatwierdź stan"}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssetFigure({ asset, number }: { asset: Asset; number: number }) {
|
function AssetFigure({
|
||||||
|
asset,
|
||||||
|
number,
|
||||||
|
progress,
|
||||||
|
onSetStatus,
|
||||||
|
}: {
|
||||||
|
asset: Asset;
|
||||||
|
number: number;
|
||||||
|
progress: Progress | null;
|
||||||
|
onSetStatus: SetProgressStatus;
|
||||||
|
}) {
|
||||||
const interactiveKind = asset.interactive?.kind;
|
const interactiveKind = asset.interactive?.kind;
|
||||||
const isInteractive = Boolean(interactiveKind);
|
const isInteractive = Boolean(interactiveKind);
|
||||||
return (
|
return (
|
||||||
@@ -463,7 +691,12 @@ function AssetFigure({ asset, number }: { asset: Asset; number: number }) {
|
|||||||
id={asset.label.replace(":", "-")}
|
id={asset.label.replace(":", "-")}
|
||||||
>
|
>
|
||||||
{interactiveKind === "uml-sequence" ? (
|
{interactiveKind === "uml-sequence" ? (
|
||||||
<InteractiveUmlSequence config={asset.interactive!} asset={asset} />
|
<InteractiveUmlSequence
|
||||||
|
config={asset.interactive!}
|
||||||
|
asset={asset}
|
||||||
|
progress={progress}
|
||||||
|
onSetStatus={onSetStatus}
|
||||||
|
/>
|
||||||
) : interactiveKind === "allocator-memory-flow" ? (
|
) : interactiveKind === "allocator-memory-flow" ? (
|
||||||
<>
|
<>
|
||||||
<InteractiveMemoryFlow
|
<InteractiveMemoryFlow
|
||||||
@@ -548,11 +781,13 @@ function SectionPage({
|
|||||||
section,
|
section,
|
||||||
progress,
|
progress,
|
||||||
onCycle,
|
onCycle,
|
||||||
|
onSetStatus,
|
||||||
figureOffset,
|
figureOffset,
|
||||||
}: {
|
}: {
|
||||||
section: Section;
|
section: Section;
|
||||||
progress: Progress | null;
|
progress: Progress | null;
|
||||||
onCycle: (id: string, status: Status) => void;
|
onCycle: (id: string, status: Status) => void;
|
||||||
|
onSetStatus: SetProgressStatus;
|
||||||
figureOffset: number;
|
figureOffset: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -579,8 +814,19 @@ function SectionPage({
|
|||||||
key={asset.label}
|
key={asset.label}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
number={figureOffset + index + 1}
|
number={figureOffset + index + 1}
|
||||||
|
progress={progress}
|
||||||
|
onSetStatus={onSetStatus}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{section.tasks.map(
|
||||||
|
(task) =>
|
||||||
|
task.conclusion_html && (
|
||||||
|
<aside className="task-conclusion" key={`${task.ref}-conclusion`}>
|
||||||
|
<strong>WNIOSEK</strong>
|
||||||
|
<Html html={task.conclusion_html} />
|
||||||
|
</aside>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -655,17 +901,12 @@ function App({ model }: { model: CardModel }) {
|
|||||||
window.addEventListener("wheel", wheel, { passive: false });
|
window.addEventListener("wheel", wheel, { passive: false });
|
||||||
return () => window.removeEventListener("wheel", wheel);
|
return () => window.removeEventListener("wheel", wheel);
|
||||||
}, []);
|
}, []);
|
||||||
const cycle = async (id: string, current: Status) => {
|
const setStatus: SetProgressStatus = async (id, status) => {
|
||||||
const next: Record<Status, Status> = {
|
|
||||||
not_started: "discussed",
|
|
||||||
discussed: "completed",
|
|
||||||
completed: "not_started",
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/progress/${encodeURIComponent(id)}`, {
|
const response = await fetch(`/api/progress/${encodeURIComponent(id)}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ status: next[current] }),
|
body: JSON.stringify({ status }),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(await response.text());
|
if (!response.ok) throw new Error(await response.text());
|
||||||
setProgress((await response.json()).progress);
|
setProgress((await response.json()).progress);
|
||||||
@@ -676,6 +917,14 @@ function App({ model }: { model: CardModel }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const cycle = (id: string, current: Status) => {
|
||||||
|
const next: Record<Status, Status> = {
|
||||||
|
not_started: "discussed",
|
||||||
|
discussed: "completed",
|
||||||
|
completed: "not_started",
|
||||||
|
};
|
||||||
|
void setStatus(id, next[current]);
|
||||||
|
};
|
||||||
let figureCount = 0;
|
let figureCount = 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -691,6 +940,7 @@ function App({ model }: { model: CardModel }) {
|
|||||||
section={section}
|
section={section}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
onCycle={cycle}
|
onCycle={cycle}
|
||||||
|
onSetStatus={setStatus}
|
||||||
figureOffset={offset}
|
figureOffset={offset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
+118
-11
@@ -167,8 +167,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
border-bottom: 1px solid #bbb;
|
border-bottom: 1px solid #bbb;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
@@ -186,31 +186,93 @@
|
|||||||
}
|
}
|
||||||
.uml-actions {
|
.uml-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 3px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.uml-actions button {
|
.uml-actions button {
|
||||||
border: 1px solid #777;
|
border: 1px solid #aaa;
|
||||||
background: #fff;
|
border-radius: 2px;
|
||||||
padding: 4px 8px;
|
background: #fcfcfc;
|
||||||
|
color: #444;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font:
|
||||||
|
600 10px/1.25 ui-monospace,
|
||||||
|
monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.uml-actions button:hover {
|
||||||
|
border-color: #64879a;
|
||||||
|
background: #f5fafc;
|
||||||
|
}
|
||||||
|
.uml-actions button[data-completed="true"] {
|
||||||
|
border-color: #62a47f;
|
||||||
|
background: #edf8f1;
|
||||||
|
color: #14613a;
|
||||||
|
}
|
||||||
.uml-actions button[aria-pressed="true"] {
|
.uml-actions button[aria-pressed="true"] {
|
||||||
border-color: #315f78;
|
border-color: #287495;
|
||||||
background: #eaf4fa;
|
background: #edf7fb;
|
||||||
font-weight: 700;
|
color: #174d64;
|
||||||
|
box-shadow: inset 0 0 0 1px #b9dce9;
|
||||||
}
|
}
|
||||||
.uml-canvas {
|
.uml-canvas {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.uml-canvas img {
|
.uml-canvas img,
|
||||||
|
.uml-svg-host svg {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
max-height: 760px;
|
max-height: 760px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
.uml-svg-host {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.uml-stage-region {
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
fill 140ms ease,
|
||||||
|
stroke 140ms ease,
|
||||||
|
stroke-width 140ms ease;
|
||||||
|
}
|
||||||
|
.uml-stage-region-completed.uml-stage-region-bg {
|
||||||
|
fill: #e7f6ed !important;
|
||||||
|
fill-opacity: 1 !important;
|
||||||
|
stroke: #258154 !important;
|
||||||
|
stroke-width: 3px !important;
|
||||||
|
}
|
||||||
|
.uml-stage-region-completed.uml-stage-region-outline {
|
||||||
|
stroke: #258154 !important;
|
||||||
|
stroke-width: 3px !important;
|
||||||
|
}
|
||||||
|
.uml-stage-region-active.uml-stage-region-bg {
|
||||||
|
fill: #e7f6fc !important;
|
||||||
|
fill-opacity: 1 !important;
|
||||||
|
stroke: #187ea7 !important;
|
||||||
|
stroke-width: 3px !important;
|
||||||
|
}
|
||||||
|
.uml-stage-region-active.uml-stage-region-outline {
|
||||||
|
stroke: #187ea7 !important;
|
||||||
|
stroke-width: 3px !important;
|
||||||
|
}
|
||||||
|
.uml-stage-region-active.uml-stage-region-completed.uml-stage-region-bg {
|
||||||
|
fill: #e7f6ed !important;
|
||||||
|
}
|
||||||
|
.uml-stage-region:focus {
|
||||||
|
outline: none;
|
||||||
|
stroke: #005f87 !important;
|
||||||
|
stroke-width: 4px !important;
|
||||||
|
}
|
||||||
|
.uml-inline-warning {
|
||||||
|
display: block;
|
||||||
|
padding-top: 5px;
|
||||||
|
color: #8b2d20;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
.uml-stage {
|
.uml-stage {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
@@ -227,6 +289,50 @@
|
|||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
.uml-approval {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 8px 8px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid #d2d2d2;
|
||||||
|
color: #555;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.uml-approval button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid #477a5d;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #edf8f1;
|
||||||
|
color: #174e30;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font: 600 10px/1.25 system-ui, sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.uml-approval button:disabled {
|
||||||
|
border-color: #bbb;
|
||||||
|
background: #f3f3f3;
|
||||||
|
color: #888;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.task-conclusion {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border: 1px solid #315f78;
|
||||||
|
background: #f3f9fc;
|
||||||
|
}
|
||||||
|
.task-conclusion > strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
color: #315f78;
|
||||||
|
font:
|
||||||
|
700 10px/1.2 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
.task-conclusion p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.asset-source-link {
|
.asset-source-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
@@ -320,6 +426,7 @@
|
|||||||
}
|
}
|
||||||
.uml-actions,
|
.uml-actions,
|
||||||
.uml-stage,
|
.uml-stage,
|
||||||
|
.uml-approval,
|
||||||
.asset-source-link,
|
.asset-source-link,
|
||||||
.lesson-progress-dock,
|
.lesson-progress-dock,
|
||||||
.lesson-progress-control {
|
.lesson-progress-control {
|
||||||
|
|||||||
@@ -663,7 +663,17 @@
|
|||||||
"id": { "type": "string", "minLength": 1 },
|
"id": { "type": "string", "minLength": 1 },
|
||||||
"label": { "type": "string", "minLength": 1 },
|
"label": { "type": "string", "minLength": 1 },
|
||||||
"description": { "type": "string", "minLength": 1 },
|
"description": { "type": "string", "minLength": 1 },
|
||||||
"code_ref": { "type": "string" }
|
"code_ref": { "type": "string" },
|
||||||
|
"progress_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Identyfikator zatwierdzenia etapu zapisywany w JSON-ie przebiegu zajęć."
|
||||||
|
},
|
||||||
|
"svg_label": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Początek etykiety grupy PlantUML zaznaczanej w inline SVG."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -724,6 +734,10 @@
|
|||||||
"criterion": {
|
"criterion": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"conclusion_tex": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Końcowy wniosek renderowany po zasobach sekcji."
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
|
|||||||
+41
-8
@@ -765,6 +765,7 @@ def render_preamble(data: dict) -> list[str]:
|
|||||||
r"\newtcolorbox{ESCTaskFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black,boxsep=0pt,left=1.4mm,right=1.4mm,top=0.8mm,bottom=0.8mm,colbacktitle=black!7,coltitle=black,title={\ttfamily\bfseries TASK\quad #1}}",
|
r"\newtcolorbox{ESCTaskFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black,boxsep=0pt,left=1.4mm,right=1.4mm,top=0.8mm,bottom=0.8mm,colbacktitle=black!7,coltitle=black,title={\ttfamily\bfseries TASK\quad #1}}",
|
||||||
r"\newtcolorbox{ESCBlockFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!55,boxsep=0pt,left=1.0mm,right=1.0mm,top=0.6mm,bottom=0.6mm,colbacktitle=black!4,coltitle=black,title={\ttfamily\bfseries BLOCK\quad #1}}",
|
r"\newtcolorbox{ESCBlockFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!55,boxsep=0pt,left=1.0mm,right=1.0mm,top=0.6mm,bottom=0.6mm,colbacktitle=black!4,coltitle=black,title={\ttfamily\bfseries BLOCK\quad #1}}",
|
||||||
r"\newtcolorbox{ESCStepFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!28,boxsep=0pt,left=0.8mm,right=0.8mm,top=0.45mm,bottom=0.45mm,colbacktitle=black!2,coltitle=black,title={\ttfamily STEP\quad #1}}",
|
r"\newtcolorbox{ESCStepFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!28,boxsep=0pt,left=0.8mm,right=0.8mm,top=0.45mm,bottom=0.45mm,colbacktitle=black!2,coltitle=black,title={\ttfamily STEP\quad #1}}",
|
||||||
|
r"\newtcolorbox{ESCConclusionFrame}{enhanced,breakable,arc=0pt,boxrule=0.45pt,colback=blue!2,colframe=blue!45!black,boxsep=0pt,left=1.2mm,right=1.2mm,top=0.7mm,bottom=0.7mm,colbacktitle=blue!7,coltitle=black,title={\ttfamily\bfseries WNIOSEK}}",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -971,7 +972,6 @@ def render_task_flow_tex(task_ref: str, task: dict) -> list[str]:
|
|||||||
def render_tasks(section: dict, data: dict) -> list[str]:
|
def render_tasks(section: dict, data: dict) -> list[str]:
|
||||||
tasks = data["tasks"]
|
tasks = data["tasks"]
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
legacy_refs: list[str] = []
|
|
||||||
for index, task_ref in enumerate(section["task_refs"]):
|
for index, task_ref in enumerate(section["task_refs"]):
|
||||||
if index:
|
if index:
|
||||||
# Kolejny task jest niezależnym etapem karty: jego ramka ma
|
# Kolejny task jest niezależnym etapem karty: jego ramka ma
|
||||||
@@ -981,12 +981,22 @@ def render_tasks(section: dict, data: dict) -> list[str]:
|
|||||||
if task.get("flow"):
|
if task.get("flow"):
|
||||||
lines.extend(render_task_flow_tex(task_ref, task))
|
lines.extend(render_task_flow_tex(task_ref, task))
|
||||||
else:
|
else:
|
||||||
legacy_refs.append(task_ref)
|
lines.extend(
|
||||||
if legacy_refs:
|
[
|
||||||
lines.append(r"\begin{enumerate}[label=\textbf{\arabic*.}]")
|
rf"\begin{{enumerate}}[label=\textbf{{\arabic*.}},start={index + 1}]",
|
||||||
for task_ref in legacy_refs:
|
rf" \item {task['prompt_tex']}",
|
||||||
lines.append(rf" \item {tasks[task_ref]['prompt_tex']}")
|
r"\end{enumerate}",
|
||||||
lines.append(r"\end{enumerate}")
|
]
|
||||||
|
)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def render_task_conclusions_tex(section: dict, data: dict) -> list[str]:
|
||||||
|
lines: list[str] = []
|
||||||
|
for task_ref in section.get("task_refs", []) or []:
|
||||||
|
conclusion = str(data["tasks"][task_ref].get("conclusion_tex", "")).strip()
|
||||||
|
if conclusion:
|
||||||
|
lines.extend([r"\begin{ESCConclusionFrame}", conclusion, r"\end{ESCConclusionFrame}"])
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -1844,6 +1854,7 @@ def render_sections(data: dict) -> list[str]:
|
|||||||
else:
|
else:
|
||||||
lines.append(section.get("content_tex", ""))
|
lines.append(section.get("content_tex", ""))
|
||||||
lines.extend(render_section_assets_tex(section, data))
|
lines.extend(render_section_assets_tex(section, data))
|
||||||
|
lines.extend(render_task_conclusions_tex(section, data))
|
||||||
lines.append(r"\ESCSectionBlockEnd")
|
lines.append(r"\ESCSectionBlockEnd")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
return lines
|
return lines
|
||||||
@@ -2801,6 +2812,24 @@ def render_tasks_html(section: dict, data: dict, equation_numbers: dict[str, int
|
|||||||
return f'<div class="tasks">{"".join(items)}</div>'
|
return f'<div class="tasks">{"".join(items)}</div>'
|
||||||
|
|
||||||
|
|
||||||
|
def render_task_conclusions_html(
|
||||||
|
section: dict,
|
||||||
|
data: dict,
|
||||||
|
equation_numbers: dict[str, int],
|
||||||
|
figure_numbers: dict[str, int],
|
||||||
|
) -> str:
|
||||||
|
items: list[str] = []
|
||||||
|
for task_ref in section.get("task_refs", []) or []:
|
||||||
|
conclusion = str(data["tasks"][task_ref].get("conclusion_tex", "")).strip()
|
||||||
|
if conclusion:
|
||||||
|
items.append(
|
||||||
|
'<aside class="task-conclusion"><strong>WNIOSEK</strong>'
|
||||||
|
+ render_latex_fragment_html(conclusion, equation_numbers, figure_numbers)
|
||||||
|
+ "</aside>"
|
||||||
|
)
|
||||||
|
return "".join(items)
|
||||||
|
|
||||||
|
|
||||||
def render_learning_tree_overview_html(data: dict) -> str:
|
def render_learning_tree_overview_html(data: dict) -> str:
|
||||||
requirements = data.get("educational_requirements", {})
|
requirements = data.get("educational_requirements", {})
|
||||||
chunks: list[str] = []
|
chunks: list[str] = []
|
||||||
@@ -2910,6 +2939,7 @@ def render_main_html(data: dict) -> str:
|
|||||||
else render_latex_fragment_html(section.get("content_tex", ""), equation_numbers, figure_numbers)
|
else render_latex_fragment_html(section.get("content_tex", ""), equation_numbers, figure_numbers)
|
||||||
)
|
)
|
||||||
content += render_section_assets_html(section, figure_numbers)
|
content += render_section_assets_html(section, figure_numbers)
|
||||||
|
content += render_task_conclusions_html(section, data, equation_numbers, figure_numbers)
|
||||||
left_margin, right_margin = render_section_margins_html(section, data)
|
left_margin, right_margin = render_section_margins_html(section, data)
|
||||||
task_identity = section.get("task_identity", {}) or {}
|
task_identity = section.get("task_identity", {}) or {}
|
||||||
identity_html = ""
|
identity_html = ""
|
||||||
@@ -3026,7 +3056,7 @@ p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm
|
|||||||
figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in}
|
figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in}
|
||||||
.table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white}
|
.table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white}
|
||||||
.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em}
|
.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em}
|
||||||
code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #222;margin:1mm 0 2mm;background:#fff}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:6px;padding:5px 7px;background:#eaeaea;border-bottom:1px solid #222}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#222}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#555}.task-block{margin:6px 7px;padding:5px 6px;border:1px solid #777;background:#fff}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:5px;margin-bottom:3px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#555}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{margin:4px 0 0 16px!important;padding:0;display:grid;gap:3px}.block-steps li{margin:0;border:1px solid #b9b9b9;padding:4px 5px;background:#fff}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace;font-weight:700}.block-steps li p:last-child{margin-bottom:0}.task-exercise{margin:6px 7px;border:1px solid #777;border-left:1px solid #222;padding:5px 6px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:5px;padding-top:4px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:4px 0 0}.task-flow>footer{padding:5px 7px;border-top:1px solid #222;font-size:12px;background:#f6f6f6}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none}
|
code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #222;margin:1mm 0 2mm;background:#fff}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:6px;padding:5px 7px;background:#eaeaea;border-bottom:1px solid #222}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#222}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#555}.task-block{margin:6px 7px;padding:5px 6px;border:1px solid #777;background:#fff}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:5px;margin-bottom:3px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#555}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{margin:4px 0 0 16px!important;padding:0;display:grid;gap:3px}.block-steps li{margin:0;border:1px solid #b9b9b9;padding:4px 5px;background:#fff}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace;font-weight:700}.block-steps li p:last-child{margin-bottom:0}.task-exercise{margin:6px 7px;border:1px solid #777;border-left:1px solid #222;padding:5px 6px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:5px;padding-top:4px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:4px 0 0}.task-flow>footer{padding:5px 7px;border-top:1px solid #222;font-size:12px;background:#f6f6f6}.task-conclusion{margin:8px 0;padding:7px 9px;border:1px solid #315f78;background:#f3f9fc}.task-conclusion>strong{display:block;margin-bottom:3px;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#315f78}.task-conclusion p{margin:0}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none}
|
||||||
.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)}
|
.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)}
|
||||||
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}.viewer-zoom-status{font-size:16px}}
|
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}.viewer-zoom-status{font-size:16px}}
|
||||||
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,.block-steps li,figure{break-inside:avoid}}
|
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,.block-steps li,figure{break-inside:avoid}}
|
||||||
@@ -6974,6 +7004,9 @@ def react_task_model(
|
|||||||
"title": str(task.get("title") or task_ref),
|
"title": str(task.get("title") or task_ref),
|
||||||
"uuid": str(task.get("uuid", "")),
|
"uuid": str(task.get("uuid", "")),
|
||||||
"criterion": str(task.get("criterion", "")),
|
"criterion": str(task.get("criterion", "")),
|
||||||
|
"conclusion_html": render_latex_fragment_html(
|
||||||
|
str(task.get("conclusion_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
"prompt_html": render_latex_fragment_html(
|
"prompt_html": render_latex_fragment_html(
|
||||||
str(task.get("prompt_tex", "")), equation_numbers, figure_numbers
|
str(task.get("prompt_tex", "")), equation_numbers, figure_numbers
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user