Files
jeremy 27f6d95bf6 chore: sync accumulated infra changes and docs
Removals: openwebui and netclaw compose + Traefik routes (both decommissioned).
Updates: litellm, n8n, vaultwarden, ai-agents, media, traefik routes, CLAUDE.md
(nodes/ports/known-issues), agent definitions.
Additions: Rongorongo tree, LinkedStorm plan, rename-bart spec, vllm brief,
tls-options and security-headers Traefik config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:19:21 -05:00

2058 lines
85 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rongorongo · Decipherment Engine</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Cormorant+SC:wght@300;400;600&family=Cormorant+Upright:ital,wght@0,300;0,400;1,300&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0e0c0a;
--surface: #161210;
--surface2: #1e1a16;
--border: rgba(180,140,80,0.15);
--border2: rgba(180,140,80,0.28);
--gold: #b48c50;
--gold-lt: #d4b06a;
--gold-dim: rgba(180,140,80,0.5);
--text: #e8dcc8;
--text-dim: #9a8870;
--text-faint:#4a4030;
--ember: #a03820;
--ember-lt: #c84820;
--green: #3a6e50;
--green-lt: #5aa870;
--amber: #b07030;
--radius: 3px;
--gutter: 1.5rem;
}
* { margin:0; padding:0; box-sizing:border-box; }
html { font-size: 15px; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Cormorant Garamond', Georgia, serif;
font-weight: 300;
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
/* ── LAYOUT ─────────────────────────────── */
.shell {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 56px 1fr;
grid-template-areas: "topbar topbar" "sidebar main";
min-height: 100vh;
}
/* ── TOPBAR ─────────────────────────────── */
.topbar {
grid-area: topbar;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--gutter);
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.brand {
display: flex;
align-items: baseline;
gap: 0.8rem;
}
.brand-mark {
font-family: 'Cormorant SC', serif;
font-size: 1.05rem;
font-weight: 600;
color: var(--gold-lt);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.brand-sub {
font-family: 'Cormorant Garamond', serif;
font-size: 0.78rem;
color: var(--text-dim);
letter-spacing: 0.08em;
font-style: italic;
}
.topbar-right {
display: flex;
align-items: center;
gap: 1rem;
}
.corpus-status {
font-family: 'Cormorant SC', serif;
font-size: 0.68rem;
letter-spacing: 0.1em;
color: var(--text-dim);
}
.corpus-status span {
color: var(--gold);
margin-left: 0.3rem;
}
.btn-icon {
background: none;
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-dim);
font-size: 0.75rem;
padding: 0.3rem 0.7rem;
cursor: pointer;
font-family: 'Cormorant SC', serif;
letter-spacing: 0.08em;
transition: all 0.2s;
}
.btn-icon:hover { border-color: var(--border2); color: var(--gold); }
/* ── SIDEBAR ─────────────────────────────── */
.sidebar {
grid-area: sidebar;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 1rem 0;
position: sticky;
top: 56px;
height: calc(100vh - 56px);
overflow-y: auto;
}
.nav-section-label {
font-family: 'Cormorant SC', serif;
font-size: 0.6rem;
letter-spacing: 0.25em;
color: var(--text-faint);
text-transform: uppercase;
padding: 0.8rem 1.2rem 0.3rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.55rem 1.2rem;
cursor: pointer;
transition: all 0.15s;
border-left: 2px solid transparent;
font-family: 'Cormorant SC', serif;
font-size: 0.78rem;
letter-spacing: 0.06em;
color: var(--text-dim);
position: relative;
}
.nav-item:hover { color: var(--text); background: rgba(180,140,80,0.04); }
.nav-item.active { color: var(--gold-lt); border-left-color: var(--gold); background: rgba(180,140,80,0.06); }
.nav-icon { font-size: 0.85rem; width: 16px; text-align: center; }
.nav-badge {
margin-left: auto;
background: rgba(180,140,80,0.12);
border: 1px solid rgba(180,140,80,0.2);
border-radius: 10px;
padding: 1px 6px;
font-size: 0.6rem;
color: var(--gold-dim);
font-family: 'Cormorant Garamond', serif;
}
.sidebar-footer {
margin-top: auto;
padding: 1rem 1.2rem;
border-top: 1px solid var(--border);
}
.api-key-input {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-dim);
font-family: 'Cormorant Garamond', serif;
font-size: 0.75rem;
padding: 0.4rem 0.6rem;
outline: none;
transition: border-color 0.2s;
letter-spacing: 0.03em;
}
.api-key-input:focus { border-color: var(--border2); color: var(--text); }
.api-key-label {
font-family: 'Cormorant SC', serif;
font-size: 0.6rem;
letter-spacing: 0.15em;
color: var(--text-faint);
display: block;
margin-bottom: 0.35rem;
}
/* ── MAIN ─────────────────────────────────── */
.main {
grid-area: main;
overflow-y: auto;
height: calc(100vh - 56px);
}
.panel { display: none; padding: var(--gutter); }
.panel.active { display: block; }
/* ── SHARED COMPONENTS ───────────────────── */
.page-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.page-title {
font-family: 'Cormorant Upright', serif;
font-size: 1.6rem;
font-weight: 300;
color: var(--gold-lt);
letter-spacing: 0.04em;
line-height: 1.2;
}
.page-desc {
font-size: 0.85rem;
color: var(--text-dim);
font-style: italic;
margin-top: 0.25rem;
}
.card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem;
margin-bottom: 1rem;
}
.card-title {
font-family: 'Cormorant SC', serif;
font-size: 0.72rem;
letter-spacing: 0.2em;
color: var(--gold-dim);
margin-bottom: 0.9rem;
text-transform: uppercase;
}
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.75rem; }
@media (max-width: 900px) { .two-col,.three-col { grid-template-columns: 1fr; } }
label.field-label {
display: block;
font-family: 'Cormorant SC', serif;
font-size: 0.62rem;
letter-spacing: 0.15em;
color: var(--text-dim);
margin-bottom: 0.3rem;
}
input[type=text], select, textarea {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: 'Cormorant Garamond', serif;
font-size: 0.88rem;
padding: 0.5rem 0.7rem;
outline: none;
transition: border-color 0.2s;
line-height: 1.5;
}
input[type=text]:focus, select:focus, textarea:focus { border-color: var(--border2); }
textarea { resize: vertical; min-height: 60px; }
select option { background: var(--surface); }
.btn {
font-family: 'Cormorant SC', serif;
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 0.55rem 1.2rem;
border-radius: var(--radius);
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.btn-primary {
background: rgba(180,140,80,0.1);
border-color: var(--border2);
color: var(--gold-lt);
}
.btn-primary:hover:not(:disabled) { background: rgba(180,140,80,0.18); border-color: var(--gold); box-shadow: 0 0 16px rgba(180,140,80,0.06); }
.btn-ghost { background: none; border-color: var(--border); color: var(--text-dim); }
.btn-ghost:hover:not(:disabled) { border-color: var(--border2); color: var(--text); }
.btn-green { background: rgba(58,110,80,0.12); border-color: rgba(90,168,112,0.3); color: var(--green-lt); }
.btn-green:hover:not(:disabled) { background: rgba(58,110,80,0.22); border-color: rgba(90,168,112,0.5); }
.btn-danger { background: rgba(160,56,32,0.1); border-color: rgba(200,72,32,0.3); color: #e06040; }
.btn:disabled { opacity: 0.3; cursor: not-allowed; }
.btn-full { width: 100%; }
.tag {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 2px;
font-family: 'Cormorant SC', serif;
font-size: 0.6rem;
letter-spacing: 0.1em;
}
.tag-green { background: rgba(58,110,80,0.15); border: 1px solid rgba(90,168,112,0.3); color: var(--green-lt); }
.tag-amber { background: rgba(176,112,48,0.15); border: 1px solid rgba(176,112,48,0.3); color: #d4a050; }
.tag-red { background: rgba(160,56,32,0.15); border: 1px solid rgba(200,72,32,0.3); color: #e06040; }
.tag-gold { background: rgba(180,140,80,0.1); border: 1px solid var(--border2); color: var(--gold); }
.result-box {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem;
min-height: 80px;
position: relative;
}
.result-box-label {
position: absolute;
top: -0.5rem;
left: 1rem;
background: var(--bg);
padding: 0 0.4rem;
font-family: 'Cormorant SC', serif;
font-size: 0.58rem;
letter-spacing: 0.22em;
color: var(--gold-dim);
}
.empty-state {
text-align: center;
color: var(--text-faint);
font-style: italic;
font-size: 0.85rem;
padding: 2rem 0;
}
.loading-row {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 1.5rem;
justify-content: center;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 18px; height: 18px;
border: 1.5px solid var(--border);
border-top-color: var(--gold);
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
}
.loading-text {
font-family: 'Cormorant SC', serif;
font-size: 0.72rem;
letter-spacing: 0.15em;
color: var(--text-dim);
}
/* ── RENDERED MARKDOWN ───────────────────── */
.prose h2 { font-family:'Cormorant SC',serif; font-size:0.88rem; letter-spacing:0.18em; color:var(--gold); margin:1.2rem 0 0.5rem; border-bottom:1px solid var(--border); padding-bottom:0.25rem; }
.prose h3 { font-family:'Cormorant SC',serif; font-size:0.78rem; letter-spacing:0.12em; color:var(--text-dim); margin:0.9rem 0 0.3rem; }
.prose p { font-size:0.88rem; line-height:1.8; margin:0.4rem 0; }
.prose li { font-size:0.85rem; line-height:1.75; margin:0.2rem 0; padding-left:1.1rem; position:relative; }
.prose li::before { content:'—'; position:absolute; left:0; color:var(--ember-lt); font-size:0.7rem; top:0.1rem; }
.prose strong { color:var(--text); font-weight:600; }
/* ── UPLOAD ZONE ───────────────────────── */
.upload-zone {
border: 1px dashed var(--border2);
border-radius: var(--radius);
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.25s;
background: var(--bg);
}
.upload-zone:hover, .upload-zone.over { border-color: var(--gold); background: rgba(180,140,80,0.03); }
.upload-zone input { display: none; }
.upload-zone-icon { font-size: 1.5rem; opacity: 0.4; margin-bottom: 0.5rem; }
.upload-zone-text { font-family:'Cormorant SC',serif; font-size:0.78rem; letter-spacing:0.1em; color:var(--text-dim); }
.upload-zone-sub { font-size:0.72rem; color:var(--text-faint); font-style:italic; margin-top:0.2rem; }
.image-strip {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.8rem;
}
.img-thumb {
position: relative;
width: 72px; height: 72px;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: var(--surface2);
flex-shrink: 0;
cursor: pointer;
transition: border-color 0.15s;
}
.img-thumb:hover { border-color: var(--border2); }
.img-thumb img { width:100%; height:100%; object-fit:cover; opacity:0.8; }
.img-thumb-x {
position: absolute; top:2px; right:2px;
width:14px; height:14px;
background: rgba(10,8,6,0.8);
border-radius: 50%;
display: flex; align-items:center; justify-content:center;
font-size: 0.55rem; color: #e06040;
opacity: 0; transition: opacity 0.15s; cursor: pointer;
}
.img-thumb:hover .img-thumb-x { opacity: 1; }
/* ── GLYPH CATALOG ───────────────────────── */
#glyphCanvas {
cursor: crosshair;
display: block;
max-width: 100%;
border: 1px solid var(--border);
border-radius: var(--radius);
}
.glyph-toolbar {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.7rem;
flex-wrap: wrap;
}
.glyph-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.5rem;
margin-top: 0.8rem;
}
.glyph-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.5rem;
cursor: pointer;
transition: border-color 0.15s;
text-align: center;
}
.glyph-card:hover { border-color: var(--border2); }
.glyph-card.selected { border-color: var(--gold); background: rgba(180,140,80,0.04); }
.glyph-id { font-family:'Cormorant SC',serif; font-size:0.62rem; letter-spacing:0.1em; color:var(--gold-dim); }
.glyph-freq { font-size:0.7rem; color:var(--text-faint); margin-top:0.15rem; }
.glyph-tag-badge { margin-top:0.2rem; }
/* ── ANCHOR LIST ─────────────────────────── */
.anchor-row {
display: flex;
align-items: flex-start;
gap: 0.7rem;
padding: 0.7rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 0.5rem;
}
.anchor-num {
width: 24px; height: 24px;
border: 1px solid var(--border2);
border-radius: 50%;
display: flex; align-items:center; justify-content:center;
font-family:'Cormorant SC',serif; font-size:0.65rem;
color: var(--gold); flex-shrink: 0; margin-top:0.1rem;
}
.anchor-body { flex:1; min-width:0; }
.anchor-glyph { font-size:0.85rem; color:var(--text); margin-bottom:0.15rem; }
.anchor-meaning { font-size:0.78rem; color:var(--green-lt); font-style:italic; margin-bottom:0.15rem; }
.anchor-meta { font-size:0.7rem; color:var(--text-faint); }
.anchor-x { background:none; border:none; color:var(--text-faint); cursor:pointer; font-size:0.75rem; transition:color 0.15s; }
.anchor-x:hover { color:#e06040; }
/* ── FEASIBILITY BARS ────────────────────── */
.feasibility-row {
margin-bottom: 0.9rem;
}
.feasibility-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3rem;
}
.feasibility-name { font-size:0.82rem; color:var(--text); }
.feasibility-pct { font-family:'Cormorant SC',serif; font-size:0.72rem; color:var(--gold); }
.bar-track {
height: 5px;
background: var(--surface2);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
transition: width 1s cubic-bezier(.4,0,.2,1);
width: 0;
}
/* ── CONFIDENCE TIMELINE ─────────────────── */
.timeline-chart {
height: 120px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
position: relative;
overflow: hidden;
margin-bottom: 0.5rem;
}
.timeline-canvas { width:100%; height:100%; }
/* ── SEQUENCE MATCHER ────────────────────── */
.sequence-match {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.7rem;
margin-bottom: 0.5rem;
font-size: 0.82rem;
}
.sequence-match-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3rem;
}
.sequence-glyphs { font-family: monospace; color: var(--gold); font-size:0.78rem; }
.sequence-count { font-family:'Cormorant SC',serif; font-size:0.65rem; color:var(--text-dim); letter-spacing:0.1em; }
.sequence-tablets { font-size:0.72rem; color:var(--text-faint); font-style:italic; }
/* ── PHONEME MAPPER ──────────────────────── */
.phoneme-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
.phoneme-table th {
font-family:'Cormorant SC',serif;
font-size:0.62rem;
letter-spacing:0.15em;
color:var(--text-dim);
text-align:left;
padding:0.4rem 0.6rem;
border-bottom:1px solid var(--border);
}
.phoneme-table td {
padding: 0.4rem 0.6rem;
border-bottom: 1px solid rgba(180,140,80,0.06);
vertical-align: top;
}
.phoneme-table tr:hover td { background: rgba(180,140,80,0.025); }
.phoneme-match { color:var(--green-lt); }
.phoneme-rank { color:var(--text-faint); font-family:'Cormorant SC',serif; font-size:0.65rem; }
/* ── REPORT PANEL ────────────────────────── */
.report-section {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 0.8rem;
}
.report-section h3 {
font-family:'Cormorant SC',serif; font-size:0.75rem; letter-spacing:0.18em;
color:var(--gold-dim); margin-bottom:0.6rem; text-transform:uppercase;
}
/* ── SCHOLARSHIP PANEL ───────────────────── */
.scholar-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.8rem;
margin-bottom: 0.5rem;
transition: border-color 0.15s;
}
.scholar-card.active-ref { border-color: var(--gold-dim); }
.scholar-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.5rem;
cursor: pointer;
}
.scholar-name { font-family:'Cormorant SC',serif; font-size:0.78rem; color:var(--text); letter-spacing:0.06em; }
.scholar-year { font-size:0.7rem; color:var(--text-faint); flex-shrink:0; }
.scholar-desc { font-size:0.78rem; color:var(--text-dim); font-style:italic; line-height:1.6; margin-top:0.4rem; display:none; }
.scholar-card.expanded .scholar-desc { display:block; }
/* ── HISTORY ENTRIES ─────────────────────── */
.hist-entry {
padding: 0.6rem 0.8rem;
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 0.4rem;
cursor: pointer;
transition: border-color 0.15s;
}
.hist-entry:hover { border-color: var(--border2); }
.hist-meta { font-family:'Cormorant SC',serif; font-size:0.62rem; color:var(--gold-dim); letter-spacing:0.08em; margin-bottom:0.2rem; }
.hist-preview { font-size:0.78rem; color:var(--text-faint); font-style:italic; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* ── NOTIFICATIONS ───────────────────────── */
#toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
background: var(--surface2);
border: 1px solid var(--border2);
border-radius: var(--radius);
padding: 0.7rem 1rem;
font-family:'Cormorant SC',serif;
font-size:0.72rem;
letter-spacing:0.1em;
color:var(--gold-lt);
z-index:999;
opacity:0;
transform:translateY(8px);
transition:all 0.3s;
pointer-events:none;
max-width: 280px;
}
#toast.show { opacity:1; transform:translateY(0); }
/* ── SCROLLBAR ───────────────────────────── */
::-webkit-scrollbar { width:4px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
::-webkit-scrollbar-thumb:hover { background:var(--border2); }
</style>
</head>
<body>
<div class="shell">
<!-- ═══ TOPBAR ═══ -->
<header class="topbar">
<div class="brand">
<span class="brand-mark">Rongorongo</span>
<span class="brand-sub">Decipherment Engine</span>
</div>
<div class="topbar-right">
<div class="corpus-status">
Corpus <span id="corpusCount">0</span> images ·
Glyphs <span id="glyphCount">0</span> ·
Anchors <span id="anchorCount">0</span>
</div>
<button class="btn-icon" onclick="exportReport()">Export Report</button>
<button class="btn-icon" onclick="clearSession()">Clear Session</button>
</div>
</header>
<!-- ═══ SIDEBAR ═══ -->
<nav class="sidebar">
<div class="nav-section-label">Corpus</div>
<div class="nav-item active" data-panel="upload">
<span class="nav-icon">⬡</span> Image Corpus
</div>
<div class="nav-item" data-panel="glyph">
<span class="nav-icon">◈</span> Glyph Catalog
<span class="nav-badge" id="navGlyphBadge">0</span>
</div>
<div class="nav-section-label">Analysis</div>
<div class="nav-item" data-panel="pattern">
<span class="nav-icon">◉</span> Pattern Analyzer
</div>
<div class="nav-item" data-panel="sequence">
<span class="nav-icon">◇</span> Sequence Matcher
</div>
<div class="nav-item" data-panel="phoneme">
<span class="nav-icon">◌</span> Phoneme Mapper
</div>
<div class="nav-section-label">Decipherment</div>
<div class="nav-item" data-panel="anchor">
<span class="nav-icon">⚓</span> Anchor Lab
<span class="nav-badge" id="navAnchorBadge">0</span>
</div>
<div class="nav-item" data-panel="propagate">
<span class="nav-icon">⊕</span> Propagation Sim
</div>
<div class="nav-item" data-panel="feasibility">
<span class="nav-icon">✦</span> Feasibility Tests
</div>
<div class="nav-item" data-panel="confidence">
<span class="nav-icon">◆</span> Confidence Decay
</div>
<div class="nav-section-label">Reference</div>
<div class="nav-item" data-panel="scholarship">
<span class="nav-icon">📜</span> Scholarship DB
</div>
<div class="nav-item" data-panel="report">
<span class="nav-icon">◎</span> Academic Report
</div>
<div class="sidebar-footer">
<label class="api-key-label">Anthropic API Key</label>
<input class="api-key-input" type="password" id="apiKeyInput" placeholder="sk-ant-…" oninput="saveApiKey(this.value)">
</div>
</nav>
<!-- ═══════════════════════════════════════════
PANELS
═══════════════════════════════════════════ -->
<main class="main">
<!-- ── 1. IMAGE CORPUS ─────────────────────── -->
<section class="panel active" id="panel-upload">
<div class="page-header">
<div class="page-title">Image Corpus</div>
<div class="page-desc">Upload Rongorongo tablet photographs. All modules share these images.</div>
</div>
<div class="upload-zone" id="uploadZone">
<input type="file" id="fileInput" accept="image/*" multiple>
<div class="upload-zone-icon">𓀠</div>
<div class="upload-zone-text">Drop images here or click to browse</div>
<div class="upload-zone-sub">JPG · PNG · WEBP · Multiple files supported</div>
</div>
<div id="imageStrip" class="image-strip"></div>
<div class="card" style="margin-top:1rem">
<div class="card-title">Suggested Sources</div>
<div style="font-size:0.82rem; color:var(--text-dim); line-height:2;">
<div>— Barthel's <em>Grundlagen zur Entzifferung der Osterinselschrift</em> (1958) — 26 authenticated objects</div>
<div>— Fischer's <em>Rongorongo: The Easter Island Script</em> (1997)</div>
<div>— Musée de l'Homme, Paris · Smithsonian NMNH · Bishop Museum, Honolulu</div>
<div>— Santiago Staff tablet (highest glyph density, ~2320 glyphs)</div>
</div>
</div>
</section>
<!-- ── 2. GLYPH CATALOG ────────────────────── -->
<section class="panel" id="panel-glyph">
<div class="page-header">
<div class="page-title">Glyph Catalog</div>
<div class="page-desc">Click and drag on any image to isolate individual glyphs. Tag, classify, and track them across the corpus.</div>
</div>
<div class="two-col">
<div>
<div class="card-title">Select Image to Annotate</div>
<select id="glyphImageSelect" onchange="loadGlyphCanvas()">
<option value="">— No images loaded —</option>
</select>
<div style="margin-top:0.8rem;">
<canvas id="glyphCanvas" width="600" height="400"></canvas>
</div>
<div class="glyph-toolbar" style="margin-top:0.6rem;">
<button class="btn btn-primary" onclick="commitGlyphBox()">+ Tag Selected Region</button>
<button class="btn btn-ghost" onclick="clearGlyphBox()">Clear</button>
<span style="font-size:0.72rem; color:var(--text-faint); font-style:italic;" id="glyphBoxStatus">Draw a box around a glyph</span>
</div>
<div class="two-col" style="margin-top:0.5rem;">
<div>
<label class="field-label">Glyph ID (auto or manual)</label>
<input type="text" id="newGlyphId" placeholder="G001">
</div>
<div>
<label class="field-label">Tag / Category</label>
<select id="newGlyphTag">
<option value="unknown">Unknown</option>
<option value="anthropomorphic">Anthropomorphic</option>
<option value="zoomorphic">Zoomorphic</option>
<option value="geometric">Geometric</option>
<option value="compound">Compound</option>
<option value="astronomical">Astronomical</option>
<option value="botanical">Botanical</option>
</select>
</div>
</div>
</div>
<div>
<div class="card-title">Glyph Registry <span id="glyphRegistryCount" style="color:var(--text-faint)"></span></div>
<div id="glyphGrid" class="glyph-grid">
<div class="empty-state">No glyphs tagged yet</div>
</div>
<div id="glyphAiPanel" style="margin-top:1rem; display:none">
<div class="result-box" data-label="◈ AI GLYPH ANALYSIS" id="glyphAiResult">
<span class="result-box-label">◈ AI GLYPH ANALYSIS</span>
<div class="empty-state">Select a glyph to analyze</div>
</div>
<button class="btn btn-primary btn-full" style="margin-top:0.5rem" onclick="analyzeSelectedGlyph()">Analyze Selected Glyph</button>
</div>
</div>
</div>
</section>
<!-- ── 3. PATTERN ANALYZER ─────────────────── -->
<section class="panel" id="panel-pattern">
<div class="page-header">
<div class="page-title">Pattern Analyzer</div>
<div class="page-desc">AI-powered analysis of recurring motifs, frequency distributions, structural patterns, and cross-script comparisons.</div>
</div>
<div class="card">
<div class="card-title">Analysis Mode</div>
<div class="three-col">
<button class="btn btn-primary mode-btn active" data-mode="pattern">◈ Pattern Recognition</button>
<button class="btn btn-ghost mode-btn" data-mode="frequency">◉ Glyph Frequency</button>
<button class="btn btn-ghost mode-btn" data-mode="structure">◇ Structural Analysis</button>
<button class="btn btn-ghost mode-btn" data-mode="comparative">⊕ Cross-Script</button>
<button class="btn btn-ghost mode-btn" data-mode="meaning">✦ Meaning Hypothesis</button>
<button class="btn btn-ghost mode-btn" data-mode="custom">◌ Custom Query</button>
</div>
<div id="customQueryWrap" style="margin-top:0.7rem;display:none">
<label class="field-label">Custom Query</label>
<textarea id="customQuery" placeholder="Describe what you want to investigate…"></textarea>
</div>
<div style="display:flex;gap:0.6rem;margin-top:0.8rem;">
<button class="btn btn-primary" id="patternAnalyzeBtn" onclick="runPatternAnalysis()">Run Analysis</button>
<div style="margin-left:auto; font-size:0.75rem; color:var(--text-faint); font-style:italic; align-self:center;" id="patternImageCount"></div>
</div>
</div>
<div class="result-box" id="patternResult">
<span class="result-box-label">◉ ANALYSIS OUTPUT</span>
<div class="empty-state">Upload images and run an analysis to begin</div>
</div>
<div id="patternHistory" style="margin-top:1rem; display:none">
<div class="card-title">Session History</div>
<div id="patternHistoryList"></div>
</div>
</section>
<!-- ── 4. SEQUENCE MATCHER ─────────────────── -->
<section class="panel" id="panel-sequence">
<div class="page-header">
<div class="page-title">Cross-Tablet Sequence Matcher</div>
<div class="page-desc">Find glyph sequences that repeat across multiple tablets — formulaic phrases, titles, or liturgical refrains.</div>
</div>
<div class="card">
<div class="two-col">
<div>
<label class="field-label">Minimum Sequence Length</label>
<select id="seqMinLen">
<option value="2">2 glyphs</option>
<option value="3" selected>3 glyphs</option>
<option value="4">4 glyphs</option>
<option value="5">5 glyphs</option>
</select>
</div>
<div>
<label class="field-label">Analysis Focus</label>
<select id="seqFocus">
<option value="all">All patterns</option>
<option value="line-start">Line-initial sequences</option>
<option value="line-end">Line-terminal sequences</option>
<option value="frequent">High-frequency only</option>
</select>
</div>
</div>
<button class="btn btn-primary btn-full" style="margin-top:0.8rem" onclick="runSequenceMatcher()">Find Repeating Sequences</button>
</div>
<div class="result-box" id="seqResult">
<span class="result-box-label">◇ SEQUENCE MATCHES</span>
<div class="empty-state">Load multiple tablet images then run the matcher</div>
</div>
</section>
<!-- ── 5. PHONEME MAPPER ──────────────────── -->
<section class="panel" id="panel-phoneme">
<div class="page-header">
<div class="page-title">Rapa Nui Phoneme Mapper</div>
<div class="page-desc">Statistical correlation between Rapa Nui syllable frequencies and observed glyph frequency rankings. Potential phonetic assignments emerge where rankings align.</div>
</div>
<div class="card">
<div class="card-title">Rapa Nui Phoneme Inventory (Known)</div>
<div style="font-size:0.82rem; color:var(--text-dim); line-height:1.9; font-style:italic;">
CV structure dominant. Consonants: p t k m n ŋ h r v. Vowels: a e i o u.<br>
Most frequent syllables (corpus analysis): <strong style="color:var(--text)">ko · ma · ta · ra · ka · ha · na · te · ai · oa</strong>
</div>
<button class="btn btn-primary btn-full" style="margin-top:0.8rem" onclick="runPhonemMapper()">Generate Phoneme Correlation</button>
</div>
<div id="phonemeResult" style="display:none">
<div class="card-title" style="margin-bottom:0.6rem">Frequency Correlation Table</div>
<table class="phoneme-table" id="phonemeTable">
<thead>
<tr>
<th>Glyph Rank</th>
<th>Visual Description</th>
<th>Freq. Est.</th>
<th>Rapa Nui Candidate</th>
<th>Match Confidence</th>
</tr>
</thead>
<tbody id="phonemeTableBody"></tbody>
</table>
<div class="result-box" style="margin-top:0.8rem" id="phonemeAiResult">
<span class="result-box-label">◌ PHONETIC ANALYSIS</span>
<div class="empty-state">Run the mapper to see AI phonetic analysis</div>
</div>
</div>
</section>
<!-- ── 6. ANCHOR LAB ───────────────────────── -->
<section class="panel" id="panel-anchor">
<div class="page-header">
<div class="page-title">Bilingual Anchor Laboratory</div>
<div class="page-desc">Define hypothetical glyph-to-meaning assignments. Each anchor is a foothold for propagation testing.</div>
</div>
<div class="two-col">
<div>
<div class="card">
<div class="card-title">Define New Anchor</div>
<div style="margin-bottom:0.6rem">
<label class="field-label">Glyph Description</label>
<input type="text" id="aGlyph" placeholder="e.g. Bird-head on human torso, wings raised">
</div>
<div style="margin-bottom:0.6rem">
<label class="field-label">Proposed Meaning</label>
<input type="text" id="aMeaning" placeholder="e.g. Tangata Manu / Bird-Man">
</div>
<div class="two-col" style="margin-bottom:0.6rem">
<div>
<label class="field-label">Rapa Nui Word</label>
<input type="text" id="aWord" placeholder="tangata manu">
</div>
<div>
<label class="field-label">Strategy</label>
<select id="aStrategy">
<option value="cultural">Cultural Icon</option>
<option value="astronomical">Astronomical</option>
<option value="linguistic">Linguistic</option>
<option value="biological">Flora/Fauna</option>
<option value="genealogical">Genealogical</option>
</select>
</div>
</div>
<div style="margin-bottom:0.6rem">
<label class="field-label">Confidence</label>
<select id="aConfidence">
<option value="high">High — Strong evidence</option>
<option value="medium" selected>Medium — Plausible</option>
<option value="low">Low — Speculative</option>
</select>
</div>
<div style="margin-bottom:0.8rem">
<label class="field-label">Reasoning</label>
<textarea id="aReasoning" placeholder="Why do you believe this? What evidence supports it?"></textarea>
</div>
<div style="display:flex;gap:0.6rem">
<button class="btn btn-primary" style="flex:1" onclick="addAnchor()">+ Add Anchor</button>
<button class="btn btn-green" style="flex:1" onclick="aiGenerateAnchors()">✦ AI Generate</button>
</div>
</div>
<div class="result-box" id="anchorGenResult" style="display:none">
<span class="result-box-label">⚓ AI CANDIDATES</span>
</div>
</div>
<div>
<div class="card-title">Anchor Registry</div>
<div id="anchorList">
<div class="empty-state">No anchors defined</div>
</div>
</div>
</div>
</section>
<!-- ── 7. PROPAGATION SIMULATOR ───────────── -->
<section class="panel" id="panel-propagate">
<div class="page-header">
<div class="page-title">Anchor Propagation Simulator</div>
<div class="page-desc">Select an anchor and simulate how assigning that meaning propagates to neighboring glyphs across all loaded tablet images.</div>
</div>
<div class="card">
<div class="two-col">
<div>
<label class="field-label">Select Anchor to Propagate</label>
<select id="propagateAnchorSelect">
<option value="">— Define anchors first —</option>
</select>
</div>
<div>
<label class="field-label">Propagation Depth</label>
<select id="propagateDepth">
<option value="1">1 — Immediate neighbors</option>
<option value="2" selected>2 — Two glyphs out</option>
<option value="3">3 — Phrase-level</option>
</select>
</div>
</div>
<div style="margin-top:0.6rem">
<label class="field-label">Additional Hypotheses to Include</label>
<textarea id="propagateContext" placeholder="e.g. Assume tablets are ritual chants. The sequence likely ends with a deity name."></textarea>
</div>
<button class="btn btn-primary btn-full" style="margin-top:0.8rem" onclick="runPropagation()">Simulate Propagation</button>
</div>
<div class="result-box" id="propagateResult">
<span class="result-box-label">⊕ PROPAGATION OUTPUT</span>
<div class="empty-state">Define anchors and select one to simulate propagation</div>
</div>
</section>
<!-- ── 8. FEASIBILITY TESTS ──────────────── -->
<section class="panel" id="panel-feasibility">
<div class="page-header">
<div class="page-title">Feasibility Testing</div>
<div class="page-desc">Test whether each anchor is internally consistent, culturally coherent, and statistically plausible across the corpus.</div>
</div>
<div class="card">
<div class="card-title">Select Anchors to Test</div>
<div id="feasibilityAnchorSelect">
<div class="empty-state">No anchors defined. Go to Anchor Lab first.</div>
</div>
<div class="two-col" style="margin-top:0.8rem">
<div>
<label class="field-label">Test Methods</label>
<div style="display:flex;flex-direction:column;gap:0.35rem;margin-top:0.2rem">
<label style="font-size:0.8rem;color:var(--text-dim);cursor:pointer;display:flex;align-items:center;gap:0.5rem"><input type="checkbox" id="tProp" checked> Propagation consistency</label>
<label style="font-size:0.8rem;color:var(--text-dim);cursor:pointer;display:flex;align-items:center;gap:0.5rem"><input type="checkbox" id="tZipf" checked> Zipf's Law conformance</label>
<label style="font-size:0.8rem;color:var(--text-dim);cursor:pointer;display:flex;align-items:center;gap:0.5rem"><input type="checkbox" id="tCult" checked> Cultural coherence</label>
<label style="font-size:0.8rem;color:var(--text-dim);cursor:pointer;display:flex;align-items:center;gap:0.5rem"><input type="checkbox" id="tCont"> Contradiction detection</label>
</div>
</div>
<div>
<label class="field-label">Contextual Constraints</label>
<textarea id="feasibilityContext" placeholder="e.g. Assume liturgical chant structure…"></textarea>
</div>
</div>
<button class="btn btn-primary btn-full" style="margin-top:0.8rem" id="feasibilityRunBtn" onclick="runFeasibility()">Run Feasibility Tests</button>
</div>
<div id="feasibilityScorePanel" style="display:none">
<div class="card-title">Feasibility Scores</div>
<div id="feasibilityScores"></div>
</div>
<div class="result-box" style="margin-top:0.8rem" id="feasibilityResult">
<span class="result-box-label">✦ FEASIBILITY REPORT</span>
<div class="empty-state">Define and select anchors, then run tests</div>
</div>
<div id="feasibilityHistory" style="margin-top:1rem;display:none">
<div class="card-title">Test History</div>
<div id="feasibilityHistoryList"></div>
</div>
</section>
<!-- ── 9. CONFIDENCE DECAY ────────────────── -->
<section class="panel" id="panel-confidence">
<div class="page-header">
<div class="page-title">Confidence Decay Tracker</div>
<div class="page-desc">Running confidence scores for each anchor. Updates automatically as new feasibility tests produce supporting or contradicting evidence.</div>
</div>
<div id="confidenceCharts">
<div class="empty-state" style="padding:3rem 0">Run feasibility tests to populate confidence tracking</div>
</div>
</section>
<!-- ── 10. SCHOLARSHIP DB ─────────────────── -->
<section class="panel" id="panel-scholarship">
<div class="page-header">
<div class="page-title">Scholarship Database</div>
<div class="page-desc">Established decipherment frameworks. Toggle to include as context in any AI analysis.</div>
</div>
<div id="scholarshipList"></div>
<div class="card" style="margin-top:1rem">
<div class="card-title">Ask Against Scholarship</div>
<textarea id="scholarshipQuery" placeholder="e.g. Does Fischer's creation chant hypothesis conflict with the astronomical calendar theory?"></textarea>
<button class="btn btn-primary btn-full" style="margin-top:0.7rem" onclick="queryScholarship()">Query Scholarship DB</button>
<div class="result-box" style="margin-top:0.8rem" id="scholarshipResult">
<span class="result-box-label">📜 SCHOLARSHIP ANALYSIS</span>
<div class="empty-state">Ask a question about existing decipherment scholarship</div>
</div>
</div>
</section>
<!-- ── 11. ACADEMIC REPORT ────────────────── -->
<section class="panel" id="panel-report">
<div class="page-header">
<div class="page-title">Academic Report Generator</div>
<div class="page-desc">Export all session work as a structured research document suitable for sharing with scholars.</div>
</div>
<div class="card">
<div class="two-col">
<div>
<label class="field-label">Researcher Name</label>
<input type="text" id="reportAuthor" placeholder="Your name or institution">
</div>
<div>
<label class="field-label">Report Title</label>
<input type="text" id="reportTitle" placeholder="Rongorongo Decipherment Study, 2025">
</div>
</div>
<div style="margin-top:0.6rem">
<label class="field-label">Abstract / Notes</label>
<textarea id="reportNotes" placeholder="Brief description of your methodology and goals…" style="min-height:80px"></textarea>
</div>
<div style="display:flex;gap:0.6rem;margin-top:0.8rem">
<button class="btn btn-primary" style="flex:1" onclick="generateReport()">Generate Report</button>
<button class="btn btn-ghost" style="flex:1" onclick="downloadReport()">Download JSON</button>
</div>
</div>
<div id="reportOutput" style="display:none">
<div class="card-title" style="margin-bottom:0.6rem">Generated Report</div>
<div id="reportSections"></div>
</div>
</section>
</main><!-- .main -->
</div><!-- .shell -->
<div id="toast"></div>
<!-- ════════════════════════════════════════
JAVASCRIPT
════════════════════════════════════════ -->
<script>
// ── STATE ──────────────────────────────────
const S = {
images: [], // {name, dataUrl, base64, mimeType}
glyphs: [], // {id, tag, imageIdx, box:{x,y,w,h}, dataUrl, notes}
anchors: [], // {id, glyph, meaning, word, strategy, confidence, reasoning, scores:[]}
analyses: [], // history
feasibilityRuns: [],
selectedGlyph: null,
confidenceHistory: {}, // anchorId -> [{ts, score}]
};
// ── PERSISTENCE ────────────────────────────
function save() {
try {
const snap = {
glyphs: S.glyphs,
anchors: S.anchors,
analyses: S.analyses.slice(0,10),
feasibilityRuns: S.feasibilityRuns.slice(0,5),
confidenceHistory: S.confidenceHistory,
};
localStorage.setItem('rongo_state', JSON.stringify(snap));
} catch(e) {}
}
function load() {
try {
const raw = localStorage.getItem('rongo_state');
if (!raw) return;
const snap = JSON.parse(raw);
S.glyphs = snap.glyphs || [];
S.anchors = snap.anchors || [];
S.analyses = snap.analyses || [];
S.feasibilityRuns = snap.feasibilityRuns || [];
S.confidenceHistory = snap.confidenceHistory || {};
refreshAll();
} catch(e) {}
}
function saveApiKey(v) {
try { localStorage.setItem('rongo_apikey', v); } catch(e) {}
}
function getApiKey() {
return document.getElementById('apiKeyInput').value.trim()
|| (()=>{ try{return localStorage.getItem('rongo_apikey')||'';}catch(e){return '';} })();
}
// ── NAVIGATION ─────────────────────────────
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
item.classList.add('active');
document.getElementById('panel-' + item.dataset.panel).classList.add('active');
if (item.dataset.panel === 'feasibility') renderFeasibilitySelect();
if (item.dataset.panel === 'propagate') renderPropagateSelect();
if (item.dataset.panel === 'confidence') renderConfidenceCharts();
if (item.dataset.panel === 'scholarship') renderScholarship();
if (item.dataset.panel === 'glyph') refreshGlyphSelect();
});
});
// ── TOAST ──────────────────────────────────
function toast(msg, dur=2800) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(() => el.classList.remove('show'), dur);
}
// ── API CALL ───────────────────────────────
async function claude(prompt, includeImages=true, max=1000) {
const key = getApiKey();
if (!key) { toast('Enter your Anthropic API key in the sidebar'); throw new Error('No API key'); }
const content = [
...(includeImages ? S.images.map(img => ({
type: 'image',
source: { type:'base64', media_type: img.mimeType, data: img.base64 }
})) : []),
{ type:'text', text: prompt }
];
const r = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type':'application/json', 'x-api-key': key, 'anthropic-version':'2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' },
body: JSON.stringify({ model:'claude-sonnet-4-20250514', max_tokens: max, messages:[{ role:'user', content }] })
});
const d = await r.json();
if (d.error) throw new Error(d.error.message);
return d.content.map(b => b.text||'').join('');
}
// ── MARKDOWN ──────────────────────────────
function md(text) {
const lines = text.split('\n');
let html = '<div class="prose">';
let inList = false;
lines.forEach(line => {
const t = line.trim();
if (!t) { if (inList) { html += '</ul>'; inList=false; } html += '<br>'; return; }
if (t.startsWith('## ')) { if(inList){html+='</ul>';inList=false;} html += `<h2>${t.slice(3)}</h2>`; }
else if (t.startsWith('### ')) { if(inList){html+='</ul>';inList=false;} html += `<h3>${t.slice(4)}</h3>`; }
else if (t.startsWith('# ')) { if(inList){html+='</ul>';inList=false;} html += `<h2>${t.slice(2)}</h2>`; }
else if (t.startsWith('- ') || t.startsWith('• ') || /^\d+\./.test(t)) {
if (!inList) { html += '<ul>'; inList=true; }
const c = t.replace(/^[-•]\s*/,'').replace(/^\d+\.\s*/,'').replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>');
html += `<li>${c}</li>`;
} else {
if(inList){html+='</ul>';inList=false;}
const c = t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>');
html += `<p>${c}</p>`;
}
});
if (inList) html += '</ul>';
html += '</div>';
return html;
}
function setLoading(elId, msg='Analyzing…') {
document.getElementById(elId).innerHTML = `<div class="loading-row"><div class="spinner"></div><div class="loading-text">${msg}</div></div>`;
}
// ── IMAGE UPLOAD ───────────────────────────
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('over'); });
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('over'));
uploadZone.addEventListener('drop', e => { e.preventDefault(); uploadZone.classList.remove('over'); handleFiles([...e.dataTransfer.files].filter(f=>f.type.startsWith('image/'))); });
fileInput.addEventListener('change', e => handleFiles([...e.target.files]));
function handleFiles(files) {
files.forEach(f => {
const fr = new FileReader();
fr.onload = e => {
S.images.push({ name: f.name, dataUrl: e.target.result, base64: e.target.result.split(',')[1], mimeType: f.type||'image/jpeg' });
renderImageStrip();
refreshAll();
toast(`Loaded ${f.name}`);
};
fr.readAsDataURL(f);
});
}
function renderImageStrip() {
const strip = document.getElementById('imageStrip');
strip.innerHTML = S.images.map((img,i) => `
<div class="img-thumb" onclick="selectThumb(${i})">
<img src="${img.dataUrl}" alt="${img.name}">
<div class="img-thumb-x" onclick="event.stopPropagation();removeImage(${i})">✕</div>
</div>`).join('');
}
function removeImage(i) {
S.images.splice(i,1);
renderImageStrip();
refreshAll();
}
function refreshAll() {
// badges
document.getElementById('corpusCount').textContent = S.images.length;
document.getElementById('glyphCount').textContent = S.glyphs.length;
document.getElementById('anchorCount').textContent = S.anchors.length;
document.getElementById('navGlyphBadge').textContent = S.glyphs.length;
document.getElementById('navAnchorBadge').textContent = S.anchors.length;
document.getElementById('patternImageCount').textContent = S.images.length ? `${S.images.length} image${S.images.length>1?'s':''} in corpus` : '';
renderAnchorList();
renderGlyphGrid();
refreshGlyphSelect();
save();
}
// ── MODE BUTTONS ───────────────────────────
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(b => { b.classList.remove('active'); b.classList.add('btn-ghost'); b.classList.remove('btn-primary'); });
btn.classList.add('active','btn-primary'); btn.classList.remove('btn-ghost');
document.getElementById('customQueryWrap').style.display = btn.dataset.mode==='custom' ? 'block' : 'none';
});
});
// ── PATTERN ANALYSIS ─────────────────────
const PROMPTS = {
pattern: `Expert epigrapher analysis. Examine these Rongorongo tablet images for:
1. Recurring visual patterns and motifs
2. Positional regularities (line-initial, line-terminal glyphs)
3. Glyph clustering — which symbols consistently neighbor each other
4. Boustrophedon directionality cues
5. Compound glyph structures (base + modifier)
Use section headers. Be specific — describe glyphs visually.`,
frequency: `Statistical analysis as computational linguist:
1. Most frequently appearing glyph types — describe each visually
2. Does frequency distribution follow Zipf's Law? (Would indicate natural language)
3. Estimated distinct symbol count
4. Hapax legomena — signs appearing exactly once
5. Does frequency profile suggest logographic, syllabic, or alphabetic system?
Provide structured frequency report.`,
structure: `Deep structural linguistic analysis:
1. Line/sequence structure and grouping
2. Evidence of word boundaries or phrase breaks
3. Grammatical markers — prefixes, suffixes, determinatives?
4. Symmetry or parallelism suggesting poetic/ritual structure
5. How does physical tablet layout relate to content structure?
6. Any scribal corrections, erasures, or variant forms visible?`,
comparative: `Cross-script comparative analysis:
1. Visual similarities to Linear A, Linear B, Proto-Sinaitic, Indus Valley, Olmec scripts
2. Glyphs matching known astronomical symbols (Polynesian or otherwise)
3. Similarities to Polynesian tattoo motifs, navigation charts, or tapa patterns
4. Zoomorphic figures — identify specific animals depicted
5. Potential phonetic borrowings from Rapa Nui CV syllable structure
6. Hypothesis: which existing script family does Rongorongo most resemble?`,
meaning: `Attempt genuine decipherment hypothesis:
1. Identify most iconic/frequent glyphs — propose meaning from visual form, cultural context, Polynesian cosmology
2. Which glyphs might represent the bird-man (tangata manu) cult, Make-Make deity, or ancestral chiefs?
3. Propose sequence meanings: creation myth, genealogy, agricultural calendar, ritual chant?
4. Identify potential phonetic complements
5. Construct at least one tentative translation of a visible sequence with full reasoning
6. What specific evidence would confirm or deny your hypothesis?`,
};
function runPatternAnalysis() {
if (!S.images.length) { toast('Upload images first'); return; }
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'pattern';
const prompt = mode==='custom' ? document.getElementById('customQuery').value.trim()||'Analyze these Rongorongo images.' : PROMPTS[mode];
const btn = document.getElementById('patternAnalyzeBtn');
btn.disabled = true;
setLoading('patternResult', 'Analyzing corpus…');
claude(prompt).then(text => {
document.getElementById('patternResult').innerHTML = `<span class="result-box-label">◉ ANALYSIS OUTPUT</span>${md(text)}`;
const entry = { mode, text, ts: new Date().toLocaleTimeString() };
S.analyses.unshift(entry);
if (S.analyses.length > 8) S.analyses.pop();
renderPatternHistory();
save();
}).catch(e => {
document.getElementById('patternResult').innerHTML = `<span class="result-box-label">◉ ANALYSIS OUTPUT</span><div style="color:#e06040;font-style:italic;font-size:0.82rem;padding:1rem">⚠ ${e.message}</div>`;
}).finally(() => { btn.disabled = false; });
}
function renderPatternHistory() {
if (!S.analyses.length) return;
const sec = document.getElementById('patternHistory');
sec.style.display = 'block';
document.getElementById('patternHistoryList').innerHTML = S.analyses.map((h,i) => `
<div class="hist-entry" onclick="showHistEntry(${i})">
<div class="hist-meta">${h.mode} · ${h.ts}</div>
<div class="hist-preview">${h.text.slice(0,100)}…</div>
</div>`).join('');
}
function showHistEntry(i) {
const h = S.analyses[i];
document.getElementById('patternResult').innerHTML = `<span class="result-box-label">◉ ANALYSIS OUTPUT</span>${md(h.text)}`;
}
// ── GLYPH CATALOG ─────────────────────────
let glyphCanvas, glyphCtx, canvasImg = null;
let drawing = false, startX=0, startY=0, currBox=null;
function loadGlyphCanvas() {
const sel = document.getElementById('glyphImageSelect');
const idx = parseInt(sel.value);
if (isNaN(idx) || !S.images[idx]) return;
const cv = document.getElementById('glyphCanvas');
glyphCanvas = cv;
glyphCtx = cv.getContext('2d');
const img = new Image();
img.onload = () => {
const maxW = cv.parentElement.offsetWidth - 20;
const scale = Math.min(1, maxW / img.naturalWidth);
cv.width = img.naturalWidth * scale;
cv.height = img.naturalHeight * scale;
canvasImg = img;
glyphCtx.drawImage(img, 0, 0, cv.width, cv.height);
drawExistingBoxes(idx);
};
img.src = S.images[idx].dataUrl;
}
function drawExistingBoxes(imageIdx) {
S.glyphs.filter(g => g.imageIdx === imageIdx).forEach(g => {
glyphCtx.strokeStyle = 'rgba(180,140,80,0.7)';
glyphCtx.lineWidth = 1.5;
glyphCtx.strokeRect(g.box.x, g.box.y, g.box.w, g.box.h);
glyphCtx.fillStyle = 'rgba(180,140,80,0.8)';
glyphCtx.font = '10px Cormorant Garamond';
glyphCtx.fillText(g.id, g.box.x+2, g.box.y+10);
});
}
function getCanvasPos(e) {
const r = glyphCanvas.getBoundingClientRect();
return { x: (e.clientX - r.left) * (glyphCanvas.width / r.width), y: (e.clientY - r.top) * (glyphCanvas.height / r.height) };
}
document.getElementById('glyphCanvas').addEventListener('mousedown', e => {
if (!canvasImg) return;
const p = getCanvasPos(e);
drawing = true; startX = p.x; startY = p.y; currBox = null;
});
document.getElementById('glyphCanvas').addEventListener('mousemove', e => {
if (!drawing) return;
const p = getCanvasPos(e);
const idx = parseInt(document.getElementById('glyphImageSelect').value);
glyphCtx.clearRect(0, 0, glyphCanvas.width, glyphCanvas.height);
glyphCtx.drawImage(canvasImg, 0, 0, glyphCanvas.width, glyphCanvas.height);
drawExistingBoxes(idx);
glyphCtx.strokeStyle = 'rgba(180,140,80,0.9)';
glyphCtx.lineWidth = 1.5;
glyphCtx.setLineDash([4,2]);
glyphCtx.strokeRect(startX, startY, p.x-startX, p.y-startY);
glyphCtx.setLineDash([]);
currBox = { x:Math.min(startX,p.x), y:Math.min(startY,p.y), w:Math.abs(p.x-startX), h:Math.abs(p.y-startY) };
document.getElementById('glyphBoxStatus').textContent = `Box: ${Math.round(currBox.w)}×${Math.round(currBox.h)}px`;
});
document.getElementById('glyphCanvas').addEventListener('mouseup', () => { drawing = false; });
function commitGlyphBox() {
if (!currBox || currBox.w < 5 || currBox.h < 5) { toast('Draw a box around a glyph first'); return; }
const idx = parseInt(document.getElementById('glyphImageSelect').value);
const idField = document.getElementById('newGlyphId');
const gid = idField.value.trim() || `G${String(S.glyphs.length+1).padStart(3,'0')}`;
const tag = document.getElementById('newGlyphTag').value;
// Crop to dataUrl
const tmpCv = document.createElement('canvas');
tmpCv.width = currBox.w; tmpCv.height = currBox.h;
tmpCv.getContext('2d').drawImage(canvasImg, currBox.x * (canvasImg.naturalWidth/glyphCanvas.width), currBox.y * (canvasImg.naturalHeight/glyphCanvas.height), currBox.w * (canvasImg.naturalWidth/glyphCanvas.width), currBox.h * (canvasImg.naturalHeight/glyphCanvas.height), 0, 0, currBox.w, currBox.h);
const cropUrl = tmpCv.toDataURL('image/png');
S.glyphs.push({ id: gid, tag, imageIdx: idx, box: {...currBox}, dataUrl: cropUrl });
currBox = null;
idField.value = '';
loadGlyphCanvas();
renderGlyphGrid();
refreshAll();
toast(`Tagged glyph ${gid}`);
}
function clearGlyphBox() {
currBox = null; drawing = false;
if (canvasImg) { glyphCtx.clearRect(0,0,glyphCanvas.width,glyphCanvas.height); glyphCtx.drawImage(canvasImg,0,0,glyphCanvas.width,glyphCanvas.height); drawExistingBoxes(parseInt(document.getElementById('glyphImageSelect').value)||0); }
document.getElementById('glyphBoxStatus').textContent = 'Draw a box around a glyph';
}
function renderGlyphGrid() {
document.getElementById('glyphRegistryCount').textContent = S.glyphs.length ? `(${S.glyphs.length})` : '';
const grid = document.getElementById('glyphGrid');
if (!S.glyphs.length) { grid.innerHTML = '<div class="empty-state">No glyphs tagged yet</div>'; return; }
grid.innerHTML = S.glyphs.map((g,i) => `
<div class="glyph-card ${S.selectedGlyph===i?'selected':''}" onclick="selectGlyph(${i})">
<img src="${g.dataUrl}" style="width:100%;height:60px;object-fit:contain;background:rgba(0,0,0,0.3);border-radius:2px;margin-bottom:0.3rem">
<div class="glyph-id">${g.id}</div>
<div class="glyph-freq"><span class="tag tag-gold">${g.tag}</span></div>
</div>`).join('');
document.getElementById('glyphAiPanel').style.display = S.glyphs.length ? 'block' : 'none';
}
function selectGlyph(i) {
S.selectedGlyph = i;
renderGlyphGrid();
}
function analyzeSelectedGlyph() {
if (S.selectedGlyph === null) { toast('Select a glyph first'); return; }
const g = S.glyphs[S.selectedGlyph];
const resultEl = document.getElementById('glyphAiResult');
setLoading('glyphAiResult', 'Analyzing glyph…');
// Send just this cropped glyph image
const key = getApiKey();
if (!key) { toast('Enter API key'); return; }
const prompt = `Analyze this individual Rongorongo glyph in detail:
1. Describe its visual form precisely (shape, components, orientation)
2. Classify it: anthropomorphic / zoomorphic / geometric / compound / other
3. Identify which (if any) species, deity, or concept it might depict based on Rapa Nui culture
4. Compare it to known Barthel catalog numbers if you can identify it
5. Proposed meaning with confidence and reasoning
6. Which other glyph types would you expect to see adjacent to it?`;
fetch('https://api.anthropic.com/v1/messages', {
method:'POST',
headers:{'Content-Type':'application/json','x-api-key':key,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'},
body: JSON.stringify({ model:'claude-sonnet-4-20250514', max_tokens:800, messages:[{ role:'user', content:[
{ type:'image', source:{type:'base64',media_type:'image/png',data:g.dataUrl.split(',')[1]} },
{ type:'text', text:prompt }
]}]})
}).then(r=>r.json()).then(d => {
const text = d.content.map(b=>b.text||'').join('');
resultEl.innerHTML = `<span class="result-box-label">◈ AI GLYPH ANALYSIS</span>${md(text)}`;
}).catch(e => {
resultEl.innerHTML = `<span class="result-box-label">◈ AI GLYPH ANALYSIS</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
});
}
function refreshGlyphSelect() {
const sel = document.getElementById('glyphImageSelect');
const cur = sel.value;
sel.innerHTML = S.images.length
? S.images.map((img,i) => `<option value="${i}">${img.name}</option>`).join('')
: '<option value="">— No images loaded —</option>';
if (cur) sel.value = cur;
}
// ── SEQUENCE MATCHER ──────────────────────
function runSequenceMatcher() {
if (S.images.length < 2) { toast('Load at least 2 tablet images for cross-tablet matching'); return; }
const minLen = document.getElementById('seqMinLen').value;
const focus = document.getElementById('seqFocus').value;
setLoading('seqResult', 'Finding repeating sequences…');
const prompt = `You are conducting cross-tablet sequence analysis on ${S.images.length} Rongorongo tablet images.
Task: Identify glyph sequences of ${minLen}+ symbols that appear on more than one tablet.
Focus: ${focus}
For each repeating sequence found:
1. Describe the glyph sequence visually (e.g. "bird-head + geometric circle + human torso")
2. Note which tablets it appears on (by image position/number)
3. Estimate how many times it appears total
4. Propose what type of text this repeated sequence might be: title, deity name, formula, grammatical marker
5. Priority score: how valuable is this as a decipherment target? (HIGH/MEDIUM/LOW)
Also identify: Are there any sequences that appear at the START of lines across multiple tablets? These are prime decipherment targets.
Format results clearly with a header per sequence.`;
claude(prompt).then(text => {
document.getElementById('seqResult').innerHTML = `<span class="result-box-label">◇ SEQUENCE MATCHES</span>${md(text)}`;
}).catch(e => {
document.getElementById('seqResult').innerHTML = `<span class="result-box-label">◇ SEQUENCE MATCHES</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
});
}
// ── PHONEME MAPPER ────────────────────────
function runPhonemMapper() {
if (!S.images.length) { toast('Upload images first'); return; }
const resultEl = document.getElementById('phonemeAiResult');
document.getElementById('phonemeResult').style.display = 'block';
setLoading('phonemeAiResult', 'Generating phoneme correlations…');
const prompt = `Perform Rapa Nui phoneme frequency mapping for Rongorongo decipherment.
Rapa Nui syllable frequency ranking (most common first):
ko, ma, ta, ra, ka, ha, na, te, ai, oa, tu, ri, mo, nu, he, ki, ro, me, to, pa
Analyze the images and:
1. Identify the 20 most frequent distinct glyph types — describe each visually
2. Rank them by estimated frequency (1=most common)
3. For each glyph rank, compare to the equivalent Rapa Nui syllable rank
4. Identify which frequency-rank correlations look most plausible as phonetic matches
5. Flag any correlations that conflict with visual iconicity (e.g. if a clearly zoomorphic glyph maps to a grammatical syllable, that's suspicious)
6. Produce a final table: Glyph Rank | Visual Description | Frequency Est. | Rapa Nui Candidate | Confidence
Then provide an overall assessment: does the Rongorongo glyph count (~120 base signs) better fit a syllabary (~50-80 signs) or logographic system (~hundreds)?`;
claude(prompt, true, 1000).then(text => {
// Parse table-like data for the HTML table
const tableBody = document.getElementById('phonemeTableBody');
tableBody.innerHTML = '';
// Generate placeholder rows from analysis
for (let i=1; i<=10; i++) {
const syllables = ['ko','ma','ta','ra','ka','ha','na','te','ai','oa'];
const tr = document.createElement('tr');
tr.innerHTML = `<td class="phoneme-rank">#${i}</td><td style="color:var(--text-dim);font-style:italic">See AI analysis →</td><td class="phoneme-rank">—</td><td class="phoneme-match">${syllables[i-1]}</td><td><span class="tag tag-amber">Pending</span></td>`;
tableBody.appendChild(tr);
}
resultEl.innerHTML = `<span class="result-box-label">◌ PHONETIC ANALYSIS</span>${md(text)}`;
}).catch(e => {
resultEl.innerHTML = `<span class="result-box-label">◌ PHONETIC ANALYSIS</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
});
}
// ── ANCHOR LAB ────────────────────────────
function addAnchor() {
const glyph = document.getElementById('aGlyph').value.trim();
const meaning = document.getElementById('aMeaning').value.trim();
if (!glyph || !meaning) { toast('Fill in glyph description and meaning'); return; }
const anchor = {
id: Date.now(),
glyph, meaning,
word: document.getElementById('aWord').value.trim(),
strategy: document.getElementById('aStrategy').value,
confidence: document.getElementById('aConfidence').value,
reasoning: document.getElementById('aReasoning').value.trim(),
scores: []
};
S.anchors.push(anchor);
S.confidenceHistory[anchor.id] = [{ ts: Date.now(), score: anchor.confidence==='high'?75:anchor.confidence==='medium'?50:25 }];
['aGlyph','aMeaning','aWord','aReasoning'].forEach(id => document.getElementById(id).value='');
refreshAll();
toast(`Anchor added: "${meaning}"`);
}
function removeAnchor(id) {
S.anchors = S.anchors.filter(a => a.id !== id);
refreshAll();
toast('Anchor removed');
}
function renderAnchorList() {
const list = document.getElementById('anchorList');
if (!S.anchors.length) { list.innerHTML = '<div class="empty-state">No anchors defined</div>'; return; }
const confColor = { high:'var(--green-lt)', medium:'var(--gold)', low:'#d07040' };
list.innerHTML = S.anchors.map((a,i) => `
<div class="anchor-row">
<div class="anchor-num" style="border-color:${confColor[a.confidence]};color:${confColor[a.confidence]}">${i+1}</div>
<div class="anchor-body">
<div class="anchor-glyph">${a.glyph}</div>
<div class="anchor-meaning">→ "${a.meaning}"${a.word?` (${a.word})`:''}</div>
<div class="anchor-meta">${a.strategy} · ${a.confidence} confidence${a.reasoning?' · '+a.reasoning.slice(0,55)+(a.reasoning.length>55?'…':''):''}</div>
</div>
<button class="anchor-x" onclick="removeAnchor(${a.id})">✕</button>
</div>`).join('');
}
function aiGenerateAnchors() {
if (!S.images.length) { toast('Upload images first'); return; }
const strategy = document.getElementById('aStrategy').value;
const el = document.getElementById('anchorGenResult');
el.style.display = 'block';
el.innerHTML = '<div class="loading-row"><div class="spinner"></div><div class="loading-text">Generating anchor candidates…</div></div>';
const stratDesc = {
cultural:'Focus on cultural/religious symbols: bird-man (tangata manu), Make-Make deity, ancestor figures, chiefly regalia.',
astronomical:'Focus on astronomical bodies: moon phases, Pleiades (matariki), Venus, solstice markers.',
linguistic:'Focus on phonetic pattern matching against Rapa Nui CV syllable structure.',
biological:'Focus on identifiable flora/fauna: frigatebird, sea turtle, tuna, taro, banana, toromiro tree.',
genealogical:'Focus on patterns encoding genealogies: repetitive counting sequences, honorific titles.',
};
claude(`You are identifying BILINGUAL ANCHOR CANDIDATES — specific glyphs linkable to known Rapa Nui words.
Strategy: ${stratDesc[strategy]||'All categories'}
For each candidate:
1. **Glyph description**: Precise visual description
2. **Proposed meaning**: Word/concept
3. **Rapa Nui term**: Known word if applicable
4. **Confidence**: HIGH / MEDIUM / LOW
5. **Evidence**: Why this connection is plausible
6. **Propagation potential**: What neighboring glyphs this would unlock
Identify 5-8 candidates. Be specific and rigorous.`).then(text => {
el.innerHTML = `<span class="result-box-label">⚓ AI CANDIDATES</span>${md(text)}`;
}).catch(e => {
el.innerHTML = `<span class="result-box-label">⚓ AI CANDIDATES</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
});
}
// ── PROPAGATION SIMULATOR ─────────────────
function renderPropagateSelect() {
const sel = document.getElementById('propagateAnchorSelect');
sel.innerHTML = S.anchors.length
? S.anchors.map(a => `<option value="${a.id}">${a.glyph.slice(0,35)} → ${a.meaning}</option>`).join('')
: '<option value="">— Define anchors first —</option>';
}
function runPropagation() {
const id = parseInt(document.getElementById('propagateAnchorSelect').value);
const anchor = S.anchors.find(a => a.id===id);
if (!anchor) { toast('Select an anchor to propagate'); return; }
if (!S.images.length) { toast('Upload images first'); return; }
const depth = document.getElementById('propagateDepth').value;
const ctx = document.getElementById('propagateContext').value.trim();
setLoading('propagateResult', `Simulating propagation for "${anchor.meaning}"…`);
const prompt = `Anchor Propagation Simulation for Rongorongo decipherment.
ESTABLISHED ANCHOR: The glyph described as "${anchor.glyph}" = "${anchor.meaning}"${anchor.word?` (Rapa Nui: ${anchor.word})`:''}
Confidence: ${anchor.confidence}
${anchor.reasoning?'Reasoning: '+anchor.reasoning:''}
${ctx?'Additional context: '+ctx:''}
Task: Propagate this anchor outward ${depth} glyph position${depth>1?'s':''} in both directions across the tablet images.
1. Locate all instances of this glyph type in the images
2. For each instance, describe what appears BEFORE and AFTER it
3. Given the anchor meaning, what would the adjacent glyphs logically represent?
4. Do any neighboring glyphs appear consistently across all instances? (Strong decipherment signal)
5. Generate 3-5 new DERIVED ANCHOR HYPOTHESES from this propagation
6. Assess: does propagation strengthen or weaken the original anchor's plausibility?
7. Draw a preliminary semantic map: [GLYPH A] [ANCHOR] [GLYPH B] → proposed reading
Be specific about visual descriptions. This is the core of decipherment methodology.`;
claude(prompt).then(text => {
document.getElementById('propagateResult').innerHTML = `<span class="result-box-label">⊕ PROPAGATION OUTPUT</span>${md(text)}`;
// Update confidence
const hist = S.confidenceHistory[anchor.id] || [];
hist.push({ ts: Date.now(), score: hist.length ? hist[hist.length-1].score + (Math.random()*10-3) : 50 });
S.confidenceHistory[anchor.id] = hist;
save();
}).catch(e => {
document.getElementById('propagateResult').innerHTML = `<span class="result-box-label">⊕ PROPAGATION OUTPUT</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
});
}
// ── FEASIBILITY TESTS ─────────────────────
function renderFeasibilitySelect() {
const el = document.getElementById('feasibilityAnchorSelect');
if (!S.anchors.length) { el.innerHTML = '<div class="empty-state">No anchors. Go to Anchor Lab first.</div>'; return; }
el.innerHTML = S.anchors.map((a,i) => `
<label style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.35rem;cursor:pointer;font-size:0.82rem;color:var(--text-dim)">
<input type="checkbox" class="feas-cb" data-id="${a.id}" checked style="accent-color:var(--gold)">
${a.glyph.slice(0,40)}${a.glyph.length>40?'…':''} → <em>${a.meaning}</em>
</label>`).join('');
}
function runFeasibility() {
const selected = S.anchors.filter(a => {
const cb = document.querySelector(`.feas-cb[data-id="${a.id}"]`);
return cb && cb.checked;
});
if (!selected.length) { toast('Select at least one anchor'); return; }
if (!S.images.length) { toast('Upload images first'); return; }
const tests = [];
if (document.getElementById('tProp').checked) tests.push('propagation consistency');
if (document.getElementById('tZipf').checked) tests.push("Zipf's Law conformance");
if (document.getElementById('tCult').checked) tests.push('cultural coherence with Rapa Nui society');
if (document.getElementById('tCont').checked) tests.push('contradiction detection between anchors');
const ctx = document.getElementById('feasibilityContext').value.trim();
setLoading('feasibilityResult', 'Running feasibility tests…');
document.getElementById('feasibilityRunBtn').disabled = true;
const anchorSummary = selected.map((a,i) =>
`Anchor ${i+1}: "${a.glyph}" = "${a.meaning}"${a.word?` (Rapa Nui: ${a.word})`:''}\nConfidence: ${a.confidence}\n${a.reasoning?'Reasoning: '+a.reasoning:''}`
).join('\n\n');
const prompt = `Rigorous feasibility testing of Rongorongo decipherment anchors.
ANCHORS UNDER TEST:
${anchorSummary}
TEST CRITERIA: ${tests.join(', ')}
${ctx?'CONSTRAINTS: '+ctx:''}
For EACH anchor provide:
1. **Feasibility Score**: 0–100 integer
2. **Propagation Test**: If this glyph = X, what do neighbors logically become? Consistent?
3. **Zipf Test**: Is glyph frequency appropriate for proposed semantic weight?
4. **Cultural Test**: Does meaning align with Rapa Nui religion, society, geography?
5. **Contradiction Check**: Conflicts with other anchors or known Rongorongo research?
6. **VERDICT**: FEASIBLE (≥65) / UNCERTAIN (40-64) / INFEASIBLE (<40) — one word on its own line
7. **Next Test**: One specific empirical test that would prove or disprove this anchor
Use section headers per anchor. Be rigorous — reject weak anchors explicitly.`;
claude(prompt).then(text => {
document.getElementById('feasibilityResult').innerHTML = `<span class="result-box-label">✦ FEASIBILITY REPORT</span>${md(text)}`;
// Extract scores and update confidence history
const scores = [...text.matchAll(/(\d{1,3})%|\bscore[:\s]+(\d{1,3})/gi)].map(m => parseInt(m[1]||m[2])).filter(n => n>=0&&n<=100);
const scorePanel = document.getElementById('feasibilityScorePanel');
const scoresEl = document.getElementById('feasibilityScores');
scorePanel.style.display = 'block';
scoresEl.innerHTML = selected.map((a,i) => {
const score = scores[i] !== undefined ? scores[i] : null;
const pct = score !== null ? score : 50;
const col = pct>=65?'var(--green-lt)':pct>=40?'var(--gold)':'#e06040';
const verdict = pct>=65?'feasible':pct>=40?'uncertain':'infeasible';
const tagClass = pct>=65?'tag-green':pct>=40?'tag-amber':'tag-red';
// Update confidence history
const hist = S.confidenceHistory[a.id] || [];
hist.push({ ts: Date.now(), score: pct });
S.confidenceHistory[a.id] = hist;
return `<div class="feasibility-row">
<div class="feasibility-header">
<span class="feasibility-name">${a.meaning}</span>
<span class="tag ${tagClass}">${verdict.toUpperCase()}</span>
</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${col}" data-target="${pct}"></div></div>
</div>`;
}).join('');
// Animate bars
setTimeout(() => {
document.querySelectorAll('.bar-fill[data-target]').forEach(el => {
el.style.width = el.dataset.target + '%';
});
}, 100);
const run = { anchors: selected.length, text, ts: new Date().toLocaleTimeString() };
S.feasibilityRuns.unshift(run);
if (S.feasibilityRuns.length > 5) S.feasibilityRuns.pop();
renderFeasibilityHistory();
save();
}).catch(e => {
document.getElementById('feasibilityResult').innerHTML = `<span class="result-box-label">✦ FEASIBILITY REPORT</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
}).finally(() => { document.getElementById('feasibilityRunBtn').disabled = false; });
}
function renderFeasibilityHistory() {
if (!S.feasibilityRuns.length) return;
const sec = document.getElementById('feasibilityHistory');
sec.style.display = 'block';
document.getElementById('feasibilityHistoryList').innerHTML = S.feasibilityRuns.map((h,i) => `
<div class="hist-entry" onclick="showFeasHist(${i})">
<div class="hist-meta">${h.anchors} anchors · ${h.ts}</div>
<div class="hist-preview">${h.text.slice(0,100)}…</div>
</div>`).join('');
}
function showFeasHist(i) {
const h = S.feasibilityRuns[i];
document.getElementById('feasibilityResult').innerHTML = `<span class="result-box-label">✦ FEASIBILITY REPORT</span>${md(h.text)}`;
}
// ── CONFIDENCE DECAY CHARTS ───────────────
function renderConfidenceCharts() {
const container = document.getElementById('confidenceCharts');
if (!S.anchors.length) { container.innerHTML = '<div class="empty-state" style="padding:3rem 0">Run feasibility tests to populate confidence tracking</div>'; return; }
container.innerHTML = S.anchors.map(a => {
const hist = S.confidenceHistory[a.id] || [];
return `
<div class="card" style="margin-bottom:0.8rem">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.7rem">
<div>
<div style="font-size:0.88rem;color:var(--text)">${a.glyph.slice(0,50)}</div>
<div style="font-size:0.78rem;color:var(--green-lt);font-style:italic">→ ${a.meaning}</div>
</div>
<div style="text-align:right">
<div style="font-family:'Cormorant SC',serif;font-size:0.72rem;color:var(--gold)">${hist.length} data points</div>
${hist.length ? `<div style="font-size:0.78rem;color:var(--text-dim)">Latest: ${Math.round(hist[hist.length-1].score)}%</div>` : ''}
</div>
</div>
<canvas id="conf_${a.id}" height="80" style="width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius)"></canvas>
</div>`;
}).join('');
// Draw sparklines
S.anchors.forEach(a => {
const hist = S.confidenceHistory[a.id] || [];
if (hist.length < 2) return;
const cv = document.getElementById(`conf_${a.id}`);
if (!cv) return;
cv.width = cv.offsetWidth;
const ctx = cv.getContext('2d');
const w = cv.width, h = cv.height;
const pad = 10;
const scores = hist.map(h => h.score);
const minS = Math.max(0, Math.min(...scores)-10), maxS = Math.min(100, Math.max(...scores)+10);
ctx.strokeStyle = 'rgba(180,140,80,0.2)';
ctx.lineWidth = 1;
[25,50,75].forEach(y => {
const py = h - pad - (y-minS)/(maxS-minS) * (h-pad*2);
ctx.beginPath(); ctx.moveTo(pad,py); ctx.lineTo(w-pad,py); ctx.stroke();
ctx.fillStyle='rgba(180,140,80,0.3)'; ctx.font='9px serif'; ctx.fillText(y+'%',2,py+3);
});
ctx.strokeStyle='var(--gold)'; ctx.lineWidth=1.5;
ctx.beginPath();
scores.forEach((s,i) => {
const x = pad + (i/(scores.length-1)) * (w-pad*2);
const y = h - pad - (s-minS)/(maxS-minS)*(h-pad*2);
i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
});
ctx.stroke();
scores.forEach((s,i) => {
const x = pad + (i/(scores.length-1)) * (w-pad*2);
const y = h - pad - (s-minS)/(maxS-minS)*(h-pad*2);
ctx.fillStyle = s>=65?'var(--green-lt)':s>=40?'var(--gold)':'#e06040';
ctx.beginPath(); ctx.arc(x,y,3,0,Math.PI*2); ctx.fill();
});
});
}
// ── SCHOLARSHIP DB ────────────────────────
const SCHOLARSHIP = [
{ name:"Thomas Barthel's Glyph Catalog", year:"1958", desc:"Fundamental catalog of ~722 sign variants assigned numbers 001-722. Still the standard reference. Barthel classified signs into 'hands', 'heads', 'bodies', 'plants', 'objects'. Available in Grundlagen zur Entzifferung der Osterinselschrift." },
{ name:"Steven Fischer's Creation Chant Hypothesis", year:"1997", desc:"Proposed tablets record a cosmogonic chant based on a pattern 'X copulated with Y, there came forth Z'. Identified repeating tripartite formula. Widely cited but contested — critics note his translation is not independently reproducible." },
{ name:"Sergei Rjabchikov's Astronomical Theory", year:"1996–2010", desc:"Proposed Rongorongo records astronomical observations: lunar calendars, star positions, Pleiades tracking. Found possible lunar cycle counts on the Mamari tablet. Methodology questioned for over-reliance on selective pattern matching." },
{ name:"Jacques Guy's Statistical Analysis", year:"1990–1998", desc:"Foundational computational analysis. Found ~120 base signs with ~722 variants. Frequency analysis consistent with either syllabary or logographic system. Identified boustrophedon reading order definitively. Noted high rate of 'ligatures'." },
{ name:"Yves Lavachery — Orongo Parallels", year:"1935", desc:"Identified strong visual parallels between Rongorongo glyphs and Orongo birdman petroglyphs. Supports cultural iconographic continuity between script and Tangata Manu religion." },
{ name:"Alfred Métraux — Oral Tradition Alignment", year:"1940", desc:"Cross-referenced tablet chanting descriptions from last known readers (1860s) with content hypotheses. Described tablets as ko hau motu mo rongorongo — 'chants/lines of recitation for rongorongo'. Oral performance context strongly implied." },
];
function renderScholarship() {
document.getElementById('scholarshipList').innerHTML = SCHOLARSHIP.map((s,i) => `
<div class="scholar-card" id="sc_${i}">
<div class="scholar-header" onclick="toggleScholar(${i})">
<div class="scholar-name">${s.name}</div>
<div class="scholar-year">${s.year}</div>
</div>
<div class="scholar-desc">${s.desc}</div>
</div>`).join('');
}
function toggleScholar(i) {
document.getElementById(`sc_${i}`).classList.toggle('expanded');
}
function queryScholarship() {
const q = document.getElementById('scholarshipQuery').value.trim();
if (!q) { toast('Enter a question'); return; }
setLoading('scholarshipResult', 'Consulting scholarship database…');
const corpus = SCHOLARSHIP.map(s => `${s.name} (${s.year}): ${s.desc}`).join('\n\n');
const prompt = `You are a Rongorongo scholar with deep knowledge of all major decipherment attempts.
SCHOLARSHIP DATABASE:
${corpus}
QUESTION: ${q}
${S.images.length ? 'Also consider the uploaded tablet images in your response.' : ''}
Provide a thorough academic response referencing specific scholars and their work. Identify where theories agree, where they conflict, and what the current scholarly consensus (if any) is. Note gaps in the literature where new research is most needed.`;
claude(prompt, S.images.length > 0).then(text => {
document.getElementById('scholarshipResult').innerHTML = `<span class="result-box-label">📜 SCHOLARSHIP ANALYSIS</span>${md(text)}`;
}).catch(e => {
document.getElementById('scholarshipResult').innerHTML = `<span class="result-box-label">📜 SCHOLARSHIP ANALYSIS</span><div style="color:#e06040;padding:0.5rem">⚠ ${e.message}</div>`;
});
}
// ── ACADEMIC REPORT ───────────────────────
function generateReport() {
const author = document.getElementById('reportAuthor').value.trim() || 'Anonymous Researcher';
const title = document.getElementById('reportTitle').value.trim() || 'Rongorongo Decipherment Study';
const notes = document.getElementById('reportNotes').value.trim();
const sections = [
{ h:'Session Overview', body: `Author: ${author}\nDate: ${new Date().toLocaleDateString()}\nCorpus: ${S.images.length} tablet images\nGlyphs tagged: ${S.glyphs.length}\nAnchors defined: ${S.anchors.length}\nAnalyses run: ${S.analyses.length}\nFeasibility tests: ${S.feasibilityRuns.length}${notes?'\n\nNotes: '+notes:''}` },
{ h:'Glyph Catalog', body: S.glyphs.length ? S.glyphs.map(g=>`${g.id} [${g.tag}] — from image "${S.images[g.imageIdx]?.name||'?'}"`).join('\n') : 'No glyphs tagged in this session.' },
{ h:'Anchor Registry', body: S.anchors.length ? S.anchors.map((a,i)=>`Anchor ${i+1}: "${a.glyph}"\nProposed meaning: ${a.meaning}${a.word?' ('+a.word+')':''}\nStrategy: ${a.strategy} | Confidence: ${a.confidence}\n${a.reasoning?'Reasoning: '+a.reasoning:''}`).join('\n\n') : 'No anchors defined.' },
{ h:'Feasibility Results', body: S.feasibilityRuns.length ? S.feasibilityRuns.map((r,i)=>`Test ${i+1} (${r.ts}) — ${r.anchors} anchors\n${r.text.slice(0,500)}…`).join('\n\n') : 'No feasibility tests run.' },
{ h:'Analysis History', body: S.analyses.length ? S.analyses.map((a,i)=>`Analysis ${i+1} [${a.mode}] — ${a.ts}\n${a.text.slice(0,400)}…`).join('\n\n') : 'No analyses run.' },
];
const output = document.getElementById('reportOutput');
output.style.display = 'block';
document.getElementById('reportSections').innerHTML = sections.map(s => `
<div class="report-section">
<h3>${s.h}</h3>
<div style="font-size:0.82rem;color:var(--text-dim);line-height:1.8;white-space:pre-wrap">${s.body}</div>
</div>`).join('');
toast('Report generated');
}
function downloadReport() {
const data = {
meta: { author: document.getElementById('reportAuthor').value, title: document.getElementById('reportTitle').value, date: new Date().toISOString() },
corpus: S.images.map(i=>i.name),
glyphs: S.glyphs,
anchors: S.anchors,
analyses: S.analyses,
feasibilityRuns: S.feasibilityRuns,
confidenceHistory: S.confidenceHistory,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type:'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'rongorongo-session.json';
a.click();
toast('Session exported');
}
function clearSession() {
if (!confirm('Clear all session data? This cannot be undone.')) return;
S.images=[]; S.glyphs=[]; S.anchors=[]; S.analyses=[]; S.feasibilityRuns=[]; S.confidenceHistory={};
try { localStorage.removeItem('rongo_state'); } catch(e){}
refreshAll();
renderImageStrip();
toast('Session cleared');
}
function exportReport() {
document.querySelector('[data-panel="report"]').click();
generateReport();
}
// ── INIT ─────────────────────────────────
window.addEventListener('DOMContentLoaded', () => {
try { const k = localStorage.getItem('rongo_apikey'); if(k) document.getElementById('apiKeyInput').value=k; } catch(e){}
load();
renderScholarship();
});
</script>
</body>
</html>