Girish Jeswani commited on
Commit
e000b2f
·
1 Parent(s): a4ac46b

fix export bug

Browse files
multi_llm_chatbot_backend/app/api/routes/documents.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, Request, HTTPException, UploadFile, File, Body
2
  from fastapi import Query
3
  from app.utils.document_extractor import extract_text_from_file
4
  from app.core.session_manager import get_session_manager
@@ -10,14 +10,148 @@ from app.utils.file_export import prepare_export_response, generate_pdf_file_fro
10
  from app.core.session_manager import get_session_manager
11
  from app.api.utils import get_or_create_session_for_request
12
  from app.core.bootstrap import chat_orchestrator
 
 
 
 
13
  import logging
 
 
 
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
  router = APIRouter()
18
 
19
  session_manager = get_session_manager()
20
- get_rag_manager = get_rag_manager # avoid circular import issues
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  @router.post("/upload-document")
@@ -185,61 +319,190 @@ async def get_document_insights(filename: str, request: Request):
185
  raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}")
186
 
187
  @router.get("/export-chat")
188
- async def export_chat(request: Request, format: str = Query(..., regex="^(txt|pdf|docx)$")):
 
 
 
 
 
 
 
 
 
 
189
  try:
190
- session_id = get_or_create_session_for_request(request)
191
- session = session_manager.get_session(session_id)
192
-
193
- if not session.messages:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  return {"error": "No messages in this session."}
195
 
196
- return prepare_export_response(session.messages, format)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
 
 
198
  except Exception as e:
199
  logger.error(f"Error exporting chat: {str(e)}")
200
- return {"error": "Failed to export chat.", "detail": str(e)}
 
 
 
201
 
202
  @router.get("/chat-summary")
203
  async def chat_summary(
204
  request: Request,
205
- format: str = Query("text", regex="^(txt|pdf|docx)$")
 
 
206
  ):
207
  """
208
- Generate and return a summary of the current session chat.
 
 
209
  Can return as plain txt, PDF, or DOCX.
210
  """
211
  try:
212
- session_id = get_or_create_session_for_request(request)
213
- session = session_manager.get_session(session_id)
214
-
215
- if not session.messages:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  return {"error": "No messages in this session."}
217
 
218
- llm = next(iter(chat_orchestrator.personas.values())).llm
219
- summary_text = await generate_summary_from_messages(session.messages, llm)
220
-
221
- if format == "txt":
222
- # Use improved formatting for text export
223
- formatted_summary = format_summary_for_text_export(summary_text)
224
- return prepare_export_response(formatted_summary, "txt", filename_prefix="chat_summary")
225
-
226
- elif format == "docx":
227
- # Use improved formatting for DOCX export
228
- formatted_summary = format_summary_for_text_export(summary_text)
229
- return prepare_export_response(formatted_summary, "docx", filename_prefix="chat_summary")
230
-
231
- elif format == "pdf":
232
- # Parse and render using block formatting
233
- blocks = [{"type": "heading", "text": "Chat Summary"}] + parse_summary_to_blocks(summary_text)
234
-
235
- file_stream = generate_pdf_file_from_blocks(blocks)
236
- return StreamingResponse(
237
- file_stream,
238
- media_type="application/pdf",
239
- headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"}
240
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
 
 
242
  except Exception as e:
243
  logger.error(f"Error in chat-summary endpoint: {str(e)}")
244
- return {"error": "Summary generation failed", "detail": str(e)}
 
 
 
245
 
 
1
+ from fastapi import APIRouter, Request, HTTPException, UploadFile, File, Body, Depends
2
  from fastapi import Query
3
  from app.utils.document_extractor import extract_text_from_file
4
  from app.core.session_manager import get_session_manager
 
10
  from app.core.session_manager import get_session_manager
11
  from app.api.utils import get_or_create_session_for_request
12
  from app.core.bootstrap import chat_orchestrator
13
+ from app.core.auth import get_current_active_user
14
+ from app.core.database import get_database
15
+ from app.models.user import User
16
+ from bson import ObjectId
17
  import logging
18
+ import re
19
+ from html import unescape
20
+
21
 
22
  logger = logging.getLogger(__name__)
23
 
24
  router = APIRouter()
25
 
26
  session_manager = get_session_manager()
27
+ get_rag_manager = get_rag_manager
28
+
29
+
30
+ def sanitize_html_content(content):
31
+ """
32
+ Clean up HTML content by removing or fixing malformed tags.
33
+ This prevents PDF export errors caused by invalid HTML structure.
34
+ """
35
+ if not content:
36
+ return content
37
+
38
+ try:
39
+ logger.debug(f"Sanitizing content (first 200 chars): {content[:200]}")
40
+
41
+ # First, unescape HTML entities
42
+ content = unescape(content)
43
+
44
+ # More aggressive approach: Strip ALL HTML tags first, then apply simple formatting
45
+ # This prevents malformed HTML from causing issues
46
+
47
+ # Remove all HTML tags completely (most aggressive approach)
48
+ content = re.sub(r'<[^>]*>', '', content)
49
+
50
+ # Clean up multiple spaces and normalize whitespace
51
+ content = re.sub(r'\s+', ' ', content)
52
+ content = content.strip()
53
+
54
+ # Remove any remaining HTML entities that might have been missed
55
+ content = re.sub(r'&[a-zA-Z0-9#]+;', '', content)
56
+
57
+ # Remove any remaining angle brackets that might cause issues
58
+ content = content.replace('<', '').replace('>', '')
59
+
60
+ logger.debug(f"Sanitized content (first 200 chars): {content[:200]}")
61
+ return content
62
+
63
+ except Exception as e:
64
+ logger.error(f"Error sanitizing HTML content: {str(e)}")
65
+ # Ultra-fallback: return only alphanumeric and basic punctuation
66
+ try:
67
+ import string
68
+ allowed_chars = string.ascii_letters + string.digits + string.punctuation + ' \n\r\t'
69
+ cleaned = ''.join(c for c in content if c in allowed_chars)
70
+ return re.sub(r'\s+', ' ', cleaned).strip()
71
+ except:
72
+ return "Content could not be sanitized for export"
73
+
74
+ def convert_messages_for_export(messages):
75
+ """
76
+ Convert stored message format to export-compatible format.
77
+ Stored format uses 'type', export functions expect 'role' and specific structure.
78
+ """
79
+ converted_messages = []
80
+
81
+ for i, msg in enumerate(messages):
82
+ try:
83
+ # Get and sanitize content
84
+ raw_content = msg.get('content', '')
85
+ sanitized_content = sanitize_html_content(raw_content)
86
+
87
+ # Debug logging for problematic content
88
+ if i < 5 or '<' in raw_content or '>' in raw_content: # Log first few messages and any with HTML
89
+ logger.debug(f"Message {i}: Original length: {len(raw_content)}, Sanitized length: {len(sanitized_content)}")
90
+ if raw_content != sanitized_content:
91
+ logger.debug(f"Content changed during sanitization for message {msg.get('id', 'unknown')}")
92
+
93
+ # Create base converted message
94
+ converted_msg = {
95
+ 'id': msg.get('id', 'unknown'),
96
+ 'timestamp': msg.get('timestamp', ''),
97
+ 'content': sanitized_content,
98
+ }
99
+
100
+ # Convert type to role and handle different message types
101
+ msg_type = msg.get('type', 'unknown')
102
+
103
+ if msg_type == 'user':
104
+ converted_msg['role'] = 'user'
105
+ # Add reply context if present
106
+ if 'replyTo' in msg:
107
+ reply_to = msg['replyTo']
108
+ converted_msg['content'] = f"[Reply to {reply_to.get('advisorName', 'advisor')}] {converted_msg['content']}"
109
+
110
+ elif msg_type == 'advisor':
111
+ converted_msg['role'] = 'assistant'
112
+ # Include advisor information
113
+ advisor_name = msg.get('advisorName', msg.get('persona', 'Advisor'))
114
+ converted_msg['advisor_name'] = advisor_name
115
+ converted_msg['advisor_id'] = msg.get('advisorId', msg.get('persona_id', 'unknown'))
116
+
117
+ # Mark special response types
118
+ if msg.get('isReply'):
119
+ converted_msg['content'] = f"[{advisor_name} replies] {converted_msg['content']}"
120
+ elif msg.get('isExpansion'):
121
+ converted_msg['content'] = f"[{advisor_name} expands] {converted_msg['content']}"
122
+ else:
123
+ converted_msg['content'] = f"[{advisor_name}] {converted_msg['content']}"
124
+
125
+ elif msg_type == 'system':
126
+ converted_msg['role'] = 'system'
127
+
128
+ elif msg_type == 'document_upload':
129
+ converted_msg['role'] = 'system'
130
+ converted_msg['content'] = f"📄 {converted_msg['content']}"
131
+
132
+ elif msg_type == 'error':
133
+ converted_msg['role'] = 'system'
134
+ converted_msg['content'] = f"❌ Error: {converted_msg['content']}"
135
+
136
+ else:
137
+ # Unknown type, treat as system message
138
+ converted_msg['role'] = 'system'
139
+ converted_msg['content'] = f"[{msg_type}] {converted_msg['content']}"
140
+
141
+ converted_messages.append(converted_msg)
142
+
143
+ except Exception as e:
144
+ logger.error(f"Error converting message {msg.get('id', 'unknown')}: {str(e)}")
145
+ # Add a fallback message to maintain conversation flow
146
+ converted_messages.append({
147
+ 'id': msg.get('id', 'unknown'),
148
+ 'role': 'system',
149
+ 'content': f"[Message conversion error: {str(e)}]",
150
+ 'timestamp': msg.get('timestamp', '')
151
+ })
152
+
153
+ logger.info(f"Converted {len(messages)} messages for export")
154
+ return converted_messages
155
 
156
 
157
  @router.post("/upload-document")
 
319
  raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}")
320
 
321
  @router.get("/export-chat")
322
+ async def export_chat(
323
+ request: Request,
324
+ format: str = Query(..., regex="^(txt|pdf|docx)$"),
325
+ chat_session_id: str = Query(None, description="Optional: specific chat session ID to export"),
326
+ current_user: User = Depends(get_current_active_user)
327
+ ):
328
+ """
329
+ Export chat messages.
330
+ If chat_session_id is provided, exports that specific stored chat session.
331
+ Otherwise, exports the current in-memory session.
332
+ """
333
  try:
334
+ messages = []
335
+
336
+ if chat_session_id:
337
+ # Export specific stored chat session
338
+ db = get_database()
339
+ session_data = await db.chat_sessions.find_one({
340
+ "_id": ObjectId(chat_session_id),
341
+ "user_id": current_user.id,
342
+ "is_active": True
343
+ })
344
+
345
+ if not session_data:
346
+ raise HTTPException(
347
+ status_code=404,
348
+ detail="Chat session not found or you don't have permission to access it"
349
+ )
350
+
351
+ raw_messages = session_data.get("messages", [])
352
+ # Convert stored message format to export-compatible format
353
+ messages = convert_messages_for_export(raw_messages)
354
+ else:
355
+ # Export current in-memory session (existing behavior)
356
+ session_id = get_or_create_session_for_request(request)
357
+ session = session_manager.get_session(session_id)
358
+ # In-memory messages might already be in the right format, but convert to be safe
359
+ messages = convert_messages_for_export(session.messages)
360
+
361
+ if not messages:
362
  return {"error": "No messages in this session."}
363
 
364
+ try:
365
+ return prepare_export_response(messages, format)
366
+ except Exception as export_error:
367
+ logger.error(f"Error in prepare_export_response: {str(export_error)}")
368
+ # Try with a simplified version of messages if the export fails
369
+ try:
370
+ # Create simplified messages with just basic text content
371
+ simplified_messages = []
372
+ for msg in messages:
373
+ simplified_msg = {
374
+ 'id': msg.get('id', 'unknown'),
375
+ 'role': msg.get('role', 'system'),
376
+ 'content': str(msg.get('content', '')).replace('\n', ' ').strip(),
377
+ 'timestamp': msg.get('timestamp', '')
378
+ }
379
+ if 'advisor_name' in msg:
380
+ simplified_msg['advisor_name'] = msg['advisor_name']
381
+ simplified_messages.append(simplified_msg)
382
+
383
+ return prepare_export_response(simplified_messages, format)
384
+ except Exception as fallback_error:
385
+ logger.error(f"Fallback export also failed: {str(fallback_error)}")
386
+ raise HTTPException(
387
+ status_code=500,
388
+ detail=f"Export failed due to content formatting issues. Please try a different format or contact support."
389
+ )
390
 
391
+ except HTTPException:
392
+ raise
393
  except Exception as e:
394
  logger.error(f"Error exporting chat: {str(e)}")
395
+ raise HTTPException(
396
+ status_code=500,
397
+ detail=f"Failed to export chat: {str(e)}"
398
+ )
399
 
400
  @router.get("/chat-summary")
401
  async def chat_summary(
402
  request: Request,
403
+ format: str = Query("text", regex="^(txt|pdf|docx)$"),
404
+ chat_session_id: str = Query(None, description="Optional: specific chat session ID to summarize"),
405
+ current_user: User = Depends(get_current_active_user)
406
  ):
407
  """
408
+ Generate and return a summary of chat messages.
409
+ If chat_session_id is provided, summarizes that specific stored chat session.
410
+ Otherwise, summarizes the current in-memory session.
411
  Can return as plain txt, PDF, or DOCX.
412
  """
413
  try:
414
+ messages = []
415
+
416
+ if chat_session_id:
417
+ # Summarize specific stored chat session
418
+ db = get_database()
419
+ session_data = await db.chat_sessions.find_one({
420
+ "_id": ObjectId(chat_session_id),
421
+ "user_id": current_user.id,
422
+ "is_active": True
423
+ })
424
+
425
+ if not session_data:
426
+ raise HTTPException(
427
+ status_code=404,
428
+ detail="Chat session not found or you don't have permission to access it"
429
+ )
430
+
431
+ raw_messages = session_data.get("messages", [])
432
+ # Convert stored message format for summary generation
433
+ messages = convert_messages_for_export(raw_messages)
434
+ else:
435
+ # Summarize current in-memory session (existing behavior)
436
+ session_id = get_or_create_session_for_request(request)
437
+ session = session_manager.get_session(session_id)
438
+ # Convert in-memory messages
439
+ messages = convert_messages_for_export(session.messages)
440
+
441
+ if not messages:
442
  return {"error": "No messages in this session."}
443
 
444
+ try:
445
+ llm = next(iter(chat_orchestrator.personas.values())).llm
446
+ summary_text = await generate_summary_from_messages(messages, llm)
447
+
448
+ if format == "txt":
449
+ # Use improved formatting for text export
450
+ formatted_summary = format_summary_for_text_export(summary_text)
451
+ return prepare_export_response(formatted_summary, "txt", filename_prefix="chat_summary")
452
+
453
+ elif format == "docx":
454
+ # Use improved formatting for DOCX export
455
+ formatted_summary = format_summary_for_text_export(summary_text)
456
+ return prepare_export_response(formatted_summary, "docx", filename_prefix="chat_summary")
457
+
458
+ elif format == "pdf":
459
+ # Parse and render using block formatting
460
+ blocks = [{"type": "heading", "text": "Chat Summary"}] + parse_summary_to_blocks(summary_text)
461
+
462
+ file_stream = generate_pdf_file_from_blocks(blocks)
463
+ return StreamingResponse(
464
+ file_stream,
465
+ media_type="application/pdf",
466
+ headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"}
467
+ )
468
+ except Exception as summary_error:
469
+ logger.error(f"Error generating summary: {str(summary_error)}")
470
+ # Try with simplified content
471
+ try:
472
+ # Create a basic text summary if AI summary fails
473
+ basic_summary = "Chat Summary\n\n"
474
+ for msg in messages:
475
+ if msg.get('role') == 'user':
476
+ basic_summary += f"User: {msg.get('content', '')[:200]}...\n\n"
477
+ elif msg.get('role') == 'assistant':
478
+ advisor_name = msg.get('advisor_name', 'Advisor')
479
+ basic_summary += f"{advisor_name}: {msg.get('content', '')[:200]}...\n\n"
480
+
481
+ if format == "txt":
482
+ return prepare_export_response(basic_summary, "txt", filename_prefix="chat_summary")
483
+ elif format == "docx":
484
+ return prepare_export_response(basic_summary, "docx", filename_prefix="chat_summary")
485
+ elif format == "pdf":
486
+ blocks = [{"type": "heading", "text": "Chat Summary"}, {"type": "paragraph", "text": basic_summary}]
487
+ file_stream = generate_pdf_file_from_blocks(blocks)
488
+ return StreamingResponse(
489
+ file_stream,
490
+ media_type="application/pdf",
491
+ headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"}
492
+ )
493
+ except Exception as fallback_error:
494
+ logger.error(f"Fallback summary export also failed: {str(fallback_error)}")
495
+ raise HTTPException(
496
+ status_code=500,
497
+ detail=f"Summary generation failed due to content formatting issues. Please try a different format."
498
+ )
499
 
500
+ except HTTPException:
501
+ raise
502
  except Exception as e:
503
  logger.error(f"Error in chat-summary endpoint: {str(e)}")
504
+ raise HTTPException(
505
+ status_code=500,
506
+ detail=f"Summary generation failed: {str(e)}"
507
+ )
508
 
phd-advisor-frontend/src/components/ExportButton.js CHANGED
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
2
  import { Download, FileText, FileType, File, Check, X, Loader2 } from 'lucide-react';
3
  import '../styles/ExportButton.css';
4
 
5
- const ExportButton = ({ hasMessages = false }) => {
6
  const [showDropdown, setShowDropdown] = useState(false);
7
  const [isExporting, setIsExporting] = useState(false);
8
  const [exportStatus, setExportStatus] = useState(null);
@@ -62,11 +62,24 @@ const ExportButton = ({ hasMessages = false }) => {
62
  const selectedTypeData = exportTypes.find(t => t.id === selectedType);
63
  const endpoint = selectedTypeData.endpoint;
64
 
65
- const response = await fetch(`http://localhost:8000/${endpoint}?format=${format}`, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  method: 'GET',
67
- headers: {
68
- 'Content-Type': 'application/json',
69
- },
70
  });
71
 
72
  if (!response.ok) {
@@ -87,14 +100,14 @@ const ExportButton = ({ hasMessages = false }) => {
87
 
88
  // Create blob and download
89
  const blob = await response.blob();
90
- const url = window.URL.createObjectURL(blob);
91
  const link = document.createElement('a');
92
- link.href = url;
93
  link.download = filename;
94
  document.body.appendChild(link);
95
  link.click();
96
  document.body.removeChild(link);
97
- window.URL.revokeObjectURL(url);
98
 
99
  setExportStatus('success');
100
  setTimeout(() => setExportStatus(null), 3000);
@@ -142,7 +155,10 @@ const ExportButton = ({ hasMessages = false }) => {
142
  if (isExporting) return 'Exporting chat...';
143
  if (exportStatus === 'success') return 'Export successful!';
144
  if (exportStatus === 'error') return 'Export failed - click to retry';
145
- return 'Export chat conversation';
 
 
 
146
  };
147
 
148
  return (
@@ -158,69 +174,74 @@ const ExportButton = ({ hasMessages = false }) => {
158
  </button>
159
 
160
  {showDropdown && (
161
- <div className="export-dropdown">
162
- <div className="export-dropdown-header">
163
- <h4>Export Options</h4>
164
- <p>Choose what to export and in which format</p>
165
- </div>
166
-
167
- {/* ADD THIS NEW SECTION - Export Type Selection */}
168
- <div className="export-type-section">
169
- <h5>What to export:</h5>
170
- <div className="export-type-buttons">
171
- {exportTypes.map((type) => (
172
- <button
173
- key={type.id}
174
- onClick={() => setSelectedType(type.id)}
175
- className={`export-type-button ${selectedType === type.id ? 'active' : ''}`}
176
- disabled={isExporting}
177
- >
178
- <div className="type-info">
179
- <div className="type-name">{type.name}</div>
180
- <div className="type-description">{type.description}</div>
181
- </div>
182
- </button>
183
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </div>
185
- </div>
186
-
187
- {/* EXISTING FORMAT SECTION - UPDATE THE HEADER */}
188
- <div className="export-format-section">
189
- <h5>Format:</h5>
190
- <div className="export-format-list">
191
- {exportFormats.map((format) => {
192
- const Icon = format.icon;
193
- return (
194
- <button
195
- key={format.id}
196
- onClick={() => handleFormatSelect(format.id)}
197
- className="export-format-button"
198
- disabled={isExporting}
199
- >
200
- <div className="format-icon">
201
- <Icon size={20} />
202
- </div>
203
- <div className="format-info">
204
- <div className="format-name">{format.name}</div>
205
- <div className="format-description">{format.description}</div>
206
- </div>
207
- <div className="format-extension">{format.extension}</div>
208
- </button>
209
- );
210
- })}
211
- </div>
212
- </div>
213
-
214
- <div className="export-dropdown-footer">
215
- <span>
216
- {selectedType === 'chat'
217
- ? 'Full conversation history will be included'
218
- : 'AI will generate a concise summary of your conversation'
219
- }
220
- </span>
221
- </div>
222
- </div>
223
- )}
224
 
225
  {exportStatus && (
226
  <div className={`export-status ${exportStatus}`}>
 
2
  import { Download, FileText, FileType, File, Check, X, Loader2 } from 'lucide-react';
3
  import '../styles/ExportButton.css';
4
 
5
+ const ExportButton = ({ hasMessages = false, currentSessionId = null, authToken = null }) => {
6
  const [showDropdown, setShowDropdown] = useState(false);
7
  const [isExporting, setIsExporting] = useState(false);
8
  const [exportStatus, setExportStatus] = useState(null);
 
62
  const selectedTypeData = exportTypes.find(t => t.id === selectedType);
63
  const endpoint = selectedTypeData.endpoint;
64
 
65
+ // Build the URL with session ID if available
66
+ let url = `http://localhost:8000/${endpoint}?format=${format}`;
67
+ if (currentSessionId) {
68
+ url += `&chat_session_id=${currentSessionId}`;
69
+ }
70
+
71
+ // Build headers - include auth token if available (needed for specific session export)
72
+ const headers = {
73
+ 'Content-Type': 'application/json',
74
+ };
75
+
76
+ if (authToken && currentSessionId) {
77
+ headers['Authorization'] = `Bearer ${authToken}`;
78
+ }
79
+
80
+ const response = await fetch(url, {
81
  method: 'GET',
82
+ headers: headers,
 
 
83
  });
84
 
85
  if (!response.ok) {
 
100
 
101
  // Create blob and download
102
  const blob = await response.blob();
103
+ const url_blob = window.URL.createObjectURL(blob);
104
  const link = document.createElement('a');
105
+ link.href = url_blob;
106
  link.download = filename;
107
  document.body.appendChild(link);
108
  link.click();
109
  document.body.removeChild(link);
110
+ window.URL.revokeObjectURL(url_blob);
111
 
112
  setExportStatus('success');
113
  setTimeout(() => setExportStatus(null), 3000);
 
155
  if (isExporting) return 'Exporting chat...';
156
  if (exportStatus === 'success') return 'Export successful!';
157
  if (exportStatus === 'error') return 'Export failed - click to retry';
158
+
159
+ // Show whether we're exporting current session or specific saved chat
160
+ const sessionInfo = currentSessionId ? 'this saved chat' : 'current session';
161
+ return `Export ${sessionInfo}`;
162
  };
163
 
164
  return (
 
174
  </button>
175
 
176
  {showDropdown && (
177
+ <div className="export-dropdown">
178
+ <div className="export-dropdown-header">
179
+ <h4>Export Options</h4>
180
+ <p>
181
+ {currentSessionId
182
+ ? 'Export this saved chat conversation'
183
+ : 'Export current session'
184
+ }
185
+ </p>
186
+ </div>
187
+
188
+ {/* Export Type Selection */}
189
+ <div className="export-type-section">
190
+ <h5>What to export:</h5>
191
+ <div className="export-type-buttons">
192
+ {exportTypes.map((type) => (
193
+ <button
194
+ key={type.id}
195
+ onClick={() => setSelectedType(type.id)}
196
+ className={`export-type-button ${selectedType === type.id ? 'active' : ''}`}
197
+ disabled={isExporting}
198
+ >
199
+ <div className="type-info">
200
+ <div className="type-name">{type.name}</div>
201
+ <div className="type-description">{type.description}</div>
202
+ </div>
203
+ </button>
204
+ ))}
205
+ </div>
206
+ </div>
207
+
208
+ {/* Format Selection */}
209
+ <div className="export-format-section">
210
+ <h5>Format:</h5>
211
+ <div className="export-format-list">
212
+ {exportFormats.map((format) => {
213
+ const Icon = format.icon;
214
+ return (
215
+ <button
216
+ key={format.id}
217
+ onClick={() => handleFormatSelect(format.id)}
218
+ className="export-format-button"
219
+ disabled={isExporting}
220
+ >
221
+ <div className="format-icon">
222
+ <Icon size={20} />
223
+ </div>
224
+ <div className="format-info">
225
+ <div className="format-name">{format.name}</div>
226
+ <div className="format-description">{format.description}</div>
227
+ </div>
228
+ <div className="format-extension">{format.extension}</div>
229
+ </button>
230
+ );
231
+ })}
232
+ </div>
233
+ </div>
234
+
235
+ <div className="export-dropdown-footer">
236
+ <span>
237
+ {selectedType === 'chat'
238
+ ? 'Full conversation history will be included'
239
+ : 'AI will generate a concise summary of your conversation'
240
+ }
241
+ </span>
242
+ </div>
243
  </div>
244
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  {exportStatus && (
247
  <div className={`export-status ${exportStatus}`}>
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -650,7 +650,11 @@ const handleNewChat = async (sessionId = null) => {
650
  </button>
651
 
652
  {/* Export Button */}
653
- <ExportButton hasMessages={hasConversationMessages} />
 
 
 
 
654
 
655
  {/* Provider Dropdown */}
656
  <ProviderDropdown
 
650
  </button>
651
 
652
  {/* Export Button */}
653
+ <ExportButton
654
+ hasMessages={hasConversationMessages}
655
+ currentSessionId={currentSessionId}
656
+ authToken={authToken}
657
+ />
658
 
659
  {/* Provider Dropdown */}
660
  <ProviderDropdown