Spaces:
Runtime error
Runtime error
Save generated files to sessions; attachments feature
Browse filesImplement 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.
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 |
-
|
| 447 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
]
|