Spaces:
Sleeping
Sleeping
harryagasi
[NOTICKET] feat(knowledge,analysis): add deletion confirmation, duplicate-name guard, and unusable-analysis lockout
3baad7e | import { useState } from "react"; | |
| import { Loader2, X } from "lucide-react"; | |
| import { createAnalysis, type Analysis, type DataBindItem } from "@/services/orchestrationApi"; | |
| import { BusinessQuestionsEditor } from "./BusinessQuestionsEditor"; | |
| import { DataBindSelector } from "./DataBindSelector"; | |
| import { compactQuestions } from "./utils"; | |
| function formatCreateAnalysisError(err: unknown) { | |
| const message = err instanceof Error ? err.message : "Failed to create analysis"; | |
| if (message.includes("unsupported jsonb version")) { | |
| return "Backend analysis creation is blocked by a JSONB storage error. The request payload is valid, but the Golang service/database returned an internal JSONB validation failure. Please fix POST /api/v1/analyses before creating an analysis."; | |
| } | |
| if (message.toLowerCase().includes("invalid request body")) { | |
| return "Backend rejected the analysis payload. Check that POST /api/v1/analyses accepts analysis_title, objective, business_questions, and data_bind as documented."; | |
| } | |
| return message; | |
| } | |
| function isDuplicateName(candidate: string, existing: Analysis[]): boolean { | |
| const normalized = candidate.trim().toLowerCase(); | |
| if (!normalized) return false; | |
| return existing.some((a) => a.analysis_title.trim().toLowerCase() === normalized); | |
| } | |
| interface NewAnalysisDialogProps { | |
| open: boolean; | |
| onClose: () => void; | |
| onCreated: (analysis: Analysis) => void; | |
| existingAnalyses: Analysis[]; | |
| } | |
| export function NewAnalysisDialog({ open, onClose, onCreated, existingAnalyses }: NewAnalysisDialogProps) { | |
| const [title, setTitle] = useState(""); | |
| const [objective, setObjective] = useState(""); | |
| const [questions, setQuestions] = useState<string[]>(["", ""]); | |
| const [dataBind, setDataBind] = useState<DataBindItem[]>([]); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| if (!open) return null; | |
| const duplicateName = isDuplicateName(title, existingAnalyses); | |
| const canSubmit = | |
| title.trim() && | |
| !duplicateName && | |
| objective.trim() && | |
| compactQuestions(questions).length >= 2 && | |
| dataBind.length > 0; | |
| const submit = async (event: React.FormEvent) => { | |
| event.preventDefault(); | |
| if (!canSubmit) return; | |
| setSubmitting(true); | |
| setError(null); | |
| try { | |
| const analysis = await createAnalysis({ | |
| analysis_title: title.trim(), | |
| objective: objective.trim(), | |
| business_questions: compactQuestions(questions), | |
| data_bind: dataBind, | |
| }); | |
| onCreated(analysis); | |
| setTitle(""); | |
| setObjective(""); | |
| setQuestions(["", ""]); | |
| setDataBind([]); | |
| onClose(); | |
| } catch (err) { | |
| setError(formatCreateAnalysisError(err)); | |
| } finally { | |
| setSubmitting(false); | |
| } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 p-4 backdrop-blur-sm"> | |
| <form onSubmit={submit} className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white shadow-2xl"> | |
| <div className="flex items-center justify-between border-b border-slate-200 px-5 py-4"> | |
| <div> | |
| <h2 className="text-base font-semibold text-slate-900">New Analysis</h2> | |
| <p className="text-xs text-slate-500">Set the goal and bind the sources before starting the conversation.</p> | |
| </div> | |
| <button type="button" aria-label="Close new analysis" onClick={onClose} className="rounded-md p-2 text-slate-500 hover:bg-slate-100"> | |
| <X className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| <div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4"> | |
| <div className="space-y-1.5"> | |
| <label htmlFor="analysis-title" className="text-xs font-medium text-slate-600">Title</label> | |
| <input | |
| id="analysis-title" | |
| value={title} | |
| onChange={(event) => setTitle(event.target.value)} | |
| disabled={submitting} | |
| placeholder="Q3 revenue movement" | |
| className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100" | |
| /> | |
| {duplicateName && ( | |
| <p className="text-xs text-red-600">An analysis with this name already exists. Choose a different name.</p> | |
| )} | |
| </div> | |
| <div className="space-y-1.5"> | |
| <label htmlFor="analysis-objective" className="text-xs font-medium text-slate-600">Objective</label> | |
| <textarea | |
| id="analysis-objective" | |
| value={objective} | |
| onChange={(event) => setObjective(event.target.value)} | |
| disabled={submitting} | |
| rows={3} | |
| placeholder="Find the drivers, risks, and next steps behind the business question." | |
| className="w-full resize-none rounded-lg border border-slate-200 px-3 py-2 text-sm outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100" | |
| /> | |
| </div> | |
| <BusinessQuestionsEditor value={questions} onChange={setQuestions} disabled={submitting} /> | |
| <DataBindSelector value={dataBind} onChange={setDataBind} disabled={submitting} /> | |
| {error && <p className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs leading-5 text-red-700">{error}</p>} | |
| </div> | |
| <div className="flex items-center justify-end gap-2 border-t border-slate-200 px-5 py-4"> | |
| <button type="button" onClick={onClose} className="rounded-md px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100"> | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| disabled={!canSubmit || submitting} | |
| className="inline-flex items-center gap-2 rounded-md bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50" | |
| > | |
| {submitting && <Loader2 className="h-4 w-4 animate-spin" />} | |
| Create Analysis | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| ); | |
| } | |