ishaq101 commited on
Commit
91cd4b2
Β·
1 Parent(s): d3ecfdd

update dockerfile

Browse files
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ VITE_API_BASE_URL=
Dockerfile CHANGED
@@ -18,7 +18,7 @@ COPY --from=builder /app/dist /usr/share/nginx/html
18
 
19
  # SPA fallback: route all requests to index.html
20
  RUN printf 'server {\n\
21
- listen 80;\n\
22
  root /usr/share/nginx/html;\n\
23
  index index.html;\n\
24
  location / {\n\
@@ -26,6 +26,6 @@ RUN printf 'server {\n\
26
  }\n\
27
  }\n' > /etc/nginx/conf.d/default.conf
28
 
29
- EXPOSE 80
30
 
31
  CMD ["nginx", "-g", "daemon off;"]
 
18
 
19
  # SPA fallback: route all requests to index.html
20
  RUN printf 'server {\n\
21
+ listen 7860;\n\
22
  root /usr/share/nginx/html;\n\
23
  index index.html;\n\
24
  location / {\n\
 
26
  }\n\
27
  }\n' > /etc/nginx/conf.d/default.conf
28
 
29
+ EXPOSE 7860
30
 
31
  CMD ["nginx", "-g", "daemon off;"]
src/app/components/KnowledgeManagement.tsx CHANGED
@@ -8,111 +8,144 @@ import {
8
  Database,
9
  X,
10
  } from "lucide-react";
11
-
12
- interface Document {
13
- id: string;
14
- name: string;
15
- size: number;
16
- uploadedAt: number;
17
- processed: boolean;
18
- content?: string;
19
- }
20
 
21
  interface KnowledgeManagementProps {
22
  open: boolean;
23
  onClose: () => void;
24
  }
25
 
 
 
 
 
 
 
26
  export default function KnowledgeManagement({
27
  open,
28
  onClose,
29
  }: KnowledgeManagementProps) {
30
- const [documents, setDocuments] = useState<Document[]>([]);
 
 
31
  const [uploading, setUploading] = useState(false);
 
32
  const [processing, setProcessing] = useState<string | null>(null);
 
33
 
34
  useEffect(() => {
35
- const storedDocs = localStorage.getItem("chatbot_documents");
36
- if (storedDocs) {
37
- setDocuments(JSON.parse(storedDocs));
38
- }
39
- }, []);
40
 
41
- useEffect(() => {
42
- if (documents.length > 0) {
43
- localStorage.setItem("chatbot_documents", JSON.stringify(documents));
 
 
 
 
 
 
 
 
44
  }
45
- }, [documents]);
46
 
47
  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
48
  const files = e.target.files;
49
  if (!files || files.length === 0) return;
 
 
50
 
51
  setUploading(true);
 
52
 
53
  for (let i = 0; i < files.length; i++) {
54
  const file = files[i];
55
- if (file.type !== "application/pdf") {
56
- alert("Please upload PDF files only");
57
- continue;
58
- }
59
-
60
- const newDoc: Document = {
61
- id: Date.now().toString() + i,
62
- name: file.name,
63
- size: file.size,
64
- uploadedAt: Date.now(),
65
- processed: false,
66
- };
67
 
68
- setDocuments((prev) => [...prev, newDoc]);
 
 
 
69
  }
70
 
71
  setUploading(false);
72
  e.target.value = "";
73
  };
74
 
75
- const processDocument = async (docId: string) => {
76
  setProcessing(docId);
77
-
78
- // Simulate PDF processing (text extraction)
79
- await new Promise((resolve) => setTimeout(resolve, 2000));
80
-
81
- const mockExtractedText = `This is simulated extracted text from the PDF document.
82
-
83
- In a real implementation, this would contain the actual text extracted from the PDF using a library like PDF.js or similar.
84
-
85
- The document has been processed and its content is now available for use in the chatbot knowledge base. You can use this information to answer questions and provide context-aware responses.
86
-
87
- Key features:
88
- - Text extraction from PDF
89
- - Knowledge storage
90
- - Searchable content
91
- - Integration with chat responses`;
92
-
93
  setDocuments((prev) =>
94
- prev.map((doc) =>
95
- doc.id === docId
96
- ? { ...doc, processed: true, content: mockExtractedText }
97
- : doc
98
  )
99
  );
100
-
101
- setProcessing(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  };
103
 
104
- const deleteDocument = (docId: string) => {
105
- setDocuments((prev) => prev.filter((doc) => doc.id !== docId));
106
- if (documents.length === 1) {
107
- localStorage.removeItem("chatbot_documents");
 
 
 
 
 
 
 
108
  }
109
  };
110
 
111
- const deleteAllDocuments = () => {
112
- if (window.confirm("Are you sure you want to delete all documents?")) {
113
- setDocuments([]);
114
- localStorage.removeItem("chatbot_documents");
 
 
 
 
 
 
 
115
  }
 
116
  };
117
 
118
  const formatFileSize = (bytes: number) => {
@@ -121,8 +154,43 @@ Key features:
121
  return (bytes / (1024 * 1024)).toFixed(2) + " MB";
122
  };
123
 
124
- const formatDate = (timestamp: number) => {
125
- return new Date(timestamp).toLocaleString();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  };
127
 
128
  if (!open) return null;
@@ -155,7 +223,7 @@ Key features:
155
  <Upload className="w-5 h-5 text-slate-600" />
156
  <div className="text-center">
157
  <p className="text-slate-900 font-medium text-sm">
158
- Upload PDF Documents
159
  </p>
160
  <p className="text-xs text-slate-500 mt-0.5">
161
  Click to browse or drag and drop
@@ -164,13 +232,18 @@ Key features:
164
  <input
165
  id="file-upload"
166
  type="file"
167
- accept=".pdf"
168
  multiple
169
  onChange={handleFileUpload}
170
  className="hidden"
171
  disabled={uploading}
172
  />
173
  </label>
 
 
 
 
 
174
  </div>
175
 
176
  {/* Documents List */}
@@ -190,12 +263,22 @@ Key features:
190
  )}
191
  </div>
192
 
193
- {documents.length === 0 ? (
 
 
 
 
 
 
 
 
194
  <div className="text-center py-8">
195
  <FileText className="w-12 h-12 text-slate-300 mx-auto mb-3" />
196
- <p className="text-slate-500 text-sm">No documents uploaded yet</p>
 
 
197
  <p className="text-xs text-slate-400 mt-1">
198
- Upload PDF files to build your knowledge base
199
  </p>
200
  </div>
201
  ) : (
@@ -210,61 +293,30 @@ Key features:
210
  <div className="flex items-start justify-between gap-2">
211
  <div className="flex-1 min-w-0">
212
  <h4 className="text-slate-900 font-medium truncate text-sm">
213
- {doc.name}
214
  </h4>
215
  <p className="text-xs text-slate-500 mt-0.5">
216
- {formatFileSize(doc.size)} β€’{" "}
217
- {formatDate(doc.uploadedAt)}
218
  </p>
219
  </div>
220
  <button
221
- onClick={() => deleteDocument(doc.id)}
222
- className="text-slate-400 hover:text-red-600 transition flex-shrink-0"
 
223
  title="Delete document"
224
  >
225
- <Trash2 className="w-4 h-4" />
 
 
 
 
226
  </button>
227
  </div>
228
 
229
  <div className="mt-2 flex items-center gap-2">
230
- {doc.processed ? (
231
- <div className="flex items-center gap-1.5 text-green-600">
232
- <Check className="w-3.5 h-3.5" />
233
- <span className="text-xs font-medium">
234
- Processed
235
- </span>
236
- </div>
237
- ) : (
238
- <button
239
- onClick={() => processDocument(doc.id)}
240
- disabled={processing === doc.id}
241
- className="flex items-center gap-1.5 bg-gradient-to-r from-[#00C853] to-[#00A843] text-white px-3 py-1.5 rounded-lg hover:from-[#00A843] hover:to-[#00962B] transition disabled:opacity-50 disabled:cursor-not-allowed text-xs"
242
- >
243
- {processing === doc.id ? (
244
- <>
245
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
246
- Processing...
247
- </>
248
- ) : (
249
- <>
250
- <Database className="w-3.5 h-3.5" />
251
- Process to Knowledge
252
- </>
253
- )}
254
- </button>
255
- )}
256
  </div>
257
-
258
- {doc.processed && doc.content && (
259
- <div className="mt-2 p-2 bg-white rounded border border-slate-200">
260
- <p className="text-[10px] text-slate-500 mb-1">
261
- Extracted Content Preview:
262
- </p>
263
- <p className="text-xs text-slate-700 line-clamp-3">
264
- {doc.content}
265
- </p>
266
- </div>
267
- )}
268
  </div>
269
  </div>
270
  </div>
@@ -276,8 +328,7 @@ Key features:
276
  {/* Footer */}
277
  <div className="border-t border-slate-200 p-3 bg-slate-50 rounded-b-xl">
278
  <p className="text-[10px] text-slate-500 text-center">
279
- Demo mode: PDF processing is simulated. In production, this would
280
- extract actual text from PDFs.
281
  </p>
282
  </div>
283
  </div>
 
8
  Database,
9
  X,
10
  } from "lucide-react";
11
+ import {
12
+ getDocuments,
13
+ uploadDocument,
14
+ processDocument,
15
+ deleteDocument,
16
+ type ApiDocument,
17
+ type DocumentStatus,
18
+ } from "../../services/api";
 
19
 
20
  interface KnowledgeManagementProps {
21
  open: boolean;
22
  onClose: () => void;
23
  }
24
 
25
+ const getUserId = (): string | null => {
26
+ const stored = localStorage.getItem("chatbot_user");
27
+ if (!stored) return null;
28
+ return (JSON.parse(stored).user_id as string) ?? null;
29
+ };
30
+
31
  export default function KnowledgeManagement({
32
  open,
33
  onClose,
34
  }: KnowledgeManagementProps) {
35
+ const [documents, setDocuments] = useState<ApiDocument[]>([]);
36
+ const [loadingDocs, setLoadingDocs] = useState(false);
37
+ const [docsError, setDocsError] = useState<string | null>(null);
38
  const [uploading, setUploading] = useState(false);
39
+ const [uploadError, setUploadError] = useState<string | null>(null);
40
  const [processing, setProcessing] = useState<string | null>(null);
41
+ const [deleting, setDeleting] = useState<string | null>(null);
42
 
43
  useEffect(() => {
44
+ if (!open) return;
45
+ const userId = getUserId();
46
+ if (!userId) return;
47
+ loadDocuments(userId);
48
+ }, [open]);
49
 
50
+ const loadDocuments = async (userId: string) => {
51
+ setLoadingDocs(true);
52
+ setDocsError(null);
53
+ try {
54
+ setDocuments(await getDocuments(userId));
55
+ } catch (err) {
56
+ setDocsError(
57
+ err instanceof Error ? err.message : "Failed to load documents"
58
+ );
59
+ } finally {
60
+ setLoadingDocs(false);
61
  }
62
+ };
63
 
64
  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
65
  const files = e.target.files;
66
  if (!files || files.length === 0) return;
67
+ const userId = getUserId();
68
+ if (!userId) return;
69
 
70
  setUploading(true);
71
+ setUploadError(null);
72
 
73
  for (let i = 0; i < files.length; i++) {
74
  const file = files[i];
75
+ try {
76
+ const uploadRes = await uploadDocument(userId, file);
77
+ const newDoc: ApiDocument = {
78
+ id: uploadRes.data.id,
79
+ filename: uploadRes.data.filename,
80
+ status: "pending",
81
+ file_size: file.size,
82
+ file_type: file.name.split(".").pop() ?? "",
83
+ created_at: new Date().toISOString(),
84
+ };
85
+ setDocuments((prev) => [newDoc, ...prev]);
 
86
 
87
+ await processDocumentById(userId, uploadRes.data.id);
88
+ } catch (err) {
89
+ setUploadError(err instanceof Error ? err.message : "Upload failed");
90
+ }
91
  }
92
 
93
  setUploading(false);
94
  e.target.value = "";
95
  };
96
 
97
+ const processDocumentById = async (userId: string, docId: string) => {
98
  setProcessing(docId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  setDocuments((prev) =>
100
+ prev.map((d) =>
101
+ d.id === docId ? { ...d, status: "processing" as DocumentStatus } : d
 
 
102
  )
103
  );
104
+ try {
105
+ await processDocument(userId, docId);
106
+ setDocuments((prev) =>
107
+ prev.map((d) =>
108
+ d.id === docId ? { ...d, status: "completed" as DocumentStatus } : d
109
+ )
110
+ );
111
+ } catch {
112
+ setDocuments((prev) =>
113
+ prev.map((d) =>
114
+ d.id === docId ? { ...d, status: "failed" as DocumentStatus } : d
115
+ )
116
+ );
117
+ } finally {
118
+ setProcessing(null);
119
+ }
120
  };
121
 
122
+ const handleDeleteDocument = async (docId: string) => {
123
+ const userId = getUserId();
124
+ if (!userId) return;
125
+ setDeleting(docId);
126
+ try {
127
+ await deleteDocument(userId, docId);
128
+ setDocuments((prev) => prev.filter((d) => d.id !== docId));
129
+ } catch (err) {
130
+ console.error("Delete failed:", err);
131
+ } finally {
132
+ setDeleting(null);
133
  }
134
  };
135
 
136
+ const deleteAllDocuments = async () => {
137
+ if (!window.confirm("Are you sure you want to delete all documents?"))
138
+ return;
139
+ const userId = getUserId();
140
+ if (!userId) return;
141
+ for (const doc of documents) {
142
+ try {
143
+ await deleteDocument(userId, doc.id);
144
+ } catch {
145
+ // continue deleting others
146
+ }
147
  }
148
+ setDocuments([]);
149
  };
150
 
151
  const formatFileSize = (bytes: number) => {
 
154
  return (bytes / (1024 * 1024)).toFixed(2) + " MB";
155
  };
156
 
157
+ const formatDate = (isoString: string) => {
158
+ return new Date(isoString).toLocaleString();
159
+ };
160
+
161
+ const renderStatus = (doc: ApiDocument) => {
162
+ if (doc.status === "completed") {
163
+ return (
164
+ <div className="flex items-center gap-1.5 text-green-600">
165
+ <Check className="w-3.5 h-3.5" />
166
+ <span className="text-xs font-medium">Processed</span>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ if (doc.status === "processing" || processing === doc.id) {
172
+ return (
173
+ <div className="flex items-center gap-1.5 text-blue-600">
174
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
175
+ <span className="text-xs">Processing...</span>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ // pending or failed
181
+ return (
182
+ <button
183
+ onClick={() => {
184
+ const userId = getUserId();
185
+ if (userId) processDocumentById(userId, doc.id);
186
+ }}
187
+ disabled={processing === doc.id}
188
+ className="flex items-center gap-1.5 bg-gradient-to-r from-[#00C853] to-[#00A843] text-white px-3 py-1.5 rounded-lg hover:from-[#00A843] hover:to-[#00962B] transition disabled:opacity-50 disabled:cursor-not-allowed text-xs"
189
+ >
190
+ <Database className="w-3.5 h-3.5" />
191
+ {doc.status === "failed" ? "Retry Process" : "Process to Knowledge"}
192
+ </button>
193
+ );
194
  };
195
 
196
  if (!open) return null;
 
223
  <Upload className="w-5 h-5 text-slate-600" />
224
  <div className="text-center">
225
  <p className="text-slate-900 font-medium text-sm">
226
+ Upload Documents (PDF, DOCX, TXT)
227
  </p>
228
  <p className="text-xs text-slate-500 mt-0.5">
229
  Click to browse or drag and drop
 
232
  <input
233
  id="file-upload"
234
  type="file"
235
+ accept=".pdf,.docx,.txt"
236
  multiple
237
  onChange={handleFileUpload}
238
  className="hidden"
239
  disabled={uploading}
240
  />
241
  </label>
242
+ {uploadError && (
243
+ <p className="mt-2 text-xs text-red-600 bg-red-50 border border-red-200 px-3 py-2 rounded-lg">
244
+ {uploadError}
245
+ </p>
246
+ )}
247
  </div>
248
 
249
  {/* Documents List */}
 
263
  )}
264
  </div>
265
 
266
+ {loadingDocs ? (
267
+ <div className="flex justify-center py-8">
268
+ <Loader2 className="w-6 h-6 animate-spin text-slate-400" />
269
+ </div>
270
+ ) : docsError ? (
271
+ <p className="text-center text-sm text-red-600 py-4">
272
+ {docsError}
273
+ </p>
274
+ ) : documents.length === 0 ? (
275
  <div className="text-center py-8">
276
  <FileText className="w-12 h-12 text-slate-300 mx-auto mb-3" />
277
+ <p className="text-slate-500 text-sm">
278
+ No documents uploaded yet
279
+ </p>
280
  <p className="text-xs text-slate-400 mt-1">
281
+ Upload files to build your knowledge base
282
  </p>
283
  </div>
284
  ) : (
 
293
  <div className="flex items-start justify-between gap-2">
294
  <div className="flex-1 min-w-0">
295
  <h4 className="text-slate-900 font-medium truncate text-sm">
296
+ {doc.filename}
297
  </h4>
298
  <p className="text-xs text-slate-500 mt-0.5">
299
+ {formatFileSize(doc.file_size)} β€’{" "}
300
+ {formatDate(doc.created_at)}
301
  </p>
302
  </div>
303
  <button
304
+ onClick={() => handleDeleteDocument(doc.id)}
305
+ disabled={deleting === doc.id}
306
+ className="text-slate-400 hover:text-red-600 transition flex-shrink-0 disabled:opacity-50"
307
  title="Delete document"
308
  >
309
+ {deleting === doc.id ? (
310
+ <Loader2 className="w-4 h-4 animate-spin" />
311
+ ) : (
312
+ <Trash2 className="w-4 h-4" />
313
+ )}
314
  </button>
315
  </div>
316
 
317
  <div className="mt-2 flex items-center gap-2">
318
+ {renderStatus(doc)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </div>
 
 
 
 
 
 
 
 
 
 
 
320
  </div>
321
  </div>
322
  </div>
 
328
  {/* Footer */}
329
  <div className="border-t border-slate-200 p-3 bg-slate-50 rounded-b-xl">
330
  <p className="text-[10px] text-slate-500 text-center">
331
+ Supported formats: PDF, DOCX, TXT
 
332
  </p>
333
  </div>
334
  </div>
src/app/components/Login.tsx CHANGED
@@ -1,14 +1,16 @@
1
  import { useState } from "react";
2
  import { useNavigate } from "react-router";
3
- import { LogIn } from "lucide-react";
 
4
 
5
  export default function Login() {
6
  const [email, setEmail] = useState("");
7
  const [password, setPassword] = useState("");
8
  const [error, setError] = useState("");
 
9
  const navigate = useNavigate();
10
 
11
- const handleLogin = (e: React.FormEvent) => {
12
  e.preventDefault();
13
  setError("");
14
 
@@ -22,15 +24,22 @@ export default function Login() {
22
  return;
23
  }
24
 
25
- // Demo login - store user info
26
- const user = {
27
- email,
28
- name: email.split("@")[0],
29
- loginTime: new Date().toISOString(),
30
- };
31
-
32
- localStorage.setItem("chatbot_user", JSON.stringify(user));
33
- navigate("/");
 
 
 
 
 
 
 
34
  };
35
 
36
  return (
@@ -65,6 +74,7 @@ export default function Login() {
65
  onChange={(e) => setEmail(e.target.value)}
66
  className="w-full px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#4FC3F7] focus:border-transparent transition"
67
  placeholder="you@example.com"
 
68
  />
69
  </div>
70
 
@@ -82,6 +92,7 @@ export default function Login() {
82
  onChange={(e) => setPassword(e.target.value)}
83
  className="w-full px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#4FC3F7] focus:border-transparent transition"
84
  placeholder="Enter your password"
 
85
  />
86
  </div>
87
 
@@ -93,15 +104,17 @@ export default function Login() {
93
 
94
  <button
95
  type="submit"
96
- className="w-full bg-gradient-to-r from-[#00C853] to-[#00A843] text-white py-2.5 text-sm rounded-lg hover:from-[#00A843] hover:to-[#00962B] transition font-medium"
 
97
  >
98
- Sign In
 
 
 
 
 
99
  </button>
100
  </form>
101
-
102
- <p className="text-center text-slate-500 text-xs mt-4">
103
- Demo mode: Use any email and password
104
- </p>
105
  </div>
106
  </div>
107
  </div>
 
1
  import { useState } from "react";
2
  import { useNavigate } from "react-router";
3
+ import { LogIn, Loader2 } from "lucide-react";
4
+ import { login } from "../../services/api";
5
 
6
  export default function Login() {
7
  const [email, setEmail] = useState("");
8
  const [password, setPassword] = useState("");
9
  const [error, setError] = useState("");
10
+ const [isLoading, setIsLoading] = useState(false);
11
  const navigate = useNavigate();
12
 
13
+ const handleLogin = async (e: React.FormEvent) => {
14
  e.preventDefault();
15
  setError("");
16
 
 
24
  return;
25
  }
26
 
27
+ setIsLoading(true);
28
+ try {
29
+ const res = await login(email, password);
30
+ const user = {
31
+ user_id: res.data.user_id,
32
+ email: res.data.email,
33
+ name: res.data.name,
34
+ loginTime: new Date().toISOString(),
35
+ };
36
+ localStorage.setItem("chatbot_user", JSON.stringify(user));
37
+ navigate("/");
38
+ } catch (err: unknown) {
39
+ setError(err instanceof Error ? err.message : "Login failed");
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
  };
44
 
45
  return (
 
74
  onChange={(e) => setEmail(e.target.value)}
75
  className="w-full px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#4FC3F7] focus:border-transparent transition"
76
  placeholder="you@example.com"
77
+ disabled={isLoading}
78
  />
79
  </div>
80
 
 
92
  onChange={(e) => setPassword(e.target.value)}
93
  className="w-full px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#4FC3F7] focus:border-transparent transition"
94
  placeholder="Enter your password"
95
+ disabled={isLoading}
96
  />
97
  </div>
98
 
 
104
 
105
  <button
106
  type="submit"
107
+ disabled={isLoading}
108
+ className="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-[#00C853] to-[#00A843] text-white py-2.5 text-sm rounded-lg hover:from-[#00A843] hover:to-[#00962B] transition font-medium disabled:opacity-60 disabled:cursor-not-allowed"
109
  >
110
+ {isLoading ? (
111
+ <Loader2 className="w-4 h-4 animate-spin" />
112
+ ) : (
113
+ <LogIn className="w-4 h-4" />
114
+ )}
115
+ {isLoading ? "Signing in..." : "Sign In"}
116
  </button>
117
  </form>
 
 
 
 
118
  </div>
119
  </div>
120
  </div>
src/app/components/Main.tsx CHANGED
@@ -10,73 +10,95 @@ import {
10
  MessageSquare,
11
  User,
12
  Database,
 
13
  } from "lucide-react";
14
  import KnowledgeManagement from "./KnowledgeManagement";
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  interface Message {
17
  id: string;
18
  role: "user" | "assistant";
19
  content: string;
20
  timestamp: number;
 
21
  }
22
 
23
- interface Chat {
24
  id: string;
25
  title: string;
26
  messages: Message[];
27
- createdAt: number;
28
- updatedAt: number;
29
  }
30
 
31
  export default function Main() {
32
  const navigate = useNavigate();
33
  const [sidebarOpen, setSidebarOpen] = useState(true);
34
- const [chats, setChats] = useState<Chat[]>([]);
35
  const [currentChatId, setCurrentChatId] = useState<string | null>(null);
36
  const [input, setInput] = useState("");
37
  const [isStreaming, setIsStreaming] = useState(false);
 
 
38
  const messagesEndRef = useRef<HTMLDivElement>(null);
39
- const [user, setUser] = useState<any>(null);
40
  const [knowledgeOpen, setKnowledgeOpen] = useState(false);
 
41
 
42
  useEffect(() => {
43
  const storedUser = localStorage.getItem("chatbot_user");
44
  if (storedUser) {
45
- setUser(JSON.parse(storedUser));
46
- }
47
-
48
- const storedChats = localStorage.getItem("chatbot_chats");
49
- if (storedChats) {
50
- const parsedChats = JSON.parse(storedChats);
51
- setChats(parsedChats);
52
- if (parsedChats.length > 0) {
53
- setCurrentChatId(parsedChats[0].id);
54
- }
55
  }
56
  }, []);
57
 
58
- useEffect(() => {
59
- if (chats.length > 0) {
60
- localStorage.setItem("chatbot_chats", JSON.stringify(chats));
61
- }
62
- }, [chats]);
63
-
64
  useEffect(() => {
65
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
66
  }, [currentChatId, chats]);
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  const currentChat = chats.find((chat) => chat.id === currentChatId);
69
 
70
  const createNewChat = () => {
71
- const newChat: Chat = {
72
- id: Date.now().toString(),
73
- title: "New Chat",
74
- messages: [],
75
- createdAt: Date.now(),
76
- updatedAt: Date.now(),
77
- };
78
- setChats([newChat, ...chats]);
79
- setCurrentChatId(newChat.id);
80
  };
81
 
82
  const deleteChat = (chatId: string) => {
@@ -90,7 +112,6 @@ export default function Main() {
90
  const deleteAllChats = () => {
91
  setChats([]);
92
  setCurrentChatId(null);
93
- localStorage.removeItem("chatbot_chats");
94
  };
95
 
96
  const handleLogout = () => {
@@ -98,112 +119,173 @@ export default function Main() {
98
  navigate("/login");
99
  };
100
 
101
- const simulateStreamingResponse = async (
102
- userMessage: string,
103
- chatId: string
104
- ) => {
105
- const responses = [
106
- "I'm a demo chatbot. I can help you with various tasks and answer your questions.",
107
- "That's an interesting question! In this demo version, I provide simulated responses.",
108
- "I understand. Let me help you with that. This is a demonstration of how the chat interface works.",
109
- "Great question! The actual AI responses would come from a backend API in a production environment.",
110
- "I'm here to assist you. This demo showcases the chat interface with streaming responses.",
111
- ];
112
-
113
- const response =
114
- responses[Math.floor(Math.random() * responses.length)] +
115
- " " +
116
- userMessage;
117
- const assistantMessageId = Date.now().toString() + "-assistant";
118
-
119
- setChats((prevChats) =>
120
- prevChats.map((chat) => {
121
- if (chat.id === chatId) {
122
- return {
123
- ...chat,
124
- messages: [
125
- ...chat.messages,
126
- {
127
- id: assistantMessageId,
128
- role: "assistant",
129
- content: "",
130
- timestamp: Date.now(),
131
- },
132
- ],
133
- updatedAt: Date.now(),
134
- };
135
- }
136
- return chat;
137
- })
138
- );
139
-
140
- for (let i = 0; i < response.length; i++) {
141
- await new Promise((resolve) => setTimeout(resolve, 20));
142
- setChats((prevChats) =>
143
- prevChats.map((chat) => {
144
- if (chat.id === chatId) {
145
- return {
146
- ...chat,
147
- messages: chat.messages.map((msg) =>
148
- msg.id === assistantMessageId
149
- ? { ...msg, content: response.slice(0, i + 1) }
150
- : msg
151
- ),
152
- };
153
- }
154
- return chat;
155
- })
156
- );
157
- }
158
- };
159
-
160
  const handleSend = async () => {
161
- if (!input.trim() || isStreaming) return;
162
-
163
- let chatId = currentChatId;
164
-
165
- if (!chatId) {
166
- const newChat: Chat = {
167
- id: Date.now().toString(),
168
- title: input.slice(0, 50),
169
- messages: [],
170
- createdAt: Date.now(),
171
- updatedAt: Date.now(),
172
- };
173
- setChats([newChat, ...chats]);
174
- chatId = newChat.id;
175
- setCurrentChatId(chatId);
 
 
 
 
 
176
  }
177
 
178
  const userMessage: Message = {
179
- id: Date.now().toString(),
180
  role: "user",
181
  content: input,
182
  timestamp: Date.now(),
183
  };
184
 
185
- setChats((prevChats) =>
186
- prevChats.map((chat) => {
187
- if (chat.id === chatId) {
188
- const updatedChat = {
189
- ...chat,
190
- messages: [...chat.messages, userMessage],
191
- updatedAt: Date.now(),
192
- };
193
- if (chat.messages.length === 0) {
194
- updatedChat.title = input.slice(0, 50);
195
- }
196
- return updatedChat;
197
- }
198
- return chat;
199
- })
200
  );
201
 
 
202
  setInput("");
203
  setIsStreaming(true);
204
 
205
- await simulateStreamingResponse(input, chatId);
206
- setIsStreaming(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  };
208
 
209
  const handleKeyPress = (e: React.KeyboardEvent) => {
@@ -232,33 +314,43 @@ export default function Main() {
232
  </div>
233
 
234
  <div className="flex-1 overflow-y-auto p-2 space-y-1">
235
- {chats.map((chat) => (
236
- <div
237
- key={chat.id}
238
- className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition group ${
239
- currentChatId === chat.id
240
- ? "bg-white/25"
241
- : "hover:bg-white/15"
242
- }`}
243
- >
244
- <MessageSquare className="w-3.5 h-3.5 flex-shrink-0" />
245
  <div
246
- className="flex-1 truncate text-sm"
247
- onClick={() => setCurrentChatId(chat.id)}
 
 
 
 
248
  >
249
- {chat.title}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  </div>
251
- <button
252
- onClick={(e) => {
253
- e.stopPropagation();
254
- deleteChat(chat.id);
255
- }}
256
- className="opacity-0 group-hover:opacity-100 transition"
257
- >
258
- <Trash2 className="w-3.5 h-3.5 text-red-100 hover:text-white" />
259
- </button>
260
- </div>
261
- ))}
262
  </div>
263
 
264
  <div className="border-t border-white/20 p-3 space-y-2">
@@ -352,11 +444,36 @@ export default function Main() {
352
  <p className="whitespace-pre-wrap break-words text-sm">
353
  {message.content}
354
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  </div>
356
  </div>
357
  ))}
358
 
359
- {!currentChat && chats.length === 0 && (
360
  <div className="flex items-center justify-center h-full">
361
  <div className="text-center">
362
  <MessageSquare className="w-12 h-12 text-slate-300 mx-auto mb-3" />
@@ -394,9 +511,6 @@ export default function Main() {
394
  <Send className="w-4 h-4" />
395
  </button>
396
  </div>
397
- <p className="text-[10px] text-slate-400 mt-1.5 text-center">
398
- Demo mode: Responses are simulated
399
- </p>
400
  </div>
401
  </div>
402
  </div>
 
10
  MessageSquare,
11
  User,
12
  Database,
13
+ Loader2,
14
  } from "lucide-react";
15
  import KnowledgeManagement from "./KnowledgeManagement";
16
+ import {
17
+ getRooms,
18
+ createRoom,
19
+ streamChat,
20
+ type ChatSource,
21
+ } from "../../services/api";
22
+
23
+ interface StoredUser {
24
+ user_id: string;
25
+ email: string;
26
+ name: string;
27
+ loginTime: string;
28
+ }
29
 
30
  interface Message {
31
  id: string;
32
  role: "user" | "assistant";
33
  content: string;
34
  timestamp: number;
35
+ sources?: ChatSource[];
36
  }
37
 
38
+ interface ChatRoom {
39
  id: string;
40
  title: string;
41
  messages: Message[];
42
+ createdAt: string;
43
+ updatedAt: string | null;
44
  }
45
 
46
  export default function Main() {
47
  const navigate = useNavigate();
48
  const [sidebarOpen, setSidebarOpen] = useState(true);
49
+ const [chats, setChats] = useState<ChatRoom[]>([]);
50
  const [currentChatId, setCurrentChatId] = useState<string | null>(null);
51
  const [input, setInput] = useState("");
52
  const [isStreaming, setIsStreaming] = useState(false);
53
+ const [roomsLoading, setRoomsLoading] = useState(false);
54
+ const [roomsError, setRoomsError] = useState<string | null>(null);
55
  const messagesEndRef = useRef<HTMLDivElement>(null);
56
+ const [user, setUser] = useState<StoredUser | null>(null);
57
  const [knowledgeOpen, setKnowledgeOpen] = useState(false);
58
+ const abortControllerRef = useRef<AbortController | null>(null);
59
 
60
  useEffect(() => {
61
  const storedUser = localStorage.getItem("chatbot_user");
62
  if (storedUser) {
63
+ const parsedUser: StoredUser = JSON.parse(storedUser);
64
+ setUser(parsedUser);
65
+ loadRooms(parsedUser.user_id);
 
 
 
 
 
 
 
66
  }
67
  }, []);
68
 
 
 
 
 
 
 
69
  useEffect(() => {
70
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
71
  }, [currentChatId, chats]);
72
 
73
+ const loadRooms = async (userId: string) => {
74
+ setRoomsLoading(true);
75
+ setRoomsError(null);
76
+ try {
77
+ const apiRooms = await getRooms(userId);
78
+ const mapped: ChatRoom[] = apiRooms.map((r) => ({
79
+ id: r.id,
80
+ title: r.title,
81
+ messages: [],
82
+ createdAt: r.created_at,
83
+ updatedAt: r.updated_at,
84
+ }));
85
+ setChats(mapped);
86
+ if (mapped.length > 0) {
87
+ setCurrentChatId(mapped[0].id);
88
+ }
89
+ } catch (err) {
90
+ setRoomsError(
91
+ err instanceof Error ? err.message : "Failed to load chats"
92
+ );
93
+ } finally {
94
+ setRoomsLoading(false);
95
+ }
96
+ };
97
+
98
  const currentChat = chats.find((chat) => chat.id === currentChatId);
99
 
100
  const createNewChat = () => {
101
+ setCurrentChatId(null);
 
 
 
 
 
 
 
 
102
  };
103
 
104
  const deleteChat = (chatId: string) => {
 
112
  const deleteAllChats = () => {
113
  setChats([]);
114
  setCurrentChatId(null);
 
115
  };
116
 
117
  const handleLogout = () => {
 
119
  navigate("/login");
120
  };
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  const handleSend = async () => {
123
+ if (!input.trim() || isStreaming || !user) return;
124
+
125
+ let roomId = currentChatId;
126
+
127
+ if (!roomId) {
128
+ try {
129
+ const res = await createRoom(user.user_id, input.slice(0, 50));
130
+ const newRoom: ChatRoom = {
131
+ id: res.data.id,
132
+ title: res.data.title,
133
+ messages: [],
134
+ createdAt: res.data.created_at,
135
+ updatedAt: res.data.updated_at,
136
+ };
137
+ setChats((prev) => [newRoom, ...prev]);
138
+ roomId = newRoom.id;
139
+ setCurrentChatId(roomId);
140
+ } catch {
141
+ return;
142
+ }
143
  }
144
 
145
  const userMessage: Message = {
146
+ id: crypto.randomUUID(),
147
  role: "user",
148
  content: input,
149
  timestamp: Date.now(),
150
  };
151
 
152
+ setChats((prev) =>
153
+ prev.map((chat) =>
154
+ chat.id === roomId
155
+ ? {
156
+ ...chat,
157
+ messages: [...chat.messages, userMessage],
158
+ updatedAt: new Date().toISOString(),
159
+ }
160
+ : chat
161
+ )
 
 
 
 
 
162
  );
163
 
164
+ const sentMessage = input;
165
  setInput("");
166
  setIsStreaming(true);
167
 
168
+ const assistantMsgId = crypto.randomUUID();
169
+
170
+ setChats((prev) =>
171
+ prev.map((chat) =>
172
+ chat.id === roomId
173
+ ? {
174
+ ...chat,
175
+ messages: [
176
+ ...chat.messages,
177
+ {
178
+ id: assistantMsgId,
179
+ role: "assistant",
180
+ content: "",
181
+ timestamp: Date.now(),
182
+ sources: [],
183
+ },
184
+ ],
185
+ }
186
+ : chat
187
+ )
188
+ );
189
+
190
+ abortControllerRef.current = new AbortController();
191
+
192
+ try {
193
+ const response = await streamChat(user.user_id, roomId, sentMessage);
194
+
195
+ if (!response.body) throw new Error("No response body");
196
+
197
+ const reader = response.body.getReader();
198
+ const decoder = new TextDecoder();
199
+ let buffer = "";
200
+ let currentEvent = "";
201
+
202
+ while (true) {
203
+ const { done, value } = await reader.read();
204
+ if (done) break;
205
+
206
+ buffer += decoder.decode(value, { stream: true });
207
+ const lines = buffer.split("\n");
208
+ buffer = lines.pop() ?? "";
209
+
210
+ for (const line of lines) {
211
+ if (line.startsWith("event:")) {
212
+ currentEvent = line.replace("event:", "").trim();
213
+ } else if (line.startsWith("data:")) {
214
+ const data = line.replace("data:", "").trim();
215
+
216
+ if (currentEvent === "sources" && data) {
217
+ const sources: ChatSource[] = JSON.parse(data);
218
+ setChats((prev) =>
219
+ prev.map((chat) =>
220
+ chat.id === roomId
221
+ ? {
222
+ ...chat,
223
+ messages: chat.messages.map((m) =>
224
+ m.id === assistantMsgId ? { ...m, sources } : m
225
+ ),
226
+ }
227
+ : chat
228
+ )
229
+ );
230
+ } else if (currentEvent === "chunk" && data) {
231
+ setChats((prev) =>
232
+ prev.map((chat) =>
233
+ chat.id === roomId
234
+ ? {
235
+ ...chat,
236
+ messages: chat.messages.map((m) =>
237
+ m.id === assistantMsgId
238
+ ? { ...m, content: m.content + data }
239
+ : m
240
+ ),
241
+ }
242
+ : chat
243
+ )
244
+ );
245
+ } else if (currentEvent === "message" && data) {
246
+ setChats((prev) =>
247
+ prev.map((chat) =>
248
+ chat.id === roomId
249
+ ? {
250
+ ...chat,
251
+ messages: chat.messages.map((m) =>
252
+ m.id === assistantMsgId
253
+ ? { ...m, content: data }
254
+ : m
255
+ ),
256
+ }
257
+ : chat
258
+ )
259
+ );
260
+ }
261
+ }
262
+ }
263
+ }
264
+ } catch (err: unknown) {
265
+ if ((err as Error).name !== "AbortError") {
266
+ setChats((prev) =>
267
+ prev.map((chat) =>
268
+ chat.id === roomId
269
+ ? {
270
+ ...chat,
271
+ messages: chat.messages.map((m) =>
272
+ m.id === assistantMsgId
273
+ ? {
274
+ ...m,
275
+ content:
276
+ "Error: Failed to get response. Please try again.",
277
+ }
278
+ : m
279
+ ),
280
+ }
281
+ : chat
282
+ )
283
+ );
284
+ }
285
+ } finally {
286
+ setIsStreaming(false);
287
+ abortControllerRef.current = null;
288
+ }
289
  };
290
 
291
  const handleKeyPress = (e: React.KeyboardEvent) => {
 
314
  </div>
315
 
316
  <div className="flex-1 overflow-y-auto p-2 space-y-1">
317
+ {roomsLoading ? (
318
+ <div className="flex justify-center py-4">
319
+ <Loader2 className="w-4 h-4 animate-spin text-white/70" />
320
+ </div>
321
+ ) : roomsError ? (
322
+ <p className="text-xs text-red-200 text-center px-2 py-2">
323
+ {roomsError}
324
+ </p>
325
+ ) : (
326
+ chats.map((chat) => (
327
  <div
328
+ key={chat.id}
329
+ className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition group ${
330
+ currentChatId === chat.id
331
+ ? "bg-white/25"
332
+ : "hover:bg-white/15"
333
+ }`}
334
  >
335
+ <MessageSquare className="w-3.5 h-3.5 flex-shrink-0" />
336
+ <div
337
+ className="flex-1 truncate text-sm"
338
+ onClick={() => setCurrentChatId(chat.id)}
339
+ >
340
+ {chat.title}
341
+ </div>
342
+ <button
343
+ onClick={(e) => {
344
+ e.stopPropagation();
345
+ deleteChat(chat.id);
346
+ }}
347
+ className="opacity-0 group-hover:opacity-100 transition"
348
+ >
349
+ <Trash2 className="w-3.5 h-3.5 text-red-100 hover:text-white" />
350
+ </button>
351
  </div>
352
+ ))
353
+ )}
 
 
 
 
 
 
 
 
 
354
  </div>
355
 
356
  <div className="border-t border-white/20 p-3 space-y-2">
 
444
  <p className="whitespace-pre-wrap break-words text-sm">
445
  {message.content}
446
  </p>
447
+ {message.role === "assistant" &&
448
+ message.sources &&
449
+ message.sources.length > 0 && (
450
+ <div className="mt-2 pt-2 border-t border-slate-100">
451
+ <p className="text-[10px] text-slate-400 mb-1">
452
+ Sources:
453
+ </p>
454
+ <div className="flex flex-wrap gap-1">
455
+ {message.sources.map((src, i) => (
456
+ <span
457
+ key={i}
458
+ className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded"
459
+ title={
460
+ src.page_label
461
+ ? `Page ${src.page_label}`
462
+ : undefined
463
+ }
464
+ >
465
+ {src.filename}
466
+ {src.page_label ? ` p.${src.page_label}` : ""}
467
+ </span>
468
+ ))}
469
+ </div>
470
+ </div>
471
+ )}
472
  </div>
473
  </div>
474
  ))}
475
 
476
+ {!currentChat && chats.length === 0 && !roomsLoading && (
477
  <div className="flex items-center justify-center h-full">
478
  <div className="text-center">
479
  <MessageSquare className="w-12 h-12 text-slate-300 mx-auto mb-3" />
 
511
  <Send className="w-4 h-4" />
512
  </button>
513
  </div>
 
 
 
514
  </div>
515
  </div>
516
  </div>
src/services/api.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── Types ────────────────────────────────────────────────────────────────────
2
+
3
+ export interface LoginResponse {
4
+ status: string;
5
+ message: string;
6
+ data: { user_id: string; email: string; name: string };
7
+ }
8
+
9
+ export interface Room {
10
+ id: string;
11
+ title: string;
12
+ created_at: string;
13
+ updated_at: string | null;
14
+ }
15
+
16
+ export interface CreateRoomResponse {
17
+ status: string;
18
+ message: string;
19
+ data: Room;
20
+ }
21
+
22
+ export type DocumentStatus = "pending" | "processing" | "completed" | "failed";
23
+
24
+ export interface ApiDocument {
25
+ id: string;
26
+ filename: string;
27
+ status: DocumentStatus;
28
+ file_size: number;
29
+ file_type: string;
30
+ created_at: string;
31
+ }
32
+
33
+ export interface UploadDocumentResponse {
34
+ status: string;
35
+ message: string;
36
+ data: { id: string; filename: string; status: DocumentStatus };
37
+ }
38
+
39
+ export interface ChatSource {
40
+ document_id: string;
41
+ filename: string;
42
+ page_label: string | null;
43
+ }
44
+
45
+ // ─── Base Client ──────────────────────────────────────────────────────────────
46
+
47
+ const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string) ?? "";
48
+
49
+ async function request<T>(path: string, options?: RequestInit): Promise<T> {
50
+ const res = await fetch(`${BASE_URL}${path}`, {
51
+ headers: { "Content-Type": "application/json", ...options?.headers },
52
+ ...options,
53
+ });
54
+ if (!res.ok) {
55
+ const err = await res
56
+ .json()
57
+ .catch(() => ({ detail: `HTTP ${res.status}` }));
58
+ throw new Error(err.detail ?? `HTTP ${res.status}`);
59
+ }
60
+ return res.json() as Promise<T>;
61
+ }
62
+
63
+ // ─── Auth ─────────────────────────────────────────────────────────────────────
64
+
65
+ export const login = (email: string, password: string) =>
66
+ request<LoginResponse>("/api/login", {
67
+ method: "POST",
68
+ body: JSON.stringify({ email, password }),
69
+ });
70
+
71
+ // ─── Rooms ────────────────────────────────────────────────────────────────────
72
+
73
+ export const getRooms = (userId: string) =>
74
+ request<Room[]>(`/api/v1/rooms/${userId}`);
75
+
76
+ export const createRoom = (userId: string, title?: string) =>
77
+ request<CreateRoomResponse>("/api/v1/room/create", {
78
+ method: "POST",
79
+ body: JSON.stringify({ user_id: userId, title }),
80
+ });
81
+
82
+ // ─── Documents ────────────────────────────────────────────────────────────────
83
+
84
+ export const getDocuments = (userId: string) =>
85
+ request<ApiDocument[]>(`/api/v1/documents/${userId}`);
86
+
87
+ export const uploadDocument = async (
88
+ userId: string,
89
+ file: File
90
+ ): Promise<UploadDocumentResponse> => {
91
+ const form = new FormData();
92
+ form.append("file", file);
93
+ const res = await fetch(
94
+ `${BASE_URL}/api/v1/document/upload?user_id=${userId}`,
95
+ { method: "POST", body: form }
96
+ );
97
+ if (!res.ok) {
98
+ const err = await res
99
+ .json()
100
+ .catch(() => ({ detail: `HTTP ${res.status}` }));
101
+ throw new Error(err.detail ?? `HTTP ${res.status}`);
102
+ }
103
+ return res.json() as Promise<UploadDocumentResponse>;
104
+ };
105
+
106
+ export const processDocument = (userId: string, documentId: string) =>
107
+ request<{
108
+ status: string;
109
+ message: string;
110
+ data: { document_id: string; chunks_processed: number };
111
+ }>(
112
+ `/api/v1/document/process?document_id=${documentId}&user_id=${userId}`,
113
+ { method: "POST" }
114
+ );
115
+
116
+ export const deleteDocument = (userId: string, documentId: string) =>
117
+ request<{ status: string; message: string }>(
118
+ `/api/v1/document/delete?document_id=${documentId}&user_id=${userId}`,
119
+ { method: "DELETE" }
120
+ );
121
+
122
+ // ─── Chat ─────────────────────────────────────────────────────────────────────
123
+
124
+ export const streamChat = (
125
+ userId: string,
126
+ roomId: string,
127
+ message: string
128
+ ): Promise<Response> =>
129
+ fetch(`${BASE_URL}/api/v1/chat/stream`, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({ user_id: userId, room_id: roomId, message }),
133
+ });