// ===== State =====
let allData=[], filteredData=[], currentIndex=0;
let assignmentId=null, reviewsMap={}, currentReviewStatus=null;
let editMode={emotion:false, distortions:false, empathy:false};
let editData={emotion:null, distortions:null, empathy:null}; // working copies
let selectingEvidence=null; // {card, itemIdx, field}
const DIST_COLORS=['#fb923c','#f87171','#fbbf24','#a78bfa','#34d399'];
const EMP_COLORS=['#60a5fa','#a78bfa','#34d399','#fbbf24','#fb923c','#f87171','#818cf8','#2dd4bf','#e879f9'];
const INTENSITY_OPTS=['Low','Medium','High'];
const DISTORTION_OPTS=[
'All-or-Nothing Thinking', 'Overgeneralization', 'Mental Filter',
'Disqualifying the Positive', 'Mind Reading', 'Fortune Telling',
'Catastrophizing', 'Minimization', 'Emotional Reasoning',
'Should Statements', 'Labeling', 'Personalization'
];
// ===== Helpers =====
function safeJSON(s){try{return JSON.parse(s);}catch(e){return null;}}
function escA(s){return (s||'').replace(/"/g,'"').replace(/'/g,''');}
function escH(s){return (s||'').replace(/&/g,'&').replace(//g,'>');}
function parseAnno(row){
return {
emotion:safeJSON(row['Emotion']),
distortions:safeJSON(row['Distortions']),
empathy:safeJSON(row['Empathy_Level'])
};
}
function getEffectiveAnno(row){
const review=reviewsMap[currentIndex];
if(review&&review.annotations_edit){
const ed=safeJSON(review.annotations_edit);
if(ed) return ed;
}
return parseAnno(row);
}
function emotionCls(e){
if(!e)return 'emotion-default';
const l=e.toLowerCase();
if(l.includes('fear'))return 'emotion-fear';
if(l.includes('sad'))return 'emotion-sadness';
if(l.includes('anger')||l.includes('angry'))return 'emotion-anger';
if(l.includes('joy')||l.includes('happy'))return 'emotion-joy';
if(l.includes('anxi'))return 'emotion-anxiety';
if(l.includes('shame'))return 'emotion-shame';
if(l.includes('depress'))return 'emotion-depression';
return 'emotion-default';
}
function displayEmotion(row){
return row?.emotion_type || 'โ';
}
function scoreColor(s,m=7){const r=s/m;return r>=.7?'#34d399':r>=.4?'#fbbf24':'#f87171';}
function deepCopy(o){return JSON.parse(JSON.stringify(o));}
// ===== Conversation parsing & highlights =====
function parseConv(s){
if(!s)return [];
const msgs=[];
s.split('\n').forEach(line=>{
const t=line.trim();
if(!t)return;
if(t.startsWith('speaker:')||t.startsWith('listener:')){
const isL=t.startsWith('listener:');
const c=t.replace(/^(speaker|listener):\s*/,'');
if(c)msgs.push({role:isL?'listener':'speaker',text:c});
} else if(msgs.length>0) msgs[msgs.length-1].text+=' '+t;
});
return msgs;
}
function applyHighlights(msgs,quotes){
return msgs.map(msg=>{
let text=msg.text;
quotes.forEach(({quote,cls})=>{
if(!quote||quote==='No evidence present')return;
const q=quote.replace(/^(listener|speaker):\s*/i,'').trim();
if(!q)return;
const i=text.indexOf(q);
if(i!==-1)text=text.slice(0,i)+``+text.slice(i,i+q.length)+''+text.slice(i+q.length);
});
return {...msg,htmlText:text};
});
}
function buildQuotes(anno){
const quotes=[];
(anno.distortions?.distortions||[]).forEach((d,i)=>{
if(d.evidence_quote&&d.evidence_quote!=='No evidence present')
quotes.push({quote:d.evidence_quote,cls:`dist-hl-${i%3}`,type:'dist',idx:i});
});
(anno.empathy?.empathy_dimensions||[]).forEach((dim,i)=>{
if(dim.evidence_quote&&dim.evidence_quote!=='No evidence present')
quotes.push({quote:dim.evidence_quote,cls:`emp-hl-${i%9}`,type:'emp',idx:i});
});
return quotes;
}
function scrollToEvidence(prefix,idx){
const m=document.querySelector(`.conversation-view mark.${prefix}-${idx}`);
if(!m)return;
document.querySelectorAll('mark.evidence-pulse').forEach(x=>x.classList.remove('evidence-pulse'));
m.scrollIntoView({behavior:'smooth',block:'center'});
m.classList.add('evidence-pulse');
setTimeout(()=>m.classList.remove('evidence-pulse'),2000);
}
// ===== Render: Conversation =====
function renderConversation(row){
const anno=getEffectiveAnno(row);
const quotes=buildQuotes(anno);
const msgs=applyHighlights(parseConv(row['conversation_cut']),quotes);
const view=document.getElementById('conversationView');
view.innerHTML=msgs.map(m=>`
${m.role==='listener'?'Listener':'Speaker'}
${m.htmlText||escH(m.text)}
`).join('');
view.querySelectorAll('mark.evidence-hl').forEach(mk=>{
mk.addEventListener('mouseenter',()=>showTooltip(mk,anno));
mk.addEventListener('mouseleave',hideTooltip);
});
}
function renderConvHeader(row){
const anno=getEffectiveAnno(row);
const e=anno.emotion;
const en=displayEmotion(row);
const ei=e?.intensity||'';
const intCls=ei.toLowerCase().includes('high')?'intensity-high':ei.toLowerCase().includes('medium')?'intensity-medium':'intensity-low';
document.getElementById('convMeta').innerHTML=`
${en}
${ei?`${ei}`:''}
${row.problem_type?`${row.problem_type}`:''}
${row.experience_type?`${row.experience_type}`:''}`;
}
// ===== Tooltip =====
function showTooltip(mk,anno){
const cls=Array.from(mk.classList).find(c=>c.startsWith('dist-hl-')||c.startsWith('emp-hl-'));
let html='';
if(cls?.startsWith('dist-hl-')){
const i=parseInt(cls.split('-').pop());
const d=(anno.distortions?.distortions||[])[i];
if(d){const c=DIST_COLORS[i%DIST_COLORS.length];html=`โ ๏ธ ${d.cognitive_distortion||'Distortion'}
Reasoning
${d.reasoning||'โ'}
`;}
} else if(cls?.startsWith('emp-hl-')){
const i=parseInt(cls.split('-').pop());
const dim=(anno.empathy?.empathy_dimensions||[])[i];
if(dim){const c=EMP_COLORS[i%EMP_COLORS.length],sc=scoreColor(dim.score||0);html=`๐ก ${dim.dimension_name}
Score
${dim.score}/7Reasoning
${dim.reasoning||'โ'}
`;}
}
if(!html)return;
const tip=document.getElementById('tooltip');
document.getElementById('tooltipContent').innerHTML=html;
tip.classList.add('visible');
const r=mk.getBoundingClientRect();
let top=r.bottom+8,left=r.left;
if(left+320>window.innerWidth)left=window.innerWidth-330;
if(top+200>window.innerHeight)top=r.top-200;
tip.style.top=top+'px';tip.style.left=left+'px';
}
function hideTooltip(){document.getElementById('tooltip').classList.remove('visible');}
// ===== Render: Emotion (View + Edit) =====
function renderEmotion(row){
const anno=getEffectiveAnno(row);
const e=anno.emotion;
const goldEmotion=displayEmotion(row);
const predictedEmotion=e?.primary_emotion || 'โ';
const emotionsMatch=predictedEmotion!=='โ' &&
predictedEmotion.trim().toLowerCase()===goldEmotion.trim().toLowerCase();
const el=document.getElementById('emotionContent');
const hasEdit=!!(reviewsMap[currentIndex]?.annotations_edit);
const btn=document.getElementById('editEmotionBtn');
btn.classList.toggle('active',editMode.emotion);
btn.textContent=editMode.emotion?'โ Done':'โ๏ธ Edit';
if(editMode.emotion){
const d=editData.emotion;
// Support both old (reasoning) and new (standard_reasoning + appraisal_reasoning) formats
const stdReason = d?.standard_reasoning || d?.reasoning || '';
const appReason = d?.appraisal_reasoning || '';
el.innerHTML=`
Intensity
`;
['ee-intensity','ee-term','ee-std-reason','ee-app-reason'].forEach(id=>{
document.getElementById(id)?.addEventListener('input',()=>{
editData.emotion={
// Preserve the read-only LLM prediction when saving other emotion edits.
primary_emotion:editData.emotion?.primary_emotion || d?.primary_emotion || '',
intensity:document.getElementById('ee-intensity').value,
specific_emotion_term:document.getElementById('ee-term').value,
standard_reasoning:document.getElementById('ee-std-reason').value,
appraisal_reasoning:document.getElementById('ee-app-reason').value,
};
});
});
} else {
if(!e){
el.innerHTML=`
Gold Emotion
${escH(goldEmotion)}
No LLM annotation details.
`;
return;
}
const intCls=(e.intensity||'').toLowerCase().includes('high')?'intensity-high':(e.intensity||'').toLowerCase().includes('medium')?'intensity-medium':'intensity-low';
// Determine which reasoning fields to show (new format takes priority)
const hasNewFormat = !!(e.standard_reasoning || e.appraisal_reasoning);
const stdR = e.standard_reasoning || (!hasNewFormat ? e.reasoning : '');
const appR = e.appraisal_reasoning || '';
el.innerHTML=`
Gold Emotion
${escH(goldEmotion)}
LLM Prediction
${escH(predictedEmotion)}
${emotionsMatch?'โ Match':'โ Different'}
${escH(e.intensity||'โ')}
${hasEdit?'Edited':''}
${e.specific_emotion_term?`Term: ${e.specific_emotion_term}
`:''}
${stdR?`
Standard Reasoning
${escH(stdR)}
`:''}
${appR?`
Appraisal Reasoning
${escH(appR)}
`:''}`;
}
}
// ===== Render: Distortions (View + Edit) =====
function renderDistortions(row){
const anno=getEffectiveAnno(row);
const el=document.getElementById('distortionsContent');
const dists=editMode.distortions ? editData.distortions : (anno.distortions?.distortions||[]);
const hasEdit=!!(reviewsMap[currentIndex]?.annotations_edit);
const btn=document.getElementById('editDistBtn');
btn.classList.toggle('active',editMode.distortions);
btn.textContent=editMode.distortions?'โ Done':'โ๏ธ Edit';
if(editMode.distortions){
if(!dists.length){
el.innerHTML=`No distortions
`;
} else {
el.innerHTML=dists.map((d,i)=>{
const c=DIST_COLORS[i%DIST_COLORS.length];
return `
Evidence Quote
Reasoning
`;
}).join('');
}
el.innerHTML+=``;
// events
el.querySelectorAll('[data-d-field]').forEach(inp=>{
inp.addEventListener('input',()=>{
const idx=parseInt(inp.dataset.dIdx);
const f=inp.dataset.dField;
if(f==='name') editData.distortions[idx].cognitive_distortion=inp.value;
else if(f==='quote') editData.distortions[idx].evidence_quote=inp.value;
else if(f==='reason') editData.distortions[idx].reasoning=inp.value;
});
});
el.querySelectorAll('[data-remove-dist]').forEach(btn=>{
btn.addEventListener('click',()=>{
editData.distortions.splice(parseInt(btn.dataset.removeDist),1);
renderDistortions(row);
});
});
el.querySelectorAll('[data-sel-card]').forEach(btn=>{
btn.addEventListener('click',()=>startSelectingEvidence(btn.dataset.selCard,parseInt(btn.dataset.selIdx),btn));
});
document.getElementById('addDistBtn')?.addEventListener('click',()=>{
editData.distortions.push({cognitive_distortion:'',evidence_quote:'',reasoning:''});
renderDistortions(row);
});
} else {
if(!dists.length){el.innerHTML=`No distortions detected
`;return;}
el.innerHTML=dists.map((d,i)=>{
const c=DIST_COLORS[i%DIST_COLORS.length];
const q=(d.evidence_quote||'').replace(/^(listener|speaker):\s*/i,'');
return `
${d.cognitive_distortion||'โ'} ${hasEdit&&i===0?'Edited':''}
${q?`
${q}
`:''}
${d.reasoning?`
${d.reasoning}
`:''}
`;
}).join('');
el.querySelectorAll('.distortion-item').forEach(item=>{
item.addEventListener('click',()=>scrollToEvidence('dist-hl',parseInt(item.dataset.distIdx)%3));
});
}
}
// ===== Render: Empathy (View + Edit) =====
function renderEmpathy(row){
const anno=getEffectiveAnno(row);
const el=document.getElementById('empathyContent');
const scoreEl=document.getElementById('overallScore');
const dims=editMode.empathy ? editData.empathy : (anno.empathy?.empathy_dimensions||[]);
const hasEdit=!!(reviewsMap[currentIndex]?.annotations_edit);
const btn=document.getElementById('editEmpBtn');
btn.classList.toggle('active',editMode.empathy);
btn.textContent=editMode.empathy?'โ Done':'โ๏ธ Edit';
if(!anno.empathy&&!editMode.empathy){el.innerHTML='No annotation.
';scoreEl.textContent='';return;}
const total=dims.reduce((s,d)=>s+(d.score||0),0);
scoreEl.textContent=dims.length?`${total} / ${dims.length*7}`:'';
if(editMode.empathy){
el.innerHTML=dims.map((dim,i)=>{
const c=EMP_COLORS[i%EMP_COLORS.length];
const sc=dim.score||0;
return ``;
}).join('');
el.innerHTML+=``;
// events
el.querySelectorAll('[data-e-field]').forEach(inp=>{
inp.addEventListener('input',()=>{
const idx=parseInt(inp.dataset.eIdx);
const f=inp.dataset.eField;
if(f==='name') editData.empathy[idx].dimension_name=inp.value;
else if(f==='score'){
const v=parseInt(inp.value);
editData.empathy[idx].score=v;
const disp=document.getElementById(`score-disp-${idx}`);
if(disp){disp.textContent=v;disp.style.color=scoreColor(v);}
}
else if(f==='quote') editData.empathy[idx].evidence_quote=inp.value;
else if(f==='reason') editData.empathy[idx].reasoning=inp.value;
});
});
el.querySelectorAll('[data-remove-emp]').forEach(btn=>{
btn.addEventListener('click',()=>{editData.empathy.splice(parseInt(btn.dataset.removeEmp),1);renderEmpathy(row);});
});
el.querySelectorAll('[data-sel-card]').forEach(btn=>{
btn.addEventListener('click',()=>startSelectingEvidence(btn.dataset.selCard,parseInt(btn.dataset.selIdx),btn));
});
document.getElementById('addEmpBtn')?.addEventListener('click',()=>{
editData.empathy.push({dimension_name:'',score:0,evidence_quote:'',reasoning:''});
renderEmpathy(row);
});
} else {
el.innerHTML=dims.map((dim,i)=>{
const c=EMP_COLORS[i%EMP_COLORS.length];
const sc=dim.score||0;
const pct=Math.min(100,(sc/7)*100);
const sC=scoreColor(sc);
const q=(dim.evidence_quote||'').replace(/^(listener|speaker):\s*/i,'');
return `
${q&&q!=='No evidence present'?`
"${q}"
`:''}
${dim.reasoning?`
${dim.reasoning}
`:''}
`;
}).join('');
el.querySelectorAll('.empathy-item').forEach(item=>{
item.addEventListener('click',()=>{
item.classList.toggle('active');
scrollToEvidence('emp-hl',parseInt(item.dataset.empIdx)%9);
});
});
}
}
// ===== Evidence Selection Mode =====
function startSelectingEvidence(card, itemIdx, triggerBtn){
selectingEvidence={card, itemIdx};
const panel=document.querySelector('.conversation-panel');
panel.classList.add('selecting-mode');
const hint=document.getElementById('selectionHint');
hint.classList.add('visible');
// Mark button as active
document.querySelectorAll('.select-evidence-btn').forEach(b=>b.classList.remove('selecting'));
triggerBtn?.classList.add('selecting');
}
function stopSelectingEvidence(){
selectingEvidence=null;
document.querySelector('.conversation-panel')?.classList.remove('selecting-mode');
const hint=document.getElementById('selectionHint');
hint?.classList.remove('visible');
document.querySelectorAll('.select-evidence-btn').forEach(b=>b.classList.remove('selecting'));
}
function captureSelection(){
if(!selectingEvidence)return;
const sel=window.getSelection();
if(!sel||sel.rangeCount===0||sel.isCollapsed)return;
const text=sel.toString().trim();
if(!text)return;
const {card,itemIdx}=selectingEvidence;
// Set the value in editData
if(card==='distortions'&&editData.distortions[itemIdx]!==undefined){
editData.distortions[itemIdx].evidence_quote=text;
} else if(card==='empathy'&&editData.empathy[itemIdx]!==undefined){
editData.empathy[itemIdx].evidence_quote=text;
}
sel.removeAllRanges();
stopSelectingEvidence();
// Re-render the card to update input field
const row=filteredData[currentIndex];
if(card==='distortions') renderDistortions(row);
else if(card==='empathy') renderEmpathy(row);
}
// ===== Save Annotations =====
async function saveAnnotations(){
const row=filteredData[currentIndex];
const anno=getEffectiveAnno(row);
// Merge current editData into the annotation structure
const toSave={
emotion: editData.emotion || anno.emotion,
distortions: anno.distortions
? {...anno.distortions, distortions: editData.distortions||anno.distortions?.distortions||[]}
: {distortions: editData.distortions||[]},
empathy: anno.empathy
? {...anno.empathy, empathy_dimensions: editData.empathy||anno.empathy?.empathy_dimensions||[]}
: {empathy_dimensions: editData.empathy||[]}
};
try {
const res=await fetch('/api/reviews/annotations',{
method:'PATCH',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({assignment_id:assignmentId,sample_index:currentIndex,annotations:toSave})
});
const data=await res.json();
if(res.ok){
reviewsMap[currentIndex]=data;
return true;
}
} catch(e){}
return false;
}
// ===== Toggle Edit Mode =====
function toggleEdit(card){
const row=filteredData[currentIndex];
const anno=getEffectiveAnno(row);
if(editMode[card]){
// Exiting edit mode โ auto-save
editMode[card]=false;
stopSelectingEvidence();
saveAnnotations().then(ok=>{
if(ok){
// Re-render all to show updated highlights
renderAll();
}
});
} else {
// Entering edit mode โ init editData from effective annotation
if(card==='emotion') editData.emotion=deepCopy(anno.emotion||{primary_emotion:'',intensity:'',specific_emotion_term:'',reasoning:''});
else if(card==='distortions') editData.distortions=deepCopy(anno.distortions?.distortions||[]);
else if(card==='empathy') editData.empathy=deepCopy(anno.empathy?.empathy_dimensions||[]);
editMode[card]=true;
if(card==='emotion') renderEmotion(row);
else if(card==='distortions') renderDistortions(row);
else if(card==='empathy') renderEmpathy(row);
}
}
// ===== Review State =====
function renderReviewState(){
const review=reviewsMap[currentIndex];
const badge=document.getElementById('reviewStatusBadge');
const acceptBtn=document.getElementById('acceptBtn');
const rejectBtn=document.getElementById('rejectBtn');
const comment=document.getElementById('reviewComment');
const submitBtn=document.getElementById('submitReviewBtn');
acceptBtn.classList.remove('active');
rejectBtn.classList.remove('active');
currentReviewStatus=null;
if(review&&review.status&&review.status!=='reject'||(review&&review.status==='accept')){
// Has a real review decision
}
if(review){
badge.style.display='inline-block';
badge.textContent=review.status==='accept'?'โ Accepted':'โ Rejected';
badge.className='review-status-badge '+(review.status==='accept'?'review-status-accept':'review-status-reject');
currentReviewStatus=review.status;
if(review.status==='accept') acceptBtn.classList.add('active');
else rejectBtn.classList.add('active');
comment.value=review.comment||'';
submitBtn.textContent='Update Review';
} else {
badge.style.display='none';
comment.value='';
submitBtn.textContent='Submit Review';
}
submitBtn.disabled=false;
}
// ===== Sample List =====
function renderSampleList(){
const list=document.getElementById('sampleList');
list.innerHTML='';
filteredData.forEach((row,i)=>{
const en=displayEmotion(row);
const review=reviewsMap[i];
const dotCls=review?(review.status==='accept'?'dot-accept':'dot-reject'):'dot-pending';
const hasEdit=!!(review?.annotations_edit);
const item=document.createElement('div');
item.className='sample-item'+(i===currentIndex?' active':'');
item.dataset.idx=i;
item.innerHTML=`#${i+1} ${hasEdit?'โ':''}
${en}
${row.problem_type||''}
`;
item.addEventListener('click',()=>{currentIndex=i;renderAll();});
list.appendChild(item);
});
}
// ===== Navigation =====
function updateNav(){
document.getElementById('prevBtn').disabled=currentIndex<=0;
document.getElementById('nextBtn').disabled=currentIndex>=filteredData.length-1;
document.getElementById('currentIdx').textContent=currentIndex+1;
document.getElementById('totalSamples').textContent=filteredData.length;
document.querySelectorAll('.sample-item').forEach(it=>it.classList.toggle('active',parseInt(it.dataset.idx)===currentIndex));
document.querySelector('.sample-item.active')?.scrollIntoView({block:'nearest'});
}
// ===== Render All =====
function renderAll(){
if(!filteredData.length)return;
// Exit edit modes on navigation
editMode={emotion:false,distortions:false,empathy:false};
stopSelectingEvidence();
const row=filteredData[currentIndex];
renderConvHeader(row);
renderConversation(row);
renderEmotion(row);
renderDistortions(row);
renderEmpathy(row);
renderReviewState();
updateNav();
}
// ===== Filter =====
function applyFilter(q){
q=q.toLowerCase().trim();
filteredData=q?allData.filter(row=>{
return displayEmotion(row).toLowerCase().includes(q)||
(row.problem_type||'').toLowerCase().includes(q);
}):[...allData];
currentIndex=0;renderSampleList();renderAll();
}
// ===== Submit Review =====
async function submitReview(){
if(!currentReviewStatus){alert('Please select Accept or Reject');return;}
const comment=document.getElementById('reviewComment').value.trim();
const btn=document.getElementById('submitReviewBtn');
btn.disabled=true;btn.textContent='Submitting...';
try{
const res=await fetch('/api/reviews',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({assignment_id:assignmentId,sample_index:currentIndex,status:currentReviewStatus,comment})
});
const data=await res.json();
if(res.ok){
reviewsMap[currentIndex]=data;
renderReviewState();renderSampleList();
btn.textContent='โ Saved!';
setTimeout(()=>{
btn.textContent='Update Review';btn.disabled=false;
const next=filteredData.findIndex((_,i)=>i>currentIndex&&!reviewsMap[i]);
if(next!==-1){currentIndex=next;renderAll();}
},800);
} else {alert(data.error||'Failed');btn.textContent='Submit Review';btn.disabled=false;}
} catch(e){alert('Network error');btn.textContent='Submit Review';btn.disabled=false;}
}
// ===== Annotation History =====
function closeHistory(){
const modal=document.getElementById('historyModal');
modal.classList.remove('visible');
modal.setAttribute('aria-hidden','true');
}
function historySnapshotHtml(entry,version,row){
const a=entry.annotations||{};
const emotion=a.emotion||{};
const distortions=a.distortions?.distortions||[];
const empathy=a.empathy?.empathy_dimensions||[];
const when=entry.created_at
? new Date(entry.created_at).toLocaleString()
: 'Unknown time';
const emotionParts=[
displayEmotion(row),
emotion.specific_emotion_term,
emotion.intensity
].filter(Boolean).map(v=>escH(String(v)));
const distortionNames=distortions
.map(d=>d.cognitive_distortion).filter(Boolean).map(v=>escH(String(v)));
const empathyNames=empathy.map(d=>{
const name=escH(String(d.dimension_name||'Dimension'));
return d.score===undefined||d.score===null ? name : `${name} (${escH(String(d.score))}/7)`;
});
const fullJson=escH(JSON.stringify(a,null,2));
return `
Version ${version}
${escH(entry.edited_by||'unknown')} ยท ${escH(when)}
Emotion
${emotionParts.join(' ยท ')||'โ'}
${emotion.standard_reasoning?`
Standard Reasoning
${escH(emotion.standard_reasoning)}
`:''}
${emotion.appraisal_reasoning?`
Appraisal Reasoning
${escH(emotion.appraisal_reasoning)}
`:''}
Distortions (${distortions.length})
${distortionNames.join(', ')||'None'}
Empathy dimensions (${empathy.length})
${empathyNames.join(', ')||'None'}
View full snapshot
${fullJson}
`;
}
async function openHistory(){
if(!assignmentId||!filteredData.length)return;
const modal=document.getElementById('historyModal');
const content=document.getElementById('historyContent');
const row=filteredData[currentIndex];
document.getElementById('historySubtitle').textContent=`Sample #${currentIndex+1} ยท ${displayEmotion(row)}`;
content.innerHTML='Loading history...
';
modal.classList.add('visible');
modal.setAttribute('aria-hidden','false');
try{
const res=await fetch(`/api/assignments/${assignmentId}/samples/${currentIndex}/annotation-history`);
const entries=await res.json();
if(!res.ok)throw new Error(entries.error||'Failed to load history');
if(!entries.length){
content.innerHTML='No edit history yet. A version will appear after an annotation is changed and saved.
';
return;
}
content.innerHTML=entries.map((entry,i)=>historySnapshotHtml(entry,entries.length-i,row)).join('');
}catch(err){
content.innerHTML=`${escH(err.message||'Failed to load history')}
`;
}
}
// ===== Load Data =====
async function loadAssignmentData(){
try{
let fileId,filename;
const r1=await fetch('/api/my-assignments');
const myAssign=await r1.json();
let a=myAssign.find(x=>x.id===assignmentId);
if(!a){
const r2=await fetch('/api/assignments');
if(r2.ok){const all=await r2.json();a=all.find(x=>x.id===assignmentId);}
}
if(!a){document.getElementById('reviewSubtitle').textContent='Assignment not found';return;}
fileId=a.file_id;filename=a.filename;
const r3=await fetch(`/api/files/${fileId}/data`);
const fd=await r3.json();
allData=fd.samples||[];filteredData=[...allData];
const r4=await fetch(`/api/assignments/${assignmentId}/reviews`);
reviewsMap=await r4.json();
document.getElementById('reviewTitle').textContent='Review';
document.getElementById('reviewSubtitle').textContent=filename||'Annotation Review';
currentIndex=0;renderSampleList();renderAll();
} catch(err){console.error(err);document.getElementById('reviewSubtitle').textContent='Failed to load';}
}
// ===== DOMContentLoaded =====
document.addEventListener('DOMContentLoaded',async()=>{
const parts=window.location.pathname.split('/');
assignmentId=parseInt(parts[parts.length-1]);
if(isNaN(assignmentId))return;
try{const m=await fetch('/api/me').then(r=>r.json());document.getElementById('userBadge').textContent=m.username;}catch(e){}
document.getElementById('logoutBtn').addEventListener('click',async()=>{await fetch('/api/logout',{method:'POST'});window.location.href='/login';});
document.getElementById('prevBtn').addEventListener('click',()=>{if(currentIndex>0){currentIndex--;renderAll();}});
document.getElementById('nextBtn').addEventListener('click',()=>{if(currentIndexapplyFilter(e.target.value));
document.getElementById('acceptBtn').addEventListener('click',()=>{currentReviewStatus='accept';document.getElementById('acceptBtn').classList.add('active');document.getElementById('rejectBtn').classList.remove('active');});
document.getElementById('rejectBtn').addEventListener('click',()=>{currentReviewStatus='reject';document.getElementById('rejectBtn').classList.add('active');document.getElementById('acceptBtn').classList.remove('active');});
document.getElementById('submitReviewBtn').addEventListener('click',submitReview);
document.getElementById('historyBtn').addEventListener('click',openHistory);
document.getElementById('historyCloseBtn').addEventListener('click',closeHistory);
document.getElementById('historyModal').addEventListener('click',e=>{
if(e.target===e.currentTarget)closeHistory();
});
// Edit toggle buttons
['editEmotionBtn','editDistBtn','editEmpBtn'].forEach(id=>{
document.getElementById(id)?.addEventListener('click',()=>{
const card={'editEmotionBtn':'emotion','editDistBtn':'distortions','editEmpBtn':'empathy'}[id];
toggleEdit(card);
});
});
// Capture text selection from conversation
document.getElementById('conversationView').addEventListener('mouseup',()=>{
if(selectingEvidence) captureSelection();
});
// Escape key cancels selection
document.addEventListener('keydown',e=>{
if(e.key==='Escape'){
if(document.getElementById('historyModal').classList.contains('visible'))closeHistory();
else stopSelectingEvidence();
return;
}
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;
if(e.key==='ArrowLeft'&¤tIndex>0){currentIndex--;renderAll();}
if(e.key==='ArrowRight'&¤tIndexr.json()).then(data=>{
const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download=`export_assignment_${assignmentId}.json`;
a.click();URL.revokeObjectURL(a.href);
}).catch(e=>alert('Export failed'));
}
document.getElementById('exportMenu').style.display='none';
}
// Wire up export buttons after DOM ready
document.addEventListener('DOMContentLoaded',()=>{
const exportBtn=document.getElementById('exportBtn');
const exportMenu=document.getElementById('exportMenu');
if(exportBtn&&exportMenu){
exportBtn.addEventListener('click',(e)=>{
e.stopPropagation();
exportMenu.style.display=exportMenu.style.display==='none'?'block':'none';
});
document.getElementById('exportJSON')?.addEventListener('click',()=>doExport('json'));
document.getElementById('exportCSV')?.addEventListener('click',()=>doExport('csv'));
// Close menu on outside click
document.addEventListener('click',()=>{exportMenu.style.display='none';});
}
});