import streamlit as st import requests import os from urllib.parse import urlparse # ===================================== # PAGE CONFIG # ===================================== st.set_page_config( page_title="Perplexity AI Clone", page_icon="๐Ÿ”", layout="wide", initial_sidebar_state="collapsed" ) # ===================================== # SESSION STATE # ===================================== if "messages" not in st.session_state: st.session_state.messages = [] if "mode" not in st.session_state: st.session_state.mode = "Automatic" if "current_result" not in st.session_state: st.session_state.current_result = None if "theme" not in st.session_state: st.session_state.theme = "dark" if "uploaded_files" not in st.session_state: st.session_state.uploaded_files = [] if "show_upload" not in st.session_state: st.session_state.show_upload = False if "youtube_url" not in st.session_state: st.session_state.youtube_url = "" if "video_loaded" not in st.session_state: st.session_state.video_loaded = False if "video_summary" not in st.session_state: st.session_state.video_summary = None # Stores the initial video analysis if "video_transcript" not in st.session_state: st.session_state.video_transcript = "" if "product_ideas" not in st.session_state: st.session_state.product_ideas = [] # ===================================== # CONFIGURATION # ===================================== # For Hugging Face Spaces - backend runs internally on same container API_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000") WORKSPACE = "default" # Check API health on startup if "api_checked" not in st.session_state: st.session_state.api_checked = False st.session_state.api_status = None # MODE MAPPING - All 8 modes with correct backend endpoints MODES = { "Automatic": { "icon": "๐Ÿ”", "desc": "Auto-routes to best mode", "endpoint": "/api/chat" }, "Web Search": { "icon": "๐ŸŒ", "desc": "Real-time web search", "endpoint": "/api/web" }, "RAG": { "icon": "๐Ÿ“š", "desc": "Search uploaded documents", "endpoint": "/api/rag" }, "Agentic": { "icon": "๐Ÿค–", "desc": "Multi-agent collaboration", "endpoint": "/api/agentic" }, "Deep Research": { "icon": "๐Ÿง ", "desc": "In-depth research", "endpoint": "/api/deep_research" }, "Analysis": { "icon": "๐Ÿ“Š", "desc": "Deep data analysis", "endpoint": "/api/analyze" }, "Summarize": { "icon": "๐Ÿ“", "desc": "Summarize content", "endpoint": "/api/summarize" }, "Chat": { "icon": "๐Ÿ’ฌ", "desc": "Direct AI chat", "endpoint": "/api/focus" }, "Product MVP": { "icon": "๐Ÿš€", "desc": "Idea โ†’ MVP Blueprint", "endpoint": "/api/product_mvp" }, "Video Brain": { "icon": "๐ŸŽฅ", "desc": "Understand YouTube lectures", "endpoint": "/api/video_brain" }, } # ===================================== # CSS - PERPLEXITY EXACT STYLE # ===================================== def get_css(): is_dark = st.session_state.theme == "dark" if is_dark: colors = { "bg": "#191A1A", "bg2": "#1F2020", "bg3": "#2A2B2B", "text": "#ECECEC", "text2": "#A1A1A1", "muted": "#6B6B6B", "accent": "#20B8CD", "border": "#3A3B3B", "success": "#22C55E" } else: colors = { "bg": "#FFFFFF", "bg2": "#F7F7F8", "bg3": "#EEEEEF", "text": "#1A1A1A", "text2": "#666666", "muted": "#999999", "accent": "#0EA5E9", "border": "#E5E5E5", "success": "#22C55E" } return f""" """ st.markdown(get_css(), unsafe_allow_html=True) # ===================================== # API HEALTH CHECK # ===================================== def check_api_health(): """Check if API is healthy and keys are configured.""" try: resp = requests.get(f"{API_URL}/health", timeout=5) if resp.ok: return resp.json() except: pass return None # Check API on first load if not st.session_state.api_checked: st.session_state.api_status = check_api_health() st.session_state.api_checked = True # Show warnings if API keys are missing if st.session_state.api_status: api_keys = st.session_state.api_status.get("api_keys", {}) missing_keys = [] if api_keys.get("groq") == "MISSING": missing_keys.append("GROQ_API_KEY") if api_keys.get("tavily") == "MISSING": missing_keys.append("TAVILY_API_KEY") if missing_keys: st.error(f"โš ๏ธ Missing API keys: {', '.join(missing_keys)}. Please configure them in your Hugging Face Space settings under 'Variables and secrets'.") # ===================================== # HELPER FUNCTIONS # ===================================== def call_api(query: str, mode: str, extra_data: dict = None): """Call backend API based on selected mode.""" mode_config = MODES.get(mode, MODES["Automatic"]) endpoint = mode_config["endpoint"] payload = { "message": query, "workspace_id": WORKSPACE, "mode": mode.lower().replace(" ", "_") } # Add extra data for special modes if extra_data: payload.update(extra_data) try: response = requests.post(f"{API_URL}{endpoint}", json=payload, timeout=180) response.raise_for_status() try: return response.json() except ValueError: return { "answer": f"Error: Invalid JSON response from server", "sources": [], "links": [], "images": [], "followups": [] } except Exception as e: return { "answer": f"Error: {str(e)}", "sources": [], "links": [], "images": [], "followups": [] } def upload_files(files): """Upload files to backend.""" if not files: return False files_payload = [ ("files", (f.name, f.getvalue(), f.type or "application/octet-stream")) for f in files ] try: r = requests.post( f"{API_URL}/api/upload_docs", data={"workspace_id": WORKSPACE}, files=files_payload, timeout=60 ) return r.ok except: return False def get_domain(url: str) -> str: try: return urlparse(url).netloc.replace('www.', '') except: return url[:30] # ===================================== # THEME TOGGLE # ===================================== col_spacer, col_theme = st.columns([12, 1]) with col_theme: theme_icon = "๐ŸŒ™" if st.session_state.theme == "dark" else "โ˜€๏ธ" if st.button(theme_icon, key="theme_toggle"): st.session_state.theme = "light" if st.session_state.theme == "dark" else "dark" st.rerun() # ===================================== # HERO - Always show # ===================================== if st.session_state.current_result: # Compact version when showing results st.markdown("""
""", unsafe_allow_html=True) else: # Full version on home st.markdown("""
Where knowledge begins
""", unsafe_allow_html=True) # ===================================== # UNIFIED SEARCH BOX (Hide for Video Brain - it has dedicated UI) # ===================================== if st.session_state.mode != "Video Brain": st.markdown('
', unsafe_allow_html=True) # Use a form so Enter key submits with st.form(key="search_form", clear_on_submit=False): col1, col2, col3, col4 = st.columns([2, 8, 1, 1]) with col1: # Mode selector dropdown mode_list = list(MODES.keys()) current_idx = mode_list.index(st.session_state.mode) selected = st.selectbox( "mode", mode_list, index=current_idx, format_func=lambda x: f"{MODES[x]['icon']} {x}", label_visibility="collapsed", key="mode_select" ) with col2: # Search input - Enter key will submit the form query = st.text_input( "search", placeholder="Ask anything... (Press Enter to search)", label_visibility="collapsed", key="query_input" ) with col3: # Placeholder for alignment st.write("") with col4: # Submit button submit = st.form_submit_button("โ†’", help="Search") # Handle mode change outside form if selected != st.session_state.mode: st.session_state.mode = selected st.rerun() # File upload button (outside form) col_attach, col_spacer_attach = st.columns([1, 11]) with col_attach: if st.button("๐Ÿ“Ž Attach files", key="attach_btn"): st.session_state.show_upload = not st.session_state.show_upload st.markdown('
', unsafe_allow_html=True) # Mode description st.markdown(f'
{MODES[st.session_state.mode]["icon"]} {st.session_state.mode}: {MODES[st.session_state.mode]["desc"]}
', unsafe_allow_html=True) else: # Video Brain mode - just show mode selector mode_list = list(MODES.keys()) current_idx = mode_list.index(st.session_state.mode) selected = st.selectbox( "Switch Mode", mode_list, index=current_idx, format_func=lambda x: f"{MODES[x]['icon']} {x}", key="mode_select_video" ) if selected != st.session_state.mode: st.session_state.mode = selected # Reset video state when switching away st.session_state.video_summary = None st.session_state.video_loaded = False st.session_state.youtube_url = "" st.session_state.video_transcript = "" st.rerun() # Initialize these for Video Brain mode query = "" submit = False # ===================================== # SPECIAL UI FOR PRODUCT MVP MODE # ===================================== if st.session_state.mode == "Product MVP" and not st.session_state.current_result: st.markdown("""

๐Ÿš€ Product Builder โ€“ Idea โ†’ MVP Blueprint

๐ŸŸ  Product Builder Active

""", unsafe_allow_html=True) st.markdown("

Describe your product idea:

", unsafe_allow_html=True) # ===================================== # SPECIAL UI FOR VIDEO BRAIN MODE # ===================================== if st.session_state.mode == "Video Brain": # Header st.markdown("""

๐ŸŽฅ Video Brain โ€“ Understand Any YouTube Video

Enter a YouTube URL to analyze the video

""", unsafe_allow_html=True) # STEP 1: If no video loaded yet, show URL input if not st.session_state.video_summary: st.markdown("### Step 1: Enter YouTube URL") video_url_col, load_btn_col = st.columns([4, 1]) with video_url_col: youtube_url_input = st.text_input( "YouTube URL", value=st.session_state.youtube_url, placeholder="https://www.youtube.com/watch?v=...", key="video_url_input", label_visibility="collapsed" ) with load_btn_col: load_video = st.button("๐ŸŽฌ Load", key="load_video_btn", use_container_width=True) # Also allow transcript paste as alternative with st.expander("๐Ÿ“‹ Or paste transcript manually (if URL doesn't work)"): transcript_input = st.text_area( "Paste transcript here", value=st.session_state.video_transcript, placeholder="Open YouTube โ†’ Click โ‹ฎ โ†’ Show transcript โ†’ Copy all text", height=150, key="transcript_paste_input" ) if transcript_input != st.session_state.video_transcript: st.session_state.video_transcript = transcript_input # Handle Load button click if load_video and youtube_url_input: if "youtube.com" in youtube_url_input or "youtu.be" in youtube_url_input: st.session_state.youtube_url = youtube_url_input # Call API to analyze video with st.spinner("๐Ÿ”„ Analyzing video... This may take a moment..."): payload = { "message": "Summarize this video completely. Give me: 1) What the video is about, 2) Key points and takeaways, 3) Important details mentioned, 4) Main conclusions", "workspace_id": WORKSPACE, "mode": "video_brain", "youtube_url": youtube_url_input, "transcript": st.session_state.video_transcript } try: response = requests.post(f"{API_URL}/api/video_brain", json=payload, timeout=180) if response.ok: result = response.json() st.session_state.video_summary = result st.session_state.video_loaded = True st.rerun() else: st.error(f"Error loading video: {response.text}") except Exception as e: st.error(f"Error: {str(e)}") else: st.warning("โš ๏ธ Please enter a valid YouTube URL") # Also handle Enter key in URL input if youtube_url_input and youtube_url_input != st.session_state.youtube_url: if "youtube.com" in youtube_url_input or "youtu.be" in youtube_url_input: st.session_state.youtube_url = youtube_url_input # STEP 2: Video is loaded - show summary and question input else: # Show video info st.markdown(f"**๐Ÿ“บ Loaded Video:** `{st.session_state.youtube_url}`") # Button to load different video if st.button("๐Ÿ”„ Load Different Video", key="reset_video"): st.session_state.video_summary = None st.session_state.video_loaded = False st.session_state.youtube_url = "" st.session_state.video_transcript = "" st.session_state.current_result = None st.rerun() st.markdown("---") # Show the video summary st.markdown("### ๐Ÿ“ Video Summary") summary_data = st.session_state.video_summary st.markdown(summary_data.get("answer", "No summary available")) # Show sources if available if summary_data.get("sources"): with st.expander("๐Ÿ“š Sources"): for src in summary_data["sources"]: st.markdown(f"- [{src.get('title', 'Source')}]({src.get('url', '#')})") st.markdown("---") # STEP 3: Ask follow-up questions st.markdown("### ๐Ÿ’ฌ Ask Questions About This Video") followup_question = st.text_input( "Your question", placeholder="Ask anything about the video...", key="video_followup_input", label_visibility="collapsed" ) ask_btn = st.button("๐Ÿ” Ask", key="ask_followup_btn") # Quick question buttons st.markdown("**Quick questions:**") quick_q_cols = st.columns(3) quick_questions = [ "What are the main points?", "Explain in simple terms", "What should I remember?" ] selected_quick_q = None for i, qq in enumerate(quick_questions): with quick_q_cols[i]: if st.button(qq, key=f"quick_q_{i}"): selected_quick_q = qq # Handle question submission question_to_ask = followup_question if ask_btn and followup_question else selected_quick_q if question_to_ask: with st.spinner("๐Ÿค” Thinking..."): payload = { "message": question_to_ask, "workspace_id": WORKSPACE, "mode": "video_brain", "youtube_url": st.session_state.youtube_url, "transcript": st.session_state.video_transcript } try: response = requests.post(f"{API_URL}/api/video_brain", json=payload, timeout=180) if response.ok: result = response.json() st.session_state.current_result = { "query": question_to_ask, "mode": "Video Brain", "data": result } st.rerun() except Exception as e: st.error(f"Error: {str(e)}") # Show previous Q&A if exists if st.session_state.current_result and st.session_state.current_result.get("mode") == "Video Brain": st.markdown("---") st.markdown(f"**โ“ Q:** {st.session_state.current_result['query']}") st.markdown(f"**๐Ÿ’ก A:** {st.session_state.current_result['data'].get('answer', '')}") # Show file uploader when icon is clicked if st.session_state.show_upload: uploaded = st.file_uploader( "Upload documents (PDF, TXT, MD, PPTX)", type=["pdf", "txt", "md", "pptx"], accept_multiple_files=True, key="file_uploader" ) if uploaded: with st.spinner("๐Ÿ“ค Uploading..."): if upload_files(uploaded): new_files = [f.name for f in uploaded if f.name not in st.session_state.uploaded_files] if new_files: st.session_state.uploaded_files.extend(new_files) st.success(f"โœ… {len(new_files)} file(s) uploaded!") st.session_state.show_upload = False st.rerun() # Show uploaded files count if st.session_state.uploaded_files: st.caption(f"๐Ÿ“ {len(st.session_state.uploaded_files)} file(s) ready for RAG") # ===================================== # HANDLE SEARCH (Skip for Video Brain - it has its own UI) # ===================================== if submit and query.strip() and st.session_state.mode != "Video Brain": extra_data = None actual_query = query.strip() # For Product MVP mode, save to ideas history if st.session_state.mode == "Product MVP": st.session_state.product_ideas.append({ "idea": actual_query, "time": "just now" }) with st.spinner(f"๐Ÿ”„ {st.session_state.mode}..."): result = call_api(actual_query, st.session_state.mode, extra_data) st.session_state.current_result = { "query": actual_query, "mode": st.session_state.mode, "data": result } st.rerun() # ===================================== # DISPLAY RESULTS # ===================================== if st.session_state.current_result: result = st.session_state.current_result data = result["data"] st.divider() # Special header for Product MVP mode if result['mode'] == "Product MVP": st.markdown("""

๐Ÿ“„ MVP Blueprint

""", unsafe_allow_html=True) # Special header for Video Brain mode if result['mode'] == "Video Brain": st.markdown("""

๐ŸŽฅ Video Analysis

""", unsafe_allow_html=True) # Show embedded video if st.session_state.youtube_url: video_id = "" if "v=" in st.session_state.youtube_url: video_id = st.session_state.youtube_url.split("v=")[1].split("&")[0] elif "youtu.be/" in st.session_state.youtube_url: video_id = st.session_state.youtube_url.split("youtu.be/")[1].split("?")[0] if video_id: st.video(f"https://www.youtube.com/watch?v={video_id}") # Query box mode_info = MODES.get(result['mode'], MODES['Automatic']) st.markdown(f"""
{result['query']}
{mode_info['icon']} {result['mode']}
""", unsafe_allow_html=True) # Sources count sources = data.get("sources", []) or data.get("links", []) if sources: st.success(f"โœ“ {len(sources)} sources") # Memory saved notification for Product MVP if result['mode'] == "Product MVP": st.info("๐Ÿ“ New Memory Saved") # Layout - Full width (removed duplicate sidebar sources) tabs = st.tabs(["โœจ Answer", "๐Ÿ”— Sources", "๐Ÿ–ผ๏ธ Images"]) with tabs[0]: answer = data.get("answer", "No answer.") # Display answer directly with markdown st.markdown(answer) # For Video Brain mode, show a follow-up question input if result['mode'] == "Video Brain" and st.session_state.youtube_url: st.divider() st.markdown("**๐Ÿ’ฌ Ask another question about this video:**") followup_question = st.text_input( "Follow-up question", placeholder="e.g., What are the main arguments? Explain the key concept...", key="video_followup_input", label_visibility="collapsed" ) if st.button("Ask", key="video_followup_btn"): if followup_question.strip(): with st.spinner("Analyzing..."): new_result = call_api( followup_question.strip(), "Video Brain", {"youtube_url": st.session_state.youtube_url} ) st.session_state.current_result = { "query": followup_question.strip(), "mode": "Video Brain", "data": new_result } st.rerun() followups = data.get("followups", []) if followups: st.markdown("**Related:**") for i, fu in enumerate(followups[:3]): if st.button(f"โ†’ {fu}", key=f"fu_{i}"): extra = None if st.session_state.mode == "Video Brain" and st.session_state.youtube_url: extra = {"youtube_url": st.session_state.youtube_url} with st.spinner("..."): new_result = call_api(fu, st.session_state.mode, extra) st.session_state.current_result = { "query": fu, "mode": st.session_state.mode, "data": new_result } st.rerun() with tabs[1]: links = data.get("links", []) if links: for link in links: st.markdown(f"""
{link.get('title','Source')}
{get_domain(link.get('url',''))}
""", unsafe_allow_html=True) else: st.info("No sources") with tabs[2]: images = data.get("images", []) if images: cols = st.columns(3) for i, img in enumerate(images[:9]): url = img.get("url") or img.get("thumbnail_url") if url: with cols[i % 3]: st.image(url, use_column_width=True) else: st.info("No images") # ===================================== # SIDEBAR (for settings) # ===================================== with st.sidebar: st.markdown("### โš™๏ธ Settings") st.divider() if st.button("๐Ÿ—‘๏ธ Clear Chat", use_container_width=True): st.session_state.current_result = None st.session_state.messages = [] st.rerun() if st.button("๐Ÿ—‘๏ธ Clear Files", use_container_width=True): st.session_state.uploaded_files = [] st.info("Files cleared") if st.button("๐Ÿ—‘๏ธ Clear Video", use_container_width=True): st.session_state.youtube_url = "" st.session_state.video_loaded = False st.info("Video cleared") st.divider() st.caption(f"Theme: {'๐ŸŒ™ Dark' if st.session_state.theme == 'dark' else 'โ˜€๏ธ Light'}") st.caption(f"Mode: {st.session_state.mode}") if st.session_state.uploaded_files: st.divider() st.markdown("### ๐Ÿ“ Files") for f in st.session_state.uploaded_files: st.caption(f"๐Ÿ“„ {f}") # Show video info for Video Brain mode if st.session_state.video_loaded and st.session_state.youtube_url: st.divider() st.markdown("### ๐ŸŽฅ Loaded Video") st.caption(f"๐Ÿ“บ {st.session_state.youtube_url[:40]}...") # Show recent product ideas if st.session_state.product_ideas: st.divider() st.markdown("### ๐Ÿงพ Recent Ideas") for idea in st.session_state.product_ideas[-3:]: st.caption(f"๐Ÿ’ก {idea['idea'][:30]}...")