def run_with_watch(): from watchfiles import run_process logger.info("Starting watch mode on 'app' directory") def _run(): logger.info("Reloading app.py") main() run_process( path=".", target=_run, watch_filter=lambda change, path: path.endswith(".py"), ) from dotenv import load_dotenv import os import gradio as gr from utils.app_logging import setup_logging from utils.chat import Me load_dotenv(override=True) logger = setup_logging() logger.info("Starting digital-cv") me = Me() logger.info("Me initialized") # Theming and chat styling for embedding theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate") initial_assistant_message = ( "Hello, nice to meet you! At any time, feel free to give me your name and email; " "I'll make a note and I can get back to you later." ) chatbot = gr.Chatbot( label=None, avatar_images=("assets/logo.png", "assets/dan.png"), render_markdown=True, type="messages", value=[{"role": "assistant", "content": initial_assistant_message}], elem_id="chatbot", ) logger.info("Chatbot initialized") custom_css = """ html, body, .gradio-container { height: 100%; } body { margin: 0; font-family: "Inter", "SF Pro Display", "Segoe UI", system-ui, -apple-system, sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 25%, #334155 50%, #1e293b 75%, #0f172a 100%); background-attachment: fixed; color: #f8fafc; font-feature-settings: "kern" 1, "liga" 1, "ss01" 1; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 400; line-height: 1.6; } .gradio-container { display: flex; background: transparent; padding: 28px 0 36px; } #container { max-width: 1280px; margin: 0 auto; padding: 32px 40px 40px; display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; border-radius: 32px; background: rgba(15, 23, 42, 0.95); box-shadow: 0 64px 128px rgba(0, 0, 0, 0.4), 0 32px 64px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(148, 163, 184, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05); backdrop-filter: blur(24px); border: 1px solid rgba(148, 163, 184, 0.15); position: relative; overflow: hidden; } #container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.6), rgba(147, 51, 234, 0.6), transparent ); z-index: 1; } #container::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(circle at 50% 0%, rgba(59, 130, 246, 0.05) 0%, transparent 50%); pointer-events: none; z-index: 0; } #header { align-items: center; gap: 32px; justify-content: flex-start; margin-bottom: 16px; position: relative; z-index: 2; } #logo img { max-height: 240px; width: auto; border-radius: 24px; object-fit: contain; box-shadow: 0 32px 64px rgba(0, 0, 0, 0.3), 0 16px 32px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(148, 163, 184, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); filter: brightness(1.05) contrast(1.1); } #logo img:hover { transform: translateY(-4px) scale(1.02); box-shadow: 0 48px 96px rgba(0, 0, 0, 0.4), 0 24px 48px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15); filter: brightness(1.1) contrast(1.15); } #intro-card { border-radius: 24px; padding: 32px 36px; background: linear-gradient(135deg, rgba(30, 41, 59, 0.8) 0%, rgba(51, 65, 85, 0.6) 50%, rgba(30, 41, 59, 0.8) 100% ); border: 1px solid rgba(148, 163, 184, 0.2); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 16px 32px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.1); position: relative; overflow: hidden; backdrop-filter: blur(16px); } #intro-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.5), rgba(147, 51, 234, 0.5), transparent ); } #intro-card::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(circle at 50% 0%, rgba(59, 130, 246, 0.03) 0%, transparent 70%); pointer-events: none; } #intro-card ul { margin: 0.35rem 0 0.7rem; padding-left: 1.1rem; } #intro-card li { margin-bottom: 0.3rem; } #title { text-align: center; margin: 24px 0 32px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 800; color: #f1f5f9; font-size: 1.75rem; text-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.2); position: relative; z-index: 2; background: linear-gradient(135deg, #f1f5f9 0%, #cbd5e1 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } #title::after { content: ''; position: absolute; bottom: -12px; left: 50%; transform: translateX(-50%); width: 80px; height: 3px; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.8), rgba(147, 51, 234, 0.8), transparent ); border-radius: 2px; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); } #chat-wrapper { display: flex; flex-direction: column; gap: 16px; flex: 1 1 auto; min-height: 0; } #chatbot { display: flex; flex-direction: column; min-height: 680px; height: clamp(680px, calc(100dvh - 200px), 1200px); border-radius: 28px; border: 1px solid rgba(148, 163, 184, 0.2); background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 50%, rgba(15, 23, 42, 0.95) 100% ); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 48px 96px rgba(0, 0, 0, 0.3), 0 24px 48px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(148, 163, 184, 0.1); position: relative; overflow: hidden; backdrop-filter: blur(20px); z-index: 2; } #chatbot::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.6), rgba(147, 51, 234, 0.6), transparent ); z-index: 1; } #chatbot::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(circle at 50% 0%, rgba(59, 130, 246, 0.02) 0%, transparent 70%); pointer-events: none; z-index: 0; } #chatbot .wrapper, #chatbot .bubble-wrap, #chatbot .message-wrap { flex: 1 1 auto; display: flex; min-height: 0; } #chatbot .bubble-wrap { flex-direction: column; overflow-y: auto; padding: 12px 16px 20px; gap: 16px; } #chatbot label span { color: rgba(221, 230, 255, 0.85); font-weight: 600; letter-spacing: 0.03em; } #chatbot .message-wrap .message { background: rgba(30, 41, 59, 0.9); border-radius: 24px; border: 1px solid rgba(148, 163, 184, 0.2); box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2), 0 12px 24px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05); backdrop-filter: blur(12px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; padding: 20px 24px; margin: 8px 0; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; } #chatbot .message-wrap .message::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); } #chatbot .message-wrap .message:hover { transform: translateY(-2px) scale(1.01); box-shadow: 0 32px 64px rgba(0, 0, 0, 0.3), 0 16px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1); } #chatbot .message-wrap .bot .message { background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.8) 100% ); border-color: rgba(59, 130, 246, 0.3); margin-right: 60px; margin-left: 8px; } #chatbot .message-wrap .user .message { background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.8) 100% ); border-color: rgba(147, 51, 234, 0.3); margin-left: 60px; margin-right: 8px; } .suggestion-banner { font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.95rem; color: #cbd5e1; margin-bottom: 16px; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); position: relative; z-index: 2; } .suggestion-buttons { display: flex; gap: 16px; flex-wrap: wrap; justify-content: space-between; margin-bottom: 12px; position: relative; z-index: 2; } .suggestion-buttons button { flex: 1 1 0; min-width: 0; padding: 16px 20px; border-radius: 16px; border: 1px solid rgba(148, 163, 184, 0.3); background: linear-gradient(135deg, rgba(30, 41, 59, 0.9) 0%, rgba(51, 65, 85, 0.7) 100% ); color: #f1f5f9; font-weight: 600; font-size: 0.95rem; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; position: relative; overflow: hidden; backdrop-filter: blur(12px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1); } .suggestion-buttons button::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent ); transition: left 0.6s ease; } .suggestion-buttons button::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent ); } .suggestion-buttons button:hover { transform: translateY(-4px) scale(1.02); box-shadow: 0 32px 64px rgba(0, 0, 0, 0.2), 0 16px 32px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.15); border-color: rgba(59, 130, 246, 0.5); background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.8) 100% ); } .suggestion-buttons button:hover::before { left: 100%; } .suggestion-buttons button:active { transform: translateY(-2px) scale(1.01); } .gradio-container textarea { border-radius: 20px !important; min-height: 120px !important; background: rgba(30, 41, 59, 0.95) !important; border: 1px solid rgba(148, 163, 184, 0.3) !important; color: #f1f5f9 !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 16px 32px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.1) !important; font-size: 1rem !important; font-weight: 500 !important; line-height: 1.6 !important; padding: 20px 24px !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; backdrop-filter: blur(16px) !important; position: relative !important; overflow: hidden !important; } .gradio-container textarea::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); } .gradio-container textarea:focus { outline: none !important; border-color: rgba(59, 130, 246, 0.6) !important; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 24px 48px rgba(0, 0, 0, 0.3), 0 12px 24px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.15) !important; background: rgba(30, 41, 59, 0.98) !important; transform: translateY(-1px) !important; } .gradio-container textarea::placeholder { color: rgba(203, 213, 225, 0.7) !important; font-weight: 500 !important; font-style: italic !important; } #footer { text-align: center; opacity: 0.8; font-size: 0.9rem; margin-top: 32px; letter-spacing: 0.06em; color: rgba(203, 213, 225, 0.8); font-weight: 500; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); position: relative; z-index: 2; } /* Professional loading states and animations */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes scaleIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .loading-message { animation: pulse 2s ease-in-out infinite; } .loading-shimmer { position: relative; overflow: hidden; } .loading-shimmer::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent ); animation: shimmer 2.5s infinite; } /* Professional button styles */ .gradio-container button { border-radius: 16px !important; font-weight: 600 !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; position: relative !important; overflow: hidden !important; backdrop-filter: blur(12px) !important; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; } .gradio-container button:hover { transform: translateY(-2px) scale(1.02) !important; box-shadow: 0 16px 32px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.15) !important; } .gradio-container button:active { transform: translateY(-1px) scale(1.01) !important; } /* Professional scrollbar styling */ ::-webkit-scrollbar { width: 10px; } ::-webkit-scrollbar-track { background: rgba(30, 41, 59, 0.3); border-radius: 6px; border: 1px solid rgba(148, 163, 184, 0.1); } ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, rgba(59, 130, 246, 0.6) 0%, rgba(147, 51, 234, 0.6) 100% ); border-radius: 6px; border: 1px solid rgba(148, 163, 184, 0.2); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); } ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(147, 51, 234, 0.8) 100% ); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.15); } /* Professional responsive design */ @media (max-width: 1024px) { #container { padding: 24px 32px; border-radius: 28px; max-width: 100%; } #header { gap: 24px; } #logo img { max-height: 200px; } #chatbot { height: clamp(620px, calc(100dvh - 180px), 1000px); border-radius: 24px; } #chatbot .message-wrap .bot .message { margin-right: 40px; } #chatbot .message-wrap .user .message { margin-left: 40px; } } @media (max-width: 900px) { #container { padding: 20px 24px; border-radius: 24px; } #header { flex-direction: column; text-align: center; gap: 24px; } #logo img { max-height: 180px; } #chatbot { height: clamp(580px, calc(100dvh - 160px), 900px); border-radius: 20px; } #chatbot .message-wrap .bot .message { margin-right: 20px; padding: 16px 20px; } #chatbot .message-wrap .user .message { margin-left: 20px; padding: 16px 20px; } .suggestion-buttons { flex-direction: column; gap: 12px; } .suggestion-buttons button { min-width: 100%; padding: 16px 20px; } #title { font-size: 1.4rem; margin: 20px 0 24px; } #intro-card { padding: 24px 28px; border-radius: 20px; } } @media (max-width: 640px) { .gradio-container { padding: 16px 0 24px; } #container { border-radius: 20px; padding: 16px 20px; } #chatbot { height: clamp(520px, calc(100dvh - 140px), 800px); border-radius: 18px; } #chatbot .message-wrap .bot .message { margin-right: 12px; margin-left: 4px; padding: 14px 18px; border-radius: 20px; } #chatbot .message-wrap .user .message { margin-left: 12px; margin-right: 4px; padding: 14px 18px; border-radius: 20px; } #logo img { max-height: 160px; } #intro-card { padding: 20px 24px; border-radius: 16px; } .gradio-container textarea { min-height: 100px !important; padding: 16px 20px !important; font-size: 0.95rem !important; border-radius: 16px !important; } #title { font-size: 1.2rem; margin: 16px 0 20px; } .suggestion-buttons button { padding: 14px 18px; border-radius: 14px; } } """ logger.info("Custom CSS initialized") with gr.Blocks(theme=theme, css=custom_css) as demo: with gr.Column(elem_id="container"): with gr.Row(elem_id="header"): with gr.Column(scale=2, min_width=140): gr.Image( value="assets/Logo WO Background.png", height=190, show_label=False, elem_id="logo", ) with gr.Column(scale=10): with gr.Group(elem_id="intro-card"): gr.Markdown( """ **Welcome — Chat with Daniel** - **What to ask**: projects, AI/RAG/agents, data pipelines, or career. - **Privacy**: if you share an email, I’ll only save it when you ask. - **Tip**: streaming is live; use Stop to interrupt and send a follow‑up. Example prompts: “Tell me about your last role”, “How do you design a RAG pipeline?”, “Can you scope a small automation?” """, ) gr.Markdown("## Chat with Daniel", elem_id="title") with gr.Column(elem_id="chat-wrapper"): chat_input = gr.Textbox( placeholder="Type your message here…", autofocus=True, max_lines=5, show_copy_button=True, container=False, scale=1 ) chat_iface = gr.ChatInterface( me.chat, type="messages", chatbot=chatbot, title="", description="Ask about projects, AI workflows, or get in touch.", submit_btn="Send", stop_btn="Stop", textbox=chat_input, ) gr.Markdown("**Need inspiration?** Try asking:", elem_classes="suggestion-banner") with gr.Row(elem_classes="suggestion-buttons"): examples = [ "Tell me about your last role and what you do day to day", "How would you design a small RAG pipeline for docs?", "What Python libraries are you familiar with?", ] for example in examples: gr.Button( example, variant="secondary", size="sm" ).click( lambda text=example: gr.update(value=text), outputs=chat_input, ) gr.Markdown("Made with ❤️ — CoDHe Labs", elem_id="footer") logger.info("Blocks app initialized") def main(): logger.info("Launching demo") demo.launch( server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)), favicon_path="assets/logo.png", debug=True, show_error=True, ) if __name__ == "__main__": if os.getenv("WATCH_MODE") == "1": run_with_watch() else: main() logger.info("Demo launched")