nothingworry commited on
Commit
345b8ff
·
1 Parent(s): 69aea0d

document deletion and improve tenant ID management

Browse files
backend/api/mcp_clients/rag_client.py CHANGED
@@ -10,7 +10,9 @@ class RAGClient:
10
  """
11
 
12
  def __init__(self):
13
- self.base_url = os.getenv("RAG_MCP_URL")
 
 
14
  self.search_endpoint = f"{self.base_url}/search"
15
  self.ingest_endpoint = f"{self.base_url}/ingest"
16
 
@@ -89,3 +91,65 @@ class RAGClient:
89
  except Exception as e:
90
  print("RAG List Error:", e)
91
  return {"documents": [], "total": 0, "limit": limit, "offset": offset}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  """
11
 
12
  def __init__(self):
13
+ self.base_url = os.getenv("RAG_MCP_URL", "http://localhost:8001")
14
+ if not self.base_url:
15
+ raise ValueError("RAG_MCP_URL environment variable is not set")
16
  self.search_endpoint = f"{self.base_url}/search"
17
  self.ingest_endpoint = f"{self.base_url}/ingest"
18
 
 
91
  except Exception as e:
92
  print("RAG List Error:", e)
93
  return {"documents": [], "total": 0, "limit": limit, "offset": offset}
94
+
95
+ async def delete_document(self, tenant_id: str, document_id: int):
96
+ """
97
+ Delete a specific document by ID for a tenant.
98
+ """
99
+ try:
100
+ async with httpx.AsyncClient(timeout=30.0) as client:
101
+ response = await client.delete(
102
+ f"{self.base_url}/delete/{document_id}",
103
+ params={"tenant_id": tenant_id}
104
+ )
105
+
106
+ if response.status_code == 404:
107
+ return {"error": f"Document {document_id} not found or access denied"}
108
+ if response.status_code != 200:
109
+ error_text = response.text
110
+ try:
111
+ error_json = response.json()
112
+ error_text = error_json.get("detail", error_text)
113
+ except:
114
+ pass
115
+ return {"error": f"HTTP {response.status_code}: {error_text}"}
116
+
117
+ data = response.json()
118
+ return data
119
+
120
+ except httpx.ConnectError as e:
121
+ print(f"RAG Delete Error: Cannot connect to RAG MCP server at {self.base_url}")
122
+ return {"error": f"Cannot connect to RAG MCP server. Is it running at {self.base_url}?"}
123
+ except Exception as e:
124
+ print(f"RAG Delete Error: {e}")
125
+ return {"error": str(e)}
126
+
127
+ async def delete_all_documents(self, tenant_id: str):
128
+ """
129
+ Delete all documents for a tenant.
130
+ """
131
+ try:
132
+ async with httpx.AsyncClient(timeout=30.0) as client:
133
+ response = await client.delete(
134
+ f"{self.base_url}/delete-all",
135
+ params={"tenant_id": tenant_id}
136
+ )
137
+
138
+ if response.status_code != 200:
139
+ error_text = response.text
140
+ try:
141
+ error_json = response.json()
142
+ error_text = error_json.get("detail", error_text)
143
+ except:
144
+ pass
145
+ return {"error": f"HTTP {response.status_code}: {error_text}"}
146
+
147
+ data = response.json()
148
+ return data
149
+
150
+ except httpx.ConnectError as e:
151
+ print(f"RAG Delete All Error: Cannot connect to RAG MCP server at {self.base_url}")
152
+ return {"error": f"Cannot connect to RAG MCP server. Is it running at {self.base_url}?"}
153
+ except Exception as e:
154
+ print(f"RAG Delete All Error: {e}")
155
+ return {"error": str(e)}
backend/api/routes/rag.py CHANGED
@@ -220,3 +220,53 @@ async def rag_list(
220
  return result
221
  except Exception as e:
222
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  return result
221
  except Exception as e:
222
  raise HTTPException(status_code=500, detail=str(e))
223
+
224
+
225
+ @router.delete("/delete/{document_id}")
226
+ async def rag_delete(
227
+ document_id: int,
228
+ x_tenant_id: str = Header(None)
229
+ ):
230
+ """
231
+ Delete a specific document by ID from tenant knowledge base.
232
+ """
233
+ if not x_tenant_id:
234
+ raise HTTPException(status_code=400, detail="Missing tenant ID")
235
+
236
+ try:
237
+ result = await rag_client.delete_document(x_tenant_id, document_id)
238
+ if "error" in result:
239
+ # Check if it's a connection error (500) or not found (404)
240
+ error_msg = result["error"]
241
+ if "Cannot connect" in error_msg:
242
+ raise HTTPException(status_code=503, detail=error_msg)
243
+ elif "not found" in error_msg.lower() or "access denied" in error_msg.lower():
244
+ raise HTTPException(status_code=404, detail=error_msg)
245
+ else:
246
+ raise HTTPException(status_code=500, detail=error_msg)
247
+ return result
248
+ except HTTPException:
249
+ raise
250
+ except Exception as e:
251
+ raise HTTPException(status_code=500, detail=str(e))
252
+
253
+
254
+ @router.delete("/delete-all")
255
+ async def rag_delete_all(
256
+ x_tenant_id: str = Header(None)
257
+ ):
258
+ """
259
+ Delete all documents for a tenant.
260
+ """
261
+ if not x_tenant_id:
262
+ raise HTTPException(status_code=400, detail="Missing tenant ID")
263
+
264
+ try:
265
+ result = await rag_client.delete_all_documents(x_tenant_id)
266
+ if "error" in result:
267
+ raise HTTPException(status_code=500, detail=result["error"])
268
+ return result
269
+ except HTTPException:
270
+ raise
271
+ except Exception as e:
272
+ raise HTTPException(status_code=500, detail=str(e))
backend/mcp_servers/database.py CHANGED
@@ -228,6 +228,65 @@ def list_all_documents(tenant_id: str, limit: int = 1000, offset: int = 0) -> Di
228
  return {"documents": [], "total": 0, "limit": limit, "offset": offset}
229
 
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  # -----------------------------------
232
  # Supabase Client (for REST operations)
233
  # -----------------------------------
 
228
  return {"documents": [], "total": 0, "limit": limit, "offset": offset}
229
 
230
 
231
+ def delete_document(tenant_id: str, document_id: int) -> bool:
232
+ """
233
+ Delete a specific document by ID for a tenant.
234
+ Returns True if document was deleted, False otherwise.
235
+ """
236
+ try:
237
+ conn = get_connection()
238
+ cur = conn.cursor()
239
+
240
+ # Delete the document (tenant_id check ensures tenant isolation)
241
+ cur.execute(
242
+ """
243
+ DELETE FROM documents
244
+ WHERE id = %s AND tenant_id = %s;
245
+ """,
246
+ (document_id, tenant_id)
247
+ )
248
+
249
+ deleted = cur.rowcount > 0
250
+ conn.commit()
251
+ cur.close()
252
+ conn.close()
253
+
254
+ return deleted
255
+
256
+ except Exception as e:
257
+ print("DB DELETE ERROR:", e)
258
+ return False
259
+
260
+
261
+ def delete_all_documents(tenant_id: str) -> int:
262
+ """
263
+ Delete all documents for a tenant.
264
+ Returns the number of documents deleted.
265
+ """
266
+ try:
267
+ conn = get_connection()
268
+ cur = conn.cursor()
269
+
270
+ cur.execute(
271
+ """
272
+ DELETE FROM documents
273
+ WHERE tenant_id = %s;
274
+ """,
275
+ (tenant_id,)
276
+ )
277
+
278
+ deleted_count = cur.rowcount
279
+ conn.commit()
280
+ cur.close()
281
+ conn.close()
282
+
283
+ return deleted_count
284
+
285
+ except Exception as e:
286
+ print("DB DELETE ALL ERROR:", e)
287
+ return 0
288
+
289
+
290
  # -----------------------------------
291
  # Supabase Client (for REST operations)
292
  # -----------------------------------
backend/mcp_servers/main.py CHANGED
@@ -21,7 +21,7 @@ sys.path.insert(0, os.path.join(parent_dir, "api")) # For utils
21
  # --------------------------------------------------------
22
 
23
  from embeddings import embed_text
24
- from database import insert_document_chunks, search_vectors, list_all_documents, initialize_database
25
  from utils.text_extractor import extract_text
26
 
27
 
@@ -168,6 +168,49 @@ def list_documents(tenant_id: str, limit: int = 1000, offset: int = 0):
168
  raise HTTPException(status_code=500, detail=str(e))
169
 
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  # --------------------------------------------------------
172
  # Allow "python main.py" to start server
173
  # --------------------------------------------------------
 
21
  # --------------------------------------------------------
22
 
23
  from embeddings import embed_text
24
+ from database import insert_document_chunks, search_vectors, list_all_documents, initialize_database, delete_document, delete_all_documents
25
  from utils.text_extractor import extract_text
26
 
27
 
 
168
  raise HTTPException(status_code=500, detail=str(e))
169
 
170
 
171
+ # --------------------------------------------------------
172
+ # Delete Document Route
173
+ # --------------------------------------------------------
174
+
175
+ @app.delete("/delete/{document_id}")
176
+ def delete_doc(document_id: int, tenant_id: str):
177
+ """
178
+ Delete a specific document by ID for a tenant.
179
+ """
180
+ try:
181
+ deleted = delete_document(tenant_id, document_id)
182
+ if not deleted:
183
+ raise HTTPException(status_code=404, detail="Document not found or access denied")
184
+
185
+ return {
186
+ "status": "ok",
187
+ "tenant_id": tenant_id,
188
+ "document_id": document_id,
189
+ "message": "Document deleted successfully"
190
+ }
191
+ except HTTPException:
192
+ raise
193
+ except Exception as e:
194
+ raise HTTPException(status_code=500, detail=str(e))
195
+
196
+
197
+ @app.delete("/delete-all")
198
+ def delete_all_docs(tenant_id: str):
199
+ """
200
+ Delete all documents for a tenant.
201
+ """
202
+ try:
203
+ deleted_count = delete_all_documents(tenant_id)
204
+ return {
205
+ "status": "ok",
206
+ "tenant_id": tenant_id,
207
+ "deleted_count": deleted_count,
208
+ "message": f"Deleted {deleted_count} document(s)"
209
+ }
210
+ except Exception as e:
211
+ raise HTTPException(status_code=500, detail=str(e))
212
+
213
+
214
  # --------------------------------------------------------
215
  # Allow "python main.py" to start server
216
  # --------------------------------------------------------
frontend/app/admin-rules/page.tsx CHANGED
@@ -1,9 +1,128 @@
 
 
 
1
  import Link from "next/link";
2
 
3
  import { AdminRulesPanel } from "@/components/admin-rules-panel";
4
  import { Footer } from "@/components/footer";
 
 
 
 
 
 
5
 
6
  export default function AdminRulesPage() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  return (
8
  <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
9
  <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
@@ -14,9 +133,12 @@ export default function AdminRulesPage() {
14
  </span>
15
  IntegraChat · Admin Rule Ingestion
16
  </div>
17
- <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
18
- Back Home
19
- </Link>
 
 
 
20
  </div>
21
  <p className="text-sm text-slate-300">
22
  Push governance policies, compliance workflows, and red-flag patterns to the backend&apos;s persistent rules
@@ -25,6 +147,107 @@ export default function AdminRulesPage() {
25
  </header>
26
 
27
  <AdminRulesPanel />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  <Footer />
29
  </main>
30
  );
 
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
  import Link from "next/link";
5
 
6
  import { AdminRulesPanel } from "@/components/admin-rules-panel";
7
  import { Footer } from "@/components/footer";
8
+ import { useTenant } from "@/contexts/TenantContext";
9
+ import { TenantSelector } from "@/components/tenant-selector";
10
+
11
+ const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://localhost:8000";
12
+
13
+ type StatusState = { tone: "info" | "success" | "error"; message: string } | null;
14
 
15
  export default function AdminRulesPage() {
16
+ const { tenantId } = useTenant();
17
+ const [rulesInput, setRulesInput] = useState("");
18
+ const [deleteInput, setDeleteInput] = useState("");
19
+ const [rules, setRules] = useState<string[]>([]);
20
+ const [loading, setLoading] = useState(false);
21
+ const [status, setStatus] = useState<StatusState>(null);
22
+
23
+ const headers = useMemo(() => {
24
+ if (!tenantId.trim()) return undefined;
25
+ return {
26
+ "Content-Type": "application/json",
27
+ "x-tenant-id": tenantId.trim(),
28
+ };
29
+ }, [tenantId]);
30
+
31
+ const requireTenant = useCallback(() => {
32
+ if (!tenantId.trim()) {
33
+ setStatus({ tone: "error", message: "Enter a tenant ID in the navbar first." });
34
+ return false;
35
+ }
36
+ return true;
37
+ }, [tenantId]);
38
+
39
+ const handleRefresh = useCallback(async () => {
40
+ if (!requireTenant()) return;
41
+ try {
42
+ setLoading(true);
43
+ setStatus({ tone: "info", message: "Loading rules..." });
44
+ const response = await fetch(`${BACKEND_BASE_URL}/admin/rules`, {
45
+ method: "GET",
46
+ headers,
47
+ });
48
+ if (!response.ok) {
49
+ throw new Error(`Backend error ${response.status}`);
50
+ }
51
+ const data = await response.json();
52
+ setRules(data.rules ?? []);
53
+ setStatus({ tone: "success", message: "Rules synced." });
54
+ } catch (error: any) {
55
+ setStatus({ tone: "error", message: error.message || "Failed to fetch rules" });
56
+ } finally {
57
+ setLoading(false);
58
+ }
59
+ }, [headers, requireTenant]);
60
+
61
+ const handleUpload = useCallback(async () => {
62
+ if (!requireTenant()) return;
63
+ const lines = rulesInput
64
+ .split("\n")
65
+ .map((line) => line.trim())
66
+ .filter(Boolean);
67
+
68
+ if (!lines.length) {
69
+ setStatus({ tone: "error", message: "Add at least one rule to upload." });
70
+ return;
71
+ }
72
+
73
+ try {
74
+ setLoading(true);
75
+ setStatus({ tone: "info", message: "Uploading rules..." });
76
+ const response = await fetch(`${BACKEND_BASE_URL}/admin/rules/bulk`, {
77
+ method: "POST",
78
+ headers,
79
+ body: JSON.stringify({ rules: lines }),
80
+ });
81
+ if (!response.ok) {
82
+ const details = await response.text();
83
+ throw new Error(details || `Backend error ${response.status}`);
84
+ }
85
+ await handleRefresh();
86
+ setRulesInput("");
87
+ setStatus({ tone: "success", message: "Rules uploaded successfully." });
88
+ } catch (error: any) {
89
+ setStatus({ tone: "error", message: error.message || "Failed to upload rules" });
90
+ } finally {
91
+ setLoading(false);
92
+ }
93
+ }, [handleRefresh, headers, requireTenant, rulesInput]);
94
+
95
+ const handleDelete = useCallback(async () => {
96
+ if (!requireTenant()) return;
97
+ if (!deleteInput.trim()) {
98
+ setStatus({ tone: "error", message: "Enter the rule text you want to delete." });
99
+ return;
100
+ }
101
+
102
+ try {
103
+ setLoading(true);
104
+ setStatus({ tone: "info", message: "Deleting rule..." });
105
+ const response = await fetch(
106
+ `${BACKEND_BASE_URL}/admin/rules/${encodeURIComponent(deleteInput.trim())}`,
107
+ {
108
+ method: "DELETE",
109
+ headers,
110
+ }
111
+ );
112
+ if (!response.ok) {
113
+ const details = await response.text();
114
+ throw new Error(details || `Backend error ${response.status}`);
115
+ }
116
+ await handleRefresh();
117
+ setDeleteInput("");
118
+ setStatus({ tone: "success", message: "Rule deleted." });
119
+ } catch (error: any) {
120
+ setStatus({ tone: "error", message: error.message || "Failed to delete rule" });
121
+ } finally {
122
+ setLoading(false);
123
+ }
124
+ }, [deleteInput, handleRefresh, headers, requireTenant]);
125
+
126
  return (
127
  <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
128
  <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
 
133
  </span>
134
  IntegraChat · Admin Rule Ingestion
135
  </div>
136
+ <div className="flex items-center gap-4">
137
+ <TenantSelector />
138
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
139
+ ← Back Home
140
+ </Link>
141
+ </div>
142
  </div>
143
  <p className="text-sm text-slate-300">
144
  Push governance policies, compliance workflows, and red-flag patterns to the backend&apos;s persistent rules
 
147
  </header>
148
 
149
  <AdminRulesPanel />
150
+
151
+ <section className="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-2xl shadow-slate-950/40">
152
+ <div className="flex flex-col gap-6">
153
+ <div className="flex items-center justify-end gap-3">
154
+ <button
155
+ onClick={handleRefresh}
156
+ disabled={loading}
157
+ className="flex-1 rounded-full bg-gradient-to-r from-cyan-400 to-blue-500 px-6 py-3 text-sm font-semibold text-slate-950 shadow-lg shadow-cyan-500/30 disabled:opacity-60"
158
+ >
159
+ Refresh Rules
160
+ </button>
161
+ </div>
162
+
163
+ <div className="grid gap-4 lg:grid-cols-2">
164
+ <label className="flex flex-col gap-2 text-sm font-semibold text-slate-200">
165
+ Bulk Upload Rules (one per line)
166
+ <textarea
167
+ value={rulesInput}
168
+ onChange={(e) => setRulesInput(e.target.value)}
169
+ placeholder="Disallow sharing salaries\nBlock deleting production data"
170
+ rows={6}
171
+ className="rounded-2xl border border-white/10 bg-slate-900/50 px-4 py-3 text-base text-white outline-none ring-0 focus:border-cyan-400"
172
+ />
173
+ </label>
174
+ <label className="flex flex-col gap-2 text-sm font-semibold text-slate-200">
175
+ Delete Rule (exact text match)
176
+ <textarea
177
+ value={deleteInput}
178
+ onChange={(e) => setDeleteInput(e.target.value)}
179
+ placeholder="Enter the exact rule text to remove..."
180
+ rows={6}
181
+ className="rounded-2xl border border-white/10 bg-slate-900/50 px-4 py-3 text-base text-white outline-none ring-0 focus:border-cyan-400"
182
+ />
183
+ </label>
184
+ </div>
185
+
186
+ <div className="flex flex-wrap gap-3">
187
+ <button
188
+ onClick={handleUpload}
189
+ disabled={loading}
190
+ className="rounded-full bg-gradient-to-r from-emerald-400 to-lime-400 px-6 py-3 text-sm font-semibold text-slate-900 shadow-lg shadow-emerald-500/30 disabled:opacity-60"
191
+ >
192
+ Upload / Append Rules
193
+ </button>
194
+ <button
195
+ onClick={handleDelete}
196
+ disabled={loading}
197
+ className="rounded-full border border-rose-500 px-6 py-3 text-sm font-semibold text-rose-300 transition hover:bg-rose-500/10 disabled:opacity-60"
198
+ >
199
+ Delete Rule
200
+ </button>
201
+ </div>
202
+
203
+ {status && (
204
+ <div
205
+ className={`rounded-2xl border px-4 py-3 text-sm ${
206
+ status.tone === "error"
207
+ ? "border-rose-500/40 bg-rose-500/10 text-rose-200"
208
+ : status.tone === "success"
209
+ ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-200"
210
+ : "border-cyan-500/40 bg-cyan-500/10 text-cyan-200"
211
+ }`}
212
+ >
213
+ {status.message}
214
+ </div>
215
+ )}
216
+
217
+ <div className="rounded-2xl border border-white/10 bg-slate-950/40">
218
+ <div className="flex items-center justify-between border-b border-white/5 px-5 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
219
+ <span>Rule Set</span>
220
+ <span>{rules.length} entries</span>
221
+ </div>
222
+ <div className="overflow-x-auto">
223
+ <table className="w-full text-left text-sm text-slate-200">
224
+ <thead className="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-400">
225
+ <tr>
226
+ <th className="px-4 py-3">#</th>
227
+ <th className="px-4 py-3">Rule</th>
228
+ </tr>
229
+ </thead>
230
+ <tbody>
231
+ {rules.length === 0 && (
232
+ <tr>
233
+ <td colSpan={2} className="px-4 py-6 text-center text-slate-500">
234
+ No rules loaded. Use the refresh button above.
235
+ </td>
236
+ </tr>
237
+ )}
238
+ {rules.map((rule, idx) => (
239
+ <tr key={`${rule}-${idx}`} className="border-t border-white/5">
240
+ <td className="px-4 py-3 text-slate-400">{idx + 1}</td>
241
+ <td className="px-4 py-3">{rule}</td>
242
+ </tr>
243
+ ))}
244
+ </tbody>
245
+ </table>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </section>
250
+
251
  <Footer />
252
  </main>
253
  );
frontend/app/analytics/page.tsx CHANGED
@@ -2,6 +2,7 @@ import Link from "next/link";
2
 
3
  import { AnalyticsPanel } from "@/components/analytics-panel";
4
  import { Footer } from "@/components/footer";
 
5
 
6
  export default function AnalyticsPage() {
7
  return (
@@ -14,9 +15,12 @@ export default function AnalyticsPage() {
14
  </span>
15
  IntegraChat · Analytics
16
  </div>
17
- <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
18
- Back Home
19
- </Link>
 
 
 
20
  </div>
21
  <p className="text-sm text-slate-300">
22
  Inspect tenant-wide metrics including tool usage, red-flag violations, and overall activity—all powered by the
 
2
 
3
  import { AnalyticsPanel } from "@/components/analytics-panel";
4
  import { Footer } from "@/components/footer";
5
+ import { TenantSelector } from "@/components/tenant-selector";
6
 
7
  export default function AnalyticsPage() {
8
  return (
 
15
  </span>
16
  IntegraChat · Analytics
17
  </div>
18
+ <div className="flex items-center gap-4">
19
+ <TenantSelector />
20
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
21
+ ← Back Home
22
+ </Link>
23
+ </div>
24
  </div>
25
  <p className="text-sm text-slate-300">
26
  Inspect tenant-wide metrics including tool usage, red-flag violations, and overall activity—all powered by the
frontend/app/chat/page.tsx CHANGED
@@ -2,6 +2,7 @@ import Link from "next/link";
2
 
3
  import { ChatPanel } from "@/components/chat-panel";
4
  import { Footer } from "@/components/footer";
 
5
 
6
  export default function ChatPage() {
7
  return (
@@ -14,9 +15,12 @@ export default function ChatPage() {
14
  </span>
15
  IntegraChat · Chat Bot
16
  </div>
17
- <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
18
- Back Home
19
- </Link>
 
 
 
20
  </div>
21
  <p className="text-sm text-slate-300">
22
  Experience the MCP agent orchestration layer with multi-tool reasoning, tenant isolation, and red-flag aware
 
2
 
3
  import { ChatPanel } from "@/components/chat-panel";
4
  import { Footer } from "@/components/footer";
5
+ import { TenantSelector } from "@/components/tenant-selector";
6
 
7
  export default function ChatPage() {
8
  return (
 
15
  </span>
16
  IntegraChat · Chat Bot
17
  </div>
18
+ <div className="flex items-center gap-4">
19
+ <TenantSelector />
20
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
21
+ ← Back Home
22
+ </Link>
23
+ </div>
24
  </div>
25
  <p className="text-sm text-slate-300">
26
  Experience the MCP agent orchestration layer with multi-tool reasoning, tenant isolation, and red-flag aware
frontend/app/ingestion/page.tsx CHANGED
@@ -2,6 +2,7 @@ import Link from "next/link";
2
 
3
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
4
  import { Footer } from "@/components/footer";
 
5
 
6
  export default function IngestionPage() {
7
  return (
@@ -14,9 +15,12 @@ export default function IngestionPage() {
14
  </span>
15
  IntegraChat · Data Ingestion
16
  </div>
17
- <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
18
- Back Home
19
- </Link>
 
 
 
20
  </div>
21
  <p className="text-sm text-slate-300">
22
  Upload raw text, URLs, or documents to feed the tenant-specific RAG index. All inputs flow into the FastAPI +
 
2
 
3
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
4
  import { Footer } from "@/components/footer";
5
+ import { TenantSelector } from "@/components/tenant-selector";
6
 
7
  export default function IngestionPage() {
8
  return (
 
15
  </span>
16
  IntegraChat · Data Ingestion
17
  </div>
18
+ <div className="flex items-center gap-4">
19
+ <TenantSelector />
20
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
21
+ ← Back Home
22
+ </Link>
23
+ </div>
24
  </div>
25
  <p className="text-sm text-slate-300">
26
  Upload raw text, URLs, or documents to feed the tenant-specific RAG index. All inputs flow into the FastAPI +
frontend/app/knowledge-base/page.tsx CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import { useState, useEffect } from "react";
4
  import Link from "next/link";
 
5
 
6
  type Document = {
7
  id: number;
@@ -20,15 +21,24 @@ const API_BASE =
20
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
21
 
22
  export default function KnowledgeBasePage() {
23
- const [tenantId, setTenantId] = useState("tenant123");
24
  const [documents, setDocuments] = useState<Document[]>([]);
25
  const [total, setTotal] = useState(0);
26
  const [loading, setLoading] = useState(false);
27
  const [error, setError] = useState<string | null>(null);
28
  const [searchFilter, setSearchFilter] = useState("");
29
  const [filterType, setFilterType] = useState<"all" | "pdf" | "text" | "faq" | "link">("all");
 
 
30
 
31
  async function loadDocuments() {
 
 
 
 
 
 
 
32
  setLoading(true);
33
  setError(null);
34
 
@@ -43,7 +53,18 @@ export default function KnowledgeBasePage() {
43
  );
44
 
45
  if (!response.ok) {
46
- throw new Error(`Failed to load documents: ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
 
49
  const data: DocumentListResponse = await response.json();
@@ -54,7 +75,7 @@ export default function KnowledgeBasePage() {
54
  setError(
55
  err instanceof Error
56
  ? err.message
57
- : "Failed to load knowledge base. Is the RAG MCP server running?",
58
  );
59
  } finally {
60
  setLoading(false);
@@ -62,8 +83,11 @@ export default function KnowledgeBasePage() {
62
  }
63
 
64
  useEffect(() => {
65
- loadDocuments();
66
- }, [tenantId]);
 
 
 
67
 
68
  // Filter documents based on search and type
69
  const filteredDocuments = documents.filter((doc) => {
@@ -122,6 +146,75 @@ export default function KnowledgeBasePage() {
122
  }
123
  };
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  return (
126
  <main className="mx-auto flex min-h-screen max-w-7xl flex-col gap-8 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
127
  {/* Header */}
@@ -141,12 +234,15 @@ export default function KnowledgeBasePage() {
141
  </div>
142
  </div>
143
  <div className="flex items-center gap-3">
144
- <input
145
- value={tenantId}
146
- onChange={(e) => setTenantId(e.target.value)}
147
- placeholder="Tenant ID"
148
- className="rounded-full border border-white/15 bg-white/10 px-4 py-1.5 text-sm text-white outline-none focus:border-cyan-300"
149
- />
 
 
 
150
  <button
151
  onClick={loadDocuments}
152
  disabled={loading}
@@ -205,8 +301,17 @@ export default function KnowledgeBasePage() {
205
  {/* Error Message */}
206
  {error && (
207
  <div className="rounded-2xl border border-red-500/40 bg-red-500/10 px-6 py-4 text-red-200">
208
- <p className="font-semibold">Error loading knowledge base</p>
209
  <p className="mt-1 text-sm">{error}</p>
 
 
 
 
 
 
 
 
 
210
  </div>
211
  )}
212
 
@@ -254,10 +359,19 @@ export default function KnowledgeBasePage() {
254
  <p className="text-sm leading-relaxed text-slate-200 line-clamp-6">
255
  {preview}
256
  </p>
257
- <div className="mt-4 flex items-center gap-2 text-xs text-slate-400">
258
- <span>ID: {doc.id}</span>
259
- <span>•</span>
260
- <span>{doc.text.length} chars</span>
 
 
 
 
 
 
 
 
 
261
  </div>
262
  </div>
263
  );
 
2
 
3
  import { useState, useEffect } from "react";
4
  import Link from "next/link";
5
+ import { useTenant } from "@/contexts/TenantContext";
6
 
7
  type Document = {
8
  id: number;
 
21
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
22
 
23
  export default function KnowledgeBasePage() {
24
+ const { tenantId, isLoading: tenantLoading } = useTenant();
25
  const [documents, setDocuments] = useState<Document[]>([]);
26
  const [total, setTotal] = useState(0);
27
  const [loading, setLoading] = useState(false);
28
  const [error, setError] = useState<string | null>(null);
29
  const [searchFilter, setSearchFilter] = useState("");
30
  const [filterType, setFilterType] = useState<"all" | "pdf" | "text" | "faq" | "link">("all");
31
+ const [isDeleting, setIsDeleting] = useState<number | null>(null);
32
+ const [isDeletingAll, setIsDeletingAll] = useState(false);
33
 
34
  async function loadDocuments() {
35
+ // Guard against empty tenant ID
36
+ if (!tenantId || !tenantId.trim()) {
37
+ setError("Please enter a tenant ID");
38
+ setLoading(false);
39
+ return;
40
+ }
41
+
42
  setLoading(true);
43
  setError(null);
44
 
 
53
  );
54
 
55
  if (!response.ok) {
56
+ const errorData = await response.json().catch(() => ({}));
57
+ const errorMsg = errorData.detail || errorData.message || `Failed to load documents (${response.status})`;
58
+
59
+ if (response.status === 400) {
60
+ throw new Error(errorMsg.includes("tenant")
61
+ ? "Missing tenant ID. Please enter a tenant ID in the navbar."
62
+ : errorMsg);
63
+ } else if (response.status === 503) {
64
+ throw new Error("Cannot connect to RAG MCP server. Please ensure the RAG server is running.");
65
+ } else {
66
+ throw new Error(errorMsg);
67
+ }
68
  }
69
 
70
  const data: DocumentListResponse = await response.json();
 
75
  setError(
76
  err instanceof Error
77
  ? err.message
78
+ : "Failed to load knowledge base. Please check if the backend services are running.",
79
  );
80
  } finally {
81
  setLoading(false);
 
83
  }
84
 
85
  useEffect(() => {
86
+ // Wait for tenant context to finish loading, then load documents if tenant ID is available
87
+ if (!tenantLoading && tenantId && tenantId.trim()) {
88
+ loadDocuments();
89
+ }
90
+ }, [tenantId, tenantLoading]);
91
 
92
  // Filter documents based on search and type
93
  const filteredDocuments = documents.filter((doc) => {
 
146
  }
147
  };
148
 
149
+ async function handleDeleteDocument(documentId: number) {
150
+ if (!tenantId.trim() || isDeleting !== null) return;
151
+ setIsDeleting(documentId);
152
+
153
+ try {
154
+ const response = await fetch(`${API_BASE}/rag/delete/${documentId}`, {
155
+ method: "DELETE",
156
+ headers: {
157
+ "x-tenant-id": tenantId,
158
+ },
159
+ });
160
+
161
+ if (!response.ok) {
162
+ const errorData = await response.json().catch(() => ({}));
163
+ const errorMsg = errorData.detail || `Failed to delete document: ${response.status}`;
164
+ throw new Error(errorMsg);
165
+ }
166
+
167
+ // Remove from local state and update total
168
+ setDocuments(docs => docs.filter(doc => doc.id !== documentId));
169
+ setTotal(prev => Math.max(0, prev - 1));
170
+ } catch (err) {
171
+ console.error(err);
172
+ setError(
173
+ err instanceof Error
174
+ ? err.message
175
+ : "Failed to delete document",
176
+ );
177
+ } finally {
178
+ setIsDeleting(null);
179
+ }
180
+ }
181
+
182
+ async function handleDeleteAll() {
183
+ if (!tenantId.trim() || isDeletingAll) return;
184
+
185
+ if (!confirm("Are you sure you want to delete ALL documents for this tenant? This action cannot be undone.")) {
186
+ return;
187
+ }
188
+
189
+ setIsDeletingAll(true);
190
+
191
+ try {
192
+ const response = await fetch(`${API_BASE}/rag/delete-all`, {
193
+ method: "DELETE",
194
+ headers: {
195
+ "x-tenant-id": tenantId,
196
+ },
197
+ });
198
+
199
+ if (!response.ok) {
200
+ throw new Error(`Failed to delete all documents: ${response.status}`);
201
+ }
202
+
203
+ const data = await response.json();
204
+ setDocuments([]);
205
+ setTotal(0);
206
+ } catch (err) {
207
+ console.error(err);
208
+ setError(
209
+ err instanceof Error
210
+ ? err.message
211
+ : "Failed to delete all documents",
212
+ );
213
+ } finally {
214
+ setIsDeletingAll(false);
215
+ }
216
+ }
217
+
218
  return (
219
  <main className="mx-auto flex min-h-screen max-w-7xl flex-col gap-8 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
220
  {/* Header */}
 
234
  </div>
235
  </div>
236
  <div className="flex items-center gap-3">
237
+ {documents.length > 0 && (
238
+ <button
239
+ onClick={handleDeleteAll}
240
+ disabled={isDeletingAll}
241
+ className="rounded-full border border-red-500/50 bg-red-500/10 px-5 py-2 text-sm font-semibold text-red-300 transition hover:bg-red-500/20 disabled:opacity-60"
242
+ >
243
+ {isDeletingAll ? "Deleting…" : "Delete All"}
244
+ </button>
245
+ )}
246
  <button
247
  onClick={loadDocuments}
248
  disabled={loading}
 
301
  {/* Error Message */}
302
  {error && (
303
  <div className="rounded-2xl border border-red-500/40 bg-red-500/10 px-6 py-4 text-red-200">
304
+ <p className="font-semibold">⚠️ Error loading knowledge base</p>
305
  <p className="mt-1 text-sm">{error}</p>
306
+ <button
307
+ onClick={() => {
308
+ setError(null);
309
+ loadDocuments();
310
+ }}
311
+ className="mt-3 rounded-lg border border-red-500/50 bg-red-500/20 px-4 py-2 text-sm font-semibold text-red-200 transition hover:bg-red-500/30"
312
+ >
313
+ Try Again
314
+ </button>
315
  </div>
316
  )}
317
 
 
359
  <p className="text-sm leading-relaxed text-slate-200 line-clamp-6">
360
  {preview}
361
  </p>
362
+ <div className="mt-4 flex items-center justify-between">
363
+ <div className="flex items-center gap-2 text-xs text-slate-400">
364
+ <span>ID: {doc.id}</span>
365
+ <span>•</span>
366
+ <span>{doc.text.length} chars</span>
367
+ </div>
368
+ <button
369
+ onClick={() => handleDeleteDocument(doc.id)}
370
+ disabled={isDeleting === doc.id}
371
+ className="rounded-lg border border-red-500/50 bg-red-500/10 px-3 py-1 text-xs font-semibold text-red-300 transition hover:bg-red-500/20 disabled:opacity-60"
372
+ >
373
+ {isDeleting === doc.id ? "Deleting…" : "Delete"}
374
+ </button>
375
  </div>
376
  </div>
377
  );
frontend/app/layout.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import type { Metadata } from "next";
2
  import { Geist, Geist_Mono } from "next/font/google";
3
  import "./globals.css";
 
4
 
5
  const geistSans = Geist({
6
  variable: "--font-geist-sans",
@@ -28,7 +29,9 @@ export default function RootLayout({
28
  <body
29
  className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30
  >
31
- {children}
 
 
32
  </body>
33
  </html>
34
  );
 
1
  import type { Metadata } from "next";
2
  import { Geist, Geist_Mono } from "next/font/google";
3
  import "./globals.css";
4
+ import { TenantProvider } from "@/contexts/TenantContext";
5
 
6
  const geistSans = Geist({
7
  variable: "--font-geist-sans",
 
29
  <body
30
  className={`${geistSans.variable} ${geistMono.variable} antialiased`}
31
  >
32
+ <TenantProvider>
33
+ {children}
34
+ </TenantProvider>
35
  </body>
36
  </html>
37
  );
frontend/app/page.tsx CHANGED
@@ -7,6 +7,7 @@ import { FeatureGrid } from "@/components/feature-grid";
7
  import { Footer } from "@/components/footer";
8
  import { Hero } from "@/components/hero";
9
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
 
10
 
11
  const navItems = [
12
  { label: "Data Ingestion", href: "/ingestion" },
@@ -20,42 +21,45 @@ export default function Home() {
20
  <main className="mx-auto flex min-h-screen max-w-6xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
21
  <header className="flex flex-col gap-6 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
22
  <div className="flex flex-wrap items-center justify-between gap-3 text-sm">
23
- <div className="flex items-center gap-3 text-base font-semibold">
24
- <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
25
- IC
26
- </span>
27
- IntegraChat Operator Console
28
- </div>
29
- <div className="flex flex-wrap items-center gap-3 text-xs uppercase tracking-[0.4em] text-slate-400">
30
- FastAPI · MCP Servers · Celery · Next.js
31
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </div>
33
- <nav className="flex flex-wrap gap-2">
34
- {navItems.map((item) => (
35
- <Link
36
- key={item.href}
37
- href={item.href}
38
- className="rounded-full border border-white/15 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-slate-200 transition hover:border-cyan-400 hover:text-white"
39
- >
40
- {item.label}
41
- </Link>
42
- ))}
43
- </nav>
44
  </header>
45
 
46
  <Hero />
47
  <FeatureGrid />
48
 
49
  <section id="data-ingestion" className="scroll-mt-28">
50
- <KnowledgeBasePanel />
51
  </section>
52
 
53
  <section id="chat-bot" className="scroll-mt-28">
54
- <ChatPanel />
55
  </section>
56
 
57
  <section id="analytics" className="scroll-mt-28">
58
- <AnalyticsPanel />
59
  </section>
60
 
61
  <section id="admin-rules" className="scroll-mt-28">
 
7
  import { Footer } from "@/components/footer";
8
  import { Hero } from "@/components/hero";
9
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
10
+ import { TenantSelector } from "@/components/tenant-selector";
11
 
12
  const navItems = [
13
  { label: "Data Ingestion", href: "/ingestion" },
 
21
  <main className="mx-auto flex min-h-screen max-w-6xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
22
  <header className="flex flex-col gap-6 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
23
  <div className="flex flex-wrap items-center justify-between gap-3 text-sm">
24
+ <div className="flex items-center gap-3 text-base font-semibold">
25
+ <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
26
+ IC
27
+ </span>
28
+ IntegraChat Operator Console
29
+ </div>
30
+ <div className="flex flex-wrap items-center gap-3 text-xs uppercase tracking-[0.4em] text-slate-400">
31
+ FastAPI · MCP Servers · Celery · Next.js
32
+ </div>
33
+ </div>
34
+ <div className="flex flex-wrap items-center justify-between gap-4">
35
+ <nav className="flex flex-wrap gap-2">
36
+ {navItems.map((item) => (
37
+ <Link
38
+ key={item.href}
39
+ href={item.href}
40
+ className="rounded-full border border-white/15 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-slate-200 transition hover:border-cyan-400 hover:text-white"
41
+ >
42
+ {item.label}
43
+ </Link>
44
+ ))}
45
+ </nav>
46
+ <TenantSelector />
47
  </div>
 
 
 
 
 
 
 
 
 
 
 
48
  </header>
49
 
50
  <Hero />
51
  <FeatureGrid />
52
 
53
  <section id="data-ingestion" className="scroll-mt-28">
54
+ <KnowledgeBasePanel />
55
  </section>
56
 
57
  <section id="chat-bot" className="scroll-mt-28">
58
+ <ChatPanel />
59
  </section>
60
 
61
  <section id="analytics" className="scroll-mt-28">
62
+ <AnalyticsPanel />
63
  </section>
64
 
65
  <section id="admin-rules" className="scroll-mt-28">
frontend/components/analytics-panel.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client";
2
 
3
  import { useState } from "react";
 
4
 
5
  type AnalyticsOverview = {
6
  overview: {
@@ -15,7 +16,7 @@ const API_BASE =
15
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
16
 
17
  export function AnalyticsPanel() {
18
- const [tenantId, setTenantId] = useState("tenant123");
19
  const [loading, setLoading] = useState(false);
20
  const [error, setError] = useState<string | null>(null);
21
  const [data, setData] = useState<AnalyticsOverview["overview"] | null>(null);
@@ -58,16 +59,6 @@ export function AnalyticsPanel() {
58
  </p>
59
  <h2 className="mt-2 text-3xl font-semibold">Analytics snapshot</h2>
60
  </div>
61
- <div className="flex flex-col gap-2 text-sm text-slate-200">
62
- <label className="text-xs uppercase tracking-widest text-slate-400">
63
- Tenant ID
64
- </label>
65
- <input
66
- value={tenantId}
67
- onChange={(e) => setTenantId(e.target.value)}
68
- className="rounded-full border border-white/15 bg-white/10 px-4 py-1.5 text-sm text-white outline-none focus:border-cyan-300"
69
- />
70
- </div>
71
  <button
72
  onClick={fetchAnalytics}
73
  disabled={loading}
 
1
  "use client";
2
 
3
  import { useState } from "react";
4
+ import { useTenant } from "@/contexts/TenantContext";
5
 
6
  type AnalyticsOverview = {
7
  overview: {
 
16
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
17
 
18
  export function AnalyticsPanel() {
19
+ const { tenantId } = useTenant();
20
  const [loading, setLoading] = useState(false);
21
  const [error, setError] = useState<string | null>(null);
22
  const [data, setData] = useState<AnalyticsOverview["overview"] | null>(null);
 
59
  </p>
60
  <h2 className="mt-2 text-3xl font-semibold">Analytics snapshot</h2>
61
  </div>
 
 
 
 
 
 
 
 
 
 
62
  <button
63
  onClick={fetchAnalytics}
64
  disabled={loading}
frontend/components/chat-panel.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client";
2
 
3
  import { useMemo, useState } from "react";
 
4
 
5
  type Message = {
6
  role: "user" | "assistant" | "system";
@@ -12,7 +13,7 @@ const API_BASE =
12
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
13
 
14
  export function ChatPanel() {
15
- const [tenantId, setTenantId] = useState("tenant123");
16
  const [message, setMessage] = useState("");
17
  const [isSending, setIsSending] = useState(false);
18
  const [history, setHistory] = useState<Message[]>([
@@ -106,25 +107,13 @@ export function ChatPanel() {
106
  className="gradient-border relative rounded-[28px] p-1 text-white"
107
  >
108
  <div className="glass-panel relative rounded-[26px] p-6">
109
- <div className="flex flex-wrap items-center justify-between gap-4">
110
- <div>
111
- <p className="text-sm uppercase tracking-[0.5em] text-cyan-200/70">
112
- Orchestrator Console
113
- </p>
114
- <h2 className="mt-2 text-3xl font-semibold">
115
- Talk to your enterprise agent
116
- </h2>
117
- </div>
118
- <div className="flex flex-col gap-2 text-sm text-slate-200">
119
- <label className="text-xs uppercase tracking-widest text-slate-400">
120
- Tenant ID
121
- </label>
122
- <input
123
- value={tenantId}
124
- onChange={(e) => setTenantId(e.target.value)}
125
- className="rounded-full border border-white/15 bg-white/10 px-4 py-1.5 text-sm text-white outline-none focus:border-cyan-300"
126
- />
127
- </div>
128
  </div>
129
  <div className="mt-6 h-[360px] space-y-3 overflow-y-auto rounded-2xl border border-white/10 bg-slate-950/40 p-4 scrollArea">
130
  {history.map((msg, idx) => (
 
1
  "use client";
2
 
3
  import { useMemo, useState } from "react";
4
+ import { useTenant } from "@/contexts/TenantContext";
5
 
6
  type Message = {
7
  role: "user" | "assistant" | "system";
 
13
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
14
 
15
  export function ChatPanel() {
16
+ const { tenantId } = useTenant();
17
  const [message, setMessage] = useState("");
18
  const [isSending, setIsSending] = useState(false);
19
  const [history, setHistory] = useState<Message[]>([
 
107
  className="gradient-border relative rounded-[28px] p-1 text-white"
108
  >
109
  <div className="glass-panel relative rounded-[26px] p-6">
110
+ <div>
111
+ <p className="text-sm uppercase tracking-[0.5em] text-cyan-200/70">
112
+ Orchestrator Console
113
+ </p>
114
+ <h2 className="mt-2 text-3xl font-semibold">
115
+ Talk to your enterprise agent
116
+ </h2>
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
  <div className="mt-6 h-[360px] space-y-3 overflow-y-auto rounded-2xl border border-white/10 bg-slate-950/40 p-4 scrollArea">
119
  {history.map((msg, idx) => (
frontend/components/knowledge-base-panel.tsx CHANGED
@@ -1,7 +1,8 @@
1
  "use client";
2
 
3
- import { useState, useRef } from "react";
4
  import Link from "next/link";
 
5
 
6
  type SearchResult = {
7
  text: string;
@@ -9,13 +10,19 @@ type SearchResult = {
9
  relevance?: number;
10
  };
11
 
 
 
 
 
 
 
12
  type SourceType = "raw_text" | "url" | "pdf" | "docx" | "txt" | "markdown";
13
 
14
  const API_BASE =
15
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
16
 
17
  export function KnowledgeBasePanel() {
18
- const [tenantId, setTenantId] = useState("tenant123");
19
  const [searchQuery, setSearchQuery] = useState("");
20
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
21
  const [isSearching, setIsSearching] = useState(false);
@@ -26,6 +33,10 @@ export function KnowledgeBasePanel() {
26
  const [isIngesting, setIsIngesting] = useState(false);
27
  const [ingestStatus, setIngestStatus] = useState<string | null>(null);
28
  const [searchError, setSearchError] = useState<string | null>(null);
 
 
 
 
29
  const fileInputRef = useRef<HTMLInputElement>(null);
30
 
31
  async function handleSearch() {
@@ -123,6 +134,8 @@ export function KnowledgeBasePanel() {
123
  if (fileInputRef.current) {
124
  fileInputRef.current.value = "";
125
  }
 
 
126
  } catch (err) {
127
  console.error(err);
128
  setIngestStatus(
@@ -186,6 +199,8 @@ export function KnowledgeBasePanel() {
186
  if (fileInputRef.current) {
187
  fileInputRef.current.value = "";
188
  }
 
 
189
  } catch (err) {
190
  console.error(err);
191
  setIngestStatus(
@@ -198,6 +213,125 @@ export function KnowledgeBasePanel() {
198
  }
199
  }
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  return (
202
  <section
203
  id="knowledge-base"
@@ -219,16 +353,6 @@ export function KnowledgeBasePanel() {
219
  >
220
  View All Documents →
221
  </Link>
222
- <div className="flex flex-col gap-2 text-sm text-slate-200">
223
- <label className="text-xs uppercase tracking-widest text-slate-400">
224
- Tenant ID
225
- </label>
226
- <input
227
- value={tenantId}
228
- onChange={(e) => setTenantId(e.target.value)}
229
- className="rounded-full border border-white/15 bg-white/10 px-4 py-1.5 text-sm text-white outline-none focus:border-cyan-300"
230
- />
231
- </div>
232
  </div>
233
 
234
  {/* Search Section */}
@@ -387,6 +511,84 @@ export function KnowledgeBasePanel() {
387
  )}
388
  </div>
389
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  </div>
391
  </section>
392
  );
 
1
  "use client";
2
 
3
+ import { useState, useRef, useEffect } from "react";
4
  import Link from "next/link";
5
+ import { useTenant } from "@/contexts/TenantContext";
6
 
7
  type SearchResult = {
8
  text: string;
 
10
  relevance?: number;
11
  };
12
 
13
+ type Document = {
14
+ id: number;
15
+ text: string;
16
+ created_at?: string;
17
+ };
18
+
19
  type SourceType = "raw_text" | "url" | "pdf" | "docx" | "txt" | "markdown";
20
 
21
  const API_BASE =
22
  process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
23
 
24
  export function KnowledgeBasePanel() {
25
+ const { tenantId, isLoading: tenantLoading } = useTenant();
26
  const [searchQuery, setSearchQuery] = useState("");
27
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
28
  const [isSearching, setIsSearching] = useState(false);
 
33
  const [isIngesting, setIsIngesting] = useState(false);
34
  const [ingestStatus, setIngestStatus] = useState<string | null>(null);
35
  const [searchError, setSearchError] = useState<string | null>(null);
36
+ const [documents, setDocuments] = useState<Document[]>([]);
37
+ const [isLoadingDocs, setIsLoadingDocs] = useState(false);
38
+ const [isDeleting, setIsDeleting] = useState<number | null>(null);
39
+ const [isDeletingAll, setIsDeletingAll] = useState(false);
40
  const fileInputRef = useRef<HTMLInputElement>(null);
41
 
42
  async function handleSearch() {
 
134
  if (fileInputRef.current) {
135
  fileInputRef.current.value = "";
136
  }
137
+ // Reload documents after successful ingestion
138
+ loadDocuments();
139
  } catch (err) {
140
  console.error(err);
141
  setIngestStatus(
 
199
  if (fileInputRef.current) {
200
  fileInputRef.current.value = "";
201
  }
202
+ // Reload documents after successful ingestion
203
+ loadDocuments();
204
  } catch (err) {
205
  console.error(err);
206
  setIngestStatus(
 
213
  }
214
  }
215
 
216
+ async function loadDocuments() {
217
+ // Guard against empty tenant ID
218
+ if (!tenantId || !tenantId.trim() || isLoadingDocs) return;
219
+ setIsLoadingDocs(true);
220
+
221
+ try {
222
+ const response = await fetch(`${API_BASE}/rag/list?limit=10&offset=0`, {
223
+ method: "GET",
224
+ headers: {
225
+ "x-tenant-id": tenantId,
226
+ },
227
+ });
228
+
229
+ if (!response.ok) {
230
+ const errorData = await response.json().catch(() => ({}));
231
+ const errorMsg = errorData.detail || errorData.message || `Failed to load documents (${response.status})`;
232
+
233
+ if (response.status === 400) {
234
+ // Missing tenant ID - silently fail, user will see empty list
235
+ console.warn("Cannot load documents: Missing tenant ID");
236
+ setDocuments([]);
237
+ return;
238
+ } else if (response.status === 503) {
239
+ console.error("Cannot connect to RAG MCP server");
240
+ setDocuments([]);
241
+ return;
242
+ } else {
243
+ throw new Error(errorMsg);
244
+ }
245
+ }
246
+
247
+ const data = await response.json();
248
+ setDocuments(data.documents || []);
249
+ } catch (err) {
250
+ console.error(err);
251
+ setDocuments([]);
252
+ // Don't show error in status for document loading - it's not critical
253
+ } finally {
254
+ setIsLoadingDocs(false);
255
+ }
256
+ }
257
+
258
+ async function handleDeleteDocument(documentId: number) {
259
+ if (!tenantId.trim() || isDeleting !== null) return;
260
+ setIsDeleting(documentId);
261
+
262
+ try {
263
+ const response = await fetch(`${API_BASE}/rag/delete/${documentId}`, {
264
+ method: "DELETE",
265
+ headers: {
266
+ "x-tenant-id": tenantId,
267
+ },
268
+ });
269
+
270
+ if (!response.ok) {
271
+ const errorData = await response.json().catch(() => ({}));
272
+ const errorMsg = errorData.detail || `Failed to delete document: ${response.status}`;
273
+ throw new Error(errorMsg);
274
+ }
275
+
276
+ // Remove from local state
277
+ setDocuments(docs => docs.filter(doc => doc.id !== documentId));
278
+ setIngestStatus("✅ Document deleted successfully");
279
+ } catch (err) {
280
+ console.error(err);
281
+ setIngestStatus(
282
+ err instanceof Error
283
+ ? `❌ Error: ${err.message}`
284
+ : "Failed to delete document",
285
+ );
286
+ } finally {
287
+ setIsDeleting(null);
288
+ }
289
+ }
290
+
291
+ async function handleDeleteAll() {
292
+ if (!tenantId.trim() || isDeletingAll) return;
293
+
294
+ if (!confirm("Are you sure you want to delete ALL documents for this tenant? This action cannot be undone.")) {
295
+ return;
296
+ }
297
+
298
+ setIsDeletingAll(true);
299
+
300
+ try {
301
+ const response = await fetch(`${API_BASE}/rag/delete-all`, {
302
+ method: "DELETE",
303
+ headers: {
304
+ "x-tenant-id": tenantId,
305
+ },
306
+ });
307
+
308
+ if (!response.ok) {
309
+ throw new Error(`Failed to delete all documents: ${response.status}`);
310
+ }
311
+
312
+ const data = await response.json();
313
+ setDocuments([]);
314
+ setIngestStatus(`✅ Deleted ${data.deleted_count || 0} document(s)`);
315
+ } catch (err) {
316
+ console.error(err);
317
+ setIngestStatus(
318
+ err instanceof Error
319
+ ? `❌ Error: ${err.message}`
320
+ : "Failed to delete all documents",
321
+ );
322
+ } finally {
323
+ setIsDeletingAll(false);
324
+ }
325
+ }
326
+
327
+ // Load documents on mount and when tenant changes
328
+ useEffect(() => {
329
+ // Wait for tenant context to finish loading, then load documents if tenant ID is available
330
+ if (!tenantLoading && tenantId && tenantId.trim()) {
331
+ loadDocuments();
332
+ }
333
+ }, [tenantId, tenantLoading]);
334
+
335
  return (
336
  <section
337
  id="knowledge-base"
 
353
  >
354
  View All Documents →
355
  </Link>
 
 
 
 
 
 
 
 
 
 
356
  </div>
357
 
358
  {/* Search Section */}
 
511
  )}
512
  </div>
513
  </div>
514
+
515
+ {/* Manage Documents Section */}
516
+ <div className="mt-8 rounded-2xl border border-white/10 bg-slate-950/40 p-4">
517
+ <div className="flex items-center justify-between mb-4">
518
+ <div>
519
+ <p className="text-sm uppercase tracking-[0.5em] text-slate-400">
520
+ Manage Documents
521
+ </p>
522
+ <p className="mt-2 text-sm text-slate-300">
523
+ View and delete your ingested documents
524
+ </p>
525
+ </div>
526
+ <div className="flex items-center gap-3">
527
+ <button
528
+ onClick={loadDocuments}
529
+ disabled={isLoadingDocs}
530
+ className="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold text-slate-300 transition hover:bg-white/10 disabled:opacity-60"
531
+ >
532
+ {isLoadingDocs ? "Loading…" : "Refresh"}
533
+ </button>
534
+ {documents.length > 0 && (
535
+ <button
536
+ onClick={handleDeleteAll}
537
+ disabled={isDeletingAll}
538
+ className="rounded-full border border-red-500/50 bg-red-500/10 px-4 py-2 text-xs font-semibold text-red-300 transition hover:bg-red-500/20 disabled:opacity-60"
539
+ >
540
+ {isDeletingAll ? "Deleting…" : "Delete All"}
541
+ </button>
542
+ )}
543
+ </div>
544
+ </div>
545
+
546
+ {documents.length === 0 && !isLoadingDocs && (
547
+ <p className="text-sm text-slate-400 text-center py-4">
548
+ No documents found. Ingest some content to get started.
549
+ </p>
550
+ )}
551
+
552
+ {isLoadingDocs && (
553
+ <p className="text-sm text-slate-400 text-center py-4">
554
+ Loading documents…
555
+ </p>
556
+ )}
557
+
558
+ {documents.length > 0 && (
559
+ <div className="space-y-2 max-h-96 overflow-y-auto">
560
+ {documents.map((doc) => (
561
+ <div
562
+ key={doc.id}
563
+ className="flex items-start justify-between gap-3 rounded-xl border border-white/10 bg-white/5 p-3"
564
+ >
565
+ <div className="flex-1 min-w-0">
566
+ <p className="text-xs text-slate-400 mb-1">
567
+ ID: {doc.id}
568
+ {doc.created_at && (
569
+ <span className="ml-2">
570
+ • {new Date(doc.created_at).toLocaleDateString()}
571
+ </span>
572
+ )}
573
+ </p>
574
+ <p className="text-sm text-slate-200 line-clamp-2">
575
+ {doc.text.length > 150
576
+ ? `${doc.text.substring(0, 150)}...`
577
+ : doc.text}
578
+ </p>
579
+ </div>
580
+ <button
581
+ onClick={() => handleDeleteDocument(doc.id)}
582
+ disabled={isDeleting === doc.id}
583
+ className="flex-shrink-0 rounded-lg border border-red-500/50 bg-red-500/10 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-500/20 disabled:opacity-60"
584
+ >
585
+ {isDeleting === doc.id ? "Deleting…" : "Delete"}
586
+ </button>
587
+ </div>
588
+ ))}
589
+ </div>
590
+ )}
591
+ </div>
592
  </div>
593
  </section>
594
  );
frontend/components/tenant-selector.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useTenant } from "@/contexts/TenantContext";
4
+
5
+ export function TenantSelector() {
6
+ const { tenantId, setTenantId } = useTenant();
7
+
8
+ return (
9
+ <div className="flex items-center gap-3">
10
+ <label className="text-sm font-semibold text-slate-200">Tenant ID:</label>
11
+ <input
12
+ value={tenantId}
13
+ onChange={(e) => setTenantId(e.target.value)}
14
+ placeholder="Enter tenant ID"
15
+ className="rounded-xl border border-white/10 bg-slate-900/50 px-4 py-2 text-sm text-white outline-none focus:border-cyan-400 min-w-[150px]"
16
+ />
17
+ </div>
18
+ );
19
+ }
20
+
frontend/contexts/TenantContext.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { createContext, useContext, useState, useEffect, ReactNode } from "react";
4
+ import { DEFAULT_TENANT_ID, TENANT_STORAGE_KEY } from "@/lib/constants";
5
+
6
+ type TenantContextType = {
7
+ tenantId: string;
8
+ setTenantId: (id: string) => void;
9
+ isLoading: boolean;
10
+ };
11
+
12
+ const TenantContext = createContext<TenantContextType | undefined>(undefined);
13
+
14
+ export function TenantProvider({ children }: { children: ReactNode }) {
15
+ const [tenantId, setTenantIdState] = useState("");
16
+ const [isLoading, setIsLoading] = useState(true);
17
+
18
+ // Load from localStorage on mount
19
+ useEffect(() => {
20
+ const saved = localStorage.getItem(TENANT_STORAGE_KEY);
21
+ setTenantIdState(saved || DEFAULT_TENANT_ID);
22
+ setIsLoading(false);
23
+ }, []);
24
+
25
+ const setTenantId = (id: string) => {
26
+ const trimmed = id.trim();
27
+ setTenantIdState(trimmed);
28
+ if (trimmed) {
29
+ localStorage.setItem(TENANT_STORAGE_KEY, trimmed);
30
+ } else {
31
+ localStorage.removeItem(TENANT_STORAGE_KEY);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <TenantContext.Provider value={{ tenantId, setTenantId, isLoading }}>
37
+ {children}
38
+ </TenantContext.Provider>
39
+ );
40
+ }
41
+
42
+ export function useTenant() {
43
+ const context = useContext(TenantContext);
44
+ if (!context) {
45
+ throw new Error("useTenant must be used within TenantProvider");
46
+ }
47
+ return context;
48
+ }
49
+
frontend/lib/constants.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const DEFAULT_TENANT_ID = "tenant123";
2
+ export const TENANT_STORAGE_KEY = "integrachat_tenant_id";
3
+