Skip to content

Commit 1069642

Browse files
committed
New module: Plate Workbench
Autopsy mode: the published image large beside the line-by-line transliteration with glyphs, so a reading can be checked against the plate it descends from. Facsimile drawings come before photographs (the callout reminds you a drawing is the edition's own reading, not neutral evidence); arrow keys walk all 1,721 imaged documents; an id box with autocomplete jumps anywhere; links go to the full record, SigLA, and the GORILA volumes at CEFAEL. Images stream on demand from the upstream mirror under their EfA rights note. Registry entry under Research, Help section included.
1 parent e5297a2 commit 1069642

4 files changed

Lines changed: 277 additions & 0 deletions

File tree

src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export type ModuleId =
236236
| "write"
237237
| "trainer"
238238
| "constellation"
239+
| "plates"
239240
| "help";
240241

241242
// Calibration badge surfaced inside each analytical module — makes the

src/modules/PlateWorkbench.tsx

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { useEffect, useMemo, useState } from "react";
2+
import { useWorkbench } from "../store/workbench";
3+
import { useScopedCorpus } from "../store/scope";
4+
import { upstreamAsset, siglaUrl } from "../lib/helpers";
5+
import { GORILA_CEFAEL_URL } from "../lib/citations";
6+
import { Glyph } from "../components/Glyph";
7+
8+
// Autopsy mode: the published image large on the left, the
9+
// transliteration line by line on the right, so a reading can be
10+
// checked against the plate it came from. Facsimile drawings first
11+
// (they're the edition's own reading), photographs after; prev/next
12+
// walks every imaged document. Images stream from the upstream mirror
13+
// on demand and stay under their EfA rights.
14+
15+
export default function PlateWorkbench() {
16+
const scoped = useScopedCorpus();
17+
const showInscription = useWorkbench((s) => s.showInscription);
18+
const initialIntent = useWorkbench.getState().moduleIntent;
19+
20+
const imaged = useMemo(
21+
() =>
22+
scoped.inscriptions.filter(
23+
(i) => i.facsimileImages.length > 0 || i.images.length > 0,
24+
),
25+
[scoped.inscriptions],
26+
);
27+
28+
const [idx, setIdx] = useState(() => {
29+
if (initialIntent?.focus) {
30+
const k = imaged.findIndex((i) => i.id === initialIntent.focus);
31+
if (k >= 0) return k;
32+
}
33+
return 0;
34+
});
35+
const [imgIdx, setImgIdx] = useState(0);
36+
37+
const ins = imaged[idx];
38+
const images = useMemo(
39+
() => (ins ? [...ins.facsimileImages, ...ins.images] : []),
40+
[ins],
41+
);
42+
43+
useEffect(() => setImgIdx(0), [idx]);
44+
45+
useEffect(() => {
46+
const onKey = (e: KeyboardEvent) => {
47+
if ((e.target as HTMLElement | null)?.tagName === "INPUT") return;
48+
if (e.key === "ArrowRight") setIdx((i) => Math.min(imaged.length - 1, i + 1));
49+
else if (e.key === "ArrowLeft") setIdx((i) => Math.max(0, i - 1));
50+
};
51+
window.addEventListener("keydown", onKey);
52+
return () => window.removeEventListener("keydown", onKey);
53+
}, [imaged.length]);
54+
55+
if (!ins)
56+
return (
57+
<div className="panel">
58+
<h2>Plate Workbench</h2>
59+
<div className="dim">No imaged inscriptions in the current Scope.</div>
60+
</div>
61+
);
62+
63+
return (
64+
<div className="panel" style={{ maxWidth: 1200 }}>
65+
<h2>Plate Workbench</h2>
66+
<div className="callout">
67+
<h4>Check the reading against the plate</h4>
68+
<p>
69+
Every transliteration in this corpus descends from a published
70+
image — here they sit side by side. Facsimile drawings come
71+
first (they are the edition's own reading, not neutral
72+
evidence); photographs after. Use ←/→ or the buttons to walk all{" "}
73+
{imaged.length.toLocaleString()} imaged documents, and the
74+
CEFAEL link to read the GORILA volumes themselves.
75+
</p>
76+
</div>
77+
78+
<div className="toolbar">
79+
<button
80+
className="btn btn-outline btn-sm"
81+
disabled={idx === 0}
82+
onClick={() => setIdx((i) => Math.max(0, i - 1))}
83+
>
84+
← Prev
85+
</button>
86+
<input
87+
className="input"
88+
list="plate-ids"
89+
value={ins.id}
90+
onChange={(e) => {
91+
const k = imaged.findIndex((i) => i.id === e.target.value.toUpperCase());
92+
if (k >= 0) setIdx(k);
93+
}}
94+
style={{ width: 130, fontFamily: "var(--mono)" }}
95+
/>
96+
<datalist id="plate-ids">
97+
{imaged.map((i) => (
98+
<option key={i.id} value={i.id} />
99+
))}
100+
</datalist>
101+
<button
102+
className="btn btn-outline btn-sm"
103+
disabled={idx >= imaged.length - 1}
104+
onClick={() => setIdx((i) => Math.min(imaged.length - 1, i + 1))}
105+
>
106+
Next →
107+
</button>
108+
<span className="dim" style={{ fontSize: 12 }}>
109+
{idx + 1} / {imaged.length} · {ins.site}
110+
{ins.support ? ` · ${ins.support}` : ""}
111+
</span>
112+
<span style={{ flex: 1 }} />
113+
<button
114+
className="btn btn-outline btn-sm"
115+
onClick={() => showInscription(ins.id)}
116+
>
117+
Full record
118+
</button>
119+
<a
120+
className="btn btn-outline btn-sm"
121+
href={siglaUrl(ins.id)}
122+
target="_blank"
123+
rel="noreferrer"
124+
title="Sign-by-sign paleography at SigLA"
125+
>
126+
↗ SigLA
127+
</a>
128+
<a
129+
className="btn btn-outline btn-sm"
130+
href={GORILA_CEFAEL_URL}
131+
target="_blank"
132+
rel="noreferrer"
133+
title="The GORILA volumes, digitized at CEFAEL"
134+
>
135+
↗ GORILA
136+
</a>
137+
</div>
138+
139+
<div className="col2" style={{ gridTemplateColumns: "3fr 2fr" }}>
140+
<div className="card" style={{ textAlign: "center" }}>
141+
{images.length > 0 ? (
142+
<>
143+
<img
144+
key={images[imgIdx]}
145+
src={upstreamAsset(images[imgIdx])}
146+
alt={`${ins.id}${imgIdx < ins.facsimileImages.length ? "facsimile drawing" : "photograph"}`}
147+
style={{
148+
maxWidth: "100%",
149+
maxHeight: 560,
150+
objectFit: "contain",
151+
background: "var(--surface-1)",
152+
borderRadius: 4,
153+
}}
154+
/>
155+
{images.length > 1 && (
156+
<div
157+
style={{
158+
display: "flex",
159+
gap: 6,
160+
justifyContent: "center",
161+
marginTop: 8,
162+
flexWrap: "wrap",
163+
}}
164+
>
165+
{images.map((im, i) => (
166+
<button
167+
key={im}
168+
className="btn btn-outline btn-sm"
169+
style={{
170+
fontSize: 10,
171+
background: i === imgIdx ? "var(--surface-1)" : undefined,
172+
}}
173+
onClick={() => setImgIdx(i)}
174+
>
175+
{i < ins.facsimileImages.length
176+
? `drawing ${i + 1}`
177+
: `photo ${i + 1 - ins.facsimileImages.length}`}
178+
</button>
179+
))}
180+
</div>
181+
)}
182+
<div className="dim" style={{ fontSize: 10, marginTop: 6 }}>
183+
{ins.imageRights || "© École Française d'Athènes"}
184+
{ins.imageRightsURL && (
185+
<>
186+
{" · "}
187+
<a href={ins.imageRightsURL} target="_blank" rel="noreferrer">
188+
rights
189+
</a>
190+
</>
191+
)}
192+
</div>
193+
</>
194+
) : (
195+
<div className="dim">No image files for this document.</div>
196+
)}
197+
</div>
198+
199+
<div className="card">
200+
<h4>{ins.id} — transliteration</h4>
201+
<div style={{ display: "grid", gap: 6 }}>
202+
{ins.lines.map((line, li) => (
203+
<div
204+
key={li}
205+
style={{
206+
display: "flex",
207+
gap: 8,
208+
alignItems: "baseline",
209+
flexWrap: "wrap",
210+
borderBottom: "1px solid var(--border)",
211+
paddingBottom: 4,
212+
}}
213+
>
214+
<span className="dim" style={{ fontSize: 10, width: 20 }}>
215+
.{li + 1}
216+
</span>
217+
<span style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
218+
{line.map((tok, ti) => (
219+
<span key={ti} style={{ textAlign: "center" }}>
220+
{tok.includes("-") && (
221+
<div>
222+
{tok.split("-").map((p, pi) => (
223+
<Glyph key={pi} sign={p} size={16} />
224+
))}
225+
</div>
226+
)}
227+
<span style={{ fontFamily: "var(--mono)", fontSize: 12 }}>
228+
{tok}
229+
</span>
230+
</span>
231+
))}
232+
</span>
233+
</div>
234+
))}
235+
</div>
236+
{ins.scribe && (
237+
<div className="dim" style={{ fontSize: 11, marginTop: 8 }}>
238+
hand: {ins.scribe}
239+
{ins.context ? ` · ${ins.context}` : ""}
240+
</div>
241+
)}
242+
</div>
243+
</div>
244+
</div>
245+
);
246+
}

src/modules/help/helpSections.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,28 @@ export const SECTIONS: Section[] = [
16051605
</>
16061606
),
16071607
},
1608+
{
1609+
id: "mod-plates",
1610+
group: "Interpretation modules",
1611+
title: "Plate Workbench",
1612+
keywords:
1613+
"plate workbench facsimile photograph image side by side autopsy viewer drawing",
1614+
body: (
1615+
<>
1616+
<P>
1617+
<ModuleLink to="plates">Plate Workbench</ModuleLink> is autopsy
1618+
mode: the published image large on the left, the line-by-line
1619+
transliteration (with glyphs) on the right, so any reading can
1620+
be checked against the plate it descends from. <Kbd></Kbd>/
1621+
<Kbd></Kbd> walk every imaged document; facsimile drawings
1622+
come before photographs — and remember a drawing is the
1623+
edition's own reading, not neutral evidence. Links jump to the
1624+
full record, SigLA's paleography, and the GORILA volumes at
1625+
CEFAEL.
1626+
</P>
1627+
</>
1628+
),
1629+
},
16081630
{
16091631
id: "mod-constellation",
16101632
group: "Interpretation modules",

src/modules/registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ export const MODULE_GROUPS: { group: string; items: ModuleDef[] }[] = [
5252
keywords:
5353
"health dashboard coverage completeness damage uncertain metadata quality missing scribe period image translation",
5454
},
55+
{
56+
id: "plates",
57+
name: "Plate Workbench",
58+
group: "Research",
59+
keywords:
60+
"plates facsimile images photographs gorila side by side compare drawing autopsy viewer",
61+
},
5562
],
5663
},
5764
{
@@ -322,5 +329,6 @@ export const MODULE_COMPONENTS: Record<ModuleId, ComponentType> = {
322329
write: lazy(() => import("./WriteLinearA")),
323330
trainer: lazy(() => import("./SignTrainer")),
324331
constellation: lazy(() => import("./Constellation")),
332+
plates: lazy(() => import("./PlateWorkbench")),
325333
help: lazy(() => import("./Help")),
326334
};

0 commit comments

Comments
 (0)