import asyncio import humanize import streamlit as st from loguru import logger from api.chat_service import chat_service from api.episode_profiles_service import episode_profiles_service from api.podcast_service import PodcastService # from open_notebook.plugins.podcasts import PodcastConfig from open_notebook.utils import parse_thinking_content, token_count from pages.stream_app.utils import ( convert_source_references, create_session_for_notebook, ) from .note import make_note_from_chat # todo: build a smarter, more robust context manager function async def build_context(notebook_id): # Convert context_config format for API context_config: dict[str, dict[str, str]] = {"sources": {}, "notes": {}} for id, status in st.session_state[notebook_id]["context_config"].items(): if not id: continue item_type, item_id = id.split(":") if item_type not in ["note", "source"]: continue if item_type == "source": context_config["sources"][item_id] = status elif item_type == "note": context_config["notes"][item_id] = status # Get context via API result = await chat_service.build_context( notebook_id=notebook_id, context_config=context_config ) # Store in session state for compatibility st.session_state[notebook_id]["context"] = result["context"] return st.session_state[notebook_id]["context"] async def execute_chat(txt_input, context, current_session): # Execute chat via API result = await chat_service.execute_chat( session_id=current_session["id"], message=txt_input, context=context ) # Update session state with API response st.session_state[current_session["id"]]["messages"] = result["messages"] return result def chat_sidebar(current_notebook, current_session): context = asyncio.run(build_context(notebook_id=current_notebook.id)) tokens = token_count( str(context) + str(st.session_state[current_session["id"]]["messages"]) ) chat_tab, podcast_tab = st.tabs(["Chat", "Podcast"]) with st.expander(f"Context ({tokens} tokens), {len(str(context))} chars"): st.json(context) with podcast_tab: with st.container(border=True): # Fetch available episode profiles try: episode_profiles = episode_profiles_service.get_all_episode_profiles() episode_profile_names = [ep.name for ep in episode_profiles] except Exception as e: st.error(f"Failed to load episode profiles: {str(e)}") episode_profiles = [] episode_profile_names = [] if len(episode_profiles) == 0: st.warning( "No episode profiles found. Please create profiles in the Podcast Profiles tab first." ) st.page_link("pages/5_🎙️_Podcasts.py", label="🎙️ Go to Podcast Profiles") else: # Episode Profile selection selected_episode_profile = st.selectbox( "Episode Profile", episode_profile_names ) # Get the selected episode profile object to access speaker_config selected_profile_obj = next( ( ep for ep in episode_profiles if ep.name == selected_episode_profile ), None, ) # Episode details episode_name = st.text_input( "Episode Name", placeholder="e.g., AI and the Future of Work" ) instructions = st.text_area( "Additional Instructions (Optional)", placeholder="Any specific instructions beyond the episode profile's default briefing...", help="These instructions will be added to the episode profile's default briefing.", ) # Check for context availability if len(context.get("note", [])) + len(context.get("source", [])) == 0: st.warning( "No notes or sources found in context. You don't want a boring podcast, right? So, add some context first." ) else: # Generate button if st.button("🎙️ Generate Podcast", type="primary"): if not episode_name.strip(): st.error("Please enter an episode name") else: try: with st.spinner("Starting podcast generation..."): # Use podcast service to generate podcast async def generate_podcast(): return await PodcastService.submit_generation_job( episode_profile_name=selected_episode_profile, speaker_profile_name=selected_profile_obj.speaker_config if selected_profile_obj else "", episode_name=episode_name.strip(), content=str(context), briefing_suffix=instructions.strip() if instructions.strip() else None, notebook_id=str(current_notebook.id), ) job_id = asyncio.run(generate_podcast()) if job_id: st.info( "🎉 Podcast generation started successfully! Check the **Podcasts** page to monitor progress and download results." ) else: st.error( "Failed to start podcast generation: No job ID returned" ) except Exception as e: logger.error(f"Error generating podcast: {str(e)}") st.error(f"Error generating podcast: {str(e)}") # Navigation link st.divider() st.page_link("pages/5_🎙️_Podcasts.py", label="🎙️ Go to Podcasts") with chat_tab: with st.expander( f"**Session:** {current_session['title']} - {humanize.naturaltime(current_session['updated'])}" ): new_session_name = st.text_input( "Current Session", key="new_session_name", value=current_session["title"], ) c1, c2 = st.columns(2) if c1.button("Rename", key="rename_session"): asyncio.run(chat_service.update_session(current_session["id"], new_session_name)) st.rerun() if c2.button("Delete", key="delete_session_1"): asyncio.run(chat_service.delete_session(current_session["id"])) st.session_state[current_notebook.id]["active_session"] = None st.rerun() st.divider() new_session_name = st.text_input( "New Session Name", key="new_session_name_f", placeholder="Enter a name for the new session...", ) st.caption("If no name provided, we'll use the current date.") if st.button("Create New Session", key="create_new_session"): new_session = create_session_for_notebook( notebook_id=current_notebook.id, session_name=new_session_name ) st.session_state[current_notebook.id]["active_session"] = new_session["id"] st.rerun() st.divider() sessions = asyncio.run(chat_service.get_sessions(current_notebook.id)) if len(sessions) > 1: st.markdown("**Other Sessions:**") for session in sessions: if session["id"] == current_session["id"]: continue st.markdown( f"{session['title']} - {humanize.naturaltime(session['updated'])}" ) if st.button(label="Load", key=f"load_session_{session['id']}"): st.session_state[current_notebook.id]["active_session"] = ( session["id"] ) st.rerun() with st.container(border=True): request = st.chat_input("Enter your question") # removing for now since it's not multi-model capable right now if request: response = asyncio.run(execute_chat( txt_input=request, context=context, current_session=current_session, )) st.session_state[current_session["id"]]["messages"] = response["messages"] for msg in st.session_state[current_session["id"]]["messages"][::-1]: # Handle both domain objects and dict responses from API msg_type = msg.get("type") if isinstance(msg, dict) else msg.type msg_content = msg.get("content") if isinstance(msg, dict) else msg.content msg_id = msg.get("id") if isinstance(msg, dict) else getattr(msg, 'id', 'unknown') if msg_type not in ["human", "ai"]: continue if not msg_content: continue with st.chat_message(name=msg_type): if msg_type == "ai": # Parse thinking content for AI messages thinking_content, cleaned_content = parse_thinking_content( msg_content ) # Show thinking content in expander if present if thinking_content: with st.expander("🤔 AI Reasoning", expanded=False): st.markdown(thinking_content) # Show the cleaned regular content if cleaned_content: st.markdown(convert_source_references(cleaned_content)) elif ( msg_content ): # Fallback to original if cleaning resulted in empty content st.markdown(convert_source_references(msg_content)) # New Note button for AI messages if st.button("💾 New Note", key=f"render_save_{msg_id}"): make_note_from_chat( content=msg_content, notebook_id=current_notebook.id, ) st.rerun() else: # Human messages - display normally st.markdown(convert_source_references(msg_content))