import type { View, ViewContext } from "@berry/api/web.ts"; import { guardNavigation } from "./navigation.ts"; /** An extension's view as a conversation's route opens it: `create` is `null` when it has none to show. */ export type ViewItem = { name: string; create: (() => View) | null }; /** The address of a place in a conversation's view: each segment of the view's route escaped on its own. */ export const viewHref = (tid: string, extension: string, route = "") => `#/${tid}/${extension}${route ? `/${route.split("/").map(encodeURIComponent).join("/")}` : ""}`; /** A view's route back from its address, after `//`. */ export function viewRoute(escaped: string) { return escaped.split("/").map((segment) => { try { return decodeURIComponent(segment); } catch { return segment; } }).join("/"); } /** The pane of the view a conversation has open, if any; a view is made when it opens and disposed when it closes. */ export function extensionView() { const element = document.createElement("div"); element.className = "pane extension-view"; element.hidden = true; let shown: { item: ViewItem; view: View | null; context: ViewContext | null } | null = null; let unguard: (() => void) | null = null; const close = () => { unguard?.(); unguard = null; shown?.view?.dispose?.(); shown = null; }; return { element, update(item: ViewItem | null, context: ViewContext) { if (shown?.item.name !== item?.name || shown?.item.create !== item?.create) { close(); element.replaceChildren(); if (item !== null) { shown = { item, view: item.create?.() ?? null, context: null }; const current = shown; unguard = guardNavigation((destination) => current.view?.beforeLeave?.(destination) ?? true ); if (shown.view === null) { const missing = document.createElement("p"); missing.className = "view-missing"; missing.textContent = `${item.name} has no view in this conversation`; element.append(missing); } } } element.hidden = shown === null; if (shown?.view && !sameContext(shown.context, context)) { shown.context = context; shown.view.update(context); if (shown.view.element.parentNode !== element) element.replaceChildren(shown.view.element); } }, flush() { return shown?.view?.flush?.(); }, dispose: close, }; } // a conversation draws every frame while it streams; its views hear only what changes const sameContext = (a: ViewContext | null, b: ViewContext) => a !== null && a.tid === b.tid && a.route === b.route && a.latest === b.latest && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId;