Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/stash

src/lib/kinds.ts

98 lines · 2.7 KB · TypeScript
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))
}