Claude Code Claude Opus 4.6 commited on
Commit
12c98ee
·
1 Parent(s): ec76dfd

Claude Code: Add UI/UX improvements to Cain's chat interface

Browse files

- Add conversation controls: Clear chat, Export TXT/JSON, Toggle details
- Add loading indicators with animated dots during chat processing
- Add session footer showing session ID and message count
- Add system health badge status indicator
- Enhanced CSS for chat bubble differentiation (user vs agent)
- Markdown rendering enabled in chat responses
- Session tracking variables and helper functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. .gitignore +1 -0
  2. app.py +302 -8
.gitignore CHANGED
@@ -10,3 +10,4 @@ node_modules/
10
  *.log
11
  .DS_Store
12
  frontend/index.html
 
 
10
  *.log
11
  .DS_Store
12
  frontend/index.html
13
+ __pycache__/
app.py CHANGED
@@ -314,6 +314,72 @@ class CircuitBreaker:
314
  # Global circuit breaker for httpcore operations
315
  httpcore_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30.0)
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  def track_async_task(task: asyncio.Task):
319
  """Track an async task for graceful shutdown."""
@@ -1969,6 +2035,148 @@ CUSTOM_CSS = """
1969
  0%, 80%, 100% { transform: scale(0); opacity: 0.5; }
1970
  40% { transform: scale(1); opacity: 1; }
1971
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1972
  """
1973
 
1974
  # ========== Main Application ==========
@@ -2042,11 +2250,20 @@ def create_agent_office():
2042
  with gr.Column(scale=2):
2043
  # ========== Chat Interface ==========
2044
  with gr.Group():
2045
- gr.Markdown("## 💬 Chat with Cain")
 
 
 
 
 
2046
 
2047
  chatbot = gr.Chatbot(
2048
  label="Conversation",
2049
- height=400
 
 
 
 
2050
  )
2051
 
2052
  with gr.Row():
@@ -2058,11 +2275,30 @@ def create_agent_office():
2058
  )
2059
  send_btn = gr.Button("Send", variant="primary", scale=1)
2060
 
 
 
 
 
 
 
2061
  chat_status = gr.Markdown(
2062
  value="Ready to chat",
2063
  label="Status"
2064
  )
2065
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2066
  gr.Markdown("""
2067
  **Instructions:**
2068
  - Type a message and click Send to chat with Cain
@@ -2070,6 +2306,11 @@ def create_agent_office():
2070
  - Responses are generated based on the conversation_process tool
2071
  """)
2072
 
 
 
 
 
 
2073
  # ========== Brain Information ==========
2074
  with gr.Accordion("🧠 Brain Information", open=False):
2075
  brain_info_display = gr.Markdown(
@@ -2282,19 +2523,72 @@ def create_agent_office():
2282
  )
2283
 
2284
  # Chat handlers
2285
- def handle_chat(message: str, history: List[Tuple[str, str]]) -> Tuple[List[Tuple[str, str]], str]:
2286
- return chat_with_brain(message, history)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2287
 
2288
  send_btn.click(
2289
- fn=handle_chat,
2290
  inputs=[chat_input, chatbot],
2291
- outputs=[chatbot, chat_status]
2292
  )
2293
 
2294
  chat_input.submit(
2295
- fn=handle_chat,
2296
  inputs=[chat_input, chatbot],
2297
- outputs=[chatbot, chat_status]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2298
  )
2299
 
2300
  # Refresh handlers
 
314
  # Global circuit breaker for httpcore operations
315
  httpcore_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30.0)
316
 
317
+ # ========== Session Tracking for Chat ==========
318
+ _session_id: str = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
319
+ _message_count: int = 0
320
+ _system_status_visible: bool = True
321
+
322
+
323
+ def get_session_info() -> tuple[str, str]:
324
+ """Get current session info (session_id, message_count)."""
325
+ global _message_count
326
+ return _session_id, str(_message_count)
327
+
328
+
329
+ def increment_message_count() -> int:
330
+ """Increment and return the message count."""
331
+ global _message_count
332
+ _message_count += 1
333
+ return _message_count
334
+
335
+
336
+ def reset_session():
337
+ """Reset session with a new ID and zero message count."""
338
+ global _session_id, _message_count
339
+ _session_id = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
340
+ _message_count = 0
341
+ return _session_id, str(_message_count), [], "Session reset. Ready for new conversation."
342
+
343
+
344
+ def toggle_system_status() -> tuple[bool, str]:
345
+ """Toggle system status visibility."""
346
+ global _system_status_visible
347
+ _system_status_visible = not _system_status_visible
348
+ status_text = "✅ System details shown" if _system_status_visible else "🔒 System details hidden"
349
+ return _system_status_visible, status_text
350
+
351
+
352
+ def export_chat(history: list, format_type: str = "txt") -> str:
353
+ """Export chat history to a file."""
354
+ if not history:
355
+ return "No conversation to export."
356
+
357
+ import tempfile
358
+ timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
359
+
360
+ if format_type == "json":
361
+ filepath = tempfile.gettempdir() / f"cain_chat_{timestamp}.json"
362
+ export_data = {
363
+ "session_id": _session_id,
364
+ "timestamp": datetime.utcnow().isoformat(),
365
+ "messages": [{"role": "user" if i % 2 == 0 else "assistant", "content": msg}
366
+ for pair in history for i, msg in enumerate(pair)]
367
+ }
368
+ with open(filepath, "w") as f:
369
+ json.dump(export_data, f, indent=2)
370
+ else: # txt
371
+ filepath = tempfile.gettempdir() / f"cain_chat_{timestamp}.txt"
372
+ with open(filepath, "w") as f:
373
+ f.write(f"Cain Chat Export - Session: {_session_id}\n")
374
+ f.write(f"Exported: {datetime.utcnow().isoformat()}\n")
375
+ f.write("=" * 50 + "\n\n")
376
+ for user_msg, bot_msg in history:
377
+ f.write(f"User: {user_msg}\n\n")
378
+ f.write(f"Cain: {bot_msg}\n\n")
379
+ f.write("-" * 30 + "\n\n")
380
+
381
+ return f"✅ Exported to {filepath.name}"
382
+
383
 
384
  def track_async_task(task: asyncio.Task):
385
  """Track an async task for graceful shutdown."""
 
2035
  0%, 80%, 100% { transform: scale(0); opacity: 0.5; }
2036
  40% { transform: scale(1); opacity: 1; }
2037
  }
2038
+
2039
+ /* ========== Enhanced Chat Interface Styles ========== */
2040
+ .chatbot-container {
2041
+ background: var(--bg-secondary) !important;
2042
+ border-radius: 16px !important;
2043
+ border: 1px solid var(--border-color) !important;
2044
+ }
2045
+
2046
+ /* Chat message bubbles - user vs agent differentiation */
2047
+ .chatbot-message.user {
2048
+ background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue)) !important;
2049
+ color: white !important;
2050
+ border-radius: 18px 18px 4px 18px !important;
2051
+ padding: 14px 20px !important;
2052
+ margin: 8px 0 8px auto !important;
2053
+ max-width: 75% !important;
2054
+ box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3) !important;
2055
+ }
2056
+
2057
+ .chatbot-message.bot {
2058
+ background: var(--bg-tertiary) !important;
2059
+ border: 1px solid var(--border-color) !important;
2060
+ color: var(--text-primary) !important;
2061
+ border-radius: 18px 18px 18px 4px !important;
2062
+ padding: 14px 20px !important;
2063
+ margin: 8px auto 8px 0 !important;
2064
+ max-width: 80% !important;
2065
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2) !important;
2066
+ }
2067
+
2068
+ /* Markdown styling within chat bubbles */
2069
+ .chatbot-message.bot .markdown {
2070
+ color: var(--text-primary) !important;
2071
+ }
2072
+
2073
+ .chatbot-message.bot .markdown h1,
2074
+ .chatbot-message.bot .markdown h2,
2075
+ .chatbot-message.bot .markdown h3 {
2076
+ color: var(--text-primary) !important;
2077
+ margin-top: 12px !important;
2078
+ }
2079
+
2080
+ .chatbot-message.bot .markdown code {
2081
+ background: rgba(59, 130, 246, 0.2) !important;
2082
+ color: var(--accent-blue) !important;
2083
+ padding: 3px 8px !important;
2084
+ border-radius: 6px !important;
2085
+ font-family: 'Fira Code', 'Consolas', monospace !important;
2086
+ }
2087
+
2088
+ .chatbot-message.bot .markdown pre {
2089
+ background: rgba(0, 0, 0, 0.3) !important;
2090
+ border-radius: 8px !important;
2091
+ padding: 16px !important;
2092
+ overflow-x: auto !important;
2093
+ border: 1px solid var(--border-color) !important;
2094
+ }
2095
+
2096
+ .chatbot-message.bot .markdown pre code {
2097
+ background: transparent !important;
2098
+ padding: 0 !important;
2099
+ }
2100
+
2101
+ /* ========== Session Footer ========== */
2102
+ .session-footer {
2103
+ background: linear-gradient(135deg, var(--bg-tertiary), var(--bg-secondary)) !important;
2104
+ border: 1px solid var(--border-color) !important;
2105
+ border-radius: 10px !important;
2106
+ padding: 12px 20px !important;
2107
+ margin-top: 10px !important;
2108
+ text-align: center !important;
2109
+ color: var(--text-secondary) !important;
2110
+ font-size: 0.9em !important;
2111
+ font-family: 'Segoe UI', system-ui, sans-serif !important;
2112
+ display: flex !important;
2113
+ justify-content: center !important;
2114
+ align-items: center !important;
2115
+ gap: 20px !important;
2116
+ }
2117
+
2118
+ .session-footer span {
2119
+ display: inline-flex !important;
2120
+ align-items: center !important;
2121
+ gap: 6px !important;
2122
+ }
2123
+
2124
+ /* ========== Enhanced Button Styles ========== */
2125
+ .gradio-button.secondary {
2126
+ background: var(--bg-tertiary) !important;
2127
+ border: 1px solid var(--border-color) !important;
2128
+ color: var(--text-secondary) !important;
2129
+ }
2130
+
2131
+ .gradio-button.secondary:hover {
2132
+ background: var(--bg-secondary) !important;
2133
+ border-color: var(--accent-purple) !important;
2134
+ color: var(--text-primary) !important;
2135
+ }
2136
+
2137
+ /* ========== Loading Animation Enhancement ========== */
2138
+ .streaming-indicator {
2139
+ display: flex !important;
2140
+ justify-content: center !important;
2141
+ align-items: center !important;
2142
+ gap: 6px !important;
2143
+ padding: 16px !important;
2144
+ background: var(--bg-tertiary) !important;
2145
+ border-radius: 12px !important;
2146
+ margin: 8px 0 !important;
2147
+ }
2148
+
2149
+ .streaming-dot {
2150
+ width: 10px !important;
2151
+ height: 10px !important;
2152
+ background: var(--accent-purple) !important;
2153
+ border-radius: 50% !important;
2154
+ animation: streamingBounce 1.4s infinite ease-in-out !important;
2155
+ box-shadow: 0 0 10px rgba(124, 58, 237, 0.6) !important;
2156
+ }
2157
+
2158
+ /* ========== Health Badge Styles ========== */
2159
+ .status-indicator {
2160
+ display: inline-flex !important;
2161
+ align-items: center !important;
2162
+ gap: 8px !important;
2163
+ padding: 8px 16px !important;
2164
+ border-radius: 24px !important;
2165
+ font-weight: 600 !important;
2166
+ font-size: 0.9em !important;
2167
+ }
2168
+
2169
+ .status-indicator.online {
2170
+ background: rgba(16, 185, 129, 0.15) !important;
2171
+ color: var(--accent-green) !important;
2172
+ border: 1px solid rgba(16, 185, 129, 0.4) !important;
2173
+ }
2174
+
2175
+ .status-indicator.offline {
2176
+ background: rgba(239, 68, 68, 0.15) !important;
2177
+ color: var(--accent-red) !important;
2178
+ border: 1px solid rgba(239, 68, 68, 0.4) !important;
2179
+ }
2180
  """
2181
 
2182
  # ========== Main Application ==========
 
2250
  with gr.Column(scale=2):
2251
  # ========== Chat Interface ==========
2252
  with gr.Group():
2253
+ # Chat header with status badge
2254
+ with gr.Row():
2255
+ gr.Markdown("## 💬 Chat with Cain")
2256
+ system_healthy_badge = gr.HTML(
2257
+ value='<span class="status-indicator online"><span class="status-dot online"></span>System Healthy</span>'
2258
+ )
2259
 
2260
  chatbot = gr.Chatbot(
2261
  label="Conversation",
2262
+ height=400,
2263
+ show_label=True,
2264
+ elem_classes=["chatbot-container"],
2265
+ render_markdown=True,
2266
+ bubble_full_width=False
2267
  )
2268
 
2269
  with gr.Row():
 
2275
  )
2276
  send_btn = gr.Button("Send", variant="primary", scale=1)
2277
 
2278
+ # Loading indicator (hidden by default)
2279
+ loading_indicator = gr.HTML(
2280
+ value="",
2281
+ visible=False
2282
+ )
2283
+
2284
  chat_status = gr.Markdown(
2285
  value="Ready to chat",
2286
  label="Status"
2287
  )
2288
 
2289
+ # Conversation Controls
2290
+ with gr.Row():
2291
+ clear_chat_btn = gr.Button("🗑️ Clear", size="sm", variant="stop")
2292
+ export_txt_btn = gr.Button("📄 Export TXT", size="sm")
2293
+ export_json_btn = gr.Button("📋 Export JSON", size="sm")
2294
+ toggle_status_btn = gr.Button("🔍 Toggle Details", size="sm")
2295
+
2296
+ export_status = gr.Markdown(
2297
+ value="",
2298
+ label="Export Status",
2299
+ visible=False
2300
+ )
2301
+
2302
  gr.Markdown("""
2303
  **Instructions:**
2304
  - Type a message and click Send to chat with Cain
 
2306
  - Responses are generated based on the conversation_process tool
2307
  """)
2308
 
2309
+ # Session Info Footer
2310
+ session_info_footer = gr.HTML(
2311
+ value=f'<div class="session-footer">Session: {_session_id} | Messages: 0</div>'
2312
+ )
2313
+
2314
  # ========== Brain Information ==========
2315
  with gr.Accordion("🧠 Brain Information", open=False):
2316
  brain_info_display = gr.Markdown(
 
2523
  )
2524
 
2525
  # Chat handlers
2526
+ def handle_chat(message: str, history: List[Tuple[str, str]]) -> Tuple[List[Tuple[str, str]], str, str, str]:
2527
+ """Handle chat with loading indicator and session tracking."""
2528
+ increment_message_count()
2529
+ session_id, msg_count = get_session_info()
2530
+ history, status = chat_with_brain(message, history)
2531
+ session_html = f'<div class="session-footer">Session: {session_id} | Messages: {msg_count}</div>'
2532
+ return history, status, "", session_html
2533
+
2534
+ def show_loading():
2535
+ """Show loading indicator."""
2536
+ return '<div class="streaming-indicator"><div class="streaming-dot"></div><div class="streaming-dot"></div><div class="streaming-dot"></div></div>'
2537
+
2538
+ # Send button with loading indicator
2539
+ def send_with_loading(message: str, history: List[Tuple[str, str]]) -> Tuple:
2540
+ loading_html = show_loading()
2541
+ return handle_chat(message, history) + (loading_html,)
2542
 
2543
  send_btn.click(
2544
+ fn=send_with_loading,
2545
  inputs=[chat_input, chatbot],
2546
+ outputs=[chatbot, chat_status, loading_indicator, session_info_footer]
2547
  )
2548
 
2549
  chat_input.submit(
2550
+ fn=send_with_loading,
2551
  inputs=[chat_input, chatbot],
2552
+ outputs=[chatbot, chat_status, loading_indicator, session_info_footer]
2553
+ )
2554
+
2555
+ # Clear chat button
2556
+ clear_chat_btn.click(
2557
+ fn=reset_session,
2558
+ inputs=[],
2559
+ outputs=[session_info_footer, chat_status, chatbot, export_status]
2560
+ )
2561
+
2562
+ # Export buttons
2563
+ def export_txt(history):
2564
+ result = export_chat(history, "txt")
2565
+ return result, result
2566
+
2567
+ def export_json(history):
2568
+ result = export_chat(history, "json")
2569
+ return result, result
2570
+
2571
+ export_txt_btn.click(
2572
+ fn=export_txt,
2573
+ inputs=[chatbot],
2574
+ outputs=[export_status, chat_status]
2575
+ )
2576
+
2577
+ export_json_btn.click(
2578
+ fn=export_json,
2579
+ inputs=[chatbot],
2580
+ outputs=[export_status, chat_status]
2581
+ )
2582
+
2583
+ # Toggle status details
2584
+ def update_status_visibility():
2585
+ visible, status_text = toggle_system_status()
2586
+ return status_text
2587
+
2588
+ toggle_status_btn.click(
2589
+ fn=update_status_visibility,
2590
+ inputs=[],
2591
+ outputs=[chat_status]
2592
  )
2593
 
2594
  # Refresh handlers