SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
5.1 KB · 112 lines typescript
Raw Blame History
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