Update app.py
Browse files
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: \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 |
-
|
410 |
-
|
411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
412 |
st.session_state.image_hashes.add(img_hash)
|
413 |
await save_chat_history_with_image(username, filepath)
|
414 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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="
|
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="
|
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
|
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]
|
|
|
550 |
node_id = f"{user}_{i}"
|
551 |
-
nodes[node_id] = f"{user}
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
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
|
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 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
|
|
|
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.
|
|
|
602 |
|
603 |
-
st.
|
604 |
-
|
|
|
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.
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
|
624 |
-
|
625 |
-
|
626 |
-
|
627 |
-
|
628 |
-
|
629 |
-
|
630 |
-
|
631 |
-
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
|
636 |
-
|
637 |
-
|
638 |
-
|
639 |
-
|
640 |
-
|
641 |
-
|
642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
643 |
|
644 |
# Mermaid Graph Visualization
|
645 |
-
st.
|
646 |
-
|
647 |
-
|
648 |
-
|
649 |
-
|
650 |
-
|
651 |
-
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
657 |
-
|
658 |
-
|
659 |
-
|
660 |
-
if
|
661 |
-
st.session_state.
|
662 |
-
|
663 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
672 |
st.rerun()
|
673 |
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
|
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 |
-
|
734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
735 |
else:
|
736 |
-
|
737 |
-
|
738 |
-
|
739 |
-
|
740 |
-
|
741 |
-
|
742 |
-
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
751 |
|
752 |
-
|
753 |
-
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
|
763 |
# Separate Galleries for Own and Shared Files
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
768 |
-
|
769 |
-
|
770 |
-
|
771 |
-
|
772 |
-
|
773 |
-
|
774 |
-
|
775 |
-
|
776 |
-
|
777 |
-
|
778 |
-
|
779 |
-
|
780 |
-
|
781 |
-
|
782 |
-
|
783 |
-
|
784 |
-
|
785 |
-
|
786 |
-
|
787 |
-
|
788 |
-
|
789 |
-
|
790 |
-
|
791 |
-
|
792 |
-
|
793 |
-
|
794 |
-
|
795 |
-
|
796 |
-
|
797 |
-
|
798 |
-
|
799 |
-
|
800 |
-
|
801 |
-
|
802 |
-
|
803 |
-
|
804 |
-
|
805 |
-
|
806 |
-
|
807 |
-
|
808 |
-
|
809 |
-
|
810 |
-
|
811 |
-
|
812 |
-
|
813 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
817 |
|
818 |
# Full Log at End with Download
|
819 |
-
st.
|
820 |
-
|
821 |
-
|
822 |
-
|
823 |
-
|
|
|
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: \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**: "
|
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 |
|