awacke1 commited on
Commit
9d1d48c
Β·
verified Β·
1 Parent(s): 0a38743

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +312 -243
app.py CHANGED
@@ -68,6 +68,7 @@ QUOTE_VOTES_FILE = "quote_votes.md"
68
  MEDIA_VOTES_FILE = "media_votes.md"
69
  HISTORY_FILE = "chat_history.md"
70
  STATE_FILE = "user_state.txt"
 
71
 
72
  # Fancy digits - numbers got style! πŸ”’πŸ’ƒ
73
  UNICODE_DIGITS = {i: f"{i}\uFE0F⃣" for i in range(10)}
@@ -267,6 +268,19 @@ async def save_chat_entry(username, message, is_markdown=False, quote_line=None,
267
  with open(CHAT_FILE, 'a') as f:
268
  f.write(f"{indent}[{timestamp}] Audio: {audio_filename}\n")
269
  if media_file:
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  with open(CHAT_FILE, 'a') as f:
271
  f.write(f"{indent}[{timestamp}] Media: ![Media]({media_file})\n")
272
  with open(user_history_file, 'a') as f:
@@ -391,7 +405,7 @@ def play_and_download_audio(file_path):
391
  audio_data = f.read()
392
  b64 = base64.b64encode(audio_data).decode()
393
  audio_html = f'''
394
- <audio controls style="display:inline; vertical-align:middle;">
395
  <source src="data:audio/mpeg;base64,{b64}" type="audio/mpeg">
396
  </audio>
397
  <a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file_path)}" style="vertical-align:middle;">🎡</a>
@@ -399,19 +413,42 @@ def play_and_download_audio(file_path):
399
  return audio_html
400
  return ""
401
 
402
- # Image saver - pics preserved with naming! πŸ“ΈπŸ’Ύ
403
  async def save_pasted_image(image, username):
404
  await asyncio.to_thread(log_action, username, "πŸ“ΈπŸ’Ύ - Image saver - pics preserved!")
405
  img_hash = compute_image_hash(image)
406
  if img_hash in st.session_state.image_hashes:
407
  return None
408
  timestamp = format_timestamp_prefix(username)
409
- filename = f"{timestamp}-{img_hash}.png"
410
- filepath = filename # Top-level file
411
- await asyncio.to_thread(image.save, filepath, "PNG")
 
 
 
 
 
 
412
  st.session_state.image_hashes.add(img_hash)
413
  await save_chat_history_with_image(username, filepath)
414
- return filepath
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
  # PDF saver and audio generator
417
  async def save_pdf_and_generate_audio(pdf_file, username, max_pages=10):
@@ -433,7 +470,7 @@ async def save_pdf_and_generate_audio(pdf_file, username, max_pages=10):
433
  for i in range(total_pages):
434
  text = reader.pages[i].extract_text()
435
  texts.append(text)
436
- audio_filename = f"{timestamp}-page{i+1}-{file_hash}.mp3"
437
  audio_data = await audio_processor.create_audio(text, voice, audio_filename)
438
  if audio_data:
439
  audio_files.append(audio_filename)
@@ -441,7 +478,7 @@ async def save_pdf_and_generate_audio(pdf_file, username, max_pages=10):
441
  return pdf_filename, texts, audio_files
442
 
443
  # Video renderer - movies roll with autoplay! πŸŽ₯🎬
444
- def get_video_html(video_path, width="100%"):
445
  video_url = f"data:video/mp4;base64,{base64.b64encode(open(video_path, 'rb').read()).decode()}"
446
  return f'''
447
  <video width="{width}" controls autoplay muted loop>
@@ -451,7 +488,7 @@ def get_video_html(video_path, width="100%"):
451
  '''
452
 
453
  # Audio renderer - sounds soar! 🎢✈️
454
- async def get_audio_html(audio_path, width="100%"):
455
  username = st.session_state.get('username', 'System 🌟')
456
  await asyncio.to_thread(log_action, username, "🎢✈️ - Audio renderer - sounds soar!")
457
  audio_url = f"data:audio/mpeg;base64,{base64.b64encode(await asyncio.to_thread(open, audio_path, 'rb').read()).decode()}"
@@ -510,7 +547,7 @@ async def run_websocket_server():
510
 
511
  # Delete all user files function
512
  def delete_user_files():
513
- protected_files = {'app.py', 'requirements.txt', 'README.md', CHAT_FILE, QUOTE_VOTES_FILE, MEDIA_VOTES_FILE, HISTORY_FILE, STATE_FILE}
514
  deleted_files = []
515
  for file in os.listdir('.'):
516
  if file not in protected_files and not file.endswith('_history.md'):
@@ -519,6 +556,14 @@ def delete_user_files():
519
  deleted_files.append(file)
520
  except Exception as e:
521
  st.error(f"Failed to delete {file}: {e}")
 
 
 
 
 
 
 
 
522
  st.session_state.image_hashes.clear()
523
  st.session_state.audio_cache.clear()
524
  st.session_state.base64_cache.clear()
@@ -532,9 +577,10 @@ def check_query_params():
532
  if q_value and q_value in FUN_USERNAMES:
533
  st.session_state.username = q_value
534
  st.session_state.voice = FUN_USERNAMES[q_value]
 
535
  elif q_value:
536
  st.session_state.user_id = q_value # Use as user_id if not a valid username
537
- return q_value
538
 
539
  # Mermaid graph generator
540
  def generate_mermaid_graph(chat_lines):
@@ -542,33 +588,30 @@ def generate_mermaid_graph(chat_lines):
542
  nodes = {}
543
  edges = []
544
  for i, line in enumerate(chat_lines):
545
- if line.strip():
546
  timestamp = line.split('] ')[0][1:] if '] ' in line else "Unknown"
547
- minute_key = timestamp[:16] # Up to minute
548
  content = line.split(': ', 1)[1] if ': ' in line else line
549
- user = content.split(' ')[0] if ' ' in content else "System"
 
550
  node_id = f"{user}_{i}"
551
- nodes[node_id] = f"{user} @ {timestamp}"
552
-
553
- if "Audio:" in line:
554
- audio_file = line.split("Audio: ")[-1].strip()
555
- nodes[f"audio_{i}"] = f"🎡 {os.path.basename(audio_file)}"
556
- edges.append(f"{node_id} --> audio_{i}")
557
- elif "Media:" in line:
558
- media_file = line.split("Media: ")[-1].strip('![]()')
559
- media_type = "πŸ–Ό" if media_file.endswith(('.png', '.jpg')) else "πŸŽ₯" if media_file.endswith('.mp4') else "πŸ“œ"
560
- nodes[f"media_{i}"] = f"{media_type} {os.path.basename(media_file)}"
561
- edges.append(f"{node_id} --> media_{i}")
562
- elif "> " in line: # Quote
563
- parent_line = chat_lines[i-1] if i > 0 else None
564
- if parent_line:
565
- parent_id = f"{parent_line.split(': ')[1].split(' ')[0]}_{i-1}"
566
- edges.append(f"{parent_id} --> {node_id}")
567
 
568
  for node_id, label in nodes.items():
569
  mermaid_code += f" {node_id}[\"{label}\"]\n"
570
  mermaid_code += "\n".join(f" {edge}" for edge in edges)
571
- return f"```mermaid\n{mermaid_code}\n```"
572
 
573
  # Main execution - let’s roll! πŸŽ²πŸš€
574
  def main():
@@ -582,12 +625,13 @@ def main():
582
  if not st.session_state.user_id:
583
  st.session_state.user_id = str(uuid.uuid4())
584
  st.session_state.user_hash = await generate_user_hash()
585
- if 'username' not in st.session_state:
586
- q_value = check_query_params()
587
- if not q_value or q_value not in FUN_USERNAMES:
588
- chat_content = await load_chat()
589
- available_names = [name for name in FUN_USERNAMES if not any(f"{name} has joined" in line for line in chat_content.split('\n'))]
590
- st.session_state.username = random.choice(available_names) if available_names else random.choice(list(FUN_USERNAMES.keys()))
 
591
  st.session_state.voice = FUN_USERNAMES[st.session_state.username]
592
  st.markdown(f"**πŸŽ™οΈ Voice Selected**: {st.session_state.voice} πŸ—£οΈ for {st.session_state.username}")
593
 
@@ -598,229 +642,254 @@ def main():
598
  st.session_state.displayed_chat_lines = f.read().split('\n')
599
 
600
  user_url = f"/q={st.session_state.username}"
601
- st.markdown(f"Your unique URL path: [{user_url}]({user_url})", unsafe_allow_html=True)
 
602
 
603
- st.title(f"πŸ€–πŸ§ MMO {st.session_state.username}πŸ“πŸ”¬")
604
- st.markdown(f"Welcome to {START_ROOM} - chat, vote, upload, paste images, and enjoy quoting! πŸŽ‰ User ID: {st.session_state.user_id}")
 
605
 
606
  if not st.session_state.server_task:
607
  st.session_state.server_task = loop.create_task(run_websocket_server())
608
 
609
  # Unified Chat History at Top with Markdown Emoji Output
610
- st.subheader(f"{START_ROOM} Chat History πŸ’¬")
611
- chat_content = await load_chat()
612
- chat_lines = [line for line in chat_content.split('\n') if line.strip() and not line.startswith('#')]
613
- if chat_lines:
614
- chat_by_minute = {}
615
- for line in reversed(chat_lines):
616
- timestamp = line.split('] ')[0][1:] if '] ' in line else "Unknown"
617
- minute_key = timestamp[:16] # Up to minute
618
- if minute_key not in chat_by_minute:
619
- chat_by_minute[minute_key] = []
620
- chat_by_minute[minute_key].append(line)
621
-
622
- markdown_output = ""
623
- for minute, lines in chat_by_minute.items():
624
- minute_output = f"### {minute}\n"
625
- for line in lines:
626
- if ': ' in line and not line.startswith(' '):
627
- user_message = line.split(': ', 1)[1]
628
- minute_output += f"- πŸ’¬ **{user_message.split(' ')[0]}**: {user_message.split(' ', 1)[1] if ' ' in user_message else ''}\n"
629
- elif "Audio:" in line:
630
- audio_file = line.split("Audio: ")[-1].strip()
631
- audio_html = play_and_download_audio(audio_file)
632
- minute_output += f" - 🎡 {audio_html}\n"
633
- elif "Media:" in line:
634
- media_file = line.split("Media: ")[-1].strip('![]()')
635
- if media_file.endswith(('.png', '.jpg')):
636
- minute_output += f" - πŸ–Ό <img src='file://{media_file}' width='100'>\n"
637
- elif media_file.endswith('.mp4'):
638
- minute_output += f" - πŸŽ₯ <video src='file://{media_file}' width='100' controls autoplay muted loop></video>\n"
639
- elif media_file.endswith('.pdf'):
640
- minute_output += f" - πŸ“œ {os.path.basename(media_file)}\n"
641
- markdown_output += minute_output
642
- st.markdown(markdown_output, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
643
 
644
  # Mermaid Graph Visualization
645
- st.subheader("Chat Relationship Tree 🌳")
646
- mermaid_graph = generate_mermaid_graph(chat_lines)
647
- st.markdown(mermaid_graph)
648
-
649
- if st.session_state.quote_line:
650
- st.markdown(f"### Quoting: {st.session_state.quote_line}")
651
- quote_response = st.text_area("Add your response", key="quote_response", value=st.session_state.message_text)
652
- paste_result_quote = paste_image_button("πŸ“‹ Paste Image or Text with Quote", key="paste_button_quote")
653
- if paste_result_quote.image_data is not None:
654
- if isinstance(paste_result_quote.image_data, str):
655
- st.session_state.message_text = paste_result_quote.image_data
656
- st.text_area("Add your response", key="quote_response", value=st.session_state.message_text)
657
- else:
658
- st.image(paste_result_quote.image_data, caption="Received Image for Quote")
659
- filename = await save_pasted_image(paste_result_quote.image_data, st.session_state.username)
660
- if filename:
661
- st.session_state.pasted_image_data = filename
662
- await save_chat_entry(st.session_state.username, f"Pasted image: {filename}", quote_line=st.session_state.quote_line, media_file=filename)
663
- if st.button("Send Quote πŸš€", key="send_quote"):
664
- markdown_response = f"### Quote Response\n- **Original**: {st.session_state.quote_line}\n- **{st.session_state.username} Replies**: {quote_response}"
665
- if st.session_state.pasted_image_data:
666
- markdown_response += f"\n- **Image**: ![Pasted Image]({st.session_state.pasted_image_data})"
667
- await save_chat_entry(st.session_state.username, f"Pasted image: {st.session_state.pasted_image_data}", quote_line=st.session_state.quote_line, media_file=st.session_state.pasted_image_data)
668
- st.session_state.pasted_image_data = None
669
- await save_chat_entry(st.session_state.username, markdown_response, is_markdown=True, quote_line=st.session_state.quote_line)
670
- st.session_state.quote_line = None
671
- st.session_state.message_text = ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  st.rerun()
673
 
674
- current_selection = st.session_state.username if st.session_state.username in FUN_USERNAMES else ""
675
- new_username = st.selectbox("Change Name and Voice", [""] + list(FUN_USERNAMES.keys()), index=(list(FUN_USERNAMES.keys()).index(current_selection) + 1 if current_selection else 0), format_func=lambda x: f"{x} ({FUN_USERNAMES.get(x, 'No Voice')})" if x else "Select a name")
676
- if new_username and new_username != st.session_state.username:
677
- await save_chat_entry("System 🌟", f"{st.session_state.username} changed name to {new_username}")
678
- st.session_state.username = new_username
679
- st.session_state.voice = FUN_USERNAMES[new_username]
680
- st.markdown(f"**πŸŽ™οΈ Voice Changed**: {st.session_state.voice} πŸ—£οΈ for {st.session_state.username}")
681
- st.rerun()
682
-
683
- # Message input with Send button on the right
684
- col_input, col_send = st.columns([5, 1])
685
- with col_input:
686
- message = st.text_input(f"Message as {st.session_state.username} (Voice: {st.session_state.voice})", key="message_input", value=st.session_state.message_text)
687
- with col_send:
688
- if st.button("Send πŸš€", key="send_button"):
689
- if message.strip():
690
- audio_file = await save_chat_entry(st.session_state.username, message, is_markdown=True)
691
- if audio_file:
692
- st.session_state.audio_cache[f"{message}_{FUN_USERNAMES[st.session_state.username]}"] = audio_file
693
- st.audio(audio_file) # Immediate preview
694
- if st.session_state.pasted_image_data:
695
- await save_chat_entry(st.session_state.username, f"Pasted image: {st.session_state.pasted_image_data}", media_file=st.session_state.pasted_image_data)
696
- st.session_state.pasted_image_data = None
697
- st.session_state.message_text = ''
698
- st.rerun()
699
-
700
- paste_result_msg = paste_image_button("πŸ“‹ Paste Image or Text with Message", key="paste_button_msg")
701
- if paste_result_msg.image_data is not None:
702
- if isinstance(paste_result_msg.image_data, str):
703
- st.session_state.message_text = paste_result_msg.image_data
704
- st.text_input(f"Message as {st.session_state.username} (Voice: {st.session_state.voice})", key="message_input_paste", value=st.session_state.message_text)
705
- else:
706
- st.image(paste_result_msg.image_data, caption="Received Image for Message")
707
- filename = await save_pasted_image(paste_result_msg.image_data, st.session_state.username)
708
- if filename:
709
- await save_chat_entry(st.session_state.username, f"Pasted image: {filename}", media_file=filename)
710
- st.session_state.pasted_image_data = None
711
-
712
- tab_main = st.radio("Action:", ["πŸ“Έ Media", "πŸ” ArXiv", "πŸ“ Editor"], horizontal=True)
713
- useArxiv = st.checkbox("Search Arxiv for Research Paper Answers", value=True)
714
- useArxivAudio = st.checkbox("Generate Audio File for Research Paper Answers", value=False)
715
-
716
- st.subheader("Upload Media πŸŽ¨πŸŽΆπŸ“œπŸŽ₯")
717
- uploaded_file = st.file_uploader("Upload Media", type=['png', 'jpg', 'mp4', 'mp3', 'wav', 'pdf', 'txt', 'md', 'py'])
718
- if uploaded_file:
719
- timestamp = format_timestamp_prefix(st.session_state.username)
720
- username = st.session_state.username
721
- ext = uploaded_file.name.split('.')[-1]
722
- file_hash = hashlib.md5(uploaded_file.getbuffer()).hexdigest()[:8]
723
- if file_hash not in st.session_state.image_hashes:
724
- filename = f"{timestamp}-{file_hash}.{ext}"
725
- file_path = filename # Top-level file
726
- await asyncio.to_thread(lambda: open(file_path, 'wb').write(uploaded_file.getbuffer()))
727
- st.success(f"Uploaded {filename}")
728
- if ext == 'pdf':
729
- pdf_filename, texts, audio_files = await save_pdf_and_generate_audio(uploaded_file, username)
730
- await save_chat_entry(username, f"Uploaded PDF: {pdf_filename}", media_file=pdf_filename)
731
- for i, (text, audio_file) in enumerate(zip(texts, audio_files)):
732
  if audio_file:
733
- with open(CHAT_FILE, 'a') as f:
734
- f.write(f" [{timestamp}] Page {i+1} Audio: {audio_file}\n")
 
 
 
 
 
 
 
 
 
 
 
735
  else:
736
- await save_chat_entry(username, f"Uploaded media: {file_path}", media_file=file_path)
737
- await save_chat_history_with_image(username, file_path)
738
- st.session_state.image_hashes.add(file_hash)
739
- if file_path.endswith('.mp4'):
740
- st.session_state.media_notifications.append(file_path)
741
-
742
- # Big Red Delete Button
743
- st.subheader("πŸ›‘ Danger Zone")
744
- if st.button("Try Not To Delete It All On Your First Day", key="delete_all", help="Deletes all user-added files!", type="primary", use_container_width=True):
745
- deleted_files = delete_user_files()
746
- if deleted_files:
747
- st.markdown("### πŸ—‘οΈ Deleted Files:\n" + "\n".join([f"- `{file}`" for file in deleted_files]))
748
- else:
749
- st.markdown("### πŸ—‘οΈ Nothing to Delete!")
750
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
 
752
- st.subheader("Refresh ⏳")
753
- refresh_rate = st.slider("Refresh Rate", 1, 300, st.session_state.refresh_rate)
754
- st.session_state.refresh_rate = refresh_rate
755
- timer_placeholder = st.empty()
756
- for i in range(st.session_state.refresh_rate, -1, -1):
757
- font_name, font_func = random.choice(UNICODE_FONTS)
758
- countdown_str = "".join(UNICODE_DIGITS[int(d)] for d in str(i)) if i < 10 else font_func(str(i))
759
- timer_placeholder.markdown(f"<p class='timer'>⏳ {font_func('Refresh in:')} {countdown_str}</p>", unsafe_allow_html=True)
760
- time.sleep(1)
761
- st.rerun()
762
 
763
  # Separate Galleries for Own and Shared Files
764
- all_files = glob.glob("*.md") + glob.glob("*.pdf") + glob.glob("*.txt") + glob.glob("*.py") + glob.glob("*.png") + glob.glob("*.jpg") + glob.glob("*.mp3") + glob.glob("*.mp4")
765
- own_files = [f for f in all_files if st.session_state.user_id in f]
766
- shared_files = [f for f in all_files if f not in own_files and not f in [CHAT_FILE, QUOTE_VOTES_FILE, MEDIA_VOTES_FILE, HISTORY_FILE, STATE_FILE]]
767
-
768
- st.subheader("Your Files πŸ“‚")
769
- st.subheader("Image Gallery πŸ–Ό")
770
- own_image_files = [f for f in own_files if f.endswith(('.png', '.jpg'))]
771
- image_cols = st.slider("Image Gallery Columns πŸ–Ό (Own)", min_value=1, max_value=15, value=5)
772
- cols = st.columns(image_cols)
773
- for idx, image_file in enumerate(own_image_files):
774
- with cols[idx % image_cols]:
775
- st.image(image_file, use_container_width=True)
776
-
777
- st.subheader("Video Gallery πŸŽ₯")
778
- own_video_files = [f for f in own_files if f.endswith('.mp4')]
779
- video_cols = st.slider("Video Gallery Columns 🎬 (Own)", min_value=1, max_value=5, value=3)
780
- cols = st.columns(video_cols)
781
- for idx, video_file in enumerate(own_video_files):
782
- with cols[idx % video_cols]:
783
- st.markdown(get_video_html(video_file, width="100%"), unsafe_allow_html=True)
784
-
785
- st.subheader("Audio Gallery 🎧")
786
- own_audio_files = [f for f in own_files if f.endswith(('.mp3', '.wav'))]
787
- audio_cols = st.slider("Audio Gallery Columns 🎢 (Own)", min_value=1, max_value=15, value=5)
788
- cols = st.columns(audio_cols)
789
- for idx, audio_file in enumerate(own_audio_files):
790
- with cols[idx % audio_cols]:
791
- st.markdown(await get_audio_html(audio_file, width="100%"), unsafe_allow_html=True)
792
-
793
- st.subheader("Shared Files πŸ“€")
794
- st.subheader("Image Gallery πŸ–Ό")
795
- shared_image_files = [f for f in shared_files if f.endswith(('.png', '.jpg'))]
796
- image_cols = st.slider("Image Gallery Columns πŸ–Ό (Shared)", min_value=1, max_value=15, value=5)
797
- cols = st.columns(image_cols)
798
- for idx, image_file in enumerate(shared_image_files):
799
- with cols[idx % image_cols]:
800
- st.image(image_file, use_container_width=True)
801
-
802
- st.subheader("Video Gallery πŸŽ₯")
803
- shared_video_files = [f for f in shared_files if f.endswith('.mp4')]
804
- video_cols = st.slider("Video Gallery Columns 🎬 (Shared)", min_value=1, max_value=5, value=3)
805
- cols = st.columns(video_cols)
806
- for idx, video_file in enumerate(shared_video_files):
807
- with cols[idx % video_cols]:
808
- st.markdown(get_video_html(video_file, width="100%"), unsafe_allow_html=True)
809
-
810
- st.subheader("Audio Gallery 🎧")
811
- shared_audio_files = [f for f in shared_files if f.endswith(('.mp3', '.wav'))]
812
- audio_cols = st.slider("Audio Gallery Columns 🎢 (Shared)", min_value=1, max_value=15, value=5)
813
- cols = st.columns(audio_cols)
814
- for idx, audio_file in enumerate(shared_audio_files):
815
- with cols[idx % audio_cols]:
816
- st.markdown(await get_audio_html(audio_file, width="100%"), unsafe_allow_html=True)
 
 
 
 
 
 
 
817
 
818
  # Full Log at End with Download
819
- st.subheader("Full Chat Log πŸ“œ")
820
- with open(CHAT_FILE, 'r') as f:
821
- history_content = f.read()
822
- st.markdown(history_content)
823
- st.download_button("Download Chat Log as .md", history_content, file_name=f"chat_{st.session_state.user_id}.md", mime="text/markdown")
 
824
 
825
  loop.run_until_complete(async_interface())
826
 
 
68
  MEDIA_VOTES_FILE = "media_votes.md"
69
  HISTORY_FILE = "chat_history.md"
70
  STATE_FILE = "user_state.txt"
71
+ MEDIA_DIR = "media_base64"
72
 
73
  # Fancy digits - numbers got style! πŸ”’πŸ’ƒ
74
  UNICODE_DIGITS = {i: f"{i}\uFE0F⃣" for i in range(10)}
 
268
  with open(CHAT_FILE, 'a') as f:
269
  f.write(f"{indent}[{timestamp}] Audio: {audio_filename}\n")
270
  if media_file:
271
+ if isinstance(media_file, Image.Image):
272
+ img_hash = compute_image_hash(media_file)
273
+ timestamp_prefix = format_timestamp_prefix(username)
274
+ media_filename = f"{timestamp_prefix}-{img_hash}.b64"
275
+ media_path = os.path.join(MEDIA_DIR, media_filename)
276
+ os.makedirs(MEDIA_DIR, exist_ok=True)
277
+ img_byte_arr = io.BytesIO()
278
+ media_file.save(img_byte_arr, format='PNG')
279
+ img_bytes = img_byte_arr.getvalue()
280
+ b64_data = base64.b64encode(img_bytes).decode()
281
+ with open(media_path, 'w') as f:
282
+ f.write(b64_data)
283
+ media_file = media_filename
284
  with open(CHAT_FILE, 'a') as f:
285
  f.write(f"{indent}[{timestamp}] Media: ![Media]({media_file})\n")
286
  with open(user_history_file, 'a') as f:
 
405
  audio_data = f.read()
406
  b64 = base64.b64encode(audio_data).decode()
407
  audio_html = f'''
408
+ <audio controls style="display:inline; vertical-align:middle; width:100px;">
409
  <source src="data:audio/mpeg;base64,{b64}" type="audio/mpeg">
410
  </audio>
411
  <a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file_path)}" style="vertical-align:middle;">🎡</a>
 
413
  return audio_html
414
  return ""
415
 
416
+ # Image saver - pics preserved with naming as base64! πŸ“ΈπŸ’Ύ
417
  async def save_pasted_image(image, username):
418
  await asyncio.to_thread(log_action, username, "πŸ“ΈπŸ’Ύ - Image saver - pics preserved!")
419
  img_hash = compute_image_hash(image)
420
  if img_hash in st.session_state.image_hashes:
421
  return None
422
  timestamp = format_timestamp_prefix(username)
423
+ voice = st.session_state.voice if username == st.session_state.username else FUN_USERNAMES.get(username, "en-US-AriaNeural")
424
+ filename = f"{timestamp}-{img_hash}-voice-{voice}.b64"
425
+ filepath = os.path.join(MEDIA_DIR, filename)
426
+ os.makedirs(MEDIA_DIR, exist_ok=True)
427
+ img_byte_arr = io.BytesIO()
428
+ image.save(img_byte_arr, format='PNG')
429
+ b64_data = base64.b64encode(img_byte_arr.getvalue()).decode()
430
+ with open(filepath, 'w') as f:
431
+ f.write(b64_data)
432
  st.session_state.image_hashes.add(img_hash)
433
  await save_chat_history_with_image(username, filepath)
434
+ return filename
435
+
436
+ # Display base64 image and audio
437
+ def display_base64_media(media_file, width="100px"):
438
+ if os.path.exists(os.path.join(MEDIA_DIR, media_file)) and media_file.endswith('.b64'):
439
+ with open(os.path.join(MEDIA_DIR, media_file), 'r') as f:
440
+ b64_data = f.read()
441
+ img_data = base64.b64decode(b64_data)
442
+ img = Image.open(io.BytesIO(img_data))
443
+ st.image(img, use_container_width=True)
444
+
445
+ # Find corresponding audio file
446
+ timestamp = media_file.split('-by-')[0] + '-by-' + media_file.split('-by-')[1].split('-')[0]
447
+ voice = media_file.split('-voice-')[1].split('.b64')[0]
448
+ audio_files = glob.glob(f"{timestamp}*-{voice}.mp3")
449
+ if audio_files:
450
+ audio_file = audio_files[0]
451
+ st.audio(audio_file)
452
 
453
  # PDF saver and audio generator
454
  async def save_pdf_and_generate_audio(pdf_file, username, max_pages=10):
 
470
  for i in range(total_pages):
471
  text = reader.pages[i].extract_text()
472
  texts.append(text)
473
+ audio_filename = f"{timestamp}-page{i+1}-{file_hash}-voice-{voice}.mp3"
474
  audio_data = await audio_processor.create_audio(text, voice, audio_filename)
475
  if audio_data:
476
  audio_files.append(audio_filename)
 
478
  return pdf_filename, texts, audio_files
479
 
480
  # Video renderer - movies roll with autoplay! πŸŽ₯🎬
481
+ def get_video_html(video_path, width="100px"):
482
  video_url = f"data:video/mp4;base64,{base64.b64encode(open(video_path, 'rb').read()).decode()}"
483
  return f'''
484
  <video width="{width}" controls autoplay muted loop>
 
488
  '''
489
 
490
  # Audio renderer - sounds soar! 🎢✈️
491
+ async def get_audio_html(audio_path, width="100px"):
492
  username = st.session_state.get('username', 'System 🌟')
493
  await asyncio.to_thread(log_action, username, "🎢✈️ - Audio renderer - sounds soar!")
494
  audio_url = f"data:audio/mpeg;base64,{base64.b64encode(await asyncio.to_thread(open, audio_path, 'rb').read()).decode()}"
 
547
 
548
  # Delete all user files function
549
  def delete_user_files():
550
+ protected_files = {'app.py', 'requirements.txt', 'README.md', CHAT_FILE, QUOTE_VOTES_FILE, MEDIA_VOTES_FILE, HISTORY_FILE, STATE_FILE, MEDIA_DIR}
551
  deleted_files = []
552
  for file in os.listdir('.'):
553
  if file not in protected_files and not file.endswith('_history.md'):
 
556
  deleted_files.append(file)
557
  except Exception as e:
558
  st.error(f"Failed to delete {file}: {e}")
559
+ for root, dirs, files in os.walk(MEDIA_DIR):
560
+ for file in files:
561
+ file_path = os.path.join(root, file)
562
+ try:
563
+ os.remove(file_path)
564
+ deleted_files.append(file_path)
565
+ except Exception as e:
566
+ st.error(f"Failed to delete {file_path}: {e}")
567
  st.session_state.image_hashes.clear()
568
  st.session_state.audio_cache.clear()
569
  st.session_state.base64_cache.clear()
 
577
  if q_value and q_value in FUN_USERNAMES:
578
  st.session_state.username = q_value
579
  st.session_state.voice = FUN_USERNAMES[q_value]
580
+ return q_value
581
  elif q_value:
582
  st.session_state.user_id = q_value # Use as user_id if not a valid username
583
+ return None
584
 
585
  # Mermaid graph generator
586
  def generate_mermaid_graph(chat_lines):
 
588
  nodes = {}
589
  edges = []
590
  for i, line in enumerate(chat_lines):
591
+ if line.strip() and not line.startswith(' '):
592
  timestamp = line.split('] ')[0][1:] if '] ' in line else "Unknown"
 
593
  content = line.split(': ', 1)[1] if ': ' in line else line
594
+ user = content.split(' ')[0]
595
+ message = content.split(' ', 1)[1] if ' ' in content else ''
596
  node_id = f"{user}_{i}"
597
+ nodes[node_id] = f"{user}: {message}"
598
+ if i + 1 < len(chat_lines) and "Audio:" in chat_lines[i + 1]:
599
+ audio_node = f"audio_{i}"
600
+ nodes[audio_node] = "🎡"
601
+ edges.append(f"{node_id} --> {audio_node}")
602
+ if i + 2 < len(chat_lines) and "Media:" in chat_lines[i + 2]:
603
+ media_node = f"media_{i}"
604
+ nodes[media_node] = "πŸ–Ό"
605
+ edges.append(f"{node_id} --> {media_node}")
606
+ if i > 0 and "> " in line:
607
+ parent_user = chat_lines[i-1].split(': ')[1].split(' ')[0]
608
+ parent_id = f"{parent_user}_{i-1}"
609
+ edges.append(f"{parent_id} --> {node_id}")
 
 
 
610
 
611
  for node_id, label in nodes.items():
612
  mermaid_code += f" {node_id}[\"{label}\"]\n"
613
  mermaid_code += "\n".join(f" {edge}" for edge in edges)
614
+ return mermaid_code
615
 
616
  # Main execution - let’s roll! πŸŽ²πŸš€
617
  def main():
 
625
  if not st.session_state.user_id:
626
  st.session_state.user_id = str(uuid.uuid4())
627
  st.session_state.user_hash = await generate_user_hash()
628
+
629
+ # Check query params first to override username
630
+ q_value = check_query_params()
631
+ if not q_value and 'username' not in st.session_state:
632
+ chat_content = await load_chat()
633
+ available_names = [name for name in FUN_USERNAMES if not any(f"{name} has joined" in line for line in chat_content.split('\n'))]
634
+ st.session_state.username = random.choice(available_names) if available_names else random.choice(list(FUN_USERNAMES.keys()))
635
  st.session_state.voice = FUN_USERNAMES[st.session_state.username]
636
  st.markdown(f"**πŸŽ™οΈ Voice Selected**: {st.session_state.voice} πŸ—£οΈ for {st.session_state.username}")
637
 
 
642
  st.session_state.displayed_chat_lines = f.read().split('\n')
643
 
644
  user_url = f"/q={st.session_state.username}"
645
+ with st.container():
646
+ st.markdown(f"<small>Your unique URL path: [{user_url}]({user_url})</small>", unsafe_allow_html=True)
647
 
648
+ with st.container():
649
+ st.markdown(f"#### πŸ€–πŸ§ MMO {st.session_state.username}πŸ“πŸ”¬")
650
+ st.markdown(f"<small>Welcome to {START_ROOM} - chat, vote, upload, paste images, and enjoy quoting! πŸŽ‰ User ID: {st.session_state.user_id}</small>", unsafe_allow_html=True)
651
 
652
  if not st.session_state.server_task:
653
  st.session_state.server_task = loop.create_task(run_websocket_server())
654
 
655
  # Unified Chat History at Top with Markdown Emoji Output
656
+ with st.container():
657
+ st.markdown(f"##### {START_ROOM} Chat History πŸ’¬")
658
+ chat_content = await load_chat()
659
+ chat_lines = [line for line in chat_content.split('\n') if line.strip() and not line.startswith('#')]
660
+ if chat_lines:
661
+ chat_by_minute = {}
662
+ for line in reversed(chat_lines):
663
+ timestamp = line.split('] ')[0][1:] if '] ' in line else "Unknown"
664
+ minute_key = timestamp[:16] # Up to minute
665
+ if minute_key not in chat_by_minute:
666
+ chat_by_minute[minute_key] = []
667
+ chat_by_minute[minute_key].append(line)
668
+
669
+ markdown_output = ""
670
+ for minute, lines in chat_by_minute.items():
671
+ minute_output = f"###### {minute[-5:]}\n" # Show only HH:MM
672
+ for line in lines:
673
+ if ': ' in line and not line.startswith(' '):
674
+ user_message = line.split(': ', 1)[1]
675
+ user = user_message.split(' ')[0]
676
+ msg = user_message.split(' ', 1)[1] if ' ' in user_message else ''
677
+ audio_html = ""
678
+ media_content = ""
679
+ next_lines = chat_lines[chat_lines.index(line)+1:chat_lines.index(line)+3]
680
+ for nl in next_lines:
681
+ if "Audio:" in nl:
682
+ audio_file = nl.split("Audio: ")[-1].strip()
683
+ audio_html = play_and_download_audio(audio_file)
684
+ elif "Media:" in nl:
685
+ media_file = nl.split("Media: ")[-1].strip('![]()')
686
+ if media_file.endswith('.b64'):
687
+ media_content = display_base64_media(media_file, width="100px")
688
+ elif media_file.endswith(('.png', '.jpg')):
689
+ media_content = f"<img src='file://{media_file}' width='100'>"
690
+ elif media_file.endswith('.mp4'):
691
+ media_content = get_video_html(media_file)
692
+ elif media_file.endswith('.pdf'):
693
+ media_content = f"πŸ“œ {os.path.basename(media_file)}"
694
+ minute_output += f"- πŸ’¬ **{user}**: {msg} {audio_html} {media_content}\n"
695
+ markdown_output += minute_output
696
+ st.markdown(markdown_output, unsafe_allow_html=True)
697
 
698
  # Mermaid Graph Visualization
699
+ st.markdown("###### Chat Relationship Tree 🌳")
700
+ mermaid_code = generate_mermaid_graph(chat_lines)
701
+ mermaid_html = f"""
702
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
703
+ <div class="mermaid" style="height: 200px; overflow: auto;">{mermaid_code}</div>
704
+ <script>mermaid.initialize({{startOnLoad:true}});</script>
705
+ """
706
+ components.html(mermaid_html, height=250)
707
+
708
+ with st.container():
709
+ if st.session_state.quote_line:
710
+ st.markdown(f"###### Quoting: {st.session_state.quote_line}")
711
+ quote_response = st.text_area("Add your response", key="quote_response", value=st.session_state.message_text)
712
+ paste_result_quote = paste_image_button("πŸ“‹ Paste Image or Text with Quote", key="paste_button_quote")
713
+ if paste_result_quote.image_data is not None:
714
+ if isinstance(paste_result_quote.image_data, str):
715
+ st.session_state.message_text = paste_result_quote.image_data
716
+ st.text_area("Add your response", key="quote_response", value=st.session_state.message_text)
717
+ else:
718
+ st.image(paste_result_quote.image_data, caption="Received Image for Quote")
719
+ filename = await save_pasted_image(paste_result_quote.image_data, st.session_state.username)
720
+ if filename:
721
+ st.session_state.pasted_image_data = filename
722
+ await save_chat_entry(st.session_state.username, f"Pasted image: {filename}", quote_line=st.session_state.quote_line, media_file=paste_result_quote.image_data)
723
+ if st.button("Send Quote πŸš€", key="send_quote"):
724
+ markdown_response = f"### Quote Response\n- **Original**: {st.session_state.quote_line}\n- **{st.session_state.username} Replies**: {quote_response}"
725
+ if st.session_state.pasted_image_data:
726
+ markdown_response += f"\n- **Image**: ![Pasted Image]({st.session_state.pasted_image_data})"
727
+ await save_chat_entry(st.session_state.username, f"Pasted image: {st.session_state.pasted_image_data}", quote_line=st.session_state.quote_line, media_file=st.session_state.pasted_image_data)
728
+ st.session_state.pasted_image_data = None
729
+ await save_chat_entry(st.session_state.username, markdown_response, is_markdown=True, quote_line=st.session_state.quote_line)
730
+ st.session_state.quote_line = None
731
+ st.session_state.message_text = ''
732
+ st.rerun()
733
+
734
+ current_selection = st.session_state.username if st.session_state.username in FUN_USERNAMES else ""
735
+ new_username = st.selectbox("Change Name and Voice", [""] + list(FUN_USERNAMES.keys()), index=(list(FUN_USERNAMES.keys()).index(current_selection) + 1 if current_selection else 0), format_func=lambda x: f"{x} ({FUN_USERNAMES.get(x, 'No Voice')})" if x else "Select a name")
736
+ if new_username and new_username != st.session_state.username:
737
+ await save_chat_entry("System 🌟", f"{st.session_state.username} changed name to {new_username}")
738
+ st.session_state.username = new_username
739
+ st.session_state.voice = FUN_USERNAMES[new_username]
740
+ st.markdown(f"**πŸŽ™οΈ Voice Changed**: {st.session_state.voice} πŸ—£οΈ for {st.session_state.username}")
741
  st.rerun()
742
 
743
+ # Message input with Send button on the right
744
+ col_input, col_send = st.columns([5, 1])
745
+ with col_input:
746
+ message = st.text_input(f"Message as {st.session_state.username} (Voice: {st.session_state.voice})", key="message_input", value=st.session_state.message_text)
747
+ with col_send:
748
+ if st.button("Send πŸš€", key="send_button"):
749
+ if message.strip():
750
+ audio_file = await save_chat_entry(st.session_state.username, message, is_markdown=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  if audio_file:
752
+ st.session_state.audio_cache[f"{message}_{FUN_USERNAMES[st.session_state.username]}"] = audio_file
753
+ st.audio(audio_file) # Immediate preview
754
+ if st.session_state.pasted_image_data:
755
+ await save_chat_entry(st.session_state.username, f"Pasted image: {st.session_state.pasted_image_data}", media_file=st.session_state.pasted_image_data)
756
+ st.session_state.pasted_image_data = None
757
+ st.session_state.message_text = ''
758
+ st.rerun()
759
+
760
+ paste_result_msg = paste_image_button("πŸ“‹ Paste Image or Text with Message", key="paste_button_msg")
761
+ if paste_result_msg.image_data is not None:
762
+ if isinstance(paste_result_msg.image_data, str):
763
+ st.session_state.message_text = paste_result_msg.image_data
764
+ st.text_input(f"Message as {st.session_state.username} (Voice: {st.session_state.voice})", key="message_input_paste", value=st.session_state.message_text)
765
  else:
766
+ st.image(paste_result_msg.image_data, caption="Received Image for Quote")
767
+ filename = await save_pasted_image(paste_result_msg.image_data, st.session_state.username)
768
+ if filename:
769
+ await save_chat_entry(st.session_state.username, f"Pasted image: {filename}", media_file=paste_result_msg.image_data)
770
+ st.session_state.pasted_image_data = None
771
+
772
+ with st.container():
773
+ tab_main = st.radio("Action:", ["πŸ“Έ Media", "πŸ” ArXiv", "πŸ“ Editor"], horizontal=True, label_visibility="collapsed")
774
+ useArxiv = st.checkbox("Search Arxiv for Research Paper Answers", value=True, label_visibility="collapsed")
775
+ useArxivAudio = st.checkbox("Generate Audio File for Research Paper Answers", value=False, label_visibility="collapsed")
776
+
777
+ st.markdown("###### Upload Media πŸŽ¨πŸŽΆπŸ“œπŸŽ₯")
778
+ uploaded_file = st.file_uploader("Upload Media", type=['png', 'jpg', 'mp4', 'mp3', 'wav', 'pdf', 'txt', 'md', 'py'])
779
+ if uploaded_file:
780
+ timestamp = format_timestamp_prefix(st.session_state.username)
781
+ username = st.session_state.username
782
+ ext = uploaded_file.name.split('.')[-1]
783
+ file_hash = hashlib.md5(uploaded_file.getbuffer()).hexdigest()[:8]
784
+ if file_hash not in st.session_state.image_hashes:
785
+ filename = f"{timestamp}-{file_hash}.{ext}"
786
+ file_path = filename # Top-level file
787
+ await asyncio.to_thread(lambda: open(file_path, 'wb').write(uploaded_file.getbuffer()))
788
+ st.success(f"Uploaded {filename}")
789
+ if ext == 'pdf':
790
+ pdf_filename, texts, audio_files = await save_pdf_and_generate_audio(uploaded_file, username)
791
+ await save_chat_entry(username, f"Uploaded PDF: {pdf_filename}", media_file=pdf_filename)
792
+ for i, (text, audio_file) in enumerate(zip(texts, audio_files)):
793
+ if audio_file:
794
+ with open(CHAT_FILE, 'a') as f:
795
+ f.write(f" [{timestamp}] Page {i+1} Audio: {audio_file}\n")
796
+ else:
797
+ await save_chat_entry(username, f"Uploaded media: {file_path}", media_file=file_path)
798
+ await save_chat_history_with_image(username, file_path)
799
+ st.session_state.image_hashes.add(file_hash)
800
+ if file_path.endswith('.mp4'):
801
+ st.session_state.media_notifications.append(file_path)
802
+
803
+ # Big Red Delete Button
804
+ st.markdown("###### πŸ›‘ Danger Zone")
805
+ if st.button("Try Not To Delete It All On Your First Day", key="delete_all", help="Deletes all user-added files!", type="primary", use_container_width=True, label_visibility="collapsed"):
806
+ deleted_files = delete_user_files()
807
+ if deleted_files:
808
+ st.markdown("### πŸ—‘οΈ Deleted Files:\n" + "\n".join([f"- `{file}`" for file in deleted_files]))
809
+ else:
810
+ st.markdown("### πŸ—‘οΈ Nothing to Delete!")
811
+ st.rerun()
812
 
813
+ st.markdown("###### Refresh ⏳")
814
+ refresh_rate = st.slider("Refresh Rate", 1, 300, st.session_state.refresh_rate, label_visibility="collapsed")
815
+ st.session_state.refresh_rate = refresh_rate
816
+ timer_placeholder = st.empty()
817
+ for i in range(st.session_state.refresh_rate, -1, -1):
818
+ font_name, font_func = random.choice(UNICODE_FONTS)
819
+ countdown_str = "".join(UNICODE_DIGITS[int(d)] for d in str(i)) if i < 10 else font_func(str(i))
820
+ timer_placeholder.markdown(f"<small>⏳ {font_func('Refresh in:')} {countdown_str}</small>", unsafe_allow_html=True)
821
+ time.sleep(1)
822
+ st.rerun()
823
 
824
  # Separate Galleries for Own and Shared Files
825
+ with st.container():
826
+ all_files = glob.glob("*.md") + glob.glob("*.pdf") + glob.glob("*.txt") + glob.glob("*.py") + glob.glob("*.png") + glob.glob("*.jpg") + glob.glob("*.mp3") + glob.glob("*.mp4") + glob.glob(os.path.join(MEDIA_DIR, "*.b64"))
827
+ own_files = [f for f in all_files if st.session_state.user_id in os.path.basename(f)]
828
+ shared_files = [f for f in all_files if f not in own_files and not f in [CHAT_FILE, QUOTE_VOTES_FILE, MEDIA_VOTES_FILE, HISTORY_FILE, STATE_FILE, os.path.join(MEDIA_DIR, "*")]]
829
+
830
+ st.markdown("###### Your Files πŸ“‚")
831
+ st.markdown("###### Image Gallery πŸ–Ό")
832
+ own_image_files = [f for f in own_files if f.endswith(('.png', '.jpg', '.b64'))]
833
+ image_cols = st.slider("Image Gallery Columns πŸ–Ό (Own)", min_value=1, max_value=15, value=5, label_visibility="collapsed")
834
+ cols = st.columns(image_cols)
835
+ for idx, image_file in enumerate(own_image_files):
836
+ with cols[idx % image_cols]:
837
+ if image_file.endswith('.b64'):
838
+ display_base64_media(os.path.basename(image_file))
839
+ else:
840
+ st.image(image_file, use_container_width=True)
841
+
842
+ st.markdown("###### Video Gallery πŸŽ₯")
843
+ own_video_files = [f for f in own_files if f.endswith('.mp4')]
844
+ video_cols = st.slider("Video Gallery Columns 🎬 (Own)", min_value=1, max_value=5, value=3, label_visibility="collapsed")
845
+ cols = st.columns(video_cols)
846
+ for idx, video_file in enumerate(own_video_files):
847
+ with cols[idx % video_cols]:
848
+ st.markdown(get_video_html(video_file), unsafe_allow_html=True)
849
+
850
+ st.markdown("###### Audio Gallery 🎧")
851
+ own_audio_files = [f for f in own_files if f.endswith(('.mp3', '.wav'))]
852
+ audio_cols = st.slider("Audio Gallery Columns 🎢 (Own)", min_value=1, max_value=15, value=5, label_visibility="collapsed")
853
+ cols = st.columns(audio_cols)
854
+ for idx, audio_file in enumerate(own_audio_files):
855
+ with cols[idx % audio_cols]:
856
+ st.markdown(await get_audio_html(audio_file), unsafe_allow_html=True)
857
+
858
+ st.markdown("###### Shared Files πŸ“€")
859
+ st.markdown("###### Image Gallery πŸ–Ό")
860
+ shared_image_files = [f for f in shared_files if f.endswith(('.png', '.jpg', '.b64'))]
861
+ image_cols = st.slider("Image Gallery Columns πŸ–Ό (Shared)", min_value=1, max_value=15, value=5, label_visibility="collapsed")
862
+ cols = st.columns(image_cols)
863
+ for idx, image_file in enumerate(shared_image_files):
864
+ with cols[idx % image_cols]:
865
+ if image_file.endswith('.b64'):
866
+ display_base64_media(os.path.basename(image_file))
867
+ else:
868
+ st.image(image_file, use_container_width=True)
869
+
870
+ st.markdown("###### Video Gallery πŸŽ₯")
871
+ shared_video_files = [f for f in shared_files if f.endswith('.mp4')]
872
+ video_cols = st.slider("Video Gallery Columns 🎬 (Shared)", min_value=1, max_value=5, value=3, label_visibility="collapsed")
873
+ cols = st.columns(video_cols)
874
+ for idx, video_file in enumerate(shared_video_files):
875
+ with cols[idx % video_cols]:
876
+ st.markdown(get_video_html(video_file), unsafe_allow_html=True)
877
+
878
+ st.markdown("###### Audio Gallery 🎧")
879
+ shared_audio_files = [f for f in shared_files if f.endswith(('.mp3', '.wav'))]
880
+ audio_cols = st.slider("Audio Gallery Columns 🎢 (Shared)", min_value=1, max_value=15, value=5, label_visibility="collapsed")
881
+ cols = st.columns(audio_cols)
882
+ for idx, audio_file in enumerate(shared_audio_files):
883
+ with cols[idx % audio_cols]:
884
+ st.markdown(await get_audio_html(audio_file), unsafe_allow_html=True)
885
 
886
  # Full Log at End with Download
887
+ with st.container():
888
+ st.markdown("###### Full Chat Log πŸ“œ")
889
+ with open(CHAT_FILE, 'r') as f:
890
+ history_content = f.read()
891
+ st.markdown(history_content)
892
+ st.download_button("Download Chat Log as .md", history_content, file_name=f"chat_{st.session_state.user_id}.md", mime="text/markdown", label_visibility="collapsed")
893
 
894
  loop.run_until_complete(async_interface())
895