| 1 | import i18n, { currentLocale } from "@/i18n"; |
| 2 | |
| 3 | /** Truncate the first user message into a chat title. */ |
| 4 | export function deriveTitle(preview: string | undefined, fallback: string): string { |
| 5 | if (!preview) return fallback; |
| 6 | const oneLine = preview.replace(/\s+/g, " ").trim(); |
| 7 | if (!oneLine) return fallback; |
| 8 | return oneLine.length > 60 ? `${oneLine.slice(0, 57)}…` : oneLine; |
| 9 | } |
| 10 | |
| 11 | /** Loose ISO-or-epoch parser; returns ``null`` for missing/invalid input. */ |
| 12 | function parseDate(value: string | number | null | undefined): Date | null { |
| 13 | if (value === null || value === undefined || value === "") return null; |
| 14 | const d = new Date(value); |
| 15 | return Number.isNaN(d.getTime()) ? null : d; |
| 16 | } |
| 17 | |
| 18 | const RELATIVE_THRESHOLDS: [number, Intl.RelativeTimeFormatUnit][] = [ |
| 19 | [60, "second"], |
| 20 | [60, "minute"], |
| 21 | [24, "hour"], |
| 22 | [7, "day"], |
| 23 | [4.345, "week"], |
| 24 | [12, "month"], |
| 25 | [Number.POSITIVE_INFINITY, "year"], |
| 26 | ]; |
| 27 | |
| 28 | const relativeTimeFormatters = new Map<string, Intl.RelativeTimeFormat>(); |
| 29 | const dateTimeFormatters = new Map<string, Intl.DateTimeFormat>(); |
| 30 | |
| 31 | function activeLocale(locale?: string): string { |
| 32 | return locale || i18n.resolvedLanguage || i18n.language || currentLocale(); |
| 33 | } |
| 34 | |
| 35 | function relativeTimeFormatter(locale: string): Intl.RelativeTimeFormat { |
| 36 | const existing = relativeTimeFormatters.get(locale); |
| 37 | if (existing) return existing; |
| 38 | const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); |
| 39 | relativeTimeFormatters.set(locale, formatter); |
| 40 | return formatter; |
| 41 | } |
| 42 | |
| 43 | function dateTimeFormatter(locale: string): Intl.DateTimeFormat { |
| 44 | const existing = dateTimeFormatters.get(locale); |
| 45 | if (existing) return existing; |
| 46 | const formatter = new Intl.DateTimeFormat(locale, { |
| 47 | dateStyle: "medium", |
| 48 | timeStyle: "short", |
| 49 | }); |
| 50 | dateTimeFormatters.set(locale, formatter); |
| 51 | return formatter; |
| 52 | } |
| 53 | |
| 54 | export function relativeTime( |
| 55 | value: string | number | null | undefined, |
| 56 | locale?: string, |
| 57 | ): string { |
| 58 | const date = parseDate(value); |
| 59 | if (!date) return ""; |
| 60 | let delta = (date.getTime() - Date.now()) / 1000; |
| 61 | const formatter = relativeTimeFormatter(activeLocale(locale)); |
| 62 | for (const [step, unit] of RELATIVE_THRESHOLDS) { |
| 63 | if (Math.abs(delta) < step) { |
| 64 | return formatter.format(Math.round(delta), unit); |
| 65 | } |
| 66 | delta /= step; |
| 67 | } |
| 68 | return formatter.format(Math.round(delta), "year"); |
| 69 | } |
| 70 | |
| 71 | export function fmtDateTime( |
| 72 | value: string | number | null | undefined, |
| 73 | locale?: string, |
| 74 | ): string { |
| 75 | const date = parseDate(value); |
| 76 | return date ? dateTimeFormatter(activeLocale(locale)).format(date) : ""; |
| 77 | } |
| 78 |