Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect, useCallback } from 'react';4import type { InterviewTemplate } from '@/lib/interview/types';5import { interviewTemplatesService } from '@/lib/interview/templates-service';6import { track } from '@/lib/telemetry';7import { Button } from '@/components/ui/button';8import { Input } from '@/components/ui/input';9import { Badge } from '@/components/ui/badge';10import {11 Dialog,12 DialogContent,13 DialogDescription,14 DialogFooter,15 DialogHeader,16 DialogTitle,17} from '@/components/ui/dialog';18import { toast } from 'sonner';19import { Search, Plus, Edit, Copy, Trash2, Eye, ClipboardList, FileText } from 'lucide-react';20import { InterviewTemplateEditor } from './InterviewTemplateEditor';21 22interface InterviewTemplatesPanelProps {23 initialMode?: 'list' | 'create';24 onChanged?: () => void;25}26 27type View =28 | 'list'29 | { mode: 'create' }30 | { mode: 'edit'; template: InterviewTemplate }31 | { mode: 'view'; template: InterviewTemplate };32 33export function InterviewTemplatesPanel({34 initialMode = 'list',35 onChanged,36}: InterviewTemplatesPanelProps) {37 const [templates, setTemplates] = useState<InterviewTemplate[]>([]);38 const [view, setView] = useState<View>(initialMode === 'create' ? { mode: 'create' } : 'list');39 const [searchQuery, setSearchQuery] = useState('');40 const [showBuiltIn, setShowBuiltIn] = useState(true);41 const [showCustom, setShowCustom] = useState(true);42 const [templateToDelete, setTemplateToDelete] = useState<InterviewTemplate | null>(null);43 44 const reloadList = useCallback(async () => {45 try {46 const all = await interviewTemplatesService.getAllTemplates();47 setTemplates(all);48 } catch {49 toast.error('Failed to load interview templates');50 }51 }, []);52 53 useEffect(() => {54 reloadList();55 }, [reloadList]);56 57 const handleDuplicate = async (src: InterviewTemplate) => {58 try {59 const id = await interviewTemplatesService.generateId(src.title + ' copy');60 await interviewTemplatesService.createTemplate({61 ...src,62 id,63 title: `${src.title} copy`,64 isBuiltIn: false,65 });66 track('interview_template_created');67 await reloadList();68 onChanged?.();69 const created = await interviewTemplatesService.getTemplate(id);70 if (created) {71 toast.success(`Duplicated: ${src.title}`);72 setView({ mode: 'edit', template: created });73 }74 } catch (e) {75 const message = e instanceof Error ? e.message : 'Failed to duplicate template';76 toast.error(message);77 }78 };79 80 const confirmDelete = async () => {81 if (!templateToDelete) return;82 try {83 await interviewTemplatesService.deleteTemplate(templateToDelete.id);84 track('interview_template_deleted');85 toast.success(`Deleted: ${templateToDelete.title}`);86 await reloadList();87 onChanged?.();88 } catch (e) {89 const message = e instanceof Error ? e.message : 'Failed to delete template';90 toast.error(message);91 } finally {92 setTemplateToDelete(null);93 }94 };95 96 const handleEditorSaved = async () => {97 await reloadList();98 setView('list');99 onChanged?.();100 };101 102 const filtered = templates.filter(t => {103 const q = searchQuery.toLowerCase();104 const matchesSearch =105 t.title.toLowerCase().includes(q) || t.description.toLowerCase().includes(q);106 if (!matchesSearch) return false;107 if (t.isBuiltIn && !showBuiltIn) return false;108 if (!t.isBuiltIn && !showCustom) return false;109 return true;110 }).sort((a, b) => Number(!!a.isBuiltIn) - Number(!!b.isBuiltIn)); // custom first, then built-in111 112 const inEditor = view !== 'list';113 const editorTemplate =114 inEditor && view.mode === 'create' ? null : inEditor ? view.template : null;115 116 return (117 <>118 <div className="flex flex-col h-full">119 <div className="px-6 pt-6 pb-3 shrink-0">120 <div className="flex items-center gap-2">121 <ClipboardList className="w-5 h-5" />122 <h2 className="text-lg font-semibold leading-none tracking-tight">Interview Templates</h2>123 </div>124 <p className="text-sm text-muted-foreground mt-1.5">125 Manage the guided interviews available in interview mode.126 </p>127 </div>128 129 <div className="px-6 pb-3 shrink-0 flex flex-col gap-3">130 <div className="flex flex-col sm:flex-row gap-3">131 <div className="relative flex-1">132 <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />133 <Input134 placeholder="Search templates..."135 value={searchQuery}136 onChange={(e) => setSearchQuery(e.target.value)}137 className="pl-9"138 />139 </div>140 <Button size="sm" onClick={() => setView({ mode: 'create' })}>141 <Plus className="w-4 h-4 mr-2" />142 New143 </Button>144 </div>145 <div className="flex items-center gap-2 text-xs">146 <span className="text-muted-foreground">Show:</span>147 <Button148 variant={showBuiltIn ? 'default' : 'outline'}149 size="sm"150 className="h-7 px-2 gap-1.5"151 onClick={() => setShowBuiltIn(v => !v)}152 aria-pressed={showBuiltIn}153 >154 <FileText className="w-3 h-3" />155 Built-in156 </Button>157 <Button158 variant={showCustom ? 'default' : 'outline'}159 size="sm"160 className="h-7 px-2 gap-1.5"161 onClick={() => setShowCustom(v => !v)}162 aria-pressed={showCustom}163 >164 <ClipboardList className="w-3 h-3" />165 Custom166 </Button>167 </div>168 </div>169 170 <div className="flex-1 overflow-y-auto px-6 pb-6">171 {filtered.length === 0 ? (172 <div className="text-center py-12">173 <ClipboardList className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />174 <h3 className="text-lg font-semibold mb-2">No templates found</h3>175 <p className="text-muted-foreground mb-4">176 {!showBuiltIn && !showCustom177 ? 'Both Built-in and Custom are hidden. Enable at least one above.'178 : searchQuery179 ? 'Try a different search query'180 : 'Create your first interview template'}181 </p>182 {!searchQuery && (183 <Button onClick={() => setView({ mode: 'create' })}>184 <Plus className="w-4 h-4 mr-2" />185 New Template186 </Button>187 )}188 </div>189 ) : (190 <div className="grid gap-3">191 {filtered.map(t => (192 <div key={t.id} className="border rounded-lg p-4">193 <div className="flex items-start justify-between gap-4">194 <div className="flex-1 min-w-0">195 <div className="flex items-center gap-2 mb-1 flex-wrap">196 <h3 className="font-semibold truncate">{t.title}</h3>197 <Badge variant={t.isBuiltIn ? 'secondary' : 'outline'} className="text-xs">198 {t.isBuiltIn ? 'Built-in' : 'Custom'}199 </Badge>200 </div>201 <p className="text-sm text-muted-foreground line-clamp-2">{t.description}</p>202 {t.artifacts[0] && (203 <p className="text-xs text-muted-foreground/80 mt-1 font-mono truncate">204 {t.artifacts[0].path}205 </p>206 )}207 </div>208 <div className="flex items-center gap-1 shrink-0">209 {t.isBuiltIn ? (210 <>211 <Button212 variant="ghost"213 size="sm"214 onClick={() => setView({ mode: 'view', template: t })}215 title="View"216 >217 <Eye className="w-4 h-4" />218 </Button>219 <Button220 variant="ghost"221 size="sm"222 onClick={() => handleDuplicate(t)}223 title="Duplicate"224 >225 <Copy className="w-4 h-4" />226 </Button>227 </>228 ) : (229 <>230 <Button231 variant="ghost"232 size="sm"233 onClick={() => setView({ mode: 'edit', template: t })}234 title="Edit"235 >236 <Edit className="w-4 h-4" />237 </Button>238 <Button239 variant="ghost"240 size="sm"241 onClick={() => handleDuplicate(t)}242 title="Duplicate"243 >244 <Copy className="w-4 h-4" />245 </Button>246 <Button247 variant="ghost"248 size="sm"249 onClick={() => setTemplateToDelete(t)}250 title="Delete"251 >252 <Trash2 className="w-4 h-4" />253 </Button>254 </>255 )}256 </div>257 </div>258 </div>259 ))}260 </div>261 )}262 </div>263 </div>264 265 {/* Editor dialog (matches the Skills editor: a modal over the list) */}266 <Dialog open={inEditor} onOpenChange={(o) => !o && setView('list')}>267 <DialogContent className="max-w-[90vw] sm:max-w-[85vw] lg:max-w-3xl h-[90vh] p-0 overflow-hidden">268 <DialogHeader className="sr-only">269 <DialogTitle>270 {editorTemplate ? `Edit ${editorTemplate.title}` : 'Create interview template'}271 </DialogTitle>272 </DialogHeader>273 {inEditor && (274 <InterviewTemplateEditor275 template={editorTemplate}276 onSaved={handleEditorSaved}277 onCancel={() => setView('list')}278 />279 )}280 </DialogContent>281 </Dialog>282 283 {/* Delete confirmation */}284 <Dialog open={!!templateToDelete} onOpenChange={(o) => !o && setTemplateToDelete(null)}>285 <DialogContent>286 <DialogHeader>287 <DialogTitle>Delete Template</DialogTitle>288 <DialogDescription>289 {templateToDelete290 ? `Are you sure you want to delete "${templateToDelete.title}"? This action cannot be undone.`291 : ''}292 </DialogDescription>293 </DialogHeader>294 <DialogFooter>295 <Button variant="outline" onClick={() => setTemplateToDelete(null)}>296 Cancel297 </Button>298 <Button variant="destructive" onClick={confirmDelete}>299 Delete300 </Button>301 </DialogFooter>302 </DialogContent>303 </Dialog>304 </>305 );306}307 