"use client"; /** * Chat helpers and types shared by the chat UI. The record shapes themselves * live in `lib/schemas.ts` (zod, shared with backup/sync); this module * re-exports them so UI code has one import. */ import type { ChatAttachment, ChatMessage, ChatScope, ChatSource } from "./schemas"; export type { ChatAttachment, ChatMessage, ChatScope, ChatSource }; export type ChatError = { message: string; kind?: string }; export type ChatModelChoice = { name: string; badge: "text" | "vision" | "both"; available: boolean | null; }; /** Last model choice, remembered across visits. */ const MODEL_KEY = "freezmo.chat.model.v1"; export function readStoredChatModel(): string | null { try { const value = window.localStorage.getItem(MODEL_KEY); return value && value.trim() ? value : null; } catch { return null; } } export function storeChatModel(model: string): void { try { window.localStorage.setItem(MODEL_KEY, model); } catch { /* private mode */ } } export function chatMessageId(prefix: string): string { const rand = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10); return `${prefix}_${Date.now().toString(36)}_${rand}`; } /** Auto title from the first user turn (markdown stripped, one line). */ export function titleFromMessage(content: string): string { const line = content .replace(/[#*`>_]/g, "") .replace(/\s+/g, " ") .trim(); if (!line) return ""; return line.length > 60 ? `${line.slice(0, 57)}…` : line; } /** Case/diacritic-insensitive search over titles and message text. */ export function conversationMatches(title: string, messages: ChatMessage[], query: string): boolean { const needle = fold(query); if (!needle) return true; if (fold(title).includes(needle)) return true; return messages.some((message) => fold(message.content).includes(needle)); } function fold(value: string): string { return value .toLowerCase() .normalize("NFKD") .replace(/[\u0300-\u036f]/g, ""); }