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>
  );
}