auralynq-rag / web /lib /format.ts
MHamdan's picture
Deploy Auralynq RAG (Llama-3.3-70B via HF Inference Providers)
8c1b9fe verified
Raw
History Blame Contribute Delete
5.42 kB
// Pure, framework-free presentation helpers — unit-testable in node.
// The backend cites sources by their stored path (e.g. an upload path like
// "/app/data/storage/uploads/2024/report.pdf"). Those internal paths must never
// be shown to users; we surface a clean, human filename instead and keep the
// raw value only behind a debug disclosure.
/** True for values that look like an internal storage / upload path. */
export function isInternalPath(source: string): boolean {
if (!source) return false;
return (
source.includes("/app/data/") ||
source.includes("/storage/") ||
source.includes("/uploads/") ||
/^[a-z]+:\/\//i.test(source) === false && source.includes("/") && source.startsWith("/")
);
}
/**
* Clean a citation source into a readable, user-facing label.
* - strips directory paths → keeps the filename
* - URL-decodes percent-encoding
* - drops a leading content-hash/uuid prefix like "a1b2c3d4_name.pdf"
* - falls back to the original (trimmed) string when nothing to clean
*/
export function displaySource(source: string | null | undefined): string {
if (!source) return "Untitled source";
let s = String(source).trim();
if (!s) return "Untitled source";
// Keep real http(s) URLs but show just host + last path segment.
const urlMatch = /^(https?:\/\/)([^/]+)(\/.*)?$/i.exec(s);
if (urlMatch) {
const host = urlMatch[2].replace(/^www\./, "");
const tail = (urlMatch[3] || "").split("/").filter(Boolean).pop();
return tail ? `${host}/${decodeURIComponent(tail)}` : host;
}
// Otherwise treat as a filesystem path: take the basename.
s = s.replace(/\\/g, "/");
const base = s.split("/").filter(Boolean).pop() || s;
let name = base;
try {
name = decodeURIComponent(base);
} catch {
/* leave as-is on malformed encoding */
}
// Drop a leading hex/uuid hash prefix used for dedupe (e.g. 8+ hex then _ or -).
name = name.replace(/^[0-9a-f]{8,}[._-]/i, "");
return name || "Untitled source";
}
/** Short relative-time label from an ISO timestamp. */
export function timeAgo(iso: string | null | undefined, now: number = Date.now()): string {
if (!iso) return "never";
const t = new Date(iso).getTime();
if (Number.isNaN(t)) return "unknown";
const mins = Math.round((now - t) / 60000);
if (mins < 1) return "just now";
if (mins < 60) return `${mins}m ago`;
const hrs = Math.round(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
return `${Math.round(hrs / 24)}d ago`;
}
/** Map a 0..1 evidence-coverage score to a strength tier + label. */
export type EvidenceTier = "strong" | "weak" | "none";
export function coverageTier(coverage: number): EvidenceTier {
const pct = Math.max(0, Math.min(1, coverage));
if (pct >= 0.6) return "strong";
if (pct >= 0.35) return "weak";
return "none";
}
/**
* Heuristic: does this question ask about the *collection itself* (what's
* indexed, languages, file types, counts) rather than the documents' content?
* Such questions should render a corpus-inventory answer, not an evidence
* failure (e.g. "Is there any document in Arabic in my collection?").
*/
export function isInventoryQuestion(q: string): boolean {
if (!q) return false;
const s = q.toLowerCase();
const corpusWord = /\b(corpus|collection|knowledge base|index(?:ed)?|library|uploaded|ingest(?:ed)?|documents?|files?|docs?)\b/.test(
s,
);
const inventoryIntent =
/\b(how many|what|which|list|are there|is there|any|do you have|contain|languages?|file types?|formats?|last (?:indexed|updated)|what'?s in)\b/.test(
s,
);
// Strong single-phrase triggers.
if (/\b(in my (?:collection|corpus|library)|in the (?:collection|corpus))\b/.test(s)) return true;
if (/\bwhat (?:documents?|files?|docs?) /.test(s)) return true;
if (/\b(languages?|file types?|formats?)\b/.test(s) && corpusWord) return true;
// "Last document added/uploaded/ingested" and similar recency queries.
if (/\b(last|latest|most recent|recently)\b.*\b(document|file|doc|upload|ingest|added)\b/.test(s)) return true;
if (/\bwhat.*\b(last|latest|most recent)\b.*\b(document|file|upload)\b/.test(s)) return true;
if (/\b(last|latest)\b.*\b(document|file)\b.*(add|upload|ingest)/.test(s)) return true;
return corpusWord && inventoryIntent && s.length < 120;
}
/** Human label for a relevance/score value in [0,1]. */
export function relevanceLabel(score: number): string {
if (score >= 0.66) return "high";
if (score >= 0.4) return "medium";
return "low";
}
/**
* Heuristic: does this question request a corpus management action (delete,
* clear, reset) rather than asking about corpus content?
*/
export function isCorpusManagementQuestion(q: string): boolean {
if (!q) return false;
const s = q.toLowerCase();
// Explicit destructive intent
if (/\b(delete|remove|wipe|clear|purge|erase|drop|reset)\b/.test(s) &&
/\b(document|file|corpus|collection|index|vector|upload|pdf|docx|everything|all|data|last)\b/.test(s)) return true;
if (/\b(how|can i|how do i|can you)\b.*\b(delete|remove|clear|wipe)\b/.test(s)) return true;
if (/\bremove.*permanently\b/.test(s) || /\bdelete.*permanently\b/.test(s)) return true;
if (/\b(clear|wipe|reset)\b.*\b(corpus|collection|vectors?|index|everything|all)\b/.test(s)) return true;
if (/\b(delete|remove)\b.*\b(last|latest|most recent)\b.*\b(document|file|upload)\b/.test(s)) return true;
return false;
}