diff --git a/CLAUDE.md b/CLAUDE.md index df074b9..3afebbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,6 +150,10 @@ Results log to Braintrust under project `harmonica-facilitation` and are viewabl `src/lib/braintrust.ts` provides `getBraintrustLogger()` for production LLM call logging and `traceOperation()` for hierarchical spans. Optional — logs a warning if `BRAINTRUST_API_KEY` is not set. The `/api/admin/evals` route queries Braintrust experiments for the admin dashboard. +**Observability safety rule:** All observability code (Braintrust, PostHog) in `LLM.chat()` is wrapped in its own try/catch. Observability must never crash a participant's response. If logging fails, warn to console and move on. When adding new tracing or analytics to the LLM path, always wrap in a non-fatal try/catch. + +**Span context:** `traceOperation()` passes a `Span` object to its callback via `{ operation, span }`. When calling `LLM.chat()` inside a `traceOperation` callback, always pass `span` through so the SDK uses `span.log()` instead of the top-level `logger.log()` (which throws inside spans). + ## Code Style - Prefer React Server Components; minimize `'use client'` diff --git a/src/app/admin/Sidebar.tsx b/src/app/admin/Sidebar.tsx index c313910..3e10bb7 100644 --- a/src/app/admin/Sidebar.tsx +++ b/src/app/admin/Sidebar.tsx @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation'; import { Boxes, // For types/categories FileText, // For instructions/prompts + LayoutTemplate, // For templates FlaskConical, // For evals Settings, } from 'lucide-react'; @@ -22,6 +23,11 @@ const navigation = [ href: '/admin/prompts', icon: FileText, // Changed to FileText to represent instructions/documents }, + { + name: 'Templates', + href: '/admin/templates', + icon: LayoutTemplate, + }, { name: 'Evals', href: '/admin/evals', diff --git a/src/app/admin/templates/page.tsx b/src/app/admin/templates/page.tsx new file mode 100644 index 0000000..0c3b455 --- /dev/null +++ b/src/app/admin/templates/page.tsx @@ -0,0 +1,876 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Plus, + Edit2, + Trash2, + Loader2, + ArrowLeft, + ChevronUp, + ChevronDown, + X, + Link2, + GitBranch, +} from 'lucide-react'; +import { useToast } from 'hooks/use-toast'; +import { format } from 'date-fns'; +import { v4 as uuidv4 } from 'uuid'; +import type { + Template, + ChainStep, + ChainConfig, +} from './types'; +import { ICON_OPTIONS, CONTEXT_MODE_OPTIONS } from './types'; + +// ─── Helpers ─────────────────────────────────────────────── + +type View = 'list' | 'chain-editor'; + +const emptyForm = { + title: '', + description: '', + icon: '', + facilitation_prompt: '', + default_session_name: '', +}; + +function createEmptyStep(index: number): ChainStep { + return { + id: uuidv4(), + title: `Step ${index + 1}`, + description: '', + facilitation_prompt: '', + default_session_name: '', + context_mode: index === 0 ? 'none' : 'previous_summary', + context_template: '', + }; +} + +function parseChainConfig(raw: any): ChainConfig | null { + if (!raw) return null; + if (typeof raw === 'string') { + try { return JSON.parse(raw); } catch { return null; } + } + return raw as ChainConfig; +} + +function formatDate(dateString: string) { + return format(new Date(dateString), 'MMM d, yyyy'); +} + +// ─── Main Component ──────────────────────────────────────── + +export default function TemplatesPage() { + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [view, setView] = useState('list'); + const [tab, setTab] = useState('all'); + + // Single template dialogs + const [showCreateDialog, setShowCreateDialog] = useState(false); + const [editingSingle, setEditingSingle] = useState