import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import type { ReviewStyle } from "@/lib/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@/components/ui/combobox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { Textarea } from "@/components/ui/textarea"; import { ApiError, api, isGithubReauthError, loginUrl } from "@/lib/api"; import { normalizeRepoFullName } from "@/lib/repo"; function formatMutationError(e: Error): string { return isGithubReauthError(e) ? "GitHub token expired — sign in again using the link above." : e.message; } function statusVariant(status: ReviewStyle["status"]) { switch (status) { case "completed": return "default" as const; case "running": return "secondary" as const; case "failed": return "destructive" as const; default: return "outline" as const; } } export function ReviewStylesPanel() { const qc = useQueryClient(); const [error, setError] = useState(null); const [addRepo, setAddRepo] = useState(""); const [selected, setSelected] = useState(null); const [draftPrompt, setDraftPrompt] = useState(""); const styles = useQuery({ queryKey: ["reviewStyles"], queryFn: api.listReviewStyles, refetchInterval: (q) => { const hasRunning = (q.state.data ?? []).some((r) => r.status === "running"); return hasRunning ? 4000 : false; }, }); const repos = useQuery({ queryKey: ["repos"], queryFn: async () => { try { return await api.repos(); } catch (e) { if (e instanceof ApiError && e.status === 401) return { installations: [], repositories: [] }; throw e; } }, }); const detail = useQuery({ queryKey: ["reviewStyle", selected], queryFn: () => api.getReviewStyle(selected!), enabled: !!selected, refetchInterval: (q) => (q.state.data?.status === "running" ? 4000 : false), }); useEffect(() => { if (detail.data?.custom_prompt != null) { setDraftPrompt(detail.data.custom_prompt); } else if (detail.data) { setDraftPrompt(""); } }, [detail.data?.custom_prompt, detail.data?.full_name]); const createStyle = useMutation({ mutationFn: (full_name: string) => api.createReviewStyle(full_name), onSuccess: (record) => { void qc.invalidateQueries({ queryKey: ["reviewStyles"] }); setSelected(record.full_name); setError(null); }, onError: (e: Error) => setError(formatMutationError(e)), }); const analyze = useMutation({ mutationFn: (full_name: string) => api.analyzeReviewStyle(full_name), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["reviewStyles"] }); void qc.invalidateQueries({ queryKey: ["reviewStyle", selected] }); setError(null); }, onError: (e: Error) => setError(formatMutationError(e)), }); const savePrompt = useMutation({ mutationFn: ({ full_name, custom_prompt }: { full_name: string; custom_prompt: string }) => api.saveReviewStylePrompt(full_name, custom_prompt), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["reviewStyles"] }); void qc.invalidateQueries({ queryKey: ["reviewStyle", selected] }); setError(null); }, onError: (e: Error) => setError(formatMutationError(e)), }); const cancelAnalysis = useMutation({ mutationFn: (full_name: string) => api.cancelReviewStyle(full_name), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["reviewStyles"] }); void qc.invalidateQueries({ queryKey: ["reviewStyle", selected] }); setError(null); }, onError: (e: Error) => setError(formatMutationError(e)), }); const removeStyle = useMutation({ mutationFn: (full_name: string) => api.deleteReviewStyle(full_name), onSuccess: (_data, full_name) => { void qc.invalidateQueries({ queryKey: ["reviewStyles"] }); if (selected === full_name) { setSelected(null); setDraftPrompt(""); } setError(null); }, onError: (e: Error) => setError(formatMutationError(e)), }); if (styles.isLoading) { return ; } const configured = new Set((styles.data ?? []).map((s) => s.full_name)); const suggestedRepos = (repos.data?.repositories ?? []).filter( (r) => !configured.has(r.full_name), ); const normalizedAddRepo = normalizeRepoFullName(addRepo); const canAdd = normalizedAddRepo !== null && !configured.has(normalizedAddRepo); const active = detail.data ?? styles.data?.find((s) => s.full_name === selected) ?? null; const handleAdd = () => { if (!normalizedAddRepo || !canAdd) return; void createStyle .mutateAsync(normalizedAddRepo) .then(() => setAddRepo("")) .catch(() => undefined); }; const githubReauth = (repos.isError && isGithubReauthError(repos.error)) || (error !== null && /github token|re-login required/i.test(error)); return (
{githubReauth && (
Your GitHub connection expired.{" "} Sign in with GitHub again {" "} to list installed repos and run style analysis.
)}
setAddRepo(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleAdd(); } }} className="sm:flex-1" />
{suggestedRepos.length > 0 && ( r.full_name)} value={addRepo} onValueChange={(v) => setAddRepo(typeof v === "string" ? v : "")} > No matches {suggestedRepos.map((r) => ( {r.full_name} {r.private && ( private )} ))} )}

Repositories

{(styles.data ?? []).length === 0 ? (

No repositories yet.

) : (
    {(styles.data ?? []).map((s) => (
  • ))}
)}
{!selected || !active ? (

Select a repository above to view or edit its review style prompt.

) : ( <>

{active.full_name}

{active.status} {active.top_reviewers.length > 0 && ( Reviewers: {active.top_reviewers.join(", ")} )} {active.prs_sampled > 0 && ( {active.prs_sampled} PRs · {active.reviews_sampled} reviews sampled )}
{active.analysis_summary && (

{active.analysis_summary}

)} {active.error &&

{active.error}

}
{active.status === "running" && ( )}