/** * UltiAI embed bootstrap — must run before OpenWebUI SvelteKit init. * Layout requires localStorage.token; without embed signin it redirects to /auth → redirect loop → 500. */ ;(function () { const STORAGE_KEY = "ulti.ai.session" const CONTEXT_KEY = "ulti.ai.context" const DEFAULT_MODEL_KEY = "ulti.ai.default_model" const LAST_CHAT_PATH_KEY = "ulti.ai.last_chat_path" const SIGNIN_URL = "/ai/api/v1/auths/signin" const CONFIG_URL = "/api/v1/ai/config" const BASE_SYSTEM_PROMPT = [ "Tu es UltiAI, l'assistant intégré à la suite Ultimail (mail, drive, contacts, agenda).", "Réponds en français sauf demande contraire. Utilise les tools disponibles pour agir sur les données utilisateur.", "Après chaque appel d'outil, réponds toujours en langage naturel : résume le résultat, cite les sources, 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.", ].join(" ") function readPageUrl() { try { return new URL(window.location.href) } catch { return null } } function isTemporaryEmbed() { const url = readPageUrl() return url?.searchParams.get("temporary-chat") === "true" } function modelFromUrl() { const url = readPageUrl() if (!url) return "" const raw = url.searchParams.get("model") || url.searchParams.get("models") || "" return raw.split(",")[0]?.trim() || "" } function applySelectedModels(modelIds) { const ids = modelIds.map((id) => String(id || "").trim()).filter(Boolean) if (!ids.length) return try { sessionStorage.setItem("selectedModels", JSON.stringify(ids)) localStorage.setItem(DEFAULT_MODEL_KEY, ids[0]) } catch { // storage unavailable } } function bootstrapSelectedModels(modelHint) { const resolved = modelFromUrl() || String(modelHint || "").trim() || localStorage.getItem(DEFAULT_MODEL_KEY) || "" if (resolved) applySelectedModels([resolved]) return resolved } function restoreLastChatIfHome() { if (isTemporaryEmbed()) return const url = readPageUrl() if (!url) return const path = url.pathname.replace(/\/$/, "") || "/" if (path !== "/ai") return if (url.searchParams.get("model") || url.searchParams.get("models")) return try { const last = localStorage.getItem(LAST_CHAT_PATH_KEY) if (!last || !/\/c\/[^/]+/.test(last)) return const target = last.startsWith("/") ? last : `/ai${last}` if (target === url.pathname + url.search) return window.location.replace(target) } catch { // ignore } } function watchChatRoute() { const save = () => { if (isTemporaryEmbed()) return try { const path = window.location.pathname if (/\/c\/[^/]+/.test(path)) { localStorage.setItem(LAST_CHAT_PATH_KEY, path + window.location.search) } } catch { // ignore } } window.addEventListener("popstate", save) const timer = window.setInterval(save, 2000) window.addEventListener("beforeunload", () => window.clearInterval(timer)) save() } function ensureEmbedSignin() { try { if (localStorage.getItem("token")) return const xhr = new XMLHttpRequest() xhr.open("POST", SIGNIN_URL, false) xhr.withCredentials = true xhr.setRequestHeader("Content-Type", "application/json") xhr.send(JSON.stringify({ email: "", password: "" })) if (xhr.status !== 200) return const data = JSON.parse(xhr.responseText) if (data && data.token) { localStorage.setItem("token", data.token) } } catch { // Ultimail session not ready yet — parent may post ULTI_SESSION later } } async function fetchEmbedConfig() { try { const response = await fetch(CONFIG_URL, { credentials: "include" }) if (!response.ok) return null return await response.json() } catch { return null } } async function syncUserModelPreference(modelId) { const model = String(modelId || "").trim() if (!model) return false const token = localStorage.getItem("token") if (!token) return false try { const settingsRes = await fetch("/ai/api/v1/users/user/settings", { headers: { Authorization: `Bearer ${token}` }, credentials: "include", }) if (!settingsRes.ok) return false const settings = await settingsRes.json() const ui = settings?.ui || {} const current = ui.models || ui.model_ids if (Array.isArray(current) && current.length > 0 && String(current[0] || "").trim()) { return true } const modelsRes = await fetch("/ai/api/models", { headers: { Authorization: `Bearer ${token}` }, credentials: "include", }) if (modelsRes.ok) { const modelsPayload = await modelsRes.json() const items = Array.isArray(modelsPayload?.data) ? modelsPayload.data : Array.isArray(modelsPayload) ? modelsPayload : [] const known = items.some((entry) => entry && entry.id === model) if (!known) return false } const updateRes = await fetch("/ai/api/v1/users/user/settings/update", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, credentials: "include", body: JSON.stringify({ ui: { models: [model], model_ids: [model] } }), }) if (!updateRes.ok) return false applySelectedModels([model]) return true } catch { return false } } function scheduleModelSync(modelId) { const model = String(modelId || "").trim() if (!model) return let attempts = 0 const tick = async () => { attempts += 1 const done = await syncUserModelPreference(model) if (done || attempts >= 12) return window.setTimeout(tick, 1000) } window.setTimeout(tick, 500) } function readContextPrompt() { try { const raw = sessionStorage.getItem(CONTEXT_KEY) if (!raw) return BASE_SYSTEM_PROMPT const parsed = JSON.parse(raw) const parts = [BASE_SYSTEM_PROMPT, parsed.systemPrompt].filter(Boolean) return parts.join("\n\n") } catch { return BASE_SYSTEM_PROMPT } } function injectSystemPrompt(body) { if (!body || !Array.isArray(body.messages)) return body const prompt = readContextPrompt() if (!prompt) return body const messages = body.messages.slice() const systemIndex = messages.findIndex((m) => m && m.role === "system") if (systemIndex >= 0) { messages[systemIndex] = { ...messages[systemIndex], content: prompt } } else { messages.unshift({ role: "system", content: prompt }) } return { ...body, messages } } function patchFetch() { if (window.__ultiFetchPatched) return window.__ultiFetchPatched = true const originalFetch = window.fetch.bind(window) window.fetch = async function patchedFetch(input, init) { const url = typeof input === "string" ? input : input instanceof Request ? input.url : "" const method = (init && init.method) || (input instanceof Request ? input.method : "GET") if ( method.toUpperCase() === "POST" && /\/api\/(v1\/)?chat\/completions(?:\?|$)/.test(url) && init && typeof init.body === "string" ) { try { const body = JSON.parse(init.body) const patched = injectSystemPrompt(body) if (patched !== body) { init = { ...init, body: JSON.stringify(patched) } } } catch { // ignore malformed bodies } } return originalFetch(input, init) } } function brandDocument() { const titleEl = document.querySelector("title") if (titleEl) { const t = titleEl.textContent || "" if (t.includes("Open WebUI")) { titleEl.textContent = t.replace(/UltiAI \(Open WebUI\)/g, "UltiAI").replace(/Open WebUI/g, "UltiAI") } else if (!t.trim()) { titleEl.textContent = "UltiAI" } } } function watchDocumentTitle() { brandDocument() const titleEl = document.querySelector("title") if (!titleEl) return new MutationObserver(() => brandDocument()).observe(titleEl, { childList: true, characterData: true, subtree: true, }) } restoreLastChatIfHome() bootstrapSelectedModels("") ensureEmbedSignin() patchFetch() watchDocumentTitle() watchChatRoute() void (async () => { const config = await fetchEmbedConfig() const model = bootstrapSelectedModels(config?.default_model) if (!localStorage.getItem("token")) ensureEmbedSignin() if (model) scheduleModelSync(model) })() window.addEventListener("message", (event) => { if (event.source !== window.parent) return const data = event.data if (!data || typeof data !== "object") return if (data.type === "ULTI_SESSION") { try { sessionStorage.setItem( STORAGE_KEY, JSON.stringify({ token_secret: data.token_secret, session_id: data.session_id, mcp_url: data.mcp_url, enabled_tools: data.enabled_tools, default_model: data.default_model, updated_at: Date.now(), }) ) } catch { // sessionStorage unavailable } if (data.default_model && typeof data.default_model === "string") { const model = data.default_model.trim() if (model) { applySelectedModels([model]) scheduleModelSync(model) } } if (!localStorage.getItem("token")) { ensureEmbedSignin() } return } if (data.type === "ULTI_CONTEXT_UPDATE") { try { sessionStorage.setItem( CONTEXT_KEY, JSON.stringify({ systemPrompt: data.systemPrompt, context: data.context, updated_at: Date.now(), }) ) } catch { // sessionStorage unavailable } } }) })()