Prof-Reza commited on
Commit
d33d74b
·
verified ·
1 Parent(s): a9d92ec

Save generated files to sessions; attachments feature

Browse files

Implement persistent attachments for course creator: update db.py to include attachments table and add helpers (add_attachment, list_attachments). Modify app.py to import new helpers, add attachments output component, and update session loading and finalize/generate functions to store generated documents and zip packages in the database and update attachments display. Now each chat session lists its saved files and caches resources.

Files changed (2) hide show
  1. app.py +57 -19
  2. db.py +57 -1
app.py CHANGED
@@ -21,6 +21,8 @@ from db import (
21
  list_chats,
22
  rename_chat,
23
  delete_chat,
 
 
24
  )
25
 
26
  # Import the docx utility to generate Word documents for course outlines.
@@ -437,31 +439,58 @@ def finalize_and_doc(chat_history, chat_pairs, sources, plan, chat_key):
437
  with open(tmp_path, "w") as f:
438
  f.write(err_msg)
439
  doc_path = tmp_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  # Update plan state
441
  plan = plan_text
442
- return plan_text, chat_history, chat_pairs, sources, plan, doc_path
443
 
444
- def generate_package(plan, sources):
445
- """Generate the final course package zip file."""
446
- if plan is None or plan == "":
447
- # fallback: create a minimal plan if none exists
448
  plan = "Course plan is empty."
449
  if sources is None:
450
  sources = []
451
  try:
452
  zip_path = generate_course_zip(plan, sources)
453
- return zip_path
454
  except Exception as e:
455
- # On error, return a message as a text file inside an in-memory file path to avoid raising exceptions in Gradio
456
  err_msg = (
457
  "An error occurred while generating the course package. Please check your API keys or input.\n"
458
  f"(Error: {e})"
459
  )
460
- # Create a text file to return with the error message
461
  tmp_path = "/tmp/error.txt"
462
  with open(tmp_path, "w") as f:
463
  f.write(err_msg)
464
- return tmp_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
  with gr.Blocks() as demo:
467
  gr.Markdown(
@@ -503,6 +532,9 @@ Chat with the assistant to brainstorm your course idea. Use the panel on the lef
503
  finalize_btn = gr.Button("Finalize Outline")
504
  plan_output = gr.Textbox(label="Course outline", interactive=False)
505
  doc_output = gr.File(label="Course outline (Word)")
 
 
 
506
  generate_btn = gr.Button("Generate Course Package")
507
  file_output = gr.File(label="course.zip")
508
 
@@ -572,7 +604,13 @@ Chat with the assistant to brainstorm your course idea. Use the panel on the lef
572
  buffer[1] = content
573
  pairs.append(tuple(buffer))
574
  buffer = []
575
- return pairs, history, pairs, [], "", {}
 
 
 
 
 
 
576
 
577
  # Create a new chat session and return the new key
578
  def handle_new_chat():
@@ -601,11 +639,11 @@ Chat with the assistant to brainstorm your course idea. Use the panel on the lef
601
  None,
602
  [session_picker],
603
  )
604
- # When a session is selected, load it
605
  session_picker.change(
606
  load_session,
607
  inputs=session_picker,
608
- outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
609
  )
610
  # New chat button
611
  new_chat_btn.click(
@@ -619,7 +657,7 @@ Chat with the assistant to brainstorm your course idea. Use the panel on the lef
619
  ).then(
620
  load_session,
621
  inputs=state_chat_key,
622
- outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
623
  )
624
  # Rename button
625
  rename_btn.click(
@@ -643,7 +681,7 @@ Chat with the assistant to brainstorm your course idea. Use the panel on the lef
643
  ).then(
644
  load_session,
645
  inputs=state_chat_key,
646
- outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
647
  )
648
  # Chat submission: include chat_key for persistence
649
  msg_input.submit(
@@ -651,16 +689,16 @@ Chat with the assistant to brainstorm your course idea. Use the panel on the lef
651
  inputs=[msg_input, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache, state_chat_key],
652
  outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
653
  )
654
- # Finalise outline and produce Word doc
655
  finalize_btn.click(
656
  finalize_and_doc,
657
  inputs=[state_chat_history, state_chat_pairs, state_sources, state_plan, state_chat_key],
658
- outputs=[plan_output, state_chat_history, state_chat_pairs, state_sources, state_plan, doc_output],
659
  )
660
- # Generate course package (zip)
661
  generate_btn.click(
662
  generate_package,
663
- inputs=[state_plan, state_sources],
664
- outputs=file_output,
665
  )
666
  demo.launch()
 
21
  list_chats,
22
  rename_chat,
23
  delete_chat,
24
+ add_attachment,
25
+ list_attachments,
26
  )
27
 
28
  # Import the docx utility to generate Word documents for course outlines.
 
439
  with open(tmp_path, "w") as f:
440
  f.write(err_msg)
441
  doc_path = tmp_path
442
+ # Record the generated document as an attachment tied to this chat
443
+ if chat_key:
444
+ try:
445
+ add_attachment(chat_key, doc_path, os.path.basename(doc_path))
446
+ except Exception:
447
+ pass
448
+ # Fetch updated attachment list
449
+ attachments = []
450
+ if chat_key:
451
+ try:
452
+ attachment_records = list_attachments(chat_key)
453
+ attachments = [att.get("file_path") for att in attachment_records if att.get("file_path")]
454
+ except Exception:
455
+ attachments = []
456
  # Update plan state
457
  plan = plan_text
458
+ return plan_text, chat_history, chat_pairs, sources, plan, doc_path, attachments
459
 
460
+ def generate_package(plan, sources, chat_key):
461
+ """Generate the final course package zip file and record it as an attachment."""
462
+ # Fallback: create a minimal plan if none exists
463
+ if not plan:
464
  plan = "Course plan is empty."
465
  if sources is None:
466
  sources = []
467
  try:
468
  zip_path = generate_course_zip(plan, sources)
 
469
  except Exception as e:
470
+ # On error, return a message as a text file inside an in-memory file path
471
  err_msg = (
472
  "An error occurred while generating the course package. Please check your API keys or input.\n"
473
  f"(Error: {e})"
474
  )
 
475
  tmp_path = "/tmp/error.txt"
476
  with open(tmp_path, "w") as f:
477
  f.write(err_msg)
478
+ zip_path = tmp_path
479
+ # Record the generated zip as an attachment
480
+ if chat_key:
481
+ try:
482
+ add_attachment(chat_key, zip_path, os.path.basename(zip_path))
483
+ except Exception:
484
+ pass
485
+ # Fetch updated attachment list
486
+ attachments = []
487
+ if chat_key:
488
+ try:
489
+ records = list_attachments(chat_key)
490
+ attachments = [att.get("file_path") for att in records if att.get("file_path")]
491
+ except Exception:
492
+ attachments = []
493
+ return zip_path, attachments
494
 
495
  with gr.Blocks() as demo:
496
  gr.Markdown(
 
532
  finalize_btn = gr.Button("Finalize Outline")
533
  plan_output = gr.Textbox(label="Course outline", interactive=False)
534
  doc_output = gr.File(label="Course outline (Word)")
535
+ # Display any files generated during the chat session. This component
536
+ # will show multiple attachments and allow downloading them.
537
+ attachments_output = gr.File(label="Attachments", file_count="multiple")
538
  generate_btn = gr.Button("Generate Course Package")
539
  file_output = gr.File(label="course.zip")
540
 
 
604
  buffer[1] = content
605
  pairs.append(tuple(buffer))
606
  buffer = []
607
+ # Load any previously generated attachments for this chat
608
+ try:
609
+ attachment_records = list_attachments(selected_key)
610
+ attachments = [att.get("file_path") for att in attachment_records if att.get("file_path")]
611
+ except Exception:
612
+ attachments = []
613
+ return pairs, history, pairs, [], "", {}, attachments
614
 
615
  # Create a new chat session and return the new key
616
  def handle_new_chat():
 
639
  None,
640
  [session_picker],
641
  )
642
+ # When a session is selected, load it along with its attachments
643
  session_picker.change(
644
  load_session,
645
  inputs=session_picker,
646
+ outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache, attachments_output],
647
  )
648
  # New chat button
649
  new_chat_btn.click(
 
657
  ).then(
658
  load_session,
659
  inputs=state_chat_key,
660
+ outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache, attachments_output],
661
  )
662
  # Rename button
663
  rename_btn.click(
 
681
  ).then(
682
  load_session,
683
  inputs=state_chat_key,
684
+ outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache, attachments_output],
685
  )
686
  # Chat submission: include chat_key for persistence
687
  msg_input.submit(
 
689
  inputs=[msg_input, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache, state_chat_key],
690
  outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
691
  )
692
+ # Finalise outline and produce Word doc, recording the doc as an attachment
693
  finalize_btn.click(
694
  finalize_and_doc,
695
  inputs=[state_chat_history, state_chat_pairs, state_sources, state_plan, state_chat_key],
696
+ outputs=[plan_output, state_chat_history, state_chat_pairs, state_sources, state_plan, doc_output, attachments_output],
697
  )
698
+ # Generate course package (zip) and record it as an attachment
699
  generate_btn.click(
700
  generate_package,
701
+ inputs=[state_plan, state_sources, state_chat_key],
702
+ outputs=[file_output, attachments_output],
703
  )
704
  demo.launch()
db.py CHANGED
@@ -52,6 +52,20 @@ def _ensure_db():
52
  created_at INTEGER,
53
  FOREIGN KEY(chat_key) REFERENCES chats(chat_key)
54
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  """
56
  )
57
  conn.commit()
@@ -207,4 +221,46 @@ def delete_chat(chat_key: str) -> None:
207
  """
208
  with get_conn() as conn:
209
  conn.execute("DELETE FROM messages WHERE chat_key=?", (chat_key,))
210
- conn.execute("DELETE FROM chats WHERE chat_key=?", (chat_key,))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  created_at INTEGER,
53
  FOREIGN KEY(chat_key) REFERENCES chats(chat_key)
54
  );
55
+
56
+ -- Table to track files generated during chat sessions. Each record stores
57
+ -- the chat_key to which the file belongs, the original file name,
58
+ -- the absolute file path on disk, and the timestamp when it was
59
+ -- created. These attachments allow the UI to show downloadable files
60
+ -- generated in earlier chat interactions.
61
+ CREATE TABLE IF NOT EXISTS attachments (
62
+ id INTEGER PRIMARY KEY,
63
+ chat_key TEXT,
64
+ file_name TEXT,
65
+ file_path TEXT,
66
+ created_at INTEGER,
67
+ FOREIGN KEY(chat_key) REFERENCES chats(chat_key)
68
+ );
69
  """
70
  )
71
  conn.commit()
 
221
  """
222
  with get_conn() as conn:
223
  conn.execute("DELETE FROM messages WHERE chat_key=?", (chat_key,))
224
+ conn.execute("DELETE FROM chats WHERE chat_key=?", (chat_key,))
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Attachment management helpers
228
+ #
229
+ # These helpers allow the app to record files generated during chat sessions
230
+ # (such as outlines, scripts, zip packages) so they persist and can be
231
+ # downloaded later. Each attachment is tied to a chat via its chat_key.
232
+
233
+ def add_attachment(chat_key: str, file_path: str, file_name: str) -> None:
234
+ """Store a new file attachment for a chat.
235
+
236
+ Args:
237
+ chat_key: The unique chat key that the file belongs to.
238
+ file_path: The absolute path to the file on disk.
239
+ file_name: The base name of the file (for display).
240
+ """
241
+ now = int(time.time())
242
+ with get_conn() as conn:
243
+ conn.execute(
244
+ "INSERT INTO attachments (chat_key, file_name, file_path, created_at) VALUES (?, ?, ?, ?)",
245
+ (chat_key, file_name, file_path, now),
246
+ )
247
+
248
+
249
+ def list_attachments(chat_key: str) -> list[dict]:
250
+ """List all attachments for a given chat.
251
+
252
+ Args:
253
+ chat_key: The chat to list files for.
254
+
255
+ Returns:
256
+ A list of dictionaries, each with keys 'file_name', 'file_path' and 'created_at'.
257
+ """
258
+ with get_conn() as conn:
259
+ rows = conn.execute(
260
+ "SELECT file_name, file_path, created_at FROM attachments WHERE chat_key=? ORDER BY created_at ASC",
261
+ (chat_key,),
262
+ ).fetchall()
263
+ return [
264
+ {"file_name": fname, "file_path": fpath, "created_at": ts}
265
+ for fname, fpath, ts in rows
266
+ ]