'use client'; import React, { useState } from 'react'; import type { InterviewTemplate } from '@/lib/interview/types'; import { emptyForm, templateToForm, formToTemplate, validateTemplateForm, slugify, type TemplateForm, } from '@/lib/interview/template-form'; import { interviewTemplatesService } from '@/lib/interview/templates-service'; import { track } from '@/lib/telemetry'; 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 { Checkbox } from '@/components/ui/checkbox'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { toast } from 'sonner'; import { ArrowLeft, Save, Plus, Trash2 } from 'lucide-react'; interface InterviewTemplateEditorProps { template: InterviewTemplate | null; // null = create onSaved: () => void; onCancel: () => void; } function derivedArtifactPath(title: string): string { return `/.interviews/${slugify(title) || 'untitled'}.md`; } export function InterviewTemplateEditor({ template, onSaved, onCancel }: InterviewTemplateEditorProps) { const isCreate = template === null; const readOnly = template?.isBuiltIn === true; const [form, setForm] = useState(() => template ? templateToForm(template) : emptyForm() ); const [saving, setSaving] = useState(false); const handleTitleChange = (title: string) => { setForm(prev => { const next: TemplateForm = { ...prev, title }; // Only auto-derive the artifact path when creating and the user hasn't // manually edited it away from the previously-derived value. if (isCreate) { const prevDerived = derivedArtifactPath(prev.title); if (prev.artifactPath === prevDerived || prev.artifactPath === '/.interviews/untitled.md') { next.artifactPath = derivedArtifactPath(title); } } return next; }); }; const updateItem = (index: number, patch: Partial) => { setForm(prev => ({ ...prev, items: prev.items.map((it, i) => (i === index ? { ...it, ...patch } : it)), })); }; const addItem = () => { setForm(prev => ({ ...prev, items: [...prev.items, { question: '', criteria: '', required: true }], })); }; const removeItem = (index: number) => { setForm(prev => ({ ...prev, items: prev.items.filter((_, i) => i !== index) })); }; const handoffEnabled = form.handoff !== null; const setHandoffEnabled = (enabled: boolean) => { setForm(prev => ({ ...prev, handoff: enabled ? (prev.handoff ?? { label: '', prompt: '', mode: 'code' }) : null, })); }; const updateHandoff = (patch: Partial>) => { setForm(prev => ({ ...prev, handoff: prev.handoff ? { ...prev.handoff, ...patch } : prev.handoff, })); }; const handleSave = async () => { const err = validateTemplateForm(form); if (err) { toast.error(err); return; } setSaving(true); try { if (isCreate) { const id = await interviewTemplatesService.generateId(form.title); await interviewTemplatesService.createTemplate(formToTemplate(form, id)); track('interview_template_created'); toast.success(`Created interview template: ${form.title.trim()}`); } else { await interviewTemplatesService.updateTemplate(template.id, formToTemplate(form, template.id)); toast.success(`Updated interview template: ${form.title.trim()}`); } onSaved(); } catch (e) { const message = e instanceof Error ? e.message : 'Failed to save interview template'; toast.error(message); } finally { setSaving(false); } }; return (
{/* Header */}

{readOnly ? 'View Interview Template' : isCreate ? 'Create Interview Template' : 'Edit Interview Template'}

{readOnly ? 'Built-in templates cannot be edited. Duplicate it to make your own version.' : 'Define the questions and completion criteria for a guided interview.'}

{!readOnly && ( )}
{/* Body */}
{readOnly && (
This is a built-in template shown for reference. To customize it, use Duplicate from the list.
)} {!readOnly && (

Interview mode is one of the workspace interaction modes, alongside Chat and Code, picked from the mode selector in the chat panel. In it you choose a template like this one, and the agent works through your items as a conversation, generally one at a time. It cannot finish until every required item is covered.

It records what it learns into an artifact: a Markdown notes file under /.interviews/. The interview agent reads the whole project freely but can only write inside that one folder, so an interview never changes your actual files. Turning those notes into real work is a separate step, which you can offer as a one-tap handoff (below).

)}
handleTitleChange(e.target.value)} disabled={readOnly} className="mt-1.5" />

Name it by what it produces, like the built-ins: "Understand a company", "Plan a feature". Shown in the picker.

setForm(prev => ({ ...prev, description: e.target.value }))} disabled={readOnly} className="mt-1.5" />

One sentence: what the interview gathers and what it is for.

setForm(prev => ({ ...prev, artifactPath: e.target.value }))} disabled={readOnly} className="mt-1.5 font-mono text-sm" />

The artifact: the Markdown file this interview writes its findings into. Give it a meaningful name. Must live under /.interviews/ and end in .md.

{/* Items */}
{!readOnly && ( )}

Each item is one thing to gather. The question is what to learn from the user; the "done when" criteria is the checkable condition the completion check reads the artifact against. Order the items the way the conversation should flow.

{form.items.map((item, i) => (
Item {i + 1} {!readOnly && ( )}