import { uniqueSorted } from './util.ts' export const NOTE_KINDS = [ 'note', 'reminder', 'contact', 'instruction', 'fact', ] as const export type NoteKind = (typeof NOTE_KINDS)[number] export function inferKind(text: string): NoteKind { const lower = text.toLowerCase() if (/\b(remind|todo|to-do|to do|need to|follow up|deadline)\b/.test(lower)) { return 'reminder' } if (/\b(phone|cell|mobile|call|text|email|@)\b/.test(lower)) return 'contact' if ( /\b(instruction|instructions|how to|steps|setup|configure|recipe)\b/.test( lower, ) ) { return 'instruction' } if ( /\b(watt|watts|volt|volts|amps|amp|inch|inches|feet|ft|lbs|pounds|psi|serial|model)\b/.test( lower, ) || /\b\d+\s*w\b/.test(lower) ) { return 'fact' } return 'note' } export function summarize(text: string): string { const firstLine = text .split(/\r?\n/) .map((line) => line.trim()) .find(Boolean) ?? text.trim() return firstLine.length <= 100 ? firstLine : firstLine.slice(0, 97).trimEnd() + '...' } export function extractTags( text: string, explicitTags: unknown, kind: string, ): string[] { const hashTags = Array.from( text.matchAll(/(?:^|\s)#([a-zA-Z0-9_-]{2,40})/g), (match) => match[1] ?? '', ) const inferred = [kind] const lower = text.toLowerCase() if (lower.includes('watt') || /\b\d+\s*w\b/.test(lower)) { inferred.push('wattage') } if (lower.includes('phone') || /\b\+?\d[\d().\-\s]{6,}\d\b/.test(text)) { inferred.push('contact') } if (lower.includes('remind')) inferred.push('reminder') const explicit = Array.isArray(explicitTags) ? explicitTags.filter((tag): tag is string => typeof tag === 'string') : [] return uniqueSorted([...explicit, ...hashTags, ...inferred]) } export function extractEntities(text: string): string[] { const phoneNumbers = Array.from( text.matchAll(/\b\+?\d[\d().\-\s]{6,}\d\b/g), (match) => match[0]?.trim() ?? '', ) const emails = Array.from( text.matchAll(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi), (match) => match[0]?.trim() ?? '', ) const titlePhrases = Array.from( text.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g), (match) => match[0]?.trim() ?? '', ) return uniqueSorted([...phoneNumbers, ...emails, ...titlePhrases].slice(0, 20)) } export function scoreHaystack(haystack: string, query: string): number { const normalizedQuery = query.trim().toLowerCase() if (!normalizedQuery) return 0.1 const normalizedHaystack = haystack.toLowerCase() const terms = normalizedQuery.split(/\s+/).filter(Boolean) const matchedTerms = terms.filter((term) => normalizedHaystack.includes(term), ).length const exactBoost = normalizedHaystack.includes(normalizedQuery) ? 0.45 : 0 return Math.min(1, exactBoost + matchedTerms / Math.max(terms.length, 1)) }