import React, { useMemo, useState } from 'react'; import { Copy, Check, Download } from 'lucide-react'; /** * Transparency modal that surfaces every prompt template the * orchestrator and participants use during a chat, grouped by phase. * Each item shows a humanized title, a 1-2 sentence purpose, the * runtime template variables the backend interpolates, and the full * template in a copy-able
 block. A "Download as .txt" button
 * in the header dumps the whole catalog in a flat human-readable form.
 *
 * Same shell pattern as ConversationLimitsModal / CredentialSummaryModal.
 */
export default function PromptCatalogModal({
  isOpen,
  catalog,
  onClose,
}) {
  // Note: hooks must run on every render, so this filename memo lives
  // ABOVE the `if (!isOpen) return null` early return. The dependency
  // on `isOpen` makes the filename timestamp regenerate each time the
  // modal opens (i.e. each download gets a fresh stamp).
  const filename = useMemo(() => {
    const now = new Date();
    const pad = (n) => String(n).padStart(2, '0');
    return (
      'ccai-prompts-'
      + `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
      + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
      + '.txt'
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isOpen]);

  if (!isOpen) return null;

  const handleDownload = () => {
    if (!catalog) return;
    const txt = renderCatalogAsText(catalog);
    const blob = new Blob([txt], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    

Current chat prompts

Every prompt template the orchestrator and participants use during a chat, in conversation order. Variables in {' '}{'{braces}'} are filled in at runtime.
{!catalog && (
Loading prompts...
)} {catalog && (catalog.groups || []).map((g) => (
{g.title}
{(g.items || []).map((item) => ( ))}
))}
); } function PromptItem({ item }) { const [copied, setCopied] = useState(false); const displayName = humanizeName(item.name); const handleCopy = async () => { try { await navigator.clipboard.writeText(item.template || ''); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch (err) { console.warn('Clipboard copy failed:', err); } }; return (
{displayName}
{item.purpose}
{item.variables && item.variables.length > 0 && (
Variables:{' '} {item.variables.map((v, i) => ( {`{${v}}`} {i < item.variables.length - 1 ? ', ' : ''} ))}
)}
{item.template}
); } /** * "INITIAL_OPINION_PROMPT" -> "Initial Opinion Prompt". * Splits on underscores, lowercases everything, capitalizes each word. * Drops nothing (e.g. "Prompt" suffix stays) so the displayed name * still matches the constant a developer might grep for. */ function humanizeName(name) { if (!name) return ''; return name .split('_') .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) .join(' '); } /** * Flat human-readable .txt dump used by the Download button. Matches * the spec format: title banner, per-group separator, per-item header * with purpose + variables, then the indented template body. */ function renderCatalogAsText(catalog) { const now = new Date().toISOString(); const lines = []; const banner = '═'.repeat(64); lines.push(banner); lines.push('Collaborative Conversational AI (CCAI) Demo — Current chat prompts'); lines.push(`Generated: ${now}`); lines.push(banner); lines.push(''); for (const group of catalog.groups || []) { const sep = '─'.repeat(12); lines.push(`${sep} ${group.title} ${sep}`); lines.push(''); for (const item of (group.items || [])) { lines.push(`## ${humanizeName(item.name)}`); lines.push(`Purpose: ${item.purpose}`); if (item.variables && item.variables.length > 0) { lines.push( 'Variables: ' + item.variables.map(v => `{${v}}`).join(', '), ); } lines.push(''); // Indent each template line by 4 spaces so the body is // visually distinct from the metadata in a plain-text viewer. for (const ln of (item.template || '').split('\n')) { lines.push(' ' + ln); } lines.push(''); } } return lines.join('\n'); }