Q_TASK_EVALUATOR_EXTRACTOR_V3.jsx
Design Mockups/DOC23 Mockups/Q_TASK_EVALUATOR_EXTRACTOR_V3.jsx
import React, { useState } from "react";
// ═══════════════════════════════════════════════════════════════
// EVALUATOR + EXTRACTOR EXPANDED VIEWS — V3
// Changes from V2:
// • Per-outcome routing as default (each outcome picks its destination)
// • Aggregate "any_failed_out" port preserved for convenience
// • Meta-outcome shown as a distinct addable outcome type
// • Pipeline indicator showing position in a chain of Evaluators
// • "+ Add Outcome" with three entry paths (type · ask TA · load preset)
// • Outcome cards default to inferred-plan SUMMARY; Adjust opens form
// • Findings viewable on demand via drawer (R0.6.5 §26 action buttons)
// • Findings deliverable as inline document comments option
// ═══════════════════════════════════════════════════════════════
const c = {
bg:"#0f1117", bgCard:"#161920", bgHover:"#1c1f2a", bgSel:"#1e2233",
bgPanel:"#131620", bgPopup:"#1a1e2b", border:"#262a36",
text:"#e2e4ea", textSec:"#8b8fa3", textTer:"#5d6178",
accent:"#5b8af5", green:"#34d399", red:"#f87171",
amber:"#fbbf24", purple:"#a78bfa", cyan:"#22d3ee", orange:"#fb923c",
metaAccent:"#e879f9",
};
const sans = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
const mono = "'SF Mono','Cascadia Code','Fira Code', monospace";
// ─── Helpers ───
const SectionLabel = ({children, mt}) => (
<div style={{fontSize:9,fontWeight:700,color:c.textTer,marginTop:mt?20:0,marginBottom:8,letterSpacing:1,textTransform:"uppercase",fontFamily:sans}}>
{children}
</div>
);
const FieldLabel = ({children, mt}) => (
<div style={{fontSize:10,fontWeight:600,color:c.textSec,marginTop:mt?12:0,marginBottom:5,fontFamily:sans}}>{children}</div>
);
const HelperText = ({children}) => (
<div style={{fontSize:10,color:c.textTer,marginTop:5,lineHeight:1.5,fontStyle:"italic",fontFamily:sans}}>{children}</div>
);
const Select = ({opts, v, onChange, compact}) => (
<select defaultValue={v} onChange={onChange||(()=>{})} style={{width:"100%",background:c.bg,border:`1px solid ${c.border}`,borderRadius:4,padding:compact?"5px 7px":"7px 9px",fontSize:11,color:c.text,fontFamily:sans,outline:"none"}}>
{opts.map((o,i)=><option key={i} value={o}>{o}</option>)}
</select>
);
const Textarea = ({value, h, placeholder, onChange}) => (
<textarea defaultValue={value} placeholder={placeholder} onChange={onChange||(()=>{})}
style={{width:"100%",background:c.bg,border:`1px solid ${c.border}`,borderRadius:4,padding:"8px 10px",fontSize:11,color:c.text,fontFamily:sans,resize:"vertical",minHeight:h||60,outline:"none",lineHeight:1.5}} />
);
const Input = ({value, placeholder, onChange, w}) => (
<input defaultValue={value} placeholder={placeholder} onChange={onChange||(()=>{})}
style={{width:w||"100%",background:c.bg,border:`1px solid ${c.border}`,borderRadius:4,padding:"6px 9px",fontSize:11,color:c.text,fontFamily:sans,outline:"none"}} />
);
const Check = ({label, checked, onChange, hint}) => (
<div style={{padding:"3px 0"}}>
<label style={{display:"flex",alignItems:"center",gap:8,fontSize:11,color:c.text,cursor:"pointer",fontFamily:sans}}>
<span style={{width:14,height:14,borderRadius:3,border:`1px solid ${checked?c.accent:c.border}`,background:checked?c.accent:"transparent",display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
{checked && <span style={{color:"#fff",fontSize:9,fontWeight:700}}>✓</span>}
</span>
<span>{label}</span>
</label>
{hint && <div style={{fontSize:9,color:c.textTer,marginLeft:22,marginTop:2,fontStyle:"italic"}}>{hint}</div>}
</div>
);
const Radio = ({label, checked, hint}) => (
<div style={{padding:"3px 0"}}>
<label style={{display:"flex",alignItems:"flex-start",gap:8,fontSize:11,color:c.text,cursor:"pointer",fontFamily:sans}}>
<span style={{width:14,height:14,borderRadius:"50%",border:`1.5px solid ${checked?c.accent:c.border}`,background:c.bg,display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0,marginTop:1}}>
{checked && <span style={{width:7,height:7,borderRadius:"50%",background:c.accent}} />}
</span>
<div style={{flex:1}}>
<div>{label}</div>
{hint && <div style={{fontSize:9,color:c.textTer,marginTop:2,fontStyle:"italic",lineHeight:1.5}}>{hint}</div>}
</div>
</label>
</div>
);
const Btn = ({children, primary, accent, danger, ghost, sm, onClick}) => {
const styles = primary ? {background:c.accent,color:"#fff",border:"none"}
: accent ? {background:`${c.accent}15`,color:c.accent,border:`1px solid ${c.accent}40`}
: danger ? {background:`${c.red}15`,color:c.red,border:`1px solid ${c.red}40`}
: ghost ? {background:"transparent",color:c.textSec,border:`1px solid ${c.border}`}
: {background:c.bgHover,color:c.text,border:`1px solid ${c.border}`};
return <button onClick={onClick} style={{...styles,padding:sm?"4px 8px":"6px 11px",borderRadius:4,fontSize:sm?9:10,fontWeight:600,cursor:"pointer",fontFamily:sans,whiteSpace:"nowrap"}}>{children}</button>;
};
const Pill = ({children, color}) => (
<span style={{display:"inline-block",padding:"2px 7px",borderRadius:10,fontSize:9,fontWeight:600,background:`${color}18`,color:color,border:`1px solid ${color}40`,letterSpacing:0.3,textTransform:"uppercase",fontFamily:sans}}>{children}</span>
);
const Tag = ({children, color, removable, onClick}) => (
<span onClick={onClick} style={{display:"inline-flex",alignItems:"center",gap:5,padding:"3px 8px",borderRadius:3,fontSize:10,background:`${color||c.accent}12`,color:color||c.accent,border:`1px solid ${color||c.accent}30`,fontFamily:sans,cursor:onClick?"pointer":"default"}}>
{children}
{removable && <span style={{cursor:"pointer",opacity:0.6,fontSize:11}}>×</span>}
</span>
);
const StatusBadge = ({status}) => {
const map = {
verified: {label:"PASSED", color:c.green, icon:"✓"},
failed: {label:"FAILED", color:c.red, icon:"✕"},
incomplete: {label:"INCOMPLETE", color:c.amber, icon:"◐"},
pending: {label:"PENDING", color:c.textTer, icon:"○"},
in_progress: {label:"IN PROGRESS", color:c.cyan, icon:"⟳"},
};
const m = map[status] || map.pending;
return (
<span style={{display:"inline-flex",alignItems:"center",gap:5,padding:"3px 9px",borderRadius:3,fontSize:9,fontWeight:700,background:`${m.color}18`,color:m.color,border:`1px solid ${m.color}40`,letterSpacing:0.3,textTransform:"uppercase",fontFamily:sans}}>
<span>{m.icon}</span><span>{m.label}</span>
</span>
);
};
const ResultTypePill = ({type}) => {
const map = {
pass_fail: {label:"PASS/FAIL", color:c.accent},
checklist: {label:"CHECKLIST", color:c.cyan},
score: {label:"SCORE", color:c.purple},
meta: {label:"META", color:c.metaAccent},
};
const m = map[type];
return <Pill color={m.color}>{m.label}</Pill>;
};
const SectionHeader = ({children}) => (
<div style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:1.5,textTransform:"uppercase",fontFamily:sans,marginTop:18,marginBottom:10,paddingBottom:6,borderBottom:`1px solid ${c.border}`}}>{children}</div>
);
const UploadStrip = ({label, files, hint}) => (
<div>
<FieldLabel>{label}</FieldLabel>
<div style={{border:`1px dashed ${c.border}`,borderRadius:5,padding:files&&files.length?"6px":"12px",background:c.bg,cursor:"pointer"}}>
{(!files || !files.length) ? (
<div style={{textAlign:"center",fontSize:10,color:c.textSec,fontFamily:sans}}>
<span style={{marginRight:6}}>📁</span>Click or drag to add files
</div>
) : (
<div style={{display:"flex",flexDirection:"column",gap:3}}>
{files.map((f,i)=>(
<div key={i} style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"3px 5px",fontSize:10,color:c.text}}>
<span style={{display:"flex",alignItems:"center",gap:5}}>
<span style={{color:c.textTer}}>{f.icon}</span>
<span>{f.name}</span>
{f.size && <span style={{color:c.textTer,fontSize:9}}>· {f.size}</span>}
</span>
<span style={{cursor:"pointer",color:c.textTer,opacity:0.5,fontSize:11}}>×</span>
</div>
))}
</div>
)}
</div>
{hint && <HelperText>{hint}</HelperText>}
</div>
);
// ═══════════════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════════════
export default function Mockup() {
const [view, setView] = useState("evaluator");
return (
<div style={{width:"100%",height:"100vh",background:c.bg,color:c.text,fontFamily:sans,display:"flex",flexDirection:"column",overflow:"hidden"}}>
<div style={{height:32,minHeight:32,borderBottom:`1px solid ${c.border}`,display:"flex",alignItems:"center",padding:"0 12px",gap:8,background:c.bgPanel,fontSize:9,color:c.textTer,letterSpacing:0.5}}>
REFERENCE MOCKUP V3 — PER-OUTCOME ROUTING · META-OUTCOME · THREE-PATH CREATION
<div style={{flex:1}} />
<div style={{display:"flex",gap:4,padding:2,background:c.bg,borderRadius:5}}>
{[
{id:"evaluator",label:"⊜ Evaluator"},
{id:"extractor",label:"◈ Extractor"},
].map(t=>(
<button key={t.id} onClick={()=>setView(t.id)}
style={{padding:"3px 10px",fontSize:10,fontWeight:600,color:view===t.id?c.text:c.textSec,background:view===t.id?c.bgHover:"transparent",border:"none",borderRadius:3,cursor:"pointer",fontFamily:sans}}>
{t.label}
</button>
))}
</div>
</div>
{view === "evaluator" ? <EvaluatorView /> : <ExtractorView />}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// EVALUATOR VIEW
// ═══════════════════════════════════════════════════════════════
function EvaluatorView() {
const [portsOpen, setPortsOpen] = useState(false);
const [expanded, setExpanded] = useState("o3"); // outcome adjust form open
const [adding, setAdding] = useState(false); // showing add-outcome form
const [findingsOpen, setFindingsOpen] = useState(null); // outcome_id whose findings are open
const [pipelineExpanded, setPipelineExpanded] = useState(false);
return (
<div style={{flex:1,display:"flex",flexDirection:"column",overflow:"hidden",position:"relative"}}>
{/* Header */}
<div style={{height:52,minHeight:52,borderBottom:`1px solid ${c.border}`,display:"flex",alignItems:"center",justifyContent:"space-between",padding:"0 22px",background:c.bgPanel}}>
<div style={{display:"flex",alignItems:"center",gap:12}}>
<span style={{fontSize:20}}>⊜</span>
<div>
<div style={{fontSize:13,fontWeight:700,color:c.text}}>Final Quality Gate</div>
<div style={{fontSize:10,color:c.textTer,fontFamily:mono,marginTop:2}}>step.evaluator · stage 5 of 5 in pipeline</div>
</div>
<span style={{marginLeft:14}}><StatusBadge status="failed" /></span>
<span style={{fontSize:11,color:c.textSec}}>7 of 8 outcomes passed · iteration 2 of 3</span>
</div>
<div style={{display:"flex",gap:8,alignItems:"center"}}>
<Btn ghost>Preset…</Btn>
<Btn primary>▶ Run</Btn>
<span style={{fontSize:18,color:c.textSec,cursor:"pointer",padding:"0 4px",marginLeft:4}}>✕</span>
</div>
</div>
{/* Pipeline indicator */}
<PipelineIndicator expanded={pipelineExpanded} setExpanded={setPipelineExpanded} />
{/* Body */}
<div style={{flex:1,display:"flex",overflow:"hidden"}}>
{/* LEFT SIDEBAR */}
<div style={{width:300,minWidth:300,borderRight:`1px solid ${c.border}`,padding:"18px 16px 22px",overflowY:"auto",background:c.bgPanel}}>
<SectionLabel>Connected Inputs</SectionLabel>
<div style={{display:"flex",flexDirection:"column",gap:4,fontSize:10}}>
<div style={{padding:"5px 7px",background:`${c.accent}10`,borderRadius:3,border:`1px solid ${c.accent}25`}}>
<span style={{color:c.accent,fontFamily:mono}}>subject_in</span> <span style={{color:c.textSec}}>← ◆ Final Brief Draft</span>
</div>
<div style={{padding:"5px 7px",background:`${c.accent}10`,borderRadius:3,border:`1px solid ${c.accent}25`}}>
<span style={{color:c.accent,fontFamily:mono}}>claims_in</span> <span style={{color:c.textSec}}>← ◈ Brief Claims</span>
</div>
<div style={{padding:"5px 7px",background:`${c.accent}10`,borderRadius:3,border:`1px solid ${c.accent}25`}}>
<span style={{color:c.accent,fontFamily:mono}}>evidence_in.1</span> <span style={{color:c.textSec}}>← ◇ Case Record</span>
</div>
<div style={{padding:"5px 7px",background:`${c.accent}10`,borderRadius:3,border:`1px solid ${c.accent}25`}}>
<span style={{color:c.accent,fontFamily:mono}}>evidence_in.2</span> <span style={{color:c.textSec}}>← ◇ Motion to Dismiss</span>
</div>
</div>
<SectionLabel mt>Overall Pass Criteria</SectionLabel>
<Select opts={["All required outcomes must pass","Average score ≥ threshold","Weighted score ≥ threshold"]} v="All required outcomes must pass" />
<SectionLabel mt>Defaults</SectionLabel>
<FieldLabel>Agent for checks</FieldLabel>
<Select opts={["(task default)","Elnor","Nova","Atlas"]} v="(task default)" compact />
<FieldLabel mt>Cost cap per run</FieldLabel>
<Input value="$1.50" />
<HelperText>Outcomes can override with their own cost cap.</HelperText>
<SectionLabel mt>If Verdicts Fail</SectionLabel>
<div style={{padding:11,background:c.bg,borderRadius:6,border:`1px solid ${c.border}`,fontSize:11,lineHeight:1.65,color:c.text}}>
Try up to <Input value="3" w={36} /> revisions before giving up.
<br />
If the <em>same outcome</em> fails <Input value="3" w={28} /> times, send to human.
<br />
If no improvement after <Input value="2" w={28} /> iterations, send to human.
<br />
On regression: <Select opts={["revert to prior best","warn and continue","escalate"]} v="revert to prior best" compact />
</div>
<SectionLabel mt>When to Involve a Human</SectionLabel>
<div style={{padding:11,background:c.bg,borderRadius:6,border:`1px solid ${c.border}`}}>
<Radio label="Only when required outcomes fail (after retries)" checked={true} />
<Radio label="Before any output (review every verdict)" checked={false} />
<Radio label="On regression or stuck failures" checked={false} />
<Radio label="Per outcome (each outcome decides)" checked={false} />
<Radio label="Never (full auto)" checked={false} />
</div>
<SectionLabel mt>Sources & Materials</SectionLabel>
<UploadStrip label="Reference materials" files={[
{icon:"📄",name:"Bluebook 21st Ed.pdf",size:"4.2 MB"},
]} />
<div style={{marginTop:10}}>
<UploadStrip label="Evidence sources" files={[
{icon:"📁",name:"Case Record",size:"487 docs"},
{icon:"📄",name:"Motion to Dismiss.pdf"},
]} />
</div>
<FieldLabel mt>Connected libraries</FieldLabel>
<div style={{display:"flex",flexDirection:"column",gap:5}}>
{[
{label:"DOC73 PBE: Securities Litigation", icon:"📚"},
{label:"DOC25: Matter file (auto)", icon:"🗂"},
{label:"CourtListener", icon:"⚖"},
].map((l,i)=>(
<div key={i} style={{padding:"5px 8px",background:c.bgCard,borderRadius:3,border:`1px solid ${c.border}`,fontSize:10,color:c.text,display:"flex",alignItems:"center",gap:7}}>
<span style={{fontSize:11}}>{l.icon}</span><span>{l.label}</span>
</div>
))}
</div>
<SectionLabel mt>Custom Guidance</SectionLabel>
<Textarea value="Apply strict Bluebook 21st edition standards. Prefer formal legal register. When a citation can't be verified via CourtListener, try Westlaw before marking it unresolvable." h={70} />
</div>
{/* MAIN */}
<div style={{flex:1,overflowY:"auto",background:c.bg}}>
{/* Verdict banner */}
<div style={{padding:"18px 28px 0"}}>
<div style={{padding:14,background:`${c.amber}08`,borderRadius:8,border:`1px solid ${c.amber}30`}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:6}}>
<span style={{fontSize:12,fontWeight:700,color:c.amber}}>↺ NEEDS REVISION — iteration 2 of 3</span>
<span style={{fontSize:10,color:c.textSec,fontFamily:mono}}>last run: 9:24 AM · cost $0.41 · 14.2s</span>
</div>
<div style={{fontSize:11,color:c.text,lineHeight:1.5,marginBottom:8}}>
7 of 8 outcomes passed. <strong style={{color:c.red}}>Citations resolve to real cases</strong> failed — 3 citations didn't verify. Sent back to <span style={{color:c.accent}}>Brief Drafter</span> with structured feedback.
</div>
<div style={{display:"flex",gap:8}}>
<Btn ghost sm onClick={()=>setFindingsOpen("all")}>View all findings</Btn>
<Btn ghost sm>Compare to iteration 1</Btn>
<Btn accent sm>Re-run</Btn>
</div>
</div>
</div>
{/* Outcomes header */}
<div style={{padding:"22px 28px 0",display:"flex",alignItems:"center",justifyContent:"space-between"}}>
<div>
<div style={{fontSize:15,fontWeight:700,color:c.text,fontFamily:sans}}>Outcomes</div>
<div style={{fontSize:11,color:c.textSec,marginTop:3}}>Each outcome describes what to check and where to send the work if it fails.</div>
</div>
<Btn accent sm onClick={()=>setAdding(!adding)}>{adding ? "Cancel" : "+ Add Outcome"}</Btn>
</div>
{/* Add Outcome inline form */}
{adding && <AddOutcomeForm onClose={()=>setAdding(false)} />}
{/* Outcome cards */}
<div style={{padding:"14px 28px 28px",display:"flex",flexDirection:"column",gap:8}}>
{OUTCOMES.map(o => (
<OutcomeCard
key={o.id}
outcome={o}
expanded={expanded===o.id}
onToggle={()=>setExpanded(expanded===o.id?null:o.id)}
onViewFindings={()=>setFindingsOpen(o.id)}
/>
))}
</div>
</div>
</div>
{/* Port drawer */}
<div style={{borderTop:`1px solid ${c.border}`,background:c.bgPanel}}>
<div onClick={()=>setPortsOpen(!portsOpen)} style={{padding:"8px 22px",display:"flex",alignItems:"center",gap:8,cursor:"pointer",fontSize:10,fontFamily:sans,color:c.textSec,fontWeight:600,letterSpacing:0.5,textTransform:"uppercase"}}>
<span style={{fontSize:11}}>{portsOpen?"▾":"▸"}</span>
Ports
<span style={{color:c.textTer,textTransform:"none",letterSpacing:0,fontWeight:400,fontStyle:"italic",marginLeft:4}}>
6 outputs (5 per-outcome routes + 1 aggregate)
</span>
</div>
{portsOpen && <PortsDrawer />}
</div>
{/* Findings drawer */}
{findingsOpen && <FindingsDrawer outcomeId={findingsOpen} onClose={()=>setFindingsOpen(null)} />}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// PIPELINE INDICATOR
// ═══════════════════════════════════════════════════════════════
function PipelineIndicator({expanded, setExpanded}) {
const stages = [
{n:1, name:"Source Sufficiency", status:"verified"},
{n:2, name:"Research Complete", status:"verified"},
{n:3, name:"Outline Coverage", status:"verified"},
{n:4, name:"No Hallucination", status:"verified"},
{n:5, name:"Final Quality Gate", status:"in_progress", current:true},
];
return (
<div style={{borderBottom:`1px solid ${c.border}`,background:c.bg}}>
<div onClick={()=>setExpanded(!expanded)} style={{padding:"8px 22px",display:"flex",alignItems:"center",gap:14,cursor:"pointer"}}>
<span style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:1,textTransform:"uppercase"}}>Evaluator Pipeline</span>
<div style={{flex:1,display:"flex",alignItems:"center",gap:6,overflow:"hidden"}}>
{stages.map((s,i)=>(
<React.Fragment key={i}>
<div style={{
display:"flex",alignItems:"center",gap:5,padding:"3px 9px",
background:s.current?`${c.amber}15`:s.status==="verified"?`${c.green}10`:c.bgPanel,
border:`1px solid ${s.current?c.amber+"50":s.status==="verified"?c.green+"30":c.border}`,
borderRadius:11,
fontSize:10,fontWeight:s.current?700:500,
color:s.current?c.amber:s.status==="verified"?c.green:c.textSec,
}}>
<span style={{fontSize:8}}>{s.status==="verified"?"✓":s.current?"⟳":"○"}</span>
<span>⊜ {s.name}</span>
</div>
{i<stages.length-1 && <span style={{color:c.textTer,fontSize:9}}>→</span>}
</React.Fragment>
))}
</div>
<span style={{fontSize:9,color:c.textTer,fontStyle:"italic"}}>5 evaluators in chain · {expanded?"hide details ▴":"expand ▾"}</span>
</div>
{expanded && (
<div style={{padding:"4px 22px 12px",fontSize:10,color:c.textSec,lineHeight:1.6,fontStyle:"italic"}}>
This Evaluator is the final stage. Upstream stages each verified their concern; the work product reaches here only after they all passed (or were overridden by user). The final stage applies holistic checks and the meta-outcome ("output achieves the goal").
</div>
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// ADD OUTCOME FORM (three entry paths + inferred plan)
// ═══════════════════════════════════════════════════════════════
function AddOutcomeForm({onClose}) {
const [description, setDescription] = useState("");
const [showInferred, setShowInferred] = useState(false);
// Simulate auto-detection
const handleDescriptionChange = (e) => {
setDescription(e.target.value);
if (e.target.value.length > 20) setShowInferred(true);
};
return (
<div style={{margin:"14px 28px 0",padding:18,background:c.bgPanel,border:`1px solid ${c.accent}40`,borderRadius:8}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:12}}>
<div style={{fontSize:12,fontWeight:700,color:c.text}}>+ Add Outcome</div>
<span onClick={onClose} style={{fontSize:14,color:c.textSec,cursor:"pointer"}}>✕</span>
</div>
<FieldLabel>Describe what to check</FieldLabel>
<textarea
defaultValue="Make sure all citations resolve to real cases. The citation-checker should verify each citation against CourtListener; flag any that can't be found."
onChange={handleDescriptionChange}
placeholder="Plain language — what does success look like for this outcome?"
style={{width:"100%",background:c.bg,border:`1px solid ${c.border}`,borderRadius:4,padding:"9px 11px",fontSize:11,color:c.text,fontFamily:sans,resize:"vertical",minHeight:60,outline:"none",lineHeight:1.5}}
/>
<div style={{display:"flex",gap:8,alignItems:"center",marginTop:10,flexWrap:"wrap"}}>
<span style={{fontSize:10,color:c.textTer}}>OR start from:</span>
<Btn ghost sm>💬 Ask Task Agent</Btn>
<Btn ghost sm>📋 Load preset</Btn>
<Btn ghost sm><span style={{color:c.metaAccent}}>⊕</span> Meta — overall quality check</Btn>
</div>
{/* Inferred plan */}
{(showInferred || description.length === 0) && (
<div style={{marginTop:16,padding:14,background:c.bg,borderRadius:6,border:`1px solid ${c.accent}30`}}>
<div style={{display:"flex",alignItems:"center",gap:6,marginBottom:8}}>
<span style={{fontSize:11}}>🪄</span>
<span style={{fontSize:10,fontWeight:700,color:c.accent,letterSpacing:0.5,textTransform:"uppercase"}}>Inferred Plan</span>
<span style={{fontSize:9,color:c.textTer,fontStyle:"italic"}}>· you can adjust any of this</span>
</div>
<div style={{fontSize:11,color:c.text,lineHeight:1.7,marginBottom:12}}>
This looks like a <strong style={{color:c.accent}}>citation accuracy check</strong>. I'll use the <strong>citation-checker specialist agent</strong> to verify each citation in the brief against CourtListener.
<br/><br/>
<span style={{color:c.textSec}}>Result type:</span> <strong>Checklist</strong> (each citation gets verified individually, threshold 100%)
<br/>
<span style={{color:c.textSec}}>Reference materials needed:</span> CourtListener (already connected) · Case Record (already wired)
<br/>
<span style={{color:c.textSec}}>If it fails:</span> Route to <Select opts={["⊜ Citation Specialist (revise)","↻ Brief Drafter (revise)","👤 Human Review","+ Pick destination..."]} v="⊜ Citation Specialist (revise)" compact />
<br/>
<span style={{color:c.textSec}}>Retry:</span> up to 3 attempts · <span style={{color:c.textSec}}>Can it gather more info:</span> yes — allowed to search Westlaw if CourtListener fails (capped at $0.50)
</div>
<div style={{display:"flex",gap:8,justifyContent:"flex-end"}}>
<Btn ghost sm>Adjust details…</Btn>
<Btn ghost sm>💬 Refine with Task Agent</Btn>
<Btn primary sm onClick={onClose}>Add outcome →</Btn>
</div>
</div>
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// OUTCOME DATA
// ═══════════════════════════════════════════════════════════════
const OUTCOMES = [
{
id:"o0", isMeta:true,
name:"Output achieves the goal",
desc:"The final brief opposes the motion to dismiss by showing (1) standing exists, (2) the complaint states a claim under Rule 12(b)(6), (3) the heightened pleading standard is met, and (4) loss causation is adequately alleged.",
resultType:"score",
required:true,
score:0.82, threshold:0.85,
status:"failed",
routeOnFail:"👤 Human Review",
summary:"Holistic LLM judgment against the goal statement above. Reads the final draft, checks whether it accomplishes what the user asked for. Threshold 0.85.",
decomposed:true,
method:"agent_assess",
},
{
id:"o1", name:"Citations are present",
desc:"Brief contains at least the citations required to support the argument.",
resultType:"pass_fail", required:true,
status:"verified",
routeOnFail:"↻ Brief Drafter (revise)",
summary:"Auto-check using regex pattern matching for citation format. Fails if fewer than 5 citations found.",
method:"auto_check",
lastResultText:"19 citations found.",
},
{
id:"o2", name:"Brief addresses all motion issues",
desc:"The motion to dismiss raised 7 issues; the brief addresses each.",
resultType:"checklist", required:true,
status:"verified", count:{passed:7, total:7},
routeOnFail:"↻ Brief Drafter (with missing issues listed)",
summary:"Extract issues from Motion to Dismiss; verify each is addressed in the brief via agent assessment. Pass when all 7 addressed.",
method:"check_claims",
},
{
id:"o3", name:"Citations resolve to real cases",
desc:"All cases cited resolve to real cases in CourtListener. Each citation verified via citation-checker; cases not found within 3 lookup attempts return as failures with the case name and search attempts logged.",
resultType:"checklist", required:true,
status:"failed", count:{passed:16, total:19},
dependsOn:["Citations are present"],
routeOnFail:"⊜ Citation Specialist (revise + verify)",
summary:"Citation-checker specialist agent verifies each citation against CourtListener and Westlaw. Each citation is a checkpoint.",
method:"specialist",
referenceRequired:"required",
},
{
id:"o4", name:"No factual claims unsupported by record",
desc:"Every factual assertion in the brief is supported by an explicit reference to the record.",
resultType:"pass_fail", required:true,
status:"verified",
routeOnFail:"↻ Brief Drafter (revise unsupported facts)",
summary:"Extract FactualAssertion claims from upstream extractor; verify each against the Case Record. 100% pass rate required.",
method:"check_claims",
lastResultText:"34 of 34 factual claims supported.",
},
{
id:"o5", name:"Argument is persuasive",
desc:"The brief constructs a persuasive case, addressing counterarguments and supporting reasoning with cited authority.",
resultType:"score", required:false,
status:"verified", score:0.78, threshold:0.7,
routeOnFail:"↻ Brief Drafter (with rubric feedback)",
summary:"Agent assessment using 'My legal-arg rubric' (4 sub-criteria, weighted). Threshold 0.7 for desired pass.",
method:"agent_assess",
decomposed:true,
},
{
id:"o6", name:"Tone is professional",
desc:"Consistent with court filing standards.",
resultType:"score", required:false,
status:"verified", score:0.85, threshold:0.7,
routeOnFail:"↻ Brief Drafter (tone notes)",
summary:"Agent assessment with 1-5 anchored scale. Threshold 0.7 for desired pass.",
method:"agent_assess",
},
{
id:"o7", name:"Brief between 4000-8000 words",
desc:"Word count falls within the court's required range.",
resultType:"pass_fail", required:true,
status:"verified",
routeOnFail:"↻ Brief Drafter (trim or expand)",
summary:"Auto-check on word count. Min 4000, max 8000.",
method:"auto_check",
lastResultText:"6,247 words.",
},
];
// ═══════════════════════════════════════════════════════════════
// OUTCOME CARD
// ═══════════════════════════════════════════════════════════════
function OutcomeCard({outcome, expanded, onToggle, onViewFindings}) {
const isMeta = outcome.isMeta;
return (
<div style={{
background:c.bgCard,
border:`1px solid ${expanded ? (isMeta?c.metaAccent+"50":c.accent+"40") : isMeta?c.metaAccent+"30":c.border}`,
borderRadius:7,overflow:"hidden",transition:"border 0.15s",
...(isMeta && {boxShadow:`inset 3px 0 0 ${c.metaAccent}`})
}}>
{/* Header row */}
<div onClick={onToggle} style={{padding:"13px 16px",cursor:"pointer",display:"flex",alignItems:"center",gap:12}}>
<span style={{color:c.textTer,fontSize:11,minWidth:10}}>{expanded?"▾":"▸"}</span>
<div style={{flex:1,minWidth:0}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:4,flexWrap:"wrap"}}>
{isMeta && <span style={{fontSize:12,color:c.metaAccent}}>⊕</span>}
<span style={{fontSize:13,fontWeight:600,color:c.text}}>{outcome.name}</span>
<ResultTypePill type={isMeta?"meta":outcome.resultType} />
{outcome.required ? <Pill color={c.amber}>REQUIRED</Pill> : <Pill color={c.textTer}>OPTIONAL</Pill>}
{outcome.dependsOn && <Pill color={c.textSec}>↳ depends on {outcome.dependsOn.length}</Pill>}
{outcome.routeOnFail && (
<span style={{fontSize:9,color:c.textTer,fontFamily:sans,marginLeft:"auto"}}>
if fails → <span style={{color:c.textSec}}>{outcome.routeOnFail}</span>
</span>
)}
</div>
{!expanded && (
<div style={{fontSize:10,color:c.textSec,lineHeight:1.4,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
{outcome.desc}
</div>
)}
</div>
<div style={{display:"flex",alignItems:"center",gap:8}}>
<ResultIndicator outcome={outcome} />
{outcome.status === "failed" && (
<Btn ghost sm onClick={(e)=>{e.stopPropagation();onViewFindings();}}>View findings</Btn>
)}
</div>
</div>
{expanded && (
<OutcomeCardExpanded outcome={outcome} onViewFindings={onViewFindings} />
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// OUTCOME CARD — EXPANDED (summary view + Adjust panel)
// ═══════════════════════════════════════════════════════════════
function OutcomeCardExpanded({outcome, onViewFindings}) {
const [adjustOpen, setAdjustOpen] = useState(false);
const isMeta = outcome.isMeta;
return (
<div style={{borderTop:`1px solid ${c.border}`,padding:"4px 22px 22px",background:c.bg}}>
{/* What this checks */}
<SectionHeader>What this checks</SectionHeader>
<Textarea value={outcome.desc} h={isMeta?100:70} />
<HelperText>
{isMeta
? "This is the meta-outcome: a holistic check against the stated goal. The description above is the goal statement."
: "This description is the operative spec — what the verifier reads."}
</HelperText>
{/* Plain-English plan (summary mode default) */}
<SectionHeader>The plan</SectionHeader>
<div style={{padding:13,background:c.bgPanel,borderRadius:6,border:`1px solid ${c.border}`,fontSize:11,color:c.text,lineHeight:1.7}}>
{outcome.summary}
</div>
<div style={{marginTop:10,display:"flex",gap:8,alignItems:"center"}}>
<Btn ghost sm onClick={()=>setAdjustOpen(!adjustOpen)}>{adjustOpen ? "Hide details ▴" : "Adjust details ▾"}</Btn>
<Btn ghost sm>💬 Refine with Task Agent</Btn>
{isMeta && <span style={{fontSize:9,color:c.textTer,fontStyle:"italic",marginLeft:"auto"}}>Goal can be auto-filled from Task Blueprint when available.</span>}
</div>
{adjustOpen && <AdjustPanel outcome={outcome} />}
{/* Routing */}
<SectionHeader>If this fails</SectionHeader>
<div style={{padding:13,background:c.bgPanel,borderRadius:6,border:`1px solid ${c.border}`}}>
<div style={{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",fontSize:11,color:c.text,lineHeight:1.8}}>
Send to
<Select opts={[
outcome.routeOnFail,
"↻ Brief Drafter (revise)",
"⊜ Citation Specialist (re-verify)",
"📚 Research Module (gather more)",
"👤 Human Review (with findings)",
"↺ Loop back (default failed_out)",
"+ Custom destination..."
]} v={outcome.routeOnFail} compact />
<span style={{fontSize:10,color:c.textTer}}>· Custom port name:</span>
<Input value={outcome.id === "o3" ? "to_citation_specialist" : "auto"} w={180} />
</div>
<div style={{marginTop:10,padding:10,background:c.bg,borderRadius:5}}>
<Check label="Try again first (up to 3 retries)" checked={true} />
<Check label="Send findings as inline document comments" checked={outcome.id === "o5"} hint="Comments appear anchored to the relevant sections for human editing." />
<Check label="Send to human if still failing after retries" checked={true} />
<Check label="Can gather more info during verification" checked={outcome.id === "o3"} hint="Specialist may search Westlaw if CourtListener fails. Capped at $0.50 and 3 attempts." />
</div>
</div>
{/* Last result */}
{outcome.status && outcome.status !== "pending" && (
<>
<SectionHeader>Last result (iteration 2)</SectionHeader>
<ResultSummary outcome={outcome} onViewFindings={onViewFindings} />
</>
)}
</div>
);
}
// ─── Adjust panel (the form-fill power view) ───
function AdjustPanel({outcome}) {
return (
<div style={{marginTop:12,padding:14,background:c.bgPanel,borderRadius:6,border:`1px solid ${c.border}`}}>
<div style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:1,textTransform:"uppercase",marginBottom:10}}>Adjust Details</div>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16}}>
<div>
<FieldLabel>Check by</FieldLabel>
<Select opts={[
"Auto-check (mechanical)",
"Check claims (from extractor)",
"Specialist agent (citation-checker, etc.)",
"Agent assessment (LLM judge with rubric)",
"Multiple checks",
]} v={
outcome.method==="auto_check" ? "Auto-check (mechanical)" :
outcome.method==="check_claims" ? "Check claims (from extractor)" :
outcome.method==="specialist" ? "Specialist agent (citation-checker, etc.)" :
"Agent assessment (LLM judge with rubric)"
} compact />
<HelperText>Auto-detected from description above. Override if needed.</HelperText>
<FieldLabel mt>Specialist agent</FieldLabel>
<Select opts={["citation-checker","fact-checker","legal-researcher","custom…"]} v="citation-checker" compact />
<FieldLabel mt>Reference materials needed</FieldLabel>
<Select opts={["Required for verification","Used if available","Audit only","Not needed"]} v={outcome.referenceRequired === "required" ? "Required for verification" : "Used if available"} compact />
<FieldLabel mt>Iteration policy</FieldLabel>
<Select opts={["Re-verify always","Re-verify if subject changed","Re-verify only if previously failed","Verify once"]} v="Re-verify if subject changed" compact />
</div>
<div>
<FieldLabel>Pass threshold</FieldLabel>
{outcome.resultType === "pass_fail" && <div style={{fontSize:11,color:c.text,padding:"7px 9px",background:c.bg,borderRadius:4,border:`1px solid ${c.border}`}}>Passes when check returns true</div>}
{outcome.resultType === "checklist" && <Select opts={["all items","≥ 95% of items","at least N items"]} v="all items" compact />}
{outcome.resultType === "score" && <Input value={outcome.threshold?.toString() || "0.7"} />}
<FieldLabel mt>Cost cap (this outcome)</FieldLabel>
<Input value="$0.50" />
<FieldLabel mt>Decomposition</FieldLabel>
<div style={{padding:10,background:c.bg,borderRadius:5,border:`1px solid ${c.border}`}}>
<Check label="Break into sub-criteria (rubric)" checked={!!outcome.decomposed} />
{outcome.decomposed && <Btn ghost sm>⚡ Help me build sub-criteria</Btn>}
</div>
<FieldLabel mt>Status</FieldLabel>
<div style={{display:"flex",gap:14}}>
<Check label="Required" checked={outcome.required} />
<Check label="Inverse (must NOT be true)" checked={false} />
</div>
</div>
</div>
</div>
);
}
// ─── Result indicator (compact, card header) ───
function ResultIndicator({outcome}) {
const isMeta = outcome.isMeta;
if (!outcome.status || outcome.status === "pending") return <StatusBadge status="pending" />;
if (outcome.resultType === "pass_fail") {
return <StatusBadge status={outcome.status} />;
}
if (outcome.resultType === "checklist") {
return (
<div style={{display:"flex",alignItems:"center",gap:8}}>
<span style={{fontSize:10,color:outcome.count.passed===outcome.count.total?c.green:c.red,fontFamily:mono,fontWeight:700}}>
{outcome.count.passed} / {outcome.count.total}
</span>
<StatusBadge status={outcome.status} />
</div>
);
}
if (outcome.resultType === "score") {
const passed = outcome.score >= outcome.threshold;
return (
<div style={{display:"flex",alignItems:"center",gap:8}}>
<div style={{position:"relative",width:70,height:6,background:c.bg,borderRadius:3,overflow:"hidden"}}>
<div style={{width:`${outcome.score*100}%`,height:"100%",background:passed?c.green:c.red,borderRadius:3}} />
<div style={{position:"absolute",left:`${outcome.threshold*100}%`,top:-2,width:1,height:10,background:c.textTer}} />
</div>
<span style={{fontSize:10,color:passed?c.green:c.red,fontFamily:mono,fontWeight:700}}>{outcome.score.toFixed(2)}</span>
<StatusBadge status={passed?"verified":"failed"} />
</div>
);
}
}
// ─── Result summary (in expanded card body) ───
function ResultSummary({outcome, onViewFindings}) {
const isFail = outcome.status === "failed";
if (!isFail) {
return (
<div style={{padding:12,background:`${c.green}08`,borderRadius:6,border:`1px solid ${c.green}25`}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
<StatusBadge status="verified" />
{outcome.resultType==="score" && (
<span style={{fontSize:10,color:c.green,fontFamily:mono}}>score {outcome.score.toFixed(2)} / threshold {outcome.threshold.toFixed(2)}</span>
)}
{outcome.resultType==="checklist" && (
<span style={{fontSize:10,color:c.green,fontFamily:mono}}>{outcome.count.passed} of {outcome.count.total}</span>
)}
</div>
<div style={{fontSize:11,color:c.text}}>{outcome.lastResultText || "Passed."}</div>
</div>
);
}
// FAIL — show routing status as default, with "View findings" affordance
return (
<div style={{padding:12,background:`${c.amber}08`,borderRadius:6,border:`1px solid ${c.amber}30`}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:8}}>
<div style={{display:"flex",alignItems:"center",gap:8}}>
<Pill color={c.amber}>↻ SENT FOR REVISION</Pill>
{outcome.resultType==="checklist" && <span style={{fontSize:10,color:c.red,fontFamily:mono}}>{outcome.count.passed} of {outcome.count.total}</span>}
{outcome.resultType==="score" && <span style={{fontSize:10,color:c.red,fontFamily:mono}}>score {outcome.score.toFixed(2)} / threshold {outcome.threshold.toFixed(2)}</span>}
</div>
<span style={{fontSize:10,color:c.textSec,fontFamily:mono}}>iteration 2 of 3</span>
</div>
<div style={{fontSize:11,color:c.text,lineHeight:1.6,marginBottom:10}}>
Work routed to <strong style={{color:c.accent}}>{outcome.routeOnFail}</strong> with structured feedback. Revising now.
</div>
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
<Btn ghost sm onClick={onViewFindings}>📋 View findings (3)</Btn>
<Btn ghost sm>💬 Send comments to drafter</Btn>
<Btn ghost sm>👤 Override — send to human</Btn>
<Btn ghost sm>↻ Re-evaluate now</Btn>
</div>
<div style={{fontSize:9,color:c.textTer,marginTop:10,fontStyle:"italic",lineHeight:1.5}}>
Findings are written to the audit trail and available here on demand. The system has routed the work to the destination above; you don't need to address the findings unless you want to override or refine.
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// FINDINGS DRAWER
// ═══════════════════════════════════════════════════════════════
function FindingsDrawer({outcomeId, onClose}) {
const findings = outcomeId === "o3" ? [
{
id:"f1", severity:"high", state:"proposed",
title:"Citation #5 — Smith v. Jones, 432 F.3d 12 (5th Cir. 2007)",
detail:"Case not found in CourtListener after 3 lookup attempts. Tried alternate reporters (F.2d, F. Supp.). Westlaw also returned no match.",
authority:["tool_verified", "model_judgment_only"],
location:"§III ¶4, line 12",
},
{
id:"f2", severity:"high", state:"proposed",
title:"Citation #8 — In re Acme Corp., 215 B.R. 89 (Bankr. S.D.N.Y. 1997)",
detail:"Citation format valid but no matching case in CourtListener. Possible alternative: In re Acme Corp., 215 B.R. 89 (Bankr. D.N.J. 1997) — different jurisdiction.",
authority:["tool_verified"],
location:"§IV ¶2, line 7",
},
{
id:"f3", severity:"medium", state:"proposed",
title:"Citation #15 — Doe v. Smith (incomplete)",
detail:"Citation incomplete — missing reporter, court, and year. Cannot verify.",
authority:["deterministic_check"],
location:"§V ¶1, line 3",
},
] : [];
return (
<div onClick={onClose} style={{position:"absolute",top:0,right:0,bottom:0,left:0,background:"rgba(0,0,0,0.4)",zIndex:50,display:"flex",justifyContent:"flex-end"}}>
<div onClick={(e)=>e.stopPropagation()} style={{width:560,maxWidth:"90%",height:"100%",background:c.bgPanel,borderLeft:`1px solid ${c.border}`,display:"flex",flexDirection:"column",boxShadow:"-10px 0 30px #0008"}}>
{/* Drawer header */}
<div style={{padding:"14px 18px",borderBottom:`1px solid ${c.border}`,display:"flex",alignItems:"center",justifyContent:"space-between"}}>
<div>
<div style={{fontSize:13,fontWeight:700,color:c.text}}>Findings · Citations resolve to real cases</div>
<div style={{fontSize:10,color:c.textSec,marginTop:2,fontFamily:sans}}>3 findings · all proposed · iteration 2</div>
</div>
<span onClick={onClose} style={{fontSize:18,color:c.textSec,cursor:"pointer",padding:"0 6px"}}>✕</span>
</div>
{/* Drawer body */}
<div style={{flex:1,overflowY:"auto",padding:"14px 18px"}}>
<div style={{padding:12,background:c.bg,borderRadius:6,border:`1px solid ${c.border}`,marginBottom:14,fontSize:11,color:c.text,lineHeight:1.5}}>
<div style={{fontSize:10,fontWeight:600,color:c.textTer,letterSpacing:0.5,marginBottom:4}}>WHAT HAPPENED</div>
The system attempted to verify 19 citations. 16 verified successfully. 3 could not be verified after 3 attempts each using CourtListener; alternate reporters and Westlaw lookups also failed.
<br/><br/>
<div style={{fontSize:10,fontWeight:600,color:c.textTer,letterSpacing:0.5,marginBottom:4}}>WHAT'S BEING DONE</div>
<span style={{color:c.amber}}>↻ The work has been routed to Citation Specialist with the structured feedback below.</span> The specialist will attempt re-verification and either correct the citations or remove them. If still failing after retry, this finding escalates to you.
</div>
<div style={{fontSize:10,fontWeight:700,color:c.textTer,letterSpacing:1,textTransform:"uppercase",marginBottom:10}}>Findings ({findings.length})</div>
<div style={{display:"flex",flexDirection:"column",gap:10}}>
{findings.map((f, i) => (
<FindingCard key={f.id} finding={f} />
))}
</div>
<div style={{marginTop:18,padding:12,background:c.bgCard,borderRadius:6,border:`1px solid ${c.border}`}}>
<div style={{fontSize:10,fontWeight:600,color:c.textTer,letterSpacing:0.5,marginBottom:8,textTransform:"uppercase"}}>Delivery options</div>
<Check label="Send findings as inline document comments (anchored to brief sections)" checked={false} hint="Useful for human-edit workflows. Comments appear in the Brief draft at the cited locations." />
<Check label="Post findings to Task Forum / Board" checked={true} />
<Check label="Save full audit bundle to Run Inspector" checked={true} hint="Always on. Findings are part of the permanent audit trail regardless of UI display." />
</div>
</div>
{/* Drawer footer — actions */}
<div style={{padding:"12px 18px",borderTop:`1px solid ${c.border}`,display:"flex",gap:8,flexWrap:"wrap",background:c.bg}}>
<Btn ghost sm>📎 Open evidence</Btn>
<Btn ghost sm>✓ Accept guidance</Btn>
<Btn ghost sm>✕ Mark wrong</Btn>
<Btn ghost sm>⤓ Supersede</Btn>
<Btn ghost sm>👤 Send to human</Btn>
<Btn ghost sm>💬 Ask Task Agent</Btn>
<Btn accent sm>↻ Fork from here</Btn>
</div>
</div>
</div>
);
}
function FindingCard({finding}) {
const sevColor = finding.severity === "high" ? c.red : finding.severity === "medium" ? c.amber : c.textSec;
return (
<div style={{padding:12,background:c.bgCard,border:`1px solid ${c.border}`,borderRadius:6,borderLeft:`3px solid ${sevColor}`}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6,flexWrap:"wrap"}}>
<Pill color={sevColor}>{finding.severity}</Pill>
<Pill color={c.textSec}>{finding.state}</Pill>
{finding.authority.map((a,i) => <Tag key={i} color={c.textTer}>{a}</Tag>)}
<span style={{fontSize:9,color:c.textTer,fontFamily:mono,marginLeft:"auto"}}>{finding.location}</span>
</div>
<div style={{fontSize:11,color:c.text,fontWeight:600,marginBottom:4}}>{finding.title}</div>
<div style={{fontSize:10,color:c.textSec,lineHeight:1.5}}>{finding.detail}</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// PORT DRAWER
// ═══════════════════════════════════════════════════════════════
function PortsDrawer() {
const inputs = [
{label:"subject_in", role:"required · data", color:c.accent, wired:"◆ Final Brief Draft"},
{label:"reference_in", role:"optional · data", color:c.accent, wired:null},
{label:"evidence_in.N", role:"optional · expandable", color:c.accent, wired:"◇ Case Record + Motion"},
{label:"claims_in", role:"optional · data", color:c.accent, wired:"◈ Brief Claims"},
{label:"human_response_in", role:"optional · data", color:c.amber, wired:null, note:"when human review used"},
];
const outputs = [
{label:"verdict_out", role:"always · data", color:c.cyan, wired:null},
{label:"passed_out", role:"signal+data", color:c.green, wired:"↺ Loop.stop_in"},
{label:"any_failed_out", role:"aggregate fallback", color:c.red, wired:null, note:"fires when any outcome fails (convenience)"},
{label:"meta_failed_out", role:"per-outcome route", color:c.metaAccent, wired:"👤 Human Review"},
{label:"citations_present_failed_out", role:"per-outcome route", color:c.red, wired:"↻ Brief Drafter"},
{label:"motion_issues_failed_out", role:"per-outcome route", color:c.red, wired:"↻ Brief Drafter"},
{label:"to_citation_specialist", role:"per-outcome route · custom name", color:c.red, wired:"⊜ Citation Specialist"},
{label:"factual_claims_failed_out", role:"per-outcome route", color:c.red, wired:"↻ Brief Drafter"},
{label:"persuasive_failed_out", role:"per-outcome route", color:c.red, wired:"↻ Brief Drafter"},
{label:"tone_failed_out", role:"per-outcome route", color:c.red, wired:"↻ Brief Drafter"},
{label:"word_count_failed_out", role:"per-outcome route", color:c.red, wired:"↻ Brief Drafter"},
{label:"feedback_out", role:"data", color:c.purple, wired:"↺ Loop.feedback_in"},
{label:"findings_out", role:"data · structured", color:c.purple, wired:null, note:"to forum / comments"},
{label:"signal_out", role:"always · signal", color:c.amber, wired:null},
{label:"error_out", role:"signal+data", color:c.red, wired:null},
];
return (
<div style={{padding:"6px 22px 16px",borderTop:`1px solid ${c.border}`,background:c.bg,display:"flex",gap:24,maxHeight:280,overflowY:"auto"}}>
<div style={{flex:1}}>
<div style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:0.5,marginBottom:8}}>INPUTS</div>
{inputs.map((p,i)=><PortRow key={i} port={p} />)}
</div>
<div style={{flex:1.6}}>
<div style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:0.5,marginBottom:8}}>OUTPUTS — per-outcome routes are auto-named from outcomes; custom names override</div>
{outputs.map((p,i)=><PortRow key={i} port={p} />)}
</div>
</div>
);
}
function PortRow({port}) {
return (
<div style={{display:"flex",alignItems:"center",gap:8,padding:"4px 7px",borderRadius:3,marginBottom:2,fontSize:10}}>
<span style={{width:6,height:6,borderRadius:"50%",background:port.color,flexShrink:0}} />
<span style={{color:port.color,fontFamily:mono,minWidth:200,fontWeight:600,fontSize:10}}>{port.label}</span>
<span style={{color:c.textTer,fontFamily:mono,fontSize:9,minWidth:160}}>{port.role}</span>
{port.wired ? (
<span style={{color:c.text,fontSize:10}}>→ {port.wired}</span>
) : (
<span style={{color:c.textTer,fontSize:9,fontStyle:"italic"}}>{port.note || "unwired"}</span>
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// EXTRACTOR VIEW (lightly updated for V3 consistency)
// ═══════════════════════════════════════════════════════════════
function ExtractorView() {
const [portsOpen, setPortsOpen] = useState(false);
const [expandedType, setExpandedType] = useState("ct1");
return (
<div style={{flex:1,display:"flex",flexDirection:"column",overflow:"hidden"}}>
<div style={{height:52,minHeight:52,borderBottom:`1px solid ${c.border}`,display:"flex",alignItems:"center",justifyContent:"space-between",padding:"0 22px",background:c.bgPanel}}>
<div style={{display:"flex",alignItems:"center",gap:12}}>
<span style={{fontSize:20}}>◈</span>
<div>
<div style={{fontSize:13,fontWeight:700,color:c.text}}>Claim Extractor</div>
<div style={{fontSize:10,color:c.textTer,fontFamily:mono,marginTop:2}}>step.claim_extractor · Brief Claims</div>
</div>
<span style={{marginLeft:14}}><Pill color={c.green}>50 claims extracted</Pill></span>
<span><Pill color={c.cyan}>cache hit</Pill></span>
</div>
<div style={{display:"flex",gap:8,alignItems:"center"}}>
<Btn ghost>Preset…</Btn>
<Btn primary>▶ Run</Btn>
<span style={{fontSize:18,color:c.textSec,cursor:"pointer",padding:"0 4px",marginLeft:4}}>✕</span>
</div>
</div>
<div style={{flex:1,display:"flex",overflow:"hidden"}}>
<div style={{width:300,minWidth:300,borderRight:`1px solid ${c.border}`,padding:"18px 16px",overflowY:"auto",background:c.bgPanel}}>
<SectionLabel>Connected Inputs</SectionLabel>
<div style={{padding:"5px 7px",background:`${c.accent}10`,borderRadius:3,border:`1px solid ${c.accent}25`,fontSize:10}}>
<span style={{color:c.accent,fontFamily:mono}}>data_in</span> <span style={{color:c.textSec}}>← ◆ Final Brief Draft</span>
</div>
<SectionLabel mt>Downstream Consumers</SectionLabel>
<div style={{padding:"5px 7px",background:`${c.green}10`,borderRadius:3,border:`1px solid ${c.green}25`,fontSize:10}}>
<span style={{color:c.green,fontFamily:mono}}>claims_out</span> <span style={{color:c.textSec}}>→ ⊜ Final Quality Gate</span>
</div>
<SectionLabel mt>Extraction Agent</SectionLabel>
<Select opts={["(task default)","Elnor","Nova","claude-haiku-4-5 (cheap)"]} v="claude-haiku-4-5 (cheap)" compact />
<HelperText>Cheap models work for parsing. Authority-required types auto-upgrade.</HelperText>
<SectionLabel mt>Defaults</SectionLabel>
<FieldLabel>Min confidence</FieldLabel>
<Input value="0.7" />
<FieldLabel mt>Max claims (cap)</FieldLabel>
<Input value="50" />
<FieldLabel mt>Cost cap per run</FieldLabel>
<Input value="$1.00" />
<SectionLabel mt>Caching</SectionLabel>
<Check label="Cache results" checked={true} />
<SectionLabel mt>Custom Guidance</SectionLabel>
<Textarea value="Prefer Bluebook format. Skip footnotes that only cross-reference (e.g., 'See supra note 3'). For factual assertions, exclude background; extract only claims the brief asserts as true." h={70} />
</div>
<div style={{flex:1,overflowY:"auto",background:c.bg}}>
<div style={{padding:"18px 28px 0"}}>
<div style={{padding:14,background:c.bgCard,borderRadius:8,border:`1px solid ${c.border}`}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:6}}>
<span style={{fontSize:12,fontWeight:700,color:c.text}}>Last Run — 50 claims extracted (cached)</span>
<span style={{fontSize:10,color:c.textSec,fontFamily:mono}}>$0.00 (cached) · was $0.12 fresh</span>
</div>
<div style={{display:"grid",gridTemplateColumns:"repeat(5, 1fr)",gap:8,marginTop:10}}>
{[
{name:"CaseCitation",count:19,color:c.accent},
{name:"FactualAssertion",count:18,color:c.green},
{name:"LegalProposition",count:7,color:c.purple},
{name:"OpposingArgument",count:4,color:c.amber},
{name:"ProceduralFact",count:2,color:c.cyan},
].map((t,i)=>(
<div key={i} style={{padding:10,background:c.bg,borderRadius:5,border:`1px solid ${t.color}25`}}>
<div style={{fontSize:8,color:t.color,fontWeight:600,letterSpacing:0.5,marginBottom:2}}>{t.name.toUpperCase()}</div>
<div style={{fontSize:18,fontWeight:700,color:t.color,fontFamily:mono}}>{t.count}</div>
</div>
))}
</div>
</div>
</div>
<div style={{padding:"22px 28px 0",display:"flex",alignItems:"center",justifyContent:"space-between"}}>
<div>
<div style={{fontSize:15,fontWeight:700,color:c.text}}>Claim Types</div>
<div style={{fontSize:11,color:c.textSec,marginTop:3}}>What to extract. Each type defines its instruction, fields, and downstream use.</div>
</div>
<Btn accent sm>+ Add Claim Type</Btn>
</div>
<div style={{padding:"14px 28px 28px",display:"flex",flexDirection:"column",gap:8}}>
{[
{id:"ct1", name:"CaseCitation", desc:"Citations to legal cases — full citation, case name, holding as characterized.", count:19, evaluable:true, authority:true, consumedBy:["Citations resolve to real cases","Cited holdings characterized accurately"], expanded:expandedType==="ct1"},
{id:"ct2", name:"FactualAssertion", desc:"Factual claims about the matter — dates, amounts, events, parties.", count:18, evaluable:true, authority:false, consumedBy:["No factual claims unsupported"], expanded:expandedType==="ct2"},
{id:"ct3", name:"LegalProposition", desc:"Statements of legal rules or principles.", count:7, evaluable:true, authority:false, consumedBy:[], expanded:expandedType==="ct3"},
{id:"ct4", name:"OpposingArgument", desc:"References to opposing counsel's arguments.", count:4, evaluable:true, authority:false, consumedBy:[], expanded:expandedType==="ct4"},
{id:"ct5", name:"ProceduralFact", desc:"Statements about case procedural posture.", count:2, evaluable:false, authority:false, consumedBy:[], expanded:expandedType==="ct5"},
].map(t => (
<div key={t.id} style={{background:c.bgCard,border:`1px solid ${t.expanded?c.accent+"40":c.border}`,borderRadius:7,overflow:"hidden"}}>
<div onClick={()=>setExpandedType(t.expanded?null:t.id)} style={{padding:"13px 16px",cursor:"pointer",display:"flex",alignItems:"center",gap:12}}>
<span style={{color:c.textTer,fontSize:11,minWidth:10}}>{t.expanded?"▾":"▸"}</span>
<div style={{flex:1,minWidth:0}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:4}}>
<span style={{fontSize:13,fontWeight:600,color:c.text,fontFamily:mono}}>{t.name}</span>
{t.evaluable ? <Pill color={c.green}>EVALUABLE</Pill> : <Pill color={c.textTer}>NOT EVALUABLE</Pill>}
{t.authority && <Pill color={c.amber}>NEEDS SPECIALIST</Pill>}
{t.consumedBy.length>0 && <Pill color={c.cyan}>USED BY {t.consumedBy.length}</Pill>}
</div>
{!t.expanded && <div style={{fontSize:10,color:c.textSec,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{t.desc}</div>}
</div>
<span style={{fontSize:14,fontWeight:700,color:t.count>0?c.text:c.textTer,fontFamily:mono}}>{t.count}</span>
</div>
{t.expanded && (
<div style={{borderTop:`1px solid ${c.border}`,padding:"6px 22px 22px",background:c.bg}}>
<SectionHeader>What this extracts</SectionHeader>
<FieldLabel>Description</FieldLabel>
<Textarea value={t.desc} h={50} />
<FieldLabel mt>Extraction instruction</FieldLabel>
<Textarea value="Extract every citation to a legal case. Include the full citation, the case name, the holding as characterized by the brief, and the source span (paragraph + line)." h={60} />
{t.consumedBy.length>0 && (
<>
<SectionHeader>Used by these outcomes</SectionHeader>
<div style={{padding:11,background:`${c.cyan}08`,borderRadius:5,border:`1px solid ${c.cyan}25`}}>
{t.consumedBy.map((o,i)=>(<div key={i} style={{fontSize:11,color:c.text,padding:"3px 0",cursor:"pointer"}}>↗ <span style={{color:c.cyan}}>{o}</span></div>))}
</div>
</>
)}
</div>
)}
</div>
))}
</div>
</div>
</div>
<div style={{borderTop:`1px solid ${c.border}`,background:c.bgPanel}}>
<div onClick={()=>setPortsOpen(!portsOpen)} style={{padding:"8px 22px",display:"flex",alignItems:"center",gap:8,cursor:"pointer",fontSize:10,fontFamily:sans,color:c.textSec,fontWeight:600,letterSpacing:0.5,textTransform:"uppercase"}}>
<span style={{fontSize:11}}>{portsOpen?"▾":"▸"}</span> Ports
<span style={{color:c.textTer,textTransform:"none",letterSpacing:0,fontWeight:400,fontStyle:"italic",marginLeft:4}}>1 input · 4 outputs</span>
</div>
{portsOpen && (
<div style={{padding:"6px 22px 16px",borderTop:`1px solid ${c.border}`,background:c.bg,display:"flex",gap:24}}>
<div style={{flex:1}}>
<div style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:0.5,marginBottom:8}}>INPUTS</div>
<PortRow port={{label:"data_in",role:"required · subject",color:c.accent,wired:"◆ Final Brief Draft"}} />
</div>
<div style={{flex:1.4}}>
<div style={{fontSize:9,fontWeight:700,color:c.textTer,letterSpacing:0.5,marginBottom:8}}>OUTPUTS</div>
<PortRow port={{label:"claims_out",role:"always · data",color:c.green,wired:"⊜ Final Quality Gate"}} />
<PortRow port={{label:"metrics_out",role:"optional",color:c.cyan,wired:null,note:"unwired"}} />
<PortRow port={{label:"signal_out",role:"always",color:c.amber,wired:null,note:"unwired"}} />
<PortRow port={{label:"error_out",role:"signal+data",color:c.red,wired:null,note:"unwired"}} />
</div>
</div>
)}
</div>
</div>
);
}