File size: 2,268 Bytes
30cc31a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * 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() + "…";
}