Spaces:
Sleeping
Sleeping
File size: 6,157 Bytes
30cd0c9 3baad7e 30cd0c9 3baad7e 30cd0c9 3baad7e 30cd0c9 15d8afd 30cd0c9 3baad7e 30cd0c9 15d8afd 30cd0c9 3baad7e 30cd0c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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>
);
}
|