/** * Infer a short, pragmatic task title from the first user message. * * Biomni renames tasks within a few seconds of the first exchange — "Trastuzumab * HER2+ Breast Cancer Trials", "TCGA-BRCA RNA-seq Differential Expression", etc. * This is a small, heuristic version that runs instantly on the client: grab the * longest run of content words and compress them. */ const STOPWORDS = new Set([ "a","an","the","and","or","but","of","for","in","on","at","to","with","by","from", "is","are","was","were","be","being","been","have","has","had","do","does","did", "can","could","should","would","will","may","might","must","shall", "i","we","you","they","it","this","that","these","those","my","our","your","their", "please","kindly","help","me","us","need","want","like","get","give","tell","show", "using","based","above","below","about","into","than","then","as","so","also", "trial","run","pipeline","task","question","query", ]); /** Split to words, drop stopwords, keep scientific tokens (all-caps, digits, hyphens). */ function tokenize(text: string): string[] { return text .replace(/[\r\n]+/g, " ") .split(/[^A-Za-z0-9+\-./]+/) .filter(Boolean); } export function inferTaskTitle(firstUserMessage: string, maxChars = 48): string { if (!firstUserMessage) return "Untitled Session"; // Strip "TRIAL N (x):" style prefixes const stripped = firstUserMessage.replace(/^\s*TRIAL\s+\d+\s*\([^)]*\)\s*:\s*/i, "").trim(); const tokens = tokenize(stripped); const kept: string[] = []; for (const t of tokens) { const lower = t.toLowerCase(); if (STOPWORDS.has(lower)) continue; // Keep acronyms as-is; otherwise Title Case words const preserved = /^[A-Z0-9][A-Z0-9+./\-]+$/.test(t) ? t : t.charAt(0).toUpperCase() + t.slice(1).toLowerCase(); kept.push(preserved); if (kept.join(" ").length >= maxChars) break; } if (kept.length === 0) { // Fallback: first 6 raw words const fallback = stripped.split(/\s+/).slice(0, 6).join(" "); return truncate(fallback || "Untitled Session", maxChars); } return truncate(kept.join(" "), maxChars); } function truncate(s: string, max: number): string { return s.length <= max ? s : s.slice(0, max - 1).trimEnd() + "…"; }