File size: 3,004 Bytes
a32aee9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

/** Merge Tailwind class names with conflict resolution. */
export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}

/** Stable colour for a research-topic badge (mirrors app/topic_classifier.py). */
const TOPIC_COLOURS: Record<string, string> = {
  "Machine Learning": "#6366f1",
  "Deep Learning": "#8b5cf6",
  "NLP / Large Language Models": "#06b6d4",
  "Computer Vision": "#10b981",
  "RAG / Information Retrieval": "#f59e0b",
  "Reinforcement Learning": "#ef4444",
  "Bioinformatics / Computational Biology": "#84cc16",
  "Robotics / Control Systems": "#f97316",
  "Data Science / Statistics": "#3b82f6",
  "Security / Privacy": "#ec4899",
  "Human-Computer Interaction": "#14b8a6",
  Other: "#6b7280",
};

export function topicColour(topic: string): string {
  return TOPIC_COLOURS[topic] ?? "#6b7280";
}

/** Colour ramp for similarity/relevance scores [0,1]. */
export function scoreColour(score: number): string {
  if (score >= 0.5) return "#10b981";
  if (score >= 0.3) return "#f59e0b";
  return "#ef4444";
}

export function truncate(text: string, max: number): string {
  return text.length > max ? `${text.slice(0, max)}…` : text;
}

/**
 * Strip a leading emoji / symbol run from backend-provided strings (the model
 * catalog and provider hints ship with emoji prefixes). Keeps the UI emoji-free
 * per the design skills without touching the backend.
 */
export function stripLeadingSymbols(text: string): string {
  return text.replace(/^[^\p{L}\p{N}]+/u, "").trim();
}

/**
 * Reference source labels. The backend uses four distinct values:
 *   "upload"        — a PDF the user uploaded (in the KB)
 *   "openalex"      — an OpenAlex abstract ingested into the KB
 *   "openalex-live" — a LIVE OpenAlex fallback (NOT in the KB)
 *   "web"           — a DuckDuckGo web fallback (NOT in the KB)
 * See app/fallback.py and app/rag.py.build_context.
 */
export interface SourceMeta {
  label: string;
  /** True when the reference actually comes from the user's knowledge base. */
  fromKb: boolean;
  className: string;
}

export function sourceMeta(source: string): SourceMeta {
  switch (source) {
    case "upload":
      return { label: "Uploaded", fromKb: true, className: "bg-emerald-500/15 text-emerald-300" };
    case "openalex":
      return { label: "OpenAlex", fromKb: true, className: "bg-orange-500/15 text-orange-300" };
    case "openalex-live":
      return { label: "OpenAlex · live", fromKb: false, className: "bg-sky-500/15 text-sky-300" };
    case "web":
      return { label: "Web", fromKb: false, className: "bg-amber-500/15 text-amber-300" };
    default:
      return { label: source || "Source", fromKb: false, className: "bg-slate-600/30 text-slate-300" };
  }
}

/** Format a value that may be a number or the sentinel "?" for page numbers. */
export function formatScore(score: number): string {
  return score.toFixed(3);
}