Karan6124 commited on
Commit
8ea5095
Β·
1 Parent(s): 0e149eb

feat(ui): implement workspace section for deploying and viewing live agent investigations

Browse files
frontend/src/App.jsx CHANGED
@@ -2,12 +2,14 @@ import React, { useState } from "react";
2
  import Navbar from "./components/Navbar";
3
  import HeroSection from "./sections/HeroSection";
4
  import CapabilitiesSection from "./sections/CapabilitiesSection";
 
5
  import FadingVideo from "./components/FadingVideo";
6
  import { AuthProvider } from "./context/AuthContext";
7
  import AuthModal from "./components/AuthModal";
8
 
9
  function MainAppContent() {
10
  const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
 
11
 
12
  return (
13
  <div className="relative w-screen bg-black text-white selection:bg-white selection:text-black min-h-screen">
@@ -21,13 +23,36 @@ function MainAppContent() {
21
  </div>
22
 
23
  {/* Shared Navigation Header */}
24
- <Navbar onOpenAuth={() => setIsAuthModalOpen(true)} />
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- {/* Hero Section (Section 1) */}
27
- <HeroSection onOpenAuth={() => setIsAuthModalOpen(true)} />
 
 
 
 
 
 
28
 
29
- {/* Capabilities Section (Section 2) */}
30
- <CapabilitiesSection />
 
 
 
 
 
31
 
32
  {/* Auth Modal */}
33
  <AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
 
2
  import Navbar from "./components/Navbar";
3
  import HeroSection from "./sections/HeroSection";
4
  import CapabilitiesSection from "./sections/CapabilitiesSection";
5
+ import WorkspaceSection from "./sections/WorkspaceSection";
6
  import FadingVideo from "./components/FadingVideo";
7
  import { AuthProvider } from "./context/AuthContext";
8
  import AuthModal from "./components/AuthModal";
9
 
10
  function MainAppContent() {
11
  const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
12
+ const [view, setView] = useState("landing"); // 'landing' | 'workspace'
13
 
14
  return (
15
  <div className="relative w-screen bg-black text-white selection:bg-white selection:text-black min-h-screen">
 
23
  </div>
24
 
25
  {/* Shared Navigation Header */}
26
+ <Navbar
27
+ onOpenAuth={() => setIsAuthModalOpen(true)}
28
+ currentView={view}
29
+ onViewChange={(newView, hash) => {
30
+ setView(newView);
31
+ if (newView === "landing" && hash) {
32
+ setTimeout(() => {
33
+ const el = document.querySelector(hash);
34
+ if (el) el.scrollIntoView({ behavior: "smooth" });
35
+ }, 50);
36
+ }
37
+ }}
38
+ />
39
 
40
+ {/* View router */}
41
+ {view === "landing" ? (
42
+ <>
43
+ {/* Hero Section (Section 1) */}
44
+ <HeroSection
45
+ onOpenAuth={() => setIsAuthModalOpen(true)}
46
+ onViewWorkspace={() => setView("workspace")}
47
+ />
48
 
49
+ {/* Capabilities Section (Section 2) */}
50
+ <CapabilitiesSection />
51
+ </>
52
+ ) : (
53
+ /* Workspace / Chat Section */
54
+ <WorkspaceSection onBackToLanding={() => setView("landing")} />
55
+ )}
56
 
57
  {/* Auth Modal */}
58
  <AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
frontend/src/components/Navbar.jsx CHANGED
@@ -96,7 +96,7 @@ const AgentBondLogo = ({ className = "" }) => (
96
  </div>
97
  );
98
 
99
- export default function Navbar({ onOpenAuth }) {
100
  const { user, logout } = useAuth();
101
  const [menuOpen, setMenuOpen] = useState(false);
102
  const [scrolled, setScrolled] = useState(false);
@@ -115,6 +115,12 @@ export default function Navbar({ onOpenAuth }) {
115
  }
116
  };
117
 
 
 
 
 
 
 
118
  return (
119
  <>
120
  <motion.nav
@@ -129,7 +135,14 @@ export default function Navbar({ onOpenAuth }) {
129
  {/* ── COL 1: Logo ── */}
130
  <a
131
  href="#home"
132
- onClick={(e) => { e.preventDefault(); handleNavClick("#home"); }}
 
 
 
 
 
 
 
133
  className="justify-self-start hover:opacity-85 transition-opacity duration-200"
134
  aria-label="AgentBond AI β€” Go to home"
135
  >
@@ -140,14 +153,25 @@ export default function Navbar({ onOpenAuth }) {
140
  {/* ── COL 2: Nav pill β€” guaranteed screen center ── */}
141
  <div className="hidden md:flex justify-center">
142
  <div className="flex items-center gap-0.5 px-1.5 py-1.5 rounded-full liquid-glass">
143
- {NAV_LINKS.map(({ label, href }) => (
144
  <a
145
- key={href}
146
- href={href}
147
- onClick={(e) => { e.preventDefault(); handleNavClick(href); }}
148
- className="px-4 py-2 text-sm font-medium text-white/80 font-body hover:text-white hover:bg-white/[0.08] rounded-full transition-all duration-200"
 
 
 
 
 
 
 
 
 
 
 
149
  >
150
- {label}
151
  </a>
152
  ))}
153
 
@@ -186,45 +210,43 @@ export default function Navbar({ onOpenAuth }) {
186
  </div>
187
 
188
  {/* ── COL 3: Social icons + Mobile hamburger ── */}
189
- <div className="flex items-center gap-2 justify-self-end">
190
- {/* LinkedIn */}
191
  <a
192
  href="https://www.linkedin.com/in/karan-shelar-779381343/"
193
  target="_blank"
194
  rel="noopener noreferrer"
195
- aria-label="Karan Shelar on LinkedIn"
196
- className="w-10 h-10 rounded-full liquid-glass flex items-center justify-center text-white/80 hover:text-white hover:scale-105 active:scale-95 transition-all duration-200"
197
  >
198
  <LinkedinIcon />
199
  </a>
200
-
201
- {/* GitHub Profile */}
202
  <a
203
  href="https://github.com/Edge-Explorer"
204
  target="_blank"
205
  rel="noopener noreferrer"
206
- aria-label="Edge-Explorer on GitHub"
207
- className="w-10 h-10 rounded-full liquid-glass flex items-center justify-center text-white/80 hover:text-white hover:scale-105 active:scale-95 transition-all duration-200"
208
  >
209
  <GithubIcon />
210
  </a>
211
-
212
- {/* GitHub Repo */}
213
  <a
214
  href="https://github.com/Edge-Explorer/AgentBond-AI"
215
  target="_blank"
216
  rel="noopener noreferrer"
217
- aria-label="AgentBond-AI Repository"
218
- className="hidden sm:flex w-10 h-10 rounded-full liquid-glass items-center justify-center text-white/80 hover:text-white hover:scale-105 active:scale-95 transition-all duration-200"
219
  >
220
  <RepoIcon />
221
  </a>
 
222
 
223
- {/* Mobile hamburger */}
 
224
  <button
225
- onClick={() => setMenuOpen((v) => !v)}
226
- aria-label="Toggle mobile menu"
227
- className="md:hidden w-10 h-10 rounded-full liquid-glass flex items-center justify-center text-white/80 hover:text-white transition-all"
 
228
  >
229
  {menuOpen ? <CloseIcon /> : <MenuIcon />}
230
  </button>
@@ -250,14 +272,26 @@ export default function Navbar({ onOpenAuth }) {
250
 
251
  <div className="h-px bg-white/10 mb-1" />
252
 
253
- {NAV_LINKS.map(({ label, href }) => (
254
  <a
255
- key={href}
256
- href={href}
257
- onClick={(e) => { e.preventDefault(); handleNavClick(href); }}
258
- className="px-4 py-3 rounded-xl text-sm font-medium text-white/90 font-body hover:bg-white/[0.08] hover:text-white transition-all"
 
 
 
 
 
 
 
 
 
 
 
 
259
  >
260
- {label}
261
  </a>
262
  ))}
263
 
 
96
  </div>
97
  );
98
 
99
+ export default function Navbar({ onOpenAuth, currentView, onViewChange }) {
100
  const { user, logout } = useAuth();
101
  const [menuOpen, setMenuOpen] = useState(false);
102
  const [scrolled, setScrolled] = useState(false);
 
115
  }
116
  };
117
 
118
+ const links = [
119
+ { label: "Home", href: "#home", view: "landing" },
120
+ { label: "Capabilities", href: "#capabilities", view: "landing" },
121
+ ...(user ? [{ label: "Workspace", href: "#workspace", view: "workspace" }] : []),
122
+ ];
123
+
124
  return (
125
  <>
126
  <motion.nav
 
135
  {/* ── COL 1: Logo ── */}
136
  <a
137
  href="#home"
138
+ onClick={(e) => {
139
+ e.preventDefault();
140
+ if (onViewChange) {
141
+ onViewChange("landing", "#home");
142
+ } else {
143
+ handleNavClick("#home");
144
+ }
145
+ }}
146
  className="justify-self-start hover:opacity-85 transition-opacity duration-200"
147
  aria-label="AgentBond AI β€” Go to home"
148
  >
 
153
  {/* ── COL 2: Nav pill β€” guaranteed screen center ── */}
154
  <div className="hidden md:flex justify-center">
155
  <div className="flex items-center gap-0.5 px-1.5 py-1.5 rounded-full liquid-glass">
156
+ {links.map((item) => (
157
  <a
158
+ key={item.href}
159
+ href={item.href}
160
+ onClick={(e) => {
161
+ e.preventDefault();
162
+ if (onViewChange) {
163
+ onViewChange(item.view, item.href);
164
+ } else {
165
+ handleNavClick(item.href);
166
+ }
167
+ }}
168
+ className={`px-4 py-2 text-sm font-medium font-body rounded-full transition-all duration-200 ${
169
+ currentView === item.view
170
+ ? "text-white bg-white/[0.08]"
171
+ : "text-white/80 hover:text-white hover:bg-white/[0.08]"
172
+ }`}
173
  >
174
+ {item.label}
175
  </a>
176
  ))}
177
 
 
210
  </div>
211
 
212
  {/* ── COL 3: Social icons + Mobile hamburger ── */}
213
+ <div className="hidden md:flex items-center justify-self-end gap-3">
 
214
  <a
215
  href="https://www.linkedin.com/in/karan-shelar-779381343/"
216
  target="_blank"
217
  rel="noopener noreferrer"
218
+ className="w-8 h-8 rounded-full border border-white/10 hover:border-white/20 hover:bg-white/[0.05] flex items-center justify-center text-white/70 hover:text-white transition-all duration-200"
219
+ title="LinkedIn Profile"
220
  >
221
  <LinkedinIcon />
222
  </a>
 
 
223
  <a
224
  href="https://github.com/Edge-Explorer"
225
  target="_blank"
226
  rel="noopener noreferrer"
227
+ className="w-8 h-8 rounded-full border border-white/10 hover:border-white/20 hover:bg-white/[0.05] flex items-center justify-center text-white/70 hover:text-white transition-all duration-200"
228
+ title="Github Profile"
229
  >
230
  <GithubIcon />
231
  </a>
 
 
232
  <a
233
  href="https://github.com/Edge-Explorer/AgentBond-AI"
234
  target="_blank"
235
  rel="noopener noreferrer"
236
+ className="w-8 h-8 rounded-full border border-white/10 hover:border-white/20 hover:bg-white/[0.05] flex items-center justify-center text-white/70 hover:text-white transition-all duration-200"
237
+ title="Repository"
238
  >
239
  <RepoIcon />
240
  </a>
241
+ </div>
242
 
243
+ {/* ── Mobile Menu toggle button ── */}
244
+ <div className="col-start-3 justify-self-end md:hidden flex items-center">
245
  <button
246
+ onClick={() => setMenuOpen(!menuOpen)}
247
+ className="p-2 rounded-lg border border-white/10 bg-white/5 hover:bg-white/10 transition-colors text-white/80 hover:text-white"
248
+ aria-expanded={menuOpen}
249
+ aria-label="Toggle menu"
250
  >
251
  {menuOpen ? <CloseIcon /> : <MenuIcon />}
252
  </button>
 
272
 
273
  <div className="h-px bg-white/10 mb-1" />
274
 
275
+ {links.map((item) => (
276
  <a
277
+ key={item.href}
278
+ href={item.href}
279
+ onClick={(e) => {
280
+ e.preventDefault();
281
+ setMenuOpen(false);
282
+ if (onViewChange) {
283
+ onViewChange(item.view, item.href);
284
+ } else {
285
+ handleNavClick(item.href);
286
+ }
287
+ }}
288
+ className={`px-4 py-3 rounded-xl text-sm font-medium font-body transition-all ${
289
+ currentView === item.view
290
+ ? "text-white bg-white/[0.08]"
291
+ : "text-white/90 hover:bg-white/[0.08] hover:text-white"
292
+ }`}
293
  >
294
+ {item.label}
295
  </a>
296
  ))}
297
 
frontend/src/sections/HeroSection.jsx CHANGED
@@ -3,7 +3,7 @@ import { motion } from "framer-motion";
3
  import BlurText from "../components/BlurText";
4
  import { useAuth } from "../context/AuthContext";
5
 
6
- export default function HeroSection({ onOpenAuth }) {
7
  const { user } = useAuth();
8
  const containerVariants = {
9
  initial: {},
@@ -109,7 +109,7 @@ export default function HeroSection({ onOpenAuth }) {
109
  <button
110
  onClick={() => {
111
  if (user) {
112
- alert(`Welcome back, ${user.name || 'Agent'}. Accessing secure investigation node...`);
113
  } else {
114
  onOpenAuth();
115
  }
 
3
  import BlurText from "../components/BlurText";
4
  import { useAuth } from "../context/AuthContext";
5
 
6
+ export default function HeroSection({ onOpenAuth, onViewWorkspace }) {
7
  const { user } = useAuth();
8
  const containerVariants = {
9
  initial: {},
 
109
  <button
110
  onClick={() => {
111
  if (user) {
112
+ if (onViewWorkspace) onViewWorkspace();
113
  } else {
114
  onOpenAuth();
115
  }
frontend/src/sections/WorkspaceSection.jsx ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+
4
+ export default function WorkspaceSection({ onBackToLanding }) {
5
+ const [cases, setCases] = useState([]);
6
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
7
+ const [activeCase, setActiveCase] = useState(null);
8
+ const [loadingCases, setLoadingCases] = useState(true);
9
+
10
+ // New Case Form States
11
+ const [isCreatingNew, setIsCreatingNew] = useState(true);
12
+ const [problemStatement, setProblemStatement] = useState("");
13
+ const [constraints, setConstraints] = useState([""]);
14
+ const [isSubmitting, setIsSubmitting] = useState(false);
15
+
16
+ // Reference for keeping track of polling
17
+ const pollingIntervalRef = useRef(null);
18
+
19
+ // API base URL
20
+ const API_BASE_URL = "http://localhost:8000";
21
+
22
+ // Helper for auth headers
23
+ const getHeaders = () => {
24
+ const token = localStorage.getItem("token");
25
+ return {
26
+ "Content-Type": "application/json",
27
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
28
+ };
29
+ };
30
+
31
+ // Fetch past cases
32
+ const fetchCases = async () => {
33
+ try {
34
+ const res = await fetch(`${API_BASE_URL}/api/cases`, {
35
+ headers: getHeaders(),
36
+ });
37
+ if (res.ok) {
38
+ const data = await res.json();
39
+ setCases(data);
40
+ // If we don't have a selected case and not creating a new one, select the first case
41
+ if (data.length > 0 && !selectedCaseId && !isCreatingNew) {
42
+ setSelectedCaseId(data[0].case_id);
43
+ setIsCreatingNew(false);
44
+ }
45
+ }
46
+ } catch (err) {
47
+ console.error("Error fetching cases:", err);
48
+ } finally {
49
+ setLoadingCases(false);
50
+ }
51
+ };
52
+
53
+ // Fetch details for the selected case
54
+ const fetchCaseDetails = async (caseId) => {
55
+ try {
56
+ const res = await fetch(`${API_BASE_URL}/api/cases/${caseId}`, {
57
+ headers: getHeaders(),
58
+ });
59
+ if (res.ok) {
60
+ const data = await res.json();
61
+ setActiveCase(data);
62
+
63
+ // Update in cases list to sync status
64
+ setCases((prev) =>
65
+ prev.map((c) => (c.case_id === caseId ? { ...c, status: data.status } : c))
66
+ );
67
+
68
+ // Stop polling if completed or failed
69
+ if (data.status === "completed" || data.status === "failed") {
70
+ stopPolling();
71
+ }
72
+ } else {
73
+ stopPolling();
74
+ }
75
+ } catch (err) {
76
+ console.error("Error fetching case details:", err);
77
+ stopPolling();
78
+ }
79
+ };
80
+
81
+ // Start polling active case details
82
+ const startPolling = (caseId) => {
83
+ stopPolling();
84
+ fetchCaseDetails(caseId);
85
+ pollingIntervalRef.current = setInterval(() => {
86
+ fetchCaseDetails(caseId);
87
+ }, 1500);
88
+ };
89
+
90
+ // Stop polling
91
+ const stopPolling = () => {
92
+ if (pollingIntervalRef.current) {
93
+ clearInterval(pollingIntervalRef.current);
94
+ pollingIntervalRef.current = null;
95
+ }
96
+ };
97
+
98
+ useEffect(() => {
99
+ fetchCases();
100
+ return () => stopPolling();
101
+ }, []);
102
+
103
+ // Sync polling when selectedCaseId changes
104
+ useEffect(() => {
105
+ if (selectedCaseId) {
106
+ setIsCreatingNew(false);
107
+ startPolling(selectedCaseId);
108
+ } else {
109
+ setActiveCase(null);
110
+ stopPolling();
111
+ }
112
+ return () => stopPolling();
113
+ }, [selectedCaseId]);
114
+
115
+ // Handle selection of a case
116
+ const handleSelectCase = (caseId) => {
117
+ setSelectedCaseId(caseId);
118
+ };
119
+
120
+ // Add constraint input
121
+ const addConstraint = () => {
122
+ setConstraints([...constraints, ""]);
123
+ };
124
+
125
+ // Remove constraint input
126
+ const removeConstraint = (index) => {
127
+ setConstraints(constraints.filter((_, i) => i !== index));
128
+ };
129
+
130
+ // Update constraint text
131
+ const updateConstraint = (index, value) => {
132
+ const next = [...constraints];
133
+ next[index] = value;
134
+ setConstraints(next);
135
+ };
136
+
137
+ // Submit case to backend
138
+ const handleSubmitCase = async (e) => {
139
+ e.preventDefault();
140
+ if (!problemStatement.trim()) return;
141
+
142
+ setIsSubmitting(true);
143
+ try {
144
+ // 1. Create the case context record
145
+ const cleanConstraints = constraints.filter((c) => c.trim() !== "");
146
+ const createRes = await fetch(`${API_BASE_URL}/api/cases`, {
147
+ method: "POST",
148
+ headers: getHeaders(),
149
+ body: JSON.stringify({
150
+ problem_statement: problemStatement,
151
+ constraints: cleanConstraints,
152
+ }),
153
+ });
154
+
155
+ if (!createRes.ok) throw new Error("Failed to initialize case");
156
+ const newCase = await createRes.json();
157
+
158
+ // 2. Trigger decomposition & Celery execution pipeline
159
+ const decompRes = await fetch(`${API_BASE_URL}/api/cases/${newCase.case_id}/decompose`, {
160
+ method: "POST",
161
+ headers: getHeaders(),
162
+ });
163
+
164
+ if (!decompRes.ok) throw new Error("Failed to deploy agent pipeline");
165
+ const activeContext = await decompRes.json();
166
+
167
+ // Refresh list, select the new case, and start polling
168
+ setProblemStatement("");
169
+ setConstraints([""]);
170
+ await fetchCases();
171
+ setSelectedCaseId(activeContext.case_id);
172
+ } catch (err) {
173
+ console.error(err);
174
+ alert(err.message || "An error occurred while deploying the agent pipeline.");
175
+ } finally {
176
+ setIsSubmitting(false);
177
+ }
178
+ };
179
+
180
+ return (
181
+ <section className="relative w-screen min-h-screen pt-24 pb-12 px-6 lg:px-12 flex gap-6 z-10 text-white select-none">
182
+ {/* ── SIDEBAR: PAST CASES ── */}
183
+ <div className="w-80 rounded-[1.25rem] liquid-glass flex flex-col p-5 h-[calc(100vh-160px)] shrink-0">
184
+ <div className="flex items-center justify-between mb-6">
185
+ <h2 className="font-heading italic text-2xl tracking-tight">Investigation nodes</h2>
186
+ <button
187
+ onClick={() => {
188
+ setSelectedCaseId(null);
189
+ setIsCreatingNew(true);
190
+ }}
191
+ className="w-8 h-8 rounded-full liquid-glass-strong hover:scale-105 active:scale-95 transition-all flex items-center justify-center cursor-pointer text-white/80 hover:text-white"
192
+ title="New investigation"
193
+ >
194
+ οΌ‹
195
+ </button>
196
+ </div>
197
+
198
+ {/* Scrollable list */}
199
+ <div className="flex-1 overflow-y-auto space-y-3 pr-2 scrollbar-thin">
200
+ {loadingCases ? (
201
+ <div className="flex items-center justify-center py-10 text-white/30 text-sm">
202
+ Connecting to context store...
203
+ </div>
204
+ ) : cases.length === 0 ? (
205
+ <div className="flex flex-col items-center justify-center py-12 text-center text-white/30 text-xs px-2">
206
+ <span className="text-xl mb-2">πŸ“</span>
207
+ No investigation sessions stored yet.
208
+ </div>
209
+ ) : (
210
+ cases.map((c) => (
211
+ <button
212
+ key={c.case_id}
213
+ onClick={() => handleSelectCase(c.case_id)}
214
+ className={`w-full text-left p-4 rounded-xl border transition-all duration-200 cursor-pointer flex flex-col justify-between gap-2.5 ${
215
+ selectedCaseId === c.case_id
216
+ ? "bg-white/[0.06] border-white/20 shadow-md shadow-[#7C5CFC]/10"
217
+ : "bg-white/[0.01] border-white/5 hover:bg-white/[0.03] hover:border-white/10"
218
+ }`}
219
+ >
220
+ <span className="text-xs font-body font-light text-white/90 line-clamp-2 leading-relaxed">
221
+ {c.problem_statement}
222
+ </span>
223
+
224
+ <div className="flex items-center justify-between w-full">
225
+ {/* Time */}
226
+ <span className="text-[10px] text-white/35 font-light font-body">
227
+ {c.updated_at ? new Date(c.updated_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : "Just now"}
228
+ </span>
229
+
230
+ {/* Status Badge */}
231
+ <span
232
+ className={`rounded-full px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wider ${
233
+ c.status === "completed"
234
+ ? "bg-green-500/10 text-green-400 border border-green-500/20"
235
+ : c.status === "failed"
236
+ ? "bg-red-500/10 text-red-400 border border-red-500/20"
237
+ : c.status === "investigating"
238
+ ? "bg-[#7C5CFC]/15 text-[#9E8EFD] border border-[#7C5CFC]/30 animate-pulse"
239
+ : "bg-white/5 text-white/40 border border-white/10"
240
+ }`}
241
+ >
242
+ {c.status}
243
+ </span>
244
+ </div>
245
+ </button>
246
+ ))
247
+ )}
248
+ </div>
249
+
250
+ {/* Back to Home CTA */}
251
+ <div className="mt-4 pt-4 border-t border-white/10">
252
+ <button
253
+ onClick={onBackToLanding}
254
+ className="w-full py-2.5 text-xs font-semibold tracking-wider text-white/50 hover:text-white rounded-full border border-white/5 hover:border-white/15 transition-all text-center flex items-center justify-center gap-2 cursor-pointer"
255
+ >
256
+ ← Back to Landing
257
+ </button>
258
+ </div>
259
+ </div>
260
+
261
+ {/* ── MAIN WORKSPACE ── */}
262
+ <div className="flex-1 rounded-[1.25rem] liquid-glass flex flex-col p-8 h-[calc(100vh-160px)] overflow-y-auto scrollbar-thin">
263
+ <AnimatePresence mode="wait">
264
+ {isCreatingNew ? (
265
+ /* CREATE CASE WORKSPACE */
266
+ <motion.div
267
+ key="create-workspace"
268
+ initial={{ opacity: 0, y: 15 }}
269
+ animate={{ opacity: 1, y: 0 }}
270
+ exit={{ opacity: 0, y: -15 }}
271
+ transition={{ duration: 0.3 }}
272
+ className="max-w-2xl mx-auto w-full flex flex-col justify-center min-h-full"
273
+ >
274
+ <div className="mb-8">
275
+ <h1 className="font-heading italic text-4xl text-white tracking-tight leading-none mb-3">
276
+ Deploy Autonomous Pipeline
277
+ </h1>
278
+ <p className="text-sm text-white/55 font-light font-body leading-relaxed">
279
+ Enter an open-ended scenario or problem statement. Our Case Manager will decompose it into structured hypotheses, which are verified against DuckDuckGo searches by autonomous investigators.
280
+ </p>
281
+ </div>
282
+
283
+ <form onSubmit={handleSubmitCase} className="space-y-6">
284
+ {/* Problem Input */}
285
+ <div className="space-y-2">
286
+ <label className="block text-xs font-semibold uppercase tracking-wider text-white/50">
287
+ Investigation Objective / Problem Statement
288
+ </label>
289
+ <textarea
290
+ required
291
+ value={problemStatement}
292
+ onChange={(e) => setProblemStatement(e.target.value)}
293
+ placeholder="e.g. Why are Nvidia's stock gains decelerating in Q2 2026?"
294
+ rows={4}
295
+ className="w-full rounded-xl border border-white/10 bg-white/[0.02] px-4 py-3 text-sm text-white placeholder-white/35 focus:border-[#7C5CFC]/40 focus:outline-none focus:bg-white/[0.04] transition-all resize-none font-body leading-relaxed"
296
+ />
297
+ </div>
298
+
299
+ {/* Constraints inputs */}
300
+ <div className="space-y-3">
301
+ <div className="flex items-center justify-between">
302
+ <label className="block text-xs font-semibold uppercase tracking-wider text-white/50">
303
+ Scope boundaries / Constraints (Optional)
304
+ </label>
305
+ <button
306
+ type="button"
307
+ onClick={addConstraint}
308
+ className="text-[10px] font-semibold text-[#9E8EFD] hover:text-white transition-colors cursor-pointer"
309
+ >
310
+ οΌ‹ Add Constraint
311
+ </button>
312
+ </div>
313
+
314
+ <div className="space-y-2">
315
+ {constraints.map((c, i) => (
316
+ <div key={i} className="flex items-center gap-2">
317
+ <input
318
+ type="text"
319
+ value={c}
320
+ onChange={(e) => updateConstraint(i, e.target.value)}
321
+ placeholder={`Constraint #${i + 1} (e.g. limit context to official reports)`}
322
+ className="flex-1 rounded-full border border-white/10 bg-white/[0.02] px-4 py-2 text-xs text-white placeholder-white/35 focus:border-[#7C5CFC]/40 focus:outline-none focus:bg-white/[0.03] transition-all font-body"
323
+ />
324
+ {constraints.length > 1 && (
325
+ <button
326
+ type="button"
327
+ onClick={() => removeConstraint(i)}
328
+ className="w-8 h-8 rounded-full border border-white/10 hover:border-red-500/30 hover:bg-red-500/10 hover:text-red-400 flex items-center justify-center transition-colors text-white/40 text-xs cursor-pointer"
329
+ >
330
+ βœ•
331
+ </button>
332
+ )}
333
+ </div>
334
+ ))}
335
+ </div>
336
+ </div>
337
+
338
+ {/* Submit button */}
339
+ <div className="pt-4 flex justify-end">
340
+ <button
341
+ type="submit"
342
+ disabled={isSubmitting || !problemStatement.trim()}
343
+ className="flex items-center gap-2.5 rounded-full px-8 py-3.5 text-sm font-semibold text-white liquid-glass-strong hover:scale-105 active:scale-95 transition-all duration-200 shadow-lg disabled:opacity-40 disabled:hover:scale-100 disabled:active:scale-100 cursor-pointer"
344
+ >
345
+ {isSubmitting ? (
346
+ <>
347
+ <svg className="animate-spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
348
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
349
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
350
+ </svg>
351
+ Deploying Pipeline...
352
+ </>
353
+ ) : (
354
+ <>
355
+ <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
356
+ <circle cx="11" cy="11" r="7" />
357
+ <line x1="16.5" y1="16.5" x2="21" y2="21" />
358
+ </svg>
359
+ Deploy Agent Pipeline
360
+ </>
361
+ )}
362
+ </button>
363
+ </div>
364
+ </form>
365
+ </motion.div>
366
+ ) : (
367
+ /* ACTIVE CASE DETAILS */
368
+ <motion.div
369
+ key="active-workspace"
370
+ initial={{ opacity: 0, y: 15 }}
371
+ animate={{ opacity: 1, y: 0 }}
372
+ exit={{ opacity: 0, y: -15 }}
373
+ transition={{ duration: 0.3 }}
374
+ className="space-y-8"
375
+ >
376
+ {/* Header block */}
377
+ <div className="flex flex-col md:flex-row md:items-start justify-between gap-4 border-b border-white/10 pb-6">
378
+ <div className="space-y-2 max-w-3xl">
379
+ <span className="text-[10px] font-semibold text-[#9E8EFD] uppercase tracking-widest">
380
+ Active case context
381
+ </span>
382
+ <h1 className="text-xl md:text-2xl font-body font-light text-white/95 leading-relaxed">
383
+ {activeCase?.problem_statement}
384
+ </h1>
385
+ {activeCase?.constraints && activeCase.constraints.length > 0 && (
386
+ <div className="flex flex-wrap items-center gap-1.5 pt-1.5">
387
+ <span className="text-[10px] text-white/35 font-medium uppercase mr-1">Scope:</span>
388
+ {activeCase.constraints.map((c, i) => (
389
+ <span key={i} className="text-[9px] rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-white/60">
390
+ {c}
391
+ </span>
392
+ ))}
393
+ </div>
394
+ )}
395
+ </div>
396
+
397
+ {/* State Badge */}
398
+ <div className="flex flex-col items-end gap-1.5 shrink-0">
399
+ <span
400
+ className={`rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wider ${
401
+ activeCase?.status === "completed"
402
+ ? "bg-green-500/10 text-green-400 border border-green-500/20"
403
+ : activeCase?.status === "failed"
404
+ ? "bg-red-500/10 text-red-400 border border-red-500/20"
405
+ : "bg-[#7C5CFC]/15 text-[#9E8EFD] border border-[#7C5CFC]/30 animate-pulse"
406
+ }`}
407
+ >
408
+ {activeCase?.status}
409
+ </span>
410
+ <span className="text-[9px] text-white/30 font-mono">
411
+ ID: {activeCase?.case_id.substring(0, 8)}...
412
+ </span>
413
+ </div>
414
+ </div>
415
+
416
+ {/* Status Visual Pipeline */}
417
+ <div className="grid grid-cols-3 gap-4">
418
+ {[
419
+ {
420
+ name: "Case Manager Agent",
421
+ desc: "Hypothesis generation",
422
+ active: activeCase?.status === "investigating" && activeCase?.hypotheses.length === 0,
423
+ done: activeCase?.hypotheses.length > 0,
424
+ },
425
+ {
426
+ name: "Research Agent",
427
+ desc: "Web queries & verification",
428
+ active: activeCase?.status === "investigating" && activeCase?.hypotheses.length > 0 && activeCase?.hypotheses.some(h => h.status === "investigating"),
429
+ done: activeCase?.status === "completed" || (activeCase?.hypotheses.length > 0 && activeCase?.hypotheses.every(h => h.status !== "pending" && h.status !== "investigating")),
430
+ },
431
+ {
432
+ name: "Synthesis Specialist",
433
+ desc: "Final verdicts & sorting",
434
+ active: activeCase?.status === "investigating" && activeCase?.hypotheses.length > 0 && activeCase?.hypotheses.every(h => h.status !== "pending" && h.status !== "investigating"),
435
+ done: activeCase?.status === "completed",
436
+ },
437
+ ].map((step, idx) => (
438
+ <div
439
+ key={idx}
440
+ className={`p-4 rounded-xl border transition-all duration-300 ${
441
+ step.active
442
+ ? "bg-[#7C5CFC]/5 border-[#7C5CFC]/30 shadow-md shadow-[#7C5CFC]/5"
443
+ : step.done
444
+ ? "bg-green-500/[0.02] border-green-500/25"
445
+ : "bg-white/[0.01] border-white/5 opacity-40"
446
+ }`}
447
+ >
448
+ <div className="flex items-center justify-between mb-2">
449
+ <span className="text-xs font-semibold tracking-wide block">
450
+ {step.name}
451
+ </span>
452
+ {step.active ? (
453
+ <span className="flex h-2 w-2 relative">
454
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#9E8EFD] opacity-75" />
455
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-[#7C5CFC]" />
456
+ </span>
457
+ ) : step.done ? (
458
+ <span className="text-green-400 text-xs font-bold">βœ“</span>
459
+ ) : (
460
+ <span className="text-white/20 text-[10px]">β—‹</span>
461
+ )}
462
+ </div>
463
+ <span className="text-[10px] text-white/50 block font-light leading-snug">
464
+ {step.desc}
465
+ </span>
466
+ </div>
467
+ ))}
468
+ </div>
469
+
470
+ {/* Grid: Hypotheses and Live Evidence */}
471
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
472
+ {/* ── Left side: Hypotheses ── */}
473
+ <div className="space-y-4">
474
+ <h3 className="font-heading italic text-2xl tracking-tight border-b border-white/5 pb-2">
475
+ Generated Hypotheses
476
+ </h3>
477
+
478
+ {activeCase?.hypotheses && activeCase.hypotheses.length > 0 ? (
479
+ <div className="space-y-3">
480
+ {activeCase.hypotheses.map((h, i) => (
481
+ <div
482
+ key={h.id || i}
483
+ className="bg-white/[0.02] border border-white/5 p-4 rounded-xl flex items-start justify-between gap-4 hover:border-white/10 transition-colors"
484
+ >
485
+ <div className="space-y-2">
486
+ <span className="text-[10px] font-mono font-semibold text-white/35">
487
+ HYP-{i + 1}
488
+ </span>
489
+ <p className="text-xs text-white/95 font-body font-light leading-relaxed">
490
+ {h.statement}
491
+ </p>
492
+ {h.assigned_investigator && (
493
+ <span className="inline-block text-[9px] text-[#9E8EFD] font-mono font-semibold uppercase tracking-wider">
494
+ πŸ•΅οΈ {h.assigned_investigator}
495
+ </span>
496
+ )}
497
+ </div>
498
+
499
+ {/* Hypothesis Status */}
500
+ <span
501
+ className={`rounded-full px-2 py-0.5 text-[8px] font-bold uppercase tracking-wider block shrink-0 ${
502
+ h.status === "verified"
503
+ ? "bg-green-500/10 text-green-400 border border-green-500/20"
504
+ : h.status === "disproved"
505
+ ? "bg-red-500/10 text-red-400 border border-red-500/20"
506
+ : h.status === "inconclusive"
507
+ ? "bg-orange-500/10 text-orange-400 border border-orange-500/20"
508
+ : h.status === "investigating"
509
+ ? "bg-[#7C5CFC]/15 text-[#9E8EFD] border border-[#7C5CFC]/30 animate-pulse"
510
+ : "bg-white/5 text-white/30 border border-white/5"
511
+ }`}
512
+ >
513
+ {h.status}
514
+ </span>
515
+ </div>
516
+ ))}
517
+ </div>
518
+ ) : (
519
+ <div className="flex flex-col items-center justify-center py-10 border border-dashed border-white/10 rounded-xl text-white/30 text-xs">
520
+ <svg className="animate-spin h-5 w-5 text-[#9E8EFD] mb-3" fill="none" viewBox="0 0 24 24">
521
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
522
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
523
+ </svg>
524
+ Case Manager formulating hypotheses...
525
+ </div>
526
+ )}
527
+ </div>
528
+
529
+ {/* ── Right side: Collected Evidence & Facts ── */}
530
+ <div className="space-y-4">
531
+ <h3 className="font-heading italic text-2xl tracking-tight border-b border-white/5 pb-2">
532
+ Evidence & Findings
533
+ </h3>
534
+
535
+ {activeCase?.evidence && activeCase.evidence.length > 0 ? (
536
+ <div className="space-y-3 max-h-[400px] overflow-y-auto pr-1 scrollbar-thin">
537
+ {activeCase.evidence.map((ev, i) => (
538
+ <div
539
+ key={ev.id || i}
540
+ className="bg-white/[0.01] border border-white/5 p-4 rounded-xl space-y-2"
541
+ >
542
+ <div className="flex items-center justify-between w-full">
543
+ <span className="text-[9px] font-semibold text-white/35 font-mono truncate max-w-[75%]">
544
+ πŸ” {ev.source}
545
+ </span>
546
+ <span className="text-[9px] rounded bg-white/5 border border-white/10 px-1 py-0.5 text-white/50 font-mono font-semibold">
547
+ Conf: {(ev.confidence * 100).toFixed(0)}%
548
+ </span>
549
+ </div>
550
+
551
+ <p className="text-xs font-body font-light text-white/85 leading-relaxed">
552
+ {ev.content}
553
+ </p>
554
+ </div>
555
+ ))}
556
+ </div>
557
+ ) : activeCase?.hypotheses && activeCase.hypotheses.length > 0 ? (
558
+ <div className="flex flex-col items-center justify-center py-16 border border-dashed border-white/10 rounded-xl text-white/30 text-xs">
559
+ <svg className="animate-spin h-5 w-5 text-[#9E8EFD] mb-3" fill="none" viewBox="0 0 24 24">
560
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
561
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
562
+ </svg>
563
+ Investigators fetching live facts from DuckDuckGo...
564
+ </div>
565
+ ) : (
566
+ <div className="flex flex-col items-center justify-center py-16 border border-dashed border-white/10 rounded-xl text-white/20 text-xs">
567
+ Awaiting hypotheses...
568
+ </div>
569
+ )}
570
+ </div>
571
+ </div>
572
+
573
+ {/* ── Conclusion Section ── */}
574
+ {activeCase?.status === "completed" && activeCase?.facts && activeCase.facts.length > 0 && (
575
+ <div className="bg-[#7C5CFC]/5 border border-[#7C5CFC]/20 p-6 rounded-xl space-y-3 mt-6">
576
+ <h3 className="font-heading italic text-2xl text-white">
577
+ Final Case Verdict Summary
578
+ </h3>
579
+ <div className="space-y-4">
580
+ {activeCase.facts
581
+ .filter((f) => f.source === "ResearchAgent")
582
+ .map((f, i) => (
583
+ <div key={i} className="text-xs font-body font-light text-white/90 leading-relaxed border-l-2 border-[#7C5CFC]/50 pl-3">
584
+ {f.content.replace("Hypothesis Verdict:", "")}
585
+ </div>
586
+ ))}
587
+ </div>
588
+ </div>
589
+ )}
590
+ </motion.div>
591
+ )}
592
+ </AnimatePresence>
593
+ </div>
594
+ </section>
595
+ );
596
+ }