/** * Sample artifact builders — emitted by the demo engine at the end of a plan * execution (plus on-demand for phylo/MSA/structure prompts). * * Two shapes: * 1) Synthetic JSON/CSV artifacts embedded as `data:` URLs so downloads work * fully client-side without a backend round-trip. This mirrors the existing * test fixture at `.data/tasks/sess_b2c176cf8e6b.json`. * 2) Public sample files (`/sample-data/*.{nwk,fasta,pdb}`) referenced by URL. * Agent A owns those viewers; we just point at them with realistic sizes. */ import type { Artifact, PlanProposal, TraceEntry } from "./types"; // ---- utilities ------------------------------------------------------------- function encodeBase64(s: string): string { if (typeof window === "undefined") { return Buffer.from(s, "utf8").toString("base64"); } return btoa(unescape(encodeURIComponent(s))); } function dataUrl(mime: string, content: string): string { return `data:${mime};base64,${encodeBase64(content)}`; } function newId(): string { return "art-" + Math.random().toString(36).slice(2, 10); } function utf8Bytes(s: string): number { if (typeof TextEncoder !== "undefined") { return new TextEncoder().encode(s).length; } return Buffer.byteLength(s, "utf8"); } // ---- synthetic artifact builders ------------------------------------------ export interface DesignBriefInput { target: string; indication?: string; mechanism?: string; format?: string; epitope?: string; affinity?: string; species?: string; developability?: string; route?: string; competition?: string; seed?: string; scale?: string; } export function buildDesignBriefJson(brief: DesignBriefInput): string { const payload = { schema: "proteinea.design_brief/v1", generated_at: new Date().toISOString(), target: brief.target, indication: brief.indication || null, mechanism: brief.mechanism || null, format: brief.format || "VHH", epitope: brief.epitope || "no preference", affinity_goal: brief.affinity || null, species_cross_reactivity: brief.species || null, developability: brief.developability || null, route_of_administration: brief.route || null, competitive_landscape: brief.competition || null, starting_point: brief.seed || "de novo", design_count: parseInt(brief.scale || "50", 10) || 50, }; return JSON.stringify(payload, null, 2); } export function buildPipelinePlanJson(plan: PlanProposal): string { const payload = { schema: "proteinea.pipeline_plan/v1", generated_at: new Date().toISOString(), title: plan.title, steps: plan.steps.map((s, i) => ({ index: i + 1, label: s.label, tools: s.tools, })), }; return JSON.stringify(payload, null, 2); } export function buildExecutionTraceJson(trace: TraceEntry[]): string { const payload = { schema: "proteinea.execution_trace/v1", generated_at: new Date().toISOString(), event_count: trace.length, events: trace, }; return JSON.stringify(payload, null, 2); } export function buildTopDesignsCsv(n = 10): string { const aa = "ACDEFGHIKLMNPQRSTVWY"; const rows: string[] = [ "rank,design_id,cdr_h3,length,iPAE,pLDDT,affinity_nM,consensus_score", ]; for (let i = 0; i < n; i++) { const id = `d${String(i + 1).padStart(3, "0")}`; const len = 14 + Math.floor(Math.random() * 8); let cdr = ""; for (let j = 0; j < len; j++) cdr += aa[Math.floor(Math.random() * aa.length)]; const iPAE = (5 + Math.random() * 5).toFixed(2); const pLDDT = (82 + Math.random() * 12).toFixed(1); const kd = (0.3 + Math.random() * 40).toFixed(2); const score = (0.55 + Math.random() * 0.4).toFixed(3); rows.push(`${i + 1},${id},${cdr},${len},${iPAE},${pLDDT},${kd},${score}`); } return rows.join("\n"); } // ---- artifact constructors ------------------------------------------------- export function makeJsonArtifact(name: string, content: string): Artifact { return { id: newId(), name, bytes: utf8Bytes(content), mime: "application/json", url: dataUrl("application/json", content), createdAt: Date.now(), }; } export function makeCsvArtifact(name: string, content: string): Artifact { return { id: newId(), name, bytes: utf8Bytes(content), mime: "text/csv", url: dataUrl("text/csv", content), createdAt: Date.now(), }; } // ---- public sample-data artifacts (Agent A owns the files) ----------------- export function makePhyloTreeArtifact(): Artifact { return { id: newId(), name: "spike.nwk", bytes: 4096, mime: "text/x-nh", url: "/sample-data/spike.nwk", createdAt: Date.now(), }; } export function makeMsaArtifact(): Artifact { return { id: newId(), name: "igg-fc.fasta", bytes: 8192, mime: "text/x-fasta", url: "/sample-data/igg-fc.fasta", createdAt: Date.now(), }; } export function makeStructureArtifact(): Artifact { return { id: newId(), name: "her2-1n8z-mini.pdb", bytes: 24576, mime: "chemical/x-pdb", url: "/sample-data/her2-1n8z-mini.pdb", createdAt: Date.now(), }; } /** * Classify a raw user prompt for which sample-data viewer(s) to surface. * Keyword-based; matches are non-exclusive. */ export function detectSampleDataHints(msg: string): { phylo: boolean; msa: boolean; structure: boolean; } { const m = msg.toLowerCase(); return { phylo: m.includes("phylogen") || m.includes("tree") || m.includes(".nwk") || m.includes("newick") || m.includes("clade"), msa: m.includes("msa") || m.includes("alignment") || m.includes("multiple sequence") || m.includes("fasta") || m.includes("conservation"), structure: m.includes("structure") || m.includes("pdb") || m.includes("crystal") || m.includes("cryo-em") || m.includes("cryoem") || m.includes("3d structure"), }; } // ---- KB topic catalog ------------------------------------------------------ export interface KbTopic { name: string; ms: number; } /** * Deterministic-ish KB topics we "load" before planning. Timings are realistic * (60-300ms) and slightly randomized so no two plan previews look identical. */ export function buildKbTopics(): KbTopic[] { const jitter = (base: number, spread: number) => Math.round(base + (Math.random() - 0.5) * spread); return [ { name: "antibody design best practices", ms: jitter(150, 60) }, { name: "benchmark catalog", ms: jitter(80, 40) }, { name: "target intelligence", ms: jitter(120, 60) }, { name: "developability heuristics", ms: jitter(95, 40) }, ]; }