Spaces:
Sleeping
Sleeping
Deminiko
commited on
Commit
·
4cfa289
1
Parent(s):
227f42e
feat: Add modular UI components for Gradio 6.0 compatibility
Browse files- Create ui/ module with reusable components
- ui/styles.py: Custom CSS for consistent styling
- ui/chat_components.py: Chat tab with NAKED mode
- ui/mcp_health.py: MCP endpoint health monitoring
- ui/quick_build.py: Template circuit generation
- Update app.py to import UI styles with fallback
- app.py +15 -3
- ui/__init__.py +49 -0
- ui/chat_components.py +182 -0
- ui/mcp_health.py +183 -0
- ui/quick_build.py +112 -0
- ui/styles.py +163 -0
app.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
QAgents-Workflows: Hugging Face Space Entry Point
|
| 3 |
Path: QAgents-workflows/app.py
|
| 4 |
-
Related:
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
Provides a Gradio interface with:
|
| 7 |
- Chat UI for interacting with quantum circuit agents (NAKED mode)
|
| 8 |
- MCP Endpoints health monitoring tab
|
| 9 |
- Circuit generation and validation tools
|
|
@@ -18,6 +20,16 @@ import requests
|
|
| 18 |
import time
|
| 19 |
from typing import Optional, List, Dict, Any
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
# Configure logging
|
| 22 |
logging.basicConfig(level=logging.INFO)
|
| 23 |
logger = logging.getLogger(__name__)
|
|
@@ -214,7 +226,7 @@ def quick_build_circuit(template: str, num_qubits: int) -> str:
|
|
| 214 |
# GRADIO INTERFACE
|
| 215 |
# =============================================================================
|
| 216 |
|
| 217 |
-
with gr.Blocks(title="QAgents - Quantum Circuit Assistant") as demo:
|
| 218 |
|
| 219 |
# Header
|
| 220 |
gr.Markdown("""
|
|
|
|
| 1 |
"""
|
| 2 |
QAgents-Workflows: Hugging Face Space Entry Point
|
| 3 |
Path: QAgents-workflows/app.py
|
| 4 |
+
Related: ui/ module for Gradio components
|
| 5 |
+
client/mcp_client.py (MCP connection)
|
| 6 |
+
orchestrators/ (agent orchestration)
|
| 7 |
|
| 8 |
+
Provides a Gradio 6.0 compatible interface with:
|
| 9 |
- Chat UI for interacting with quantum circuit agents (NAKED mode)
|
| 10 |
- MCP Endpoints health monitoring tab
|
| 11 |
- Circuit generation and validation tools
|
|
|
|
| 20 |
import time
|
| 21 |
from typing import Optional, List, Dict, Any
|
| 22 |
|
| 23 |
+
# =============================================================================
|
| 24 |
+
# TRY TO IMPORT UI MODULE (provides modular components)
|
| 25 |
+
# =============================================================================
|
| 26 |
+
try:
|
| 27 |
+
from ui.styles import CUSTOM_CSS
|
| 28 |
+
UI_STYLES_AVAILABLE = True
|
| 29 |
+
except ImportError:
|
| 30 |
+
CUSTOM_CSS = ""
|
| 31 |
+
UI_STYLES_AVAILABLE = False
|
| 32 |
+
|
| 33 |
# Configure logging
|
| 34 |
logging.basicConfig(level=logging.INFO)
|
| 35 |
logger = logging.getLogger(__name__)
|
|
|
|
| 226 |
# GRADIO INTERFACE
|
| 227 |
# =============================================================================
|
| 228 |
|
| 229 |
+
with gr.Blocks(title="QAgents - Quantum Circuit Assistant", css=CUSTOM_CSS if UI_STYLES_AVAILABLE else None) as demo:
|
| 230 |
|
| 231 |
# Header
|
| 232 |
gr.Markdown("""
|
ui/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Path: QAgents-workflows/ui/__init__.py
|
| 2 |
+
# Relations: Used by app.py, imports from chat_components.py, mcp_health.py, quick_build.py
|
| 3 |
+
# Description: UI module initialization - exports all Gradio components for the app
|
| 4 |
+
"""
|
| 5 |
+
UI Module: Gradio 6.0 compatible UI components for QAgents-Workflows.
|
| 6 |
+
|
| 7 |
+
This module provides modular UI components that can be assembled
|
| 8 |
+
into the main Gradio app. Each component is designed to work
|
| 9 |
+
with the agent orchestration system.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from .chat_components import (
|
| 13 |
+
create_chat_tab,
|
| 14 |
+
chat_response,
|
| 15 |
+
generate_circuit_with_naked,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
from .mcp_health import (
|
| 19 |
+
create_mcp_health_tab,
|
| 20 |
+
check_mcp_health,
|
| 21 |
+
check_endpoint_health,
|
| 22 |
+
get_all_endpoints_health,
|
| 23 |
+
MCP_ENDPOINTS,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
from .quick_build import (
|
| 27 |
+
create_quick_build_tab,
|
| 28 |
+
quick_build_circuit,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
from .styles import CUSTOM_CSS
|
| 32 |
+
|
| 33 |
+
__all__ = [
|
| 34 |
+
# Chat
|
| 35 |
+
"create_chat_tab",
|
| 36 |
+
"chat_response",
|
| 37 |
+
"generate_circuit_with_naked",
|
| 38 |
+
# MCP Health
|
| 39 |
+
"create_mcp_health_tab",
|
| 40 |
+
"check_mcp_health",
|
| 41 |
+
"check_endpoint_health",
|
| 42 |
+
"get_all_endpoints_health",
|
| 43 |
+
"MCP_ENDPOINTS",
|
| 44 |
+
# Quick Build
|
| 45 |
+
"create_quick_build_tab",
|
| 46 |
+
"quick_build_circuit",
|
| 47 |
+
# Styles
|
| 48 |
+
"CUSTOM_CSS",
|
| 49 |
+
]
|
ui/chat_components.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Path: QAgents-workflows/ui/chat_components.py
|
| 2 |
+
# Relations: Uses orchestrators/orchestrator.py (NakedOrchestrator)
|
| 3 |
+
# Used by __init__.py, app.py
|
| 4 |
+
# Description: Chat UI components for interacting with quantum circuit agents
|
| 5 |
+
"""
|
| 6 |
+
Chat Components: Gradio 6.0 compatible chat interface for QAgents.
|
| 7 |
+
|
| 8 |
+
Provides:
|
| 9 |
+
- Chat tab creation
|
| 10 |
+
- Message handling with NAKED mode orchestrator
|
| 11 |
+
- Help and status commands
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import logging
|
| 16 |
+
from typing import List, Dict, Any, Tuple
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def generate_circuit_with_naked(prompt: str) -> str:
|
| 22 |
+
"""
|
| 23 |
+
Generate a quantum circuit using NAKED mode (direct LLM call).
|
| 24 |
+
This is the simplest and fastest mode.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
prompt: User's natural language request
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Formatted response with circuit or error message
|
| 31 |
+
"""
|
| 32 |
+
try:
|
| 33 |
+
# Lazy import to avoid startup issues
|
| 34 |
+
from orchestrators import create_orchestrator
|
| 35 |
+
|
| 36 |
+
orch = create_orchestrator("naked")
|
| 37 |
+
result = orch.run(prompt)
|
| 38 |
+
|
| 39 |
+
if result.success:
|
| 40 |
+
output = f"✅ **Success** ({result.execution_time_ms:.0f}ms)\n\n"
|
| 41 |
+
if result.final_output:
|
| 42 |
+
if 'OPENQASM' in str(result.final_output) or 'qreg' in str(result.final_output):
|
| 43 |
+
output += f"```qasm\n{result.final_output}\n```"
|
| 44 |
+
else:
|
| 45 |
+
output += str(result.final_output)
|
| 46 |
+
return output
|
| 47 |
+
else:
|
| 48 |
+
error_msg = "\n".join(result.errors) if result.errors else "Unknown error"
|
| 49 |
+
return f"❌ **Failed** ({result.execution_time_ms:.0f}ms)\n\n{error_msg}"
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.error(f"NAKED mode error: {e}")
|
| 52 |
+
return f"❌ **Error**: {str(e)}"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_help_text() -> str:
|
| 56 |
+
"""Return help text for the chat interface."""
|
| 57 |
+
return """## 🤖 QAgents Help
|
| 58 |
+
|
| 59 |
+
I can help you with quantum circuits! Try asking me to:
|
| 60 |
+
|
| 61 |
+
**Create Circuits:**
|
| 62 |
+
- "Create a Bell state"
|
| 63 |
+
- "Generate a 3-qubit GHZ state"
|
| 64 |
+
- "Make a QFT circuit for 4 qubits"
|
| 65 |
+
- "Build a simple superposition"
|
| 66 |
+
|
| 67 |
+
**Examples:**
|
| 68 |
+
- "Create a circuit that puts 2 qubits in superposition"
|
| 69 |
+
- "Generate a CNOT gate between qubit 0 and 1"
|
| 70 |
+
- "Build a quantum teleportation circuit"
|
| 71 |
+
|
| 72 |
+
💡 **Tip:** Be specific about the number of qubits and desired operations!
|
| 73 |
+
|
| 74 |
+
**Commands:**
|
| 75 |
+
- `help` - Show this help message
|
| 76 |
+
- `status` - Check system status"""
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_status_text(mcp_server_url: str) -> str:
|
| 80 |
+
"""Return status text for the chat interface."""
|
| 81 |
+
try:
|
| 82 |
+
from ui.mcp_health import check_mcp_health
|
| 83 |
+
health = check_mcp_health()
|
| 84 |
+
return f"## 📊 System Status\n\n{health}\n\n**MCP Server:** `{mcp_server_url}`"
|
| 85 |
+
except Exception as e:
|
| 86 |
+
return f"## 📊 System Status\n\n🔴 **Error checking status**: {str(e)}"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def chat_response(message: str, history: List, mcp_server_url: str = "") -> str:
|
| 90 |
+
"""
|
| 91 |
+
Handle chat messages and generate responses.
|
| 92 |
+
Uses NAKED mode for circuit generation.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
message: User's message
|
| 96 |
+
history: Chat history
|
| 97 |
+
mcp_server_url: URL of the MCP server
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
Bot's response message
|
| 101 |
+
"""
|
| 102 |
+
if not message.strip():
|
| 103 |
+
return ""
|
| 104 |
+
|
| 105 |
+
message_lower = message.lower().strip()
|
| 106 |
+
|
| 107 |
+
# Help command
|
| 108 |
+
if message_lower in ['help', '/help', '?']:
|
| 109 |
+
return get_help_text()
|
| 110 |
+
|
| 111 |
+
# Status command
|
| 112 |
+
if message_lower in ['status', '/status']:
|
| 113 |
+
return get_status_text(mcp_server_url)
|
| 114 |
+
|
| 115 |
+
# Generate circuit
|
| 116 |
+
logger.info(f"Generating circuit for: {message}")
|
| 117 |
+
return generate_circuit_with_naked(message)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def create_chat_tab(mcp_server_url: str = "") -> Tuple[gr.Chatbot, gr.Textbox, gr.Button]:
|
| 121 |
+
"""
|
| 122 |
+
Create the chat tab components for Gradio 6.0.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
mcp_server_url: URL of the MCP server for status checks
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Tuple of (chatbot, textbox, send_button) components
|
| 129 |
+
"""
|
| 130 |
+
gr.Markdown("### Chat with Quantum Circuit Agent")
|
| 131 |
+
gr.Markdown("Ask me to create quantum circuits! Try: *'Create a Bell state'* or *'Generate a 3-qubit GHZ state'*")
|
| 132 |
+
|
| 133 |
+
chatbot = gr.Chatbot(
|
| 134 |
+
value=[],
|
| 135 |
+
height=400,
|
| 136 |
+
label="Quantum Circuit Agent"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
with gr.Row():
|
| 140 |
+
msg_input = gr.Textbox(
|
| 141 |
+
placeholder="Ask me to create a quantum circuit...",
|
| 142 |
+
label="Your Message",
|
| 143 |
+
scale=4,
|
| 144 |
+
lines=1
|
| 145 |
+
)
|
| 146 |
+
send_btn = gr.Button("Send 🚀", variant="primary", scale=1)
|
| 147 |
+
|
| 148 |
+
with gr.Row():
|
| 149 |
+
clear_btn = gr.Button("🗑️ Clear", size="sm")
|
| 150 |
+
help_btn = gr.Button("❓ Help", size="sm")
|
| 151 |
+
status_btn = gr.Button("📊 Status", size="sm")
|
| 152 |
+
|
| 153 |
+
# Chat handlers
|
| 154 |
+
def respond(message: str, chat_history: List):
|
| 155 |
+
if not message.strip():
|
| 156 |
+
return "", chat_history
|
| 157 |
+
|
| 158 |
+
bot_response = chat_response(message, chat_history, mcp_server_url)
|
| 159 |
+
chat_history.append({"role": "user", "content": message})
|
| 160 |
+
chat_history.append({"role": "assistant", "content": bot_response})
|
| 161 |
+
return "", chat_history
|
| 162 |
+
|
| 163 |
+
def show_help(chat_history: List):
|
| 164 |
+
help_text = get_help_text()
|
| 165 |
+
chat_history.append({"role": "user", "content": "help"})
|
| 166 |
+
chat_history.append({"role": "assistant", "content": help_text})
|
| 167 |
+
return chat_history
|
| 168 |
+
|
| 169 |
+
def show_status(chat_history: List):
|
| 170 |
+
status_text = get_status_text(mcp_server_url)
|
| 171 |
+
chat_history.append({"role": "user", "content": "status"})
|
| 172 |
+
chat_history.append({"role": "assistant", "content": status_text})
|
| 173 |
+
return chat_history
|
| 174 |
+
|
| 175 |
+
# Wire up events
|
| 176 |
+
send_btn.click(respond, [msg_input, chatbot], [msg_input, chatbot])
|
| 177 |
+
msg_input.submit(respond, [msg_input, chatbot], [msg_input, chatbot])
|
| 178 |
+
clear_btn.click(lambda: [], outputs=[chatbot])
|
| 179 |
+
help_btn.click(show_help, [chatbot], [chatbot])
|
| 180 |
+
status_btn.click(show_status, [chatbot], [chatbot])
|
| 181 |
+
|
| 182 |
+
return chatbot, msg_input, send_btn
|
ui/mcp_health.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Path: QAgents-workflows/ui/mcp_health.py
|
| 2 |
+
# Relations: Uses client/mcp_client.py for health checks
|
| 3 |
+
# Used by __init__.py, app.py, chat_components.py
|
| 4 |
+
# Description: MCP health monitoring UI components
|
| 5 |
+
"""
|
| 6 |
+
MCP Health Components: Monitor QuantumArchitect-MCP endpoint availability.
|
| 7 |
+
|
| 8 |
+
Provides:
|
| 9 |
+
- Server health check
|
| 10 |
+
- Individual endpoint health checks
|
| 11 |
+
- Health status table display
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import requests
|
| 17 |
+
import time
|
| 18 |
+
import logging
|
| 19 |
+
from typing import Dict, List
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
# Default MCP Server URL
|
| 24 |
+
MCP_SERVER_URL = os.environ.get(
|
| 25 |
+
"MCP_SERVER_URL",
|
| 26 |
+
"https://mcp-1st-birthday-quantumarchitect-mcp.hf.space"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# MCP Endpoints definitions
|
| 30 |
+
MCP_ENDPOINTS = [
|
| 31 |
+
{"name": "create_circuit", "category": "Creation", "description": "Create circuit from template"},
|
| 32 |
+
{"name": "parse_qasm", "category": "Creation", "description": "Parse OpenQASM code"},
|
| 33 |
+
{"name": "build_circuit", "category": "Creation", "description": "Build custom circuit from gates"},
|
| 34 |
+
{"name": "validate_circuit", "category": "Validation", "description": "Full circuit validation"},
|
| 35 |
+
{"name": "check_hardware", "category": "Validation", "description": "Hardware compatibility check"},
|
| 36 |
+
{"name": "simulate", "category": "Simulation", "description": "Simulate with measurements"},
|
| 37 |
+
{"name": "get_statevector", "category": "Simulation", "description": "Extract statevector"},
|
| 38 |
+
{"name": "estimate_fidelity", "category": "Simulation", "description": "Hardware fidelity estimation"},
|
| 39 |
+
{"name": "score_circuit", "category": "Scoring", "description": "Circuit scoring metrics"},
|
| 40 |
+
{"name": "compare_circuits", "category": "Scoring", "description": "Compare multiple circuits"},
|
| 41 |
+
{"name": "get_gate_info", "category": "Documentation", "description": "Gate documentation"},
|
| 42 |
+
{"name": "get_algorithm_info", "category": "Documentation", "description": "Algorithm documentation"},
|
| 43 |
+
{"name": "list_hardware", "category": "Documentation", "description": "Available hardware profiles"},
|
| 44 |
+
{"name": "list_templates", "category": "Documentation", "description": "Available circuit templates"},
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def check_mcp_health(server_url: str = None) -> str:
|
| 49 |
+
"""
|
| 50 |
+
Check overall MCP server health.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
server_url: MCP server URL (uses default if not provided)
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
Status string with emoji indicator
|
| 57 |
+
"""
|
| 58 |
+
url = server_url or MCP_SERVER_URL
|
| 59 |
+
try:
|
| 60 |
+
response = requests.get(f"{url}/", timeout=10)
|
| 61 |
+
if response.status_code == 200:
|
| 62 |
+
return f"🟢 **Connected** to MCP Server"
|
| 63 |
+
else:
|
| 64 |
+
return f"🟡 **Partial** - Status {response.status_code}"
|
| 65 |
+
except requests.exceptions.Timeout:
|
| 66 |
+
return "🟠 **Timeout** - Server slow to respond"
|
| 67 |
+
except requests.exceptions.ConnectionError:
|
| 68 |
+
return "🔴 **Disconnected** - Cannot reach server"
|
| 69 |
+
except Exception as e:
|
| 70 |
+
return f"🔴 **Error**: {str(e)[:50]}"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def check_endpoint_health(endpoint_name: str, server_url: str = None) -> Dict:
|
| 74 |
+
"""
|
| 75 |
+
Check health of a specific MCP endpoint.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
endpoint_name: Name of the endpoint to check
|
| 79 |
+
server_url: MCP server URL (uses default if not provided)
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
Dict with status, latency_ms, and error fields
|
| 83 |
+
"""
|
| 84 |
+
url = server_url or MCP_SERVER_URL
|
| 85 |
+
start = time.perf_counter()
|
| 86 |
+
try:
|
| 87 |
+
endpoint_url = f"{url}/gradio_api/call/ui_{endpoint_name}"
|
| 88 |
+
response = requests.post(endpoint_url, json={"data": []}, timeout=15)
|
| 89 |
+
elapsed = (time.perf_counter() - start) * 1000
|
| 90 |
+
|
| 91 |
+
if response.status_code == 200:
|
| 92 |
+
return {"status": "🟢", "latency_ms": round(elapsed, 1), "error": None}
|
| 93 |
+
elif response.status_code == 404:
|
| 94 |
+
return {"status": "🟡", "latency_ms": round(elapsed, 1), "error": "Not found"}
|
| 95 |
+
else:
|
| 96 |
+
return {"status": "🟠", "latency_ms": round(elapsed, 1), "error": f"HTTP {response.status_code}"}
|
| 97 |
+
except requests.exceptions.Timeout:
|
| 98 |
+
return {"status": "🟠", "latency_ms": 15000, "error": "Timeout"}
|
| 99 |
+
except Exception as e:
|
| 100 |
+
elapsed = (time.perf_counter() - start) * 1000
|
| 101 |
+
return {"status": "🔴", "latency_ms": round(elapsed, 1), "error": str(e)[:50]}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_all_endpoints_health(server_url: str = None) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Get health status of all MCP endpoints as formatted markdown.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
server_url: MCP server URL (uses default if not provided)
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Markdown formatted table with endpoint health status
|
| 113 |
+
"""
|
| 114 |
+
url = server_url or MCP_SERVER_URL
|
| 115 |
+
output_lines = [
|
| 116 |
+
"## 🔗 MCP Endpoints Health Check",
|
| 117 |
+
f"**Server:** `{url}`\n",
|
| 118 |
+
"| Endpoint | Category | Status | Latency | Error |",
|
| 119 |
+
"|----------|----------|--------|---------|-------|"
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
for endpoint in MCP_ENDPOINTS:
|
| 123 |
+
health = check_endpoint_health(endpoint["name"], url)
|
| 124 |
+
error_str = health["error"] or "-"
|
| 125 |
+
output_lines.append(
|
| 126 |
+
f"| `{endpoint['name']}` | {endpoint['category']} | {health['status']} | {health['latency_ms']}ms | {error_str} |"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
output_lines.append(f"\n**Last checked:** {time.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
| 130 |
+
return "\n".join(output_lines)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def create_mcp_health_tab(server_url: str = None) -> gr.Markdown:
|
| 134 |
+
"""
|
| 135 |
+
Create the MCP Health monitoring tab components.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
server_url: MCP server URL (uses default if not provided)
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
Health display Markdown component
|
| 142 |
+
"""
|
| 143 |
+
url = server_url or MCP_SERVER_URL
|
| 144 |
+
|
| 145 |
+
gr.Markdown("""
|
| 146 |
+
## 🔗 MCP Endpoints Health Monitor
|
| 147 |
+
|
| 148 |
+
Monitor the health and availability of QuantumArchitect-MCP endpoints.
|
| 149 |
+
""")
|
| 150 |
+
|
| 151 |
+
with gr.Row():
|
| 152 |
+
check_all_btn = gr.Button("🔄 Check All Endpoints", variant="primary")
|
| 153 |
+
|
| 154 |
+
health_display = gr.Markdown(value="Click 'Check All Endpoints' to start health check...")
|
| 155 |
+
|
| 156 |
+
gr.Markdown("---")
|
| 157 |
+
gr.Markdown("### 🔍 Check Single Endpoint")
|
| 158 |
+
|
| 159 |
+
with gr.Row():
|
| 160 |
+
endpoint_dropdown = gr.Dropdown(
|
| 161 |
+
choices=[ep["name"] for ep in MCP_ENDPOINTS],
|
| 162 |
+
label="Select Endpoint",
|
| 163 |
+
value=None,
|
| 164 |
+
scale=3
|
| 165 |
+
)
|
| 166 |
+
check_single_btn = gr.Button("Check", scale=1)
|
| 167 |
+
|
| 168 |
+
single_result = gr.Markdown(value="")
|
| 169 |
+
|
| 170 |
+
# Event handlers
|
| 171 |
+
def check_all_handler():
|
| 172 |
+
return get_all_endpoints_health(url)
|
| 173 |
+
|
| 174 |
+
def check_single_handler(endpoint_name: str) -> str:
|
| 175 |
+
if not endpoint_name:
|
| 176 |
+
return "Please select an endpoint."
|
| 177 |
+
health = check_endpoint_health(endpoint_name, url)
|
| 178 |
+
return f"**{endpoint_name}**: {health['status']} ({health['latency_ms']}ms) - {health['error'] or 'OK'}"
|
| 179 |
+
|
| 180 |
+
check_all_btn.click(check_all_handler, outputs=[health_display])
|
| 181 |
+
check_single_btn.click(check_single_handler, [endpoint_dropdown], [single_result])
|
| 182 |
+
|
| 183 |
+
return health_display
|
ui/quick_build.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Path: QAgents-workflows/ui/quick_build.py
|
| 2 |
+
# Relations: Uses client/mcp_client.py for circuit generation
|
| 3 |
+
# Used by __init__.py, app.py
|
| 4 |
+
# Description: Quick circuit builder UI components
|
| 5 |
+
"""
|
| 6 |
+
Quick Build Components: Fast circuit generation from templates.
|
| 7 |
+
|
| 8 |
+
Provides:
|
| 9 |
+
- Template selection dropdown
|
| 10 |
+
- Qubit count slider
|
| 11 |
+
- Circuit generation via MCP client
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import logging
|
| 17 |
+
from typing import Optional
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
# Default MCP Server URL
|
| 22 |
+
MCP_SERVER_URL = os.environ.get(
|
| 23 |
+
"MCP_SERVER_URL",
|
| 24 |
+
"https://mcp-1st-birthday-quantumarchitect-mcp.hf.space"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Available templates
|
| 28 |
+
CIRCUIT_TEMPLATES = [
|
| 29 |
+
"bell_state",
|
| 30 |
+
"ghz_state",
|
| 31 |
+
"qft",
|
| 32 |
+
"grover",
|
| 33 |
+
"superposition",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def quick_build_circuit(template: str, num_qubits: int, server_url: Optional[str] = None) -> str:
|
| 38 |
+
"""
|
| 39 |
+
Generate a circuit from template using MCP client.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
template: Template name (bell_state, ghz_state, etc.)
|
| 43 |
+
num_qubits: Number of qubits
|
| 44 |
+
server_url: MCP server URL (uses default if not provided)
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
QASM code or error message
|
| 48 |
+
"""
|
| 49 |
+
url = server_url or MCP_SERVER_URL
|
| 50 |
+
try:
|
| 51 |
+
from client.mcp_client import get_client
|
| 52 |
+
mcp_client = get_client(url)
|
| 53 |
+
result = mcp_client.create_circuit_from_template(template, int(num_qubits))
|
| 54 |
+
|
| 55 |
+
if result.success and result.data:
|
| 56 |
+
if isinstance(result.data, dict) and 'qasm' in result.data:
|
| 57 |
+
return result.data['qasm']
|
| 58 |
+
return str(result.data)
|
| 59 |
+
return f"# Error: {result.error or 'Unknown error'}"
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"Quick build error: {e}")
|
| 62 |
+
return f"# Error: {str(e)}"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def create_quick_build_tab(server_url: Optional[str] = None) -> gr.Code:
|
| 66 |
+
"""
|
| 67 |
+
Create the Quick Build tab components.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
server_url: MCP server URL (uses default if not provided)
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
QASM output Code component
|
| 74 |
+
"""
|
| 75 |
+
url = server_url or MCP_SERVER_URL
|
| 76 |
+
|
| 77 |
+
gr.Markdown("""
|
| 78 |
+
## 🛠️ Quick Circuit Builder
|
| 79 |
+
|
| 80 |
+
Generate circuits directly from templates.
|
| 81 |
+
""")
|
| 82 |
+
|
| 83 |
+
with gr.Row():
|
| 84 |
+
with gr.Column():
|
| 85 |
+
template_select = gr.Dropdown(
|
| 86 |
+
choices=CIRCUIT_TEMPLATES,
|
| 87 |
+
value="bell_state",
|
| 88 |
+
label="Circuit Template"
|
| 89 |
+
)
|
| 90 |
+
qubits_slider = gr.Slider(
|
| 91 |
+
minimum=2,
|
| 92 |
+
maximum=8,
|
| 93 |
+
value=2,
|
| 94 |
+
step=1,
|
| 95 |
+
label="Number of Qubits"
|
| 96 |
+
)
|
| 97 |
+
build_btn = gr.Button("⚡ Generate Circuit", variant="primary")
|
| 98 |
+
|
| 99 |
+
with gr.Column():
|
| 100 |
+
qasm_output = gr.Code(
|
| 101 |
+
label="Generated QASM",
|
| 102 |
+
language="python",
|
| 103 |
+
lines=15
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Event handler
|
| 107 |
+
def build_handler(template: str, num_qubits: int) -> str:
|
| 108 |
+
return quick_build_circuit(template, num_qubits, url)
|
| 109 |
+
|
| 110 |
+
build_btn.click(build_handler, [template_select, qubits_slider], [qasm_output])
|
| 111 |
+
|
| 112 |
+
return qasm_output
|
ui/styles.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Path: QAgents-workflows/ui/styles.py
|
| 2 |
+
# Relations: Used by __init__.py, app.py
|
| 3 |
+
# Description: Custom CSS styles for the Gradio app interface
|
| 4 |
+
"""
|
| 5 |
+
Styles Module: Custom CSS for QAgents Gradio interface.
|
| 6 |
+
Provides consistent styling across all UI components.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
CUSTOM_CSS = """
|
| 10 |
+
/* Main container styling */
|
| 11 |
+
.gradio-container {
|
| 12 |
+
max-width: 1200px !important;
|
| 13 |
+
margin: auto !important;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/* Header styling */
|
| 17 |
+
.app-header {
|
| 18 |
+
text-align: center;
|
| 19 |
+
padding: 1rem;
|
| 20 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 21 |
+
border-radius: 10px;
|
| 22 |
+
margin-bottom: 1rem;
|
| 23 |
+
color: white;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/* Chat container */
|
| 27 |
+
.chat-container {
|
| 28 |
+
border: 1px solid #e0e0e0;
|
| 29 |
+
border-radius: 10px;
|
| 30 |
+
padding: 1rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Message styling */
|
| 34 |
+
.user-message {
|
| 35 |
+
background-color: #e3f2fd;
|
| 36 |
+
border-radius: 10px;
|
| 37 |
+
padding: 0.5rem 1rem;
|
| 38 |
+
margin: 0.25rem 0;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.bot-message {
|
| 42 |
+
background-color: #f5f5f5;
|
| 43 |
+
border-radius: 10px;
|
| 44 |
+
padding: 0.5rem 1rem;
|
| 45 |
+
margin: 0.25rem 0;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Code blocks in chat */
|
| 49 |
+
.code-block {
|
| 50 |
+
background-color: #1e1e1e;
|
| 51 |
+
color: #d4d4d4;
|
| 52 |
+
padding: 1rem;
|
| 53 |
+
border-radius: 5px;
|
| 54 |
+
font-family: 'Fira Code', 'Consolas', monospace;
|
| 55 |
+
overflow-x: auto;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* Status indicators */
|
| 59 |
+
.status-connected {
|
| 60 |
+
color: #4caf50;
|
| 61 |
+
font-weight: bold;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.status-disconnected {
|
| 65 |
+
color: #f44336;
|
| 66 |
+
font-weight: bold;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.status-partial {
|
| 70 |
+
color: #ff9800;
|
| 71 |
+
font-weight: bold;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Health check table */
|
| 75 |
+
.health-table {
|
| 76 |
+
width: 100%;
|
| 77 |
+
border-collapse: collapse;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.health-table th, .health-table td {
|
| 81 |
+
padding: 0.5rem;
|
| 82 |
+
text-align: left;
|
| 83 |
+
border-bottom: 1px solid #e0e0e0;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* Button styling */
|
| 87 |
+
.primary-btn {
|
| 88 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 89 |
+
border: none !important;
|
| 90 |
+
color: white !important;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.primary-btn:hover {
|
| 94 |
+
opacity: 0.9;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/* Tab styling */
|
| 98 |
+
.tab-nav {
|
| 99 |
+
border-bottom: 2px solid #667eea;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* Circuit output box */
|
| 103 |
+
.circuit-output {
|
| 104 |
+
font-family: 'Fira Code', 'Consolas', monospace;
|
| 105 |
+
background-color: #f8f9fa;
|
| 106 |
+
border: 1px solid #e0e0e0;
|
| 107 |
+
border-radius: 5px;
|
| 108 |
+
padding: 1rem;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* Loading indicator */
|
| 112 |
+
.loading {
|
| 113 |
+
display: inline-block;
|
| 114 |
+
width: 20px;
|
| 115 |
+
height: 20px;
|
| 116 |
+
border: 3px solid rgba(102, 126, 234, 0.3);
|
| 117 |
+
border-radius: 50%;
|
| 118 |
+
border-top-color: #667eea;
|
| 119 |
+
animation: spin 1s ease-in-out infinite;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
@keyframes spin {
|
| 123 |
+
to { transform: rotate(360deg); }
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Responsive adjustments */
|
| 127 |
+
@media (max-width: 768px) {
|
| 128 |
+
.gradio-container {
|
| 129 |
+
padding: 0.5rem !important;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.app-header h1 {
|
| 133 |
+
font-size: 1.5rem;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
# Additional component-specific styles
|
| 139 |
+
CHAT_STYLES = """
|
| 140 |
+
.chatbot-container {
|
| 141 |
+
min-height: 400px;
|
| 142 |
+
max-height: 600px;
|
| 143 |
+
}
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
MCP_HEALTH_STYLES = """
|
| 147 |
+
.endpoint-card {
|
| 148 |
+
border: 1px solid #e0e0e0;
|
| 149 |
+
border-radius: 8px;
|
| 150 |
+
padding: 1rem;
|
| 151 |
+
margin: 0.5rem 0;
|
| 152 |
+
}
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
QUICK_BUILD_STYLES = """
|
| 156 |
+
.template-selector {
|
| 157 |
+
margin-bottom: 1rem;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.qubit-slider {
|
| 161 |
+
margin: 1rem 0;
|
| 162 |
+
}
|
| 163 |
+
"""
|