TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import type { PromptKind, UsePromptResult } from "@/lib/client/types";45/**6 * Prompt → composer hand-off contract (consumed by the Chat workstream).7 *8 * 1. The prompt library calls `POST /api/prompts/:id/use { variables }` (counts the use, renders `{{vars}}`).9 * 2. It then calls `dispatchPromptInsert(detail)`, which10 * - fires `window.dispatchEvent(new CustomEvent("polyllm:insert-prompt", { detail }))` for a mounted chat view, and11 * - stashes the same payload in `sessionStorage["polyllm:pending-prompt-insert"]` for a chat view that mounts later;12 * 3. and navigates to `/app/chat?promptInsert=<promptId>&vars=<base64url JSON>` when not already on a new chat.13 *14 * A chat view should: `usePromptInsert(handler)` for live inserts, and on mount call `consumePendingPromptInsert()`15 * (fast path) or, when only the URL params are present (e.g. a shared link), POST `/api/prompts/:id/use` with16 * `decodeVars(vars)` and apply the result. Legacy `?prompt=<prompt_presets id>` keeps its existing behaviour.17 *18 * Applying a detail: `kind === "system"` → set the conversation system prompt to `systemPrompt`;19 * `user` / `template` → insert `text` into the composer draft; `structured` → insert `text` and, when `schema` is set,20 * request `responseFormat: { type: "json_schema", schema }`. `defaultModelKey` (if usable) may switch the model.21 */22export const PROMPT_INSERT_EVENT = "polyllm:insert-prompt";23export const PENDING_PROMPT_KEY = "polyllm:pending-prompt-insert";24/** Library files → composer hand-off (see `components/library/use-in-chat.ts`). */25export const PENDING_ATTACHMENTS_KEY = "polyllm:pending-attachments";2627export interface PromptInsertDetail {28 promptId: string;29 name: string;30 kind: PromptKind;31 /** Rendered body for user/template/structured prompts ("" for system prompts). */32 text: string;33 /** Rendered body for system prompts (null otherwise). */34 systemPrompt: string | null;35 /** JSON schema for structured prompts. */36 schema: Record<string, unknown> | null;37 defaultModelKey: string | null;38 /** Variable values that were used to render. */39 variables: Record<string, string>;40 /** Epoch ms; consumers ignore stale payloads (> 5 min). */41 at: number;42}4344export function toInsertDetail(res: UsePromptResult, variables: Record<string, string>): PromptInsertDetail {45 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() };46}4748function toBase64Url(s: string): string {49 const b64 = typeof window !== "undefined" ? window.btoa(unescape(encodeURIComponent(s))) : Buffer.from(s, "utf8").toString("base64");50 return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");51}52function fromBase64Url(s: string): string {53 const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (s.length % 4)) % 4);54 return typeof window !== "undefined" ? decodeURIComponent(escape(window.atob(b64))) : Buffer.from(b64, "base64").toString("utf8");55}5657export function encodeVars(vars: Record<string, string>): string {58 return toBase64Url(JSON.stringify(vars));59}60export function decodeVars(s: string | null | undefined): Record<string, string> {61 if (!s) return {};62 try {63 const v = JSON.parse(fromBase64Url(s));64 return v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, val]) => [k, String(val ?? "")])) : {};65 } catch {66 return {};67 }68}6970/** `/app/chat?promptInsert=<id>&vars=<base64url>` */71export function promptChatHref(promptId: string, vars: Record<string, string>): string {72 const q = new URLSearchParams({ promptInsert: promptId });73 if (Object.keys(vars).length) q.set("vars", encodeVars(vars));74 return `/app/chat?${q.toString()}`;75}7677export function dispatchPromptInsert(detail: PromptInsertDetail) {78 try {79 window.sessionStorage.setItem(PENDING_PROMPT_KEY, JSON.stringify(detail));80 } catch {81 /* quota / private mode */82 }83 window.dispatchEvent(new CustomEvent<PromptInsertDetail>(PROMPT_INSERT_EVENT, { detail }));84}8586/** Reads and clears the stashed payload (returns null when absent or older than 5 minutes). */87export function consumePendingPromptInsert(): PromptInsertDetail | null {88 try {89 const raw = window.sessionStorage.getItem(PENDING_PROMPT_KEY);90 if (!raw) return null;91 window.sessionStorage.removeItem(PENDING_PROMPT_KEY);92 const d = JSON.parse(raw) as PromptInsertDetail;93 if (!d?.promptId || Date.now() - (d.at ?? 0) > 5 * 60_000) return null;94 return d;95 } catch {96 return null;97 }98}99100/** Subscribe to live prompt inserts (chat view). The handler is kept in a ref, so it may change freely. */101export function usePromptInsert(handler: (detail: PromptInsertDetail) => void) {102 const ref = React.useRef(handler);103 React.useEffect(() => {104 ref.current = handler;105 }, [handler]);106 React.useEffect(() => {107 const onEvent = (e: Event) => ref.current((e as CustomEvent<PromptInsertDetail>).detail);108 window.addEventListener(PROMPT_INSERT_EVENT, onEvent);109 return () => window.removeEventListener(PROMPT_INSERT_EVENT, onEvent);110 }, []);111}112