"use client"; import * as React from "react"; import type { PromptKind, UsePromptResult } from "@/lib/client/types"; /** * Prompt → composer hand-off contract (consumed by the Chat workstream). * * 1. The prompt library calls `POST /api/prompts/:id/use { variables }` (counts the use, renders `{{vars}}`). * 2. It then calls `dispatchPromptInsert(detail)`, which * - fires `window.dispatchEvent(new CustomEvent("polyllm:insert-prompt", { detail }))` for a mounted chat view, and * - stashes the same payload in `sessionStorage["polyllm:pending-prompt-insert"]` for a chat view that mounts later; * 3. and navigates to `/app/chat?promptInsert=&vars=` when not already on a new chat. * * A chat view should: `usePromptInsert(handler)` for live inserts, and on mount call `consumePendingPromptInsert()` * (fast path) or, when only the URL params are present (e.g. a shared link), POST `/api/prompts/:id/use` with * `decodeVars(vars)` and apply the result. Legacy `?prompt=` keeps its existing behaviour. * * Applying a detail: `kind === "system"` → set the conversation system prompt to `systemPrompt`; * `user` / `template` → insert `text` into the composer draft; `structured` → insert `text` and, when `schema` is set, * request `responseFormat: { type: "json_schema", schema }`. `defaultModelKey` (if usable) may switch the model. */ export const PROMPT_INSERT_EVENT = "polyllm:insert-prompt"; export const PENDING_PROMPT_KEY = "polyllm:pending-prompt-insert"; /** Library files → composer hand-off (see `components/library/use-in-chat.ts`). */ export const PENDING_ATTACHMENTS_KEY = "polyllm:pending-attachments"; export interface PromptInsertDetail { promptId: string; name: string; kind: PromptKind; /** Rendered body for user/template/structured prompts ("" for system prompts). */ text: string; /** Rendered body for system prompts (null otherwise). */ systemPrompt: string | null; /** JSON schema for structured prompts. */ schema: Record | null; defaultModelKey: string | null; /** Variable values that were used to render. */ variables: Record; /** Epoch ms; consumers ignore stale payloads (> 5 min). */ at: number; } export function toInsertDetail(res: UsePromptResult, variables: Record): PromptInsertDetail { return { promptId: res.prompt.id, name: res.prompt.name, kind: res.kind, text: res.text, systemPrompt: res.systemPrompt, schema: res.schema, defaultModelKey: res.defaultModelKey, variables, at: Date.now() }; } function toBase64Url(s: string): string { const b64 = typeof window !== "undefined" ? window.btoa(unescape(encodeURIComponent(s))) : Buffer.from(s, "utf8").toString("base64"); return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function fromBase64Url(s: string): string { const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (s.length % 4)) % 4); return typeof window !== "undefined" ? decodeURIComponent(escape(window.atob(b64))) : Buffer.from(b64, "base64").toString("utf8"); } export function encodeVars(vars: Record): string { return toBase64Url(JSON.stringify(vars)); } export function decodeVars(s: string | null | undefined): Record { if (!s) return {}; try { const v = JSON.parse(fromBase64Url(s)); return v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, val]) => [k, String(val ?? "")])) : {}; } catch { return {}; } } /** `/app/chat?promptInsert=&vars=` */ export function promptChatHref(promptId: string, vars: Record): string { const q = new URLSearchParams({ promptInsert: promptId }); if (Object.keys(vars).length) q.set("vars", encodeVars(vars)); return `/app/chat?${q.toString()}`; } export function dispatchPromptInsert(detail: PromptInsertDetail) { try { window.sessionStorage.setItem(PENDING_PROMPT_KEY, JSON.stringify(detail)); } catch { /* quota / private mode */ } window.dispatchEvent(new CustomEvent(PROMPT_INSERT_EVENT, { detail })); } /** Reads and clears the stashed payload (returns null when absent or older than 5 minutes). */ export function consumePendingPromptInsert(): PromptInsertDetail | null { try { const raw = window.sessionStorage.getItem(PENDING_PROMPT_KEY); if (!raw) return null; window.sessionStorage.removeItem(PENDING_PROMPT_KEY); const d = JSON.parse(raw) as PromptInsertDetail; if (!d?.promptId || Date.now() - (d.at ?? 0) > 5 * 60_000) return null; return d; } catch { return null; } } /** Subscribe to live prompt inserts (chat view). The handler is kept in a ref, so it may change freely. */ export function usePromptInsert(handler: (detail: PromptInsertDetail) => void) { const ref = React.useRef(handler); React.useEffect(() => { ref.current = handler; }, [handler]); React.useEffect(() => { const onEvent = (e: Event) => ref.current((e as CustomEvent).detail); window.addEventListener(PROMPT_INSERT_EVENT, onEvent); return () => window.removeEventListener(PROMPT_INSERT_EVENT, onEvent); }, []); }