export type AiEmbedApp = "mail" | "drive" | "contacts" | "docs" | "standalone" export type AiChatContext = { app: AiEmbedApp temporary?: boolean messageId?: string accountId?: string drivePath?: string fileId?: string contactId?: string subject?: string snippet?: string /** UltiDocs — titre affiché */ documentTitle?: string /** Chemin du fichier source (.docx, .md…) si sidecar */ sourcePath?: string /** Extrait texte du document */ documentExcerpt?: string /** Sélection courante dans l'éditeur */ selectionText?: string /** JSON TipTap (éventuellement tronqué) */ documentJson?: string /** Prompt système additionnel (non sérialisé en URL) */ systemPromptExtra?: string } export type AiPostMessage = | { type: "ULTI_CONTEXT_UPDATE" context: AiChatContext systemPrompt?: string } | { type: "ULTI_DOCS_APPLY"; payload: unknown } | { type: "ULTI_ASSISTANT_TEXT"; text: string } | { type: "ULTI_THEME"; theme: "light" | "dark" } | { type: "ULTI_SESSION" token_secret?: string mcp_url?: string enabled_tools?: string[] session_id?: string } | { type: "ULTI_OPEN_LINK"; href: string } | { type: "ULTI_TOOL_RESULT"; payload: unknown } export function buildEmbedSearchParams( context: AiChatContext, defaultModel?: string, ): string { const params = new URLSearchParams() const model = defaultModel?.trim() if (model) params.set("model", model) if (context.temporary !== false) params.set("temporary-chat", "true") if (context.app) params.set("app", context.app) if (context.messageId) params.set("message_id", context.messageId) if (context.accountId) params.set("account_id", context.accountId) if (context.drivePath) params.set("path", context.drivePath) if (context.fileId) params.set("file_id", context.fileId) if (context.contactId) params.set("contact_id", context.contactId) if (context.subject) params.set("subject", context.subject) if (context.snippet) params.set("snippet", context.snippet) return params.toString() } export function systemPromptFromContext(context: AiChatContext): string { const lines = [ "Tu es UltiAI, l'assistant intégré à la suite Ultimail (mail, drive, contacts, UltiCal).", "Réponds en français sauf demande contraire. Utilise les tools disponibles pour agir sur les données utilisateur.", "Recherche suite (index local) via suite_search ; recherche web publique via web_search si configurée.", "Après chaque appel d'outil, réponds toujours en langage naturel : résume le résultat, cite les sources (sujet, chemin, nom), propose la suite.", "Ne termine jamais un tour utilisateur avec uniquement un appel d'outil sans texte explicatif.", "Respecte strictement le paramètre limit des tools (ne demande pas plus de résultats que nécessaire).", ] if (context.app === "mail" && context.subject) { lines.push(`Contexte mail — sujet: ${context.subject}`) if (context.snippet) lines.push(`Extrait: ${context.snippet}`) } if (context.app === "drive" && context.drivePath) { lines.push(`Contexte drive — fichier/dossier: ${context.drivePath}`) } if (context.app === "contacts" && context.contactId) { lines.push(`Contexte contacts — fiche: ${context.contactId}`) } if (context.app === "docs") { if (context.documentTitle) lines.push(`Document: ${context.documentTitle}`) if (context.drivePath) lines.push(`Sidecar: ${context.drivePath}`) if (context.sourcePath) lines.push(`Source: ${context.sourcePath}`) if (context.selectionText) lines.push(`Sélection: ${context.selectionText}`) if (context.documentExcerpt) lines.push(`Contenu:\n${context.documentExcerpt}`) } if (context.systemPromptExtra) { lines.push("", context.systemPromptExtra) } return lines.join("\n") }