inou/templates/dossier.tmpl

753 lines
29 KiB
Cheetah
Raw 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.

{{define "dossier"}}
<script>function toggleExpand(el) { el.classList.toggle("expanded"); var icon = el.querySelector(".expand-icon"); if (icon) icon.textContent = el.classList.contains("expanded") ? "" : "+"; var children = el.nextElementSibling; if (children && children.classList.contains("data-row-children")) children.classList.toggle("show"); }</script>
<div class="sg-container">
<div class="dossier-header" style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 32px;">
<div>
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 8px;">{{.TargetDossier.Name}}</h1>
{{if .ShowDetails}}
<p style="font-size: 1.15rem; font-weight: 300; color: var(--text-muted);">
{{if .TargetDossier.DateOfBirth}}{{.T.born}}: {{printf "%.10s" .TargetDossier.DateOfBirth}}{{end}}
{{if .TargetDossier.Sex}} · {{sexT .TargetDossier.Sex .Lang}}{{end}}
</p>
{{end}}
</div>
<a href="/dashboard" class="btn btn-secondary btn-small">← {{.T.back_to_dossiers}}</a>
</div>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
{{if .Success}}<div class="success">{{.Success}}</div>{{end}}
<!-- Imaging Section -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-indicator imaging"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_imaging}}</span>
{{if .Studies}}
<span class="data-card-summary" data-studies="{{.StudyCount}}" data-slices="{{.TotalSlices}}"></span>
{{else}}
<span class="data-card-summary">{{.T.no_imaging}}</span>
{{end}}
</div>
{{if .HasImaging}}
<a href="/viewer/?token={{.TargetDossier.DossierID}}" target="_blank" class="btn btn-small">{{.T.open_viewer}}</a>
{{end}}
</div>
{{if .Studies}}
<div class="data-table" id="studies-table">
{{range $i, $s := .Studies}}
{{if eq $s.SeriesCount 1}}
<div class="data-row single study-row" data-index="{{$i}}">
<div class="data-row-main">
<span class="data-label">{{$s.Description}}</span>
</div>
<div class="data-values">
<span class="data-date" data-date="{{$s.Date}}"></span>
<a href="/viewer/?token={{$.TargetDossier.DossierID}}&study={{$s.ID}}" target="_blank" class="btn-icon" title="{{$.T.open_viewer}}">→</a>
</div>
</div>
{{else}}
<div class="data-row expandable study-row" data-index="{{$i}}" onclick="toggleExpand(this)">
<div class="data-row-main">
<span class="expand-icon">+</span>
<span class="data-label">{{$s.Description}}</span>
</div>
<div class="data-values">
<span class="data-value mono series-count" data-count="{{$s.SeriesCount}}"></span>
<span class="data-date" data-date="{{$s.Date}}"></span>
<a href="/viewer/?token={{$.TargetDossier.DossierID}}&study={{$s.ID}}" target="_blank" class="btn-icon" onclick="event.stopPropagation()" title="{{$.T.open_viewer}}">→</a>
</div>
</div>
<div class="data-row-children" data-index="{{$i}}">
{{range $s.Series}}{{if gt .SliceCount 0}}
<div class="data-row child">
<span class="data-label">{{if .Description}}{{.Description}}{{else}}{{.Modality}}{{end}}</span>
<span class="data-value mono slice-count" data-count="{{.SliceCount}}"></span>
<a href="/viewer/?token={{$.TargetDossier.DossierID}}&study={{$s.ID}}&series={{.ID}}" target="_blank" class="btn-icon" title="{{$.T.open_viewer}}">→</a>
</div>
{{end}}{{end}}
</div>
{{end}}
{{end}}
{{if gt .StudyCount 5}}
<div class="data-row show-more" id="show-more-studies" data-count="{{.StudyCount}}"></div>
{{end}}
</div>
{{end}}
</div>
<!-- Labs Section -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-indicator labs"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_labs}}</span>
{{if .Labs}}
<span class="data-card-summary">{{len .Labs}} results</span>
{{else}}
<span class="data-card-summary">{{.T.no_lab_data}}</span>
{{end}}
</div>
</div>
{{if .Labs}}
<div class="data-table">
{{range .Labs}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Documents Section -->
<div class="data-card" {{if not .Documents}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator records"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_records}}</span>
<span class="data-card-summary">{{len .Documents}} documents</span>
</div>
</div>
{{if .Documents}}
<div class="data-table">
{{range .Documents}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Type}}<span class="data-value">{{.Type}}</span>{{end}}
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Procedures Section -->
<div class="data-card" {{if not .Procedures}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #DC2626;"></div>
<div class="data-card-title">
<span class="section-heading">Procedures &amp; Surgery</span>
<span class="data-card-summary">{{len .Procedures}} procedures</span>
</div>
</div>
{{if .Procedures}}
<div class="data-table">
{{range .Procedures}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Assessments Section -->
<div class="data-card" {{if not .Assessments}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #7C3AED;"></div>
<div class="data-card-title">
<span class="section-heading">Clinical Assessments</span>
<span class="data-card-summary">{{len .Assessments}} assessments</span>
</div>
</div>
{{if .Assessments}}
<div class="data-table">
{{range .Assessments}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Genetics Section -->
<div class="data-card" id="genetics-card" {{if not .HasGenome}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #8B5CF6;"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_genetics}}</span>
<span class="data-card-summary" id="genetics-summary">Loading...</span>
</div>
<a href="#" class="btn btn-small" id="show-all-genetics" style="display:none;" onclick="showAllGeneticsWarning(); return false;">Show all</a>
</div>
<div class="data-table" id="genetics-table"></div>
</div>
<!-- Genetics Warning Modal -->
<div id="genetics-warning-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 520px;">
<h3 style="margin-bottom: 16px;">⚠️ Before you continue</h3>
<p style="margin-bottom: 16px;">Here you can browse all your raw genetic variants. However, the real value comes from using <a href="/connect">Claude and other LLMs with your health dossier</a> — they can interpret these variants and correlate them with your labs, imaging, and medical history.</p>
<p style="margin-bottom: 12px;"><strong>Keep in mind:</strong></p>
<ul style="margin-bottom: 16px; padding-left: 20px; line-height: 1.6;">
<li>Many associations are based on early or limited research</li>
<li>A "risk variant" means slightly higher odds — not a diagnosis</li>
<li>Consumer tests (23andMe, AncestryDNA) can have false positives</li>
</ul>
<p style="margin-bottom: 20px;">These findings can be a starting point for conversations with your doctor — especially if certain conditions run in your family.</p>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="closeGeneticsWarning()">Close</button>
<button class="btn btn-primary" onclick="confirmShowAllGenetics()">I understand, show all</button>
</div>
</div>
</div>
<!-- Uploads Section -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-indicator uploads"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_uploads}}</span>
<span class="data-card-summary">
{{if .Uploads}}<span data-files="{{.UploadCount}}" data-size="{{.UploadSize}}"></span>{{else}}{{.T.no_files}}{{end}}
</span>
</div>
{{if .CanEdit}}<a href="/dossier/{{.TargetDossier.DossierID}}/upload" class="btn btn-small">{{.T.manage}}</a>{{else}}<span class="btn btn-small btn-disabled" title="{{.T.no_upload_access}}">{{.T.manage}}</span>{{end}}
</div>
</div>
<!-- Medications Section -->
<div class="data-card" {{if not .Medications}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator medications"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_medications}}</span>
<span class="data-card-summary">{{len .Medications}} medications</span>
</div>
</div>
{{if .Medications}}
<div class="data-table">
{{range .Medications}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Symptoms Section -->
<div class="data-card" {{if not .Symptoms}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #F59E0B;"></div>
<div class="data-card-title">
<span class="section-heading">Symptoms</span>
<span class="data-card-summary">{{len .Symptoms}} symptoms</span>
</div>
</div>
{{if .Symptoms}}
<div class="data-table">
{{range .Symptoms}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Hospitalizations Section -->
<div class="data-card" {{if not .Hospitalizations}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #EF4444;"></div>
<div class="data-card-title">
<span class="section-heading">Hospitalizations</span>
<span class="data-card-summary">{{len .Hospitalizations}} hospitalizations</span>
</div>
</div>
{{if .Hospitalizations}}
<div class="data-table">
{{range .Hospitalizations}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Therapies Section -->
<div class="data-card" {{if not .Therapies}}style="display:none;"{{end}}>
<div class="data-card-header">
<div class="data-card-indicator" style="background: #10B981;"></div>
<div class="data-card-title">
<span class="section-heading">Therapies</span>
<span class="data-card-summary">{{len .Therapies}} therapies</span>
</div>
</div>
{{if .Therapies}}
<div class="data-table">
{{range .Therapies}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Value}}</span>
{{if .Summary}}<span class="data-meta">{{.Summary}}</span>{{end}}
</div>
<div class="data-values">
{{if .Date}}<span class="data-date">{{.Date}}</span>{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Vitals Section - Coming Soon -->
<div class="data-card coming-soon">
<div class="data-card-header">
<div class="data-card-indicator vitals"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_vitals}}</span>
<span class="data-card-summary">{{.T.vitals_desc}}</span>
</div>
<span class="badge-soon">{{.T.coming_soon}}</span>
</div>
</div>
<!-- Privacy Section -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-indicator privacy"></div>
<div class="data-card-title">
<span class="section-heading">{{.T.section_privacy}}</span>
<span class="data-card-summary">{{len .AccessList}} {{.T.people_with_access_count}}</span>
</div>
</div>
<div class="data-table">
{{range .AccessList}}
<div class="data-row">
<div class="data-row-main">
<span class="data-label">{{.Name}}{{if .IsSelf}} ({{$.T.you}}){{else if .IsPending}} ({{$.T.pending}}){{end}}</span>
<span class="data-meta">{{.Relation}}{{if .CanEdit}} · {{$.T.can_edit}}{{end}}</span>
</div>
{{if and $.CanManageAccess (not .IsSelf)}}
<div style="display: flex; gap: 8px;">
<a href="/dossier/{{$.TargetDossier.DossierID}}/access/{{.DossierID}}" class="btn btn-secondary btn-small">Edit</a>
<form action="/dossier/{{$.TargetDossier.DossierID}}/revoke" method="POST" style="display:inline;">
<input type="hidden" name="accessor_id" value="{{.DossierID}}">
<button type="submit" class="btn btn-danger btn-small" onclick="return confirm('Remove access for {{.Name}} to {{$.TargetDossier.Name}}?')">{{$.T.remove}}</button>
</form>
</div>
{{end}}
</div>
{{end}}
{{if not .AccessList}}
<div class="data-row">
<span class="text-muted">{{.T.no_access_yet}}</span>
</div>
{{end}}
<!-- Privacy actions -->
<div class="data-row privacy-actions">
<a href="/dossier/{{.TargetDossier.DossierID}}/share" class="privacy-action">{{.T.share_access}}</a>
{{if .CanManageAccess}}<a href="/dossier/{{.TargetDossier.DossierID}}/permissions" class="privacy-action">{{.T.manage_permissions}}</a>{{end}}
<a href="/dossier/{{.TargetDossier.DossierID}}/audit" class="privacy-action">{{.T.view_audit_log}}</a>
{{if or (eq .Dossier.DossierID .TargetDossier.DossierID) .CanManageAccess}}<a href="/dossier/{{.TargetDossier.DossierID}}/export" class="privacy-action">{{.T.export_data}}</a>{{end}}
</div>
</div>
</div>
{{template "footer"}}
</div>
<script>
const dossierGUID = "{{.TargetDossier.DossierID}}";
const hasGenome = {{if .HasGenome}}true{{else}}false{{end}};
const userLang = "{{.Lang}}";
// Show all categories, but hide negative variants by default
let showAllGenetics = false; // show negative variants
let showAllCategories = false; // show all 13 categories (vs top 5)
let allCategories = {};
// i18n strings from server
const i18n = {
studies: "{{.T.imaging_summary}}",
series: "{{.T.series_count}}",
files: "{{.T.files_summary}}",
showAll: "{{.T.show_all_studies}}",
genomeEnglishOnly: "{{.T.genome_english_only}}",
genomeVariants: "{{.T.genome_variants}}",
genomeHidden: "{{.T.genome_hidden}}",
genomeShowAllCategories: "{{.T.genome_show_all_categories}}"
};
function toggleExpand(el) {
el.classList.toggle('expanded');
const icon = el.querySelector('.expand-icon');
if (icon) icon.textContent = el.classList.contains('expanded') ? '' : '+';
const children = el.nextElementSibling;
if (children && children.classList.contains('data-row-children')) {
children.classList.toggle('show');
}
}
// Format allele: ensure single semicolon between letters
function formatAllele(genotype) {
const letters = genotype.replace(/;/g, '');
return letters.split('').join(';');
}
// Load genetics categories from API
async function loadGeneticsCategories() {
if (!hasGenome) return;
try {
// Fetch category counts (includes shown/hidden per category)
const resp = await fetch(`/api/categories?dossier=${dossierGUID}&type=genome`);
allCategories = await resp.json();
// Show "Show all" link if there are any hidden variants
const totalHidden = Object.values(allCategories).reduce((sum, c) => sum + (c.hidden || 0), 0);
if (totalHidden > 0) {
document.getElementById('show-all-genetics').style.display = '';
}
renderGeneticsCategories();
} catch (e) {
document.getElementById('genetics-summary').textContent = 'Error loading';
}
}
function renderGeneticsCategories() {
let totalShown = 0;
let totalHidden = 0;
for (const counts of Object.values(allCategories)) {
totalShown += counts.shown || 0;
totalHidden += counts.hidden || 0;
}
if (totalShown === 0 && totalHidden === 0) {
document.getElementById('genetics-card').style.display = 'none';
return;
}
let summary = showAllGenetics ? `${totalShown + totalHidden} ${i18n.genomeVariants}` : `${totalShown} ${i18n.genomeVariants}`;
if (totalHidden > 0 && !showAllGenetics) {
summary += ` <span class="text-muted">(${totalHidden} ${i18n.genomeHidden})</span>`;
}
document.getElementById('genetics-summary').innerHTML = summary;
const btn = document.getElementById('show-all-genetics');
if (showAllGenetics) {
btn.textContent = 'Hide negative';
btn.onclick = () => { showAllGenetics = false; reloadAllExpandedCategories(); renderGeneticsCategories(); return false; };
} else {
btn.textContent = 'Show all';
btn.onclick = () => { showAllGeneticsWarning(); return false; };
}
const table = document.getElementById('genetics-table');
let html = '';
// Show language notice for non-English users
if (userLang !== 'en' && i18n.genomeEnglishOnly) {
html += `<div class="data-row" style="background: var(--bg-muted); color: var(--text-muted); font-size: 0.9rem; padding: 12px 16px;">
<span>🌐 ${i18n.genomeEnglishOnly}</span>
</div>`;
}
// Sort by predefined priority (less alarming first), then by count within same priority
const categoryPriority = {
'traits': 1,
'metabolism': 2,
'longevity': 3,
'blood': 4,
'cardiovascular': 5,
'neurological': 6,
'mental_health': 7,
'autoimmune': 8,
'medication': 9,
'fertility': 10,
'disease': 11,
'cancer': 12,
'other': 13
};
const sorted = Object.entries(allCategories).sort((a, b) => {
const aPriority = categoryPriority[a[0]] || 99;
const bPriority = categoryPriority[b[0]] || 99;
if (aPriority !== bPriority) return aPriority - bPriority;
// Same priority: sort by count
const aCount = showAllGenetics ? (a[1].shown + a[1].hidden) : a[1].shown;
const bCount = showAllGenetics ? (b[1].shown + b[1].hidden) : b[1].shown;
return bCount - aCount;
});
const maxCategories = 5;
const displayCategories = showAllCategories ? sorted : sorted.slice(0, maxCategories);
for (const [cat, counts] of displayCategories) {
const shown = counts.shown || 0;
const hidden = counts.hidden || 0;
const displayCount = showAllGenetics ? (shown + hidden) : shown;
if (displayCount === 0) continue;
const label = cat.charAt(0).toUpperCase() + cat.slice(1).replace(/_/g, ' ');
let countText = `${displayCount} ${i18n.genomeVariants}`;
if (hidden > 0 && !showAllGenetics) {
countText = `${shown} ${i18n.genomeVariants} <span class="text-muted">(${hidden} ${i18n.genomeHidden})</span>`;
}
html += `
<div class="data-row expandable genetics-category" data-category="${cat}" onclick="toggleGeneticsCategory(this, '${cat}')">
<div class="data-row-main">
<span class="expand-icon">+</span>
<span class="data-label">${label}</span>
</div>
<div class="data-values">
<span class="data-meta">${countText}</span>
</div>
</div>
<div class="data-row-children genetics-children" data-category="${cat}"></div>`;
}
// Show "Show all X categories" link if there are more
if (!showAllCategories && sorted.length > maxCategories) {
html += `<div class="data-row show-more" style="cursor: pointer;" onclick="showAllCategories = true; renderGeneticsCategories();">${i18n.genomeShowAllCategories.replace('%d', sorted.length)} →</div>`;
}
table.innerHTML = html;
}
// Reload data for any expanded categories (when toggling show all)
function reloadAllExpandedCategories() {
document.querySelectorAll('.genetics-children[data-loaded="true"]').forEach(el => {
el.removeAttribute('data-loaded');
});
}
function showAllGeneticsWarning() {
document.getElementById('genetics-warning-modal').style.display = 'flex';
}
function closeGeneticsWarning() {
document.getElementById('genetics-warning-modal').style.display = 'none';
}
function confirmShowAllGenetics() {
closeGeneticsWarning();
showAllGenetics = true;
renderGeneticsCategories();
}
async function toggleGeneticsCategory(el, category) {
el.classList.toggle('expanded');
const icon = el.querySelector('.expand-icon');
if (icon) icon.textContent = el.classList.contains('expanded') ? '' : '+';
const children = el.nextElementSibling;
if (!children) return;
children.classList.toggle('show');
if (children.dataset.loaded) return;
children.dataset.loaded = 'true';
children.innerHTML = '<div class="data-row child"><span class="text-muted">Loading...</span></div>';
try {
const includeHidden = showAllGenetics ? '&include_hidden=true' : '';
const resp = await fetch(`/api/genome?dossier=${dossierGUID}&category=${encodeURIComponent(category)}${includeHidden}`);
const data = await resp.json();
if (!data.matches || data.matches.length === 0) {
children.innerHTML = '<div class="data-row child"><span class="text-muted">No variants found</span></div>';
return;
}
let html = '';
const maxShow = 5;
data.matches.slice(0, maxShow).forEach(m => {
const mag = m.magnitude ? m.magnitude.toFixed(1) : '0';
const gene = m.gene || '';
const allele = formatAllele(m.genotype);
html += `
<div class="data-row child" style="flex-direction: column; align-items: flex-start; gap: 8px; padding: 12px 16px;">
<div class="sg-gene-row" style="width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div class="sg-gene-main">
<span class="sg-gene-name">${gene}</span>
<span class="sg-gene-rsid">${m.rsid}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="sg-gene-allele">${allele}</span>
<span class="badge" style="font-size: 0.7rem;">mag ${mag}</span>
</div>
</div>
<div class="sg-gene-summary">${m.summary || ''}</div>
</div>
</div>`;
});
if (data.total > maxShow) {
const remaining = data.total - maxShow;
const showMoreText = remaining > 50
? `${remaining} more variants — <a href="/connect" style="color: var(--primary);">use Claude</a> to explore intelligently`
: `Show more (${remaining} remaining) →`;
html += `<div class="sg-show-more" data-offset="${maxShow}" data-total="${data.total}" onclick="loadMoreGenetics('${category}', this)">${showMoreText}</div>`;
}
children.innerHTML = html;
} catch (e) {
children.innerHTML = '<div class="data-row child"><span class="text-muted">Error loading variants</span></div>';
}
}
async function loadMoreGenetics(category, el) {
const batchSize = 20;
const currentOffset = parseInt(el.dataset.offset || '0', 10);
const total = parseInt(el.dataset.total || '0', 10);
el.textContent = 'Loading...';
try {
const includeHidden = showAllGenetics ? '&include_hidden=true' : '';
const resp = await fetch(`/api/genome?dossier=${dossierGUID}&category=${encodeURIComponent(category)}&offset=${currentOffset}&limit=${batchSize}${includeHidden}`);
const data = await resp.json();
let html = '';
data.matches.forEach(m => {
const mag = m.magnitude ? m.magnitude.toFixed(1) : '0';
const gene = m.gene || '';
const allele = formatAllele(m.genotype);
html += `
<div class="data-row child" style="flex-direction: column; align-items: flex-start; gap: 8px; padding: 12px 16px;">
<div class="sg-gene-row" style="width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div class="sg-gene-main">
<span class="sg-gene-name">${gene}</span>
<span class="sg-gene-rsid">${m.rsid}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="sg-gene-allele">${allele}</span>
<span class="badge" style="font-size: 0.7rem;">mag ${mag}</span>
</div>
</div>
<div class="sg-gene-summary">${m.summary || ''}</div>
</div>
</div>`;
});
// Insert new items before the "Show more" button
el.insertAdjacentHTML('beforebegin', html);
const newOffset = currentOffset + data.matches.length;
const remaining = total - newOffset;
if (remaining > 0) {
el.dataset.offset = newOffset;
if (remaining > 50) {
el.innerHTML = `${remaining} more variants — <a href="/connect" style="color: var(--primary);">use Claude</a> to explore intelligently`;
} else {
el.textContent = `Show more (${remaining} remaining) →`;
}
} else {
el.remove();
}
} catch (e) {
el.textContent = 'Error loading variants';
}
}
loadGeneticsCategories();
document.querySelectorAll('[data-date]').forEach(el => {
const d = el.dataset.date;
if (d && d.length === 8) {
const date = new Date(d.slice(0,4) + '-' + d.slice(4,6) + '-' + d.slice(6,8));
el.textContent = date.toLocaleDateString();
}
});
document.querySelectorAll('[data-studies]').forEach(el => {
const studies = el.dataset.studies;
const slices = el.dataset.slices;
el.textContent = i18n.studies.replace('%d', studies).replace('%d', slices);
});
document.querySelectorAll('.series-count').forEach(el => {
const n = parseInt(el.dataset.count, 10);
el.textContent = i18n.series.replace('%d', n);
});
const plural = new Intl.PluralRules(navigator.language);
document.querySelectorAll('.slice-count').forEach(el => {
const n = parseInt(el.dataset.count, 10);
el.textContent = n + (plural.select(n) === 'one' ? ' slice' : ' slices');
});
document.querySelectorAll('[data-files]').forEach(el => {
const files = el.dataset.files;
const size = el.dataset.size;
el.textContent = i18n.files.replace('%d', files).replace('%s', size);
});
const maxVisible = 5;
document.querySelectorAll('[data-index]').forEach(el => {
if (parseInt(el.dataset.index, 10) >= maxVisible) {
el.style.display = 'none';
}
});
const showMore = document.getElementById('show-more-studies');
if (showMore) {
const count = showMore.dataset.count;
showMore.innerHTML = '<span class="text-muted">' + i18n.showAll.replace('%d', count) + '</span>';
showMore.style.cursor = 'pointer';
showMore.onclick = () => {
document.querySelectorAll('[data-index]').forEach(el => el.style.display = '');
showMore.style.display = 'none';
};
}
if (new URLSearchParams(window.location.search).get("requested") === "1") {
alert("Your request has been sent. The dossier owner will be notified and can approve access.");
history.replaceState(null, "", window.location.pathname);
}
</script>
{{end}}