CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
InterviewTemplatesPanel.tsx307 linesDownload Raw Back to interview
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