E2E-Frontend-Data-Eyond / src /app /components /analysis /NewAnalysisDialog.tsx
harryagasi
[NOTICKET] feat(knowledge,analysis): add deletion confirmation, duplicate-name guard, and unusable-analysis lockout
3baad7e
Raw
History Blame Contribute Delete
6.16 kB
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>
);
}