Spaces:
Sleeping
Sleeping
Commit
·
654175d
1
Parent(s):
5099f8f
Add ARC UI (#11)
Browse files- UI/__init__.py +2 -1
- UI/app_interface.py +358 -0
- UI/render_employee_card.py +38 -17
- UI/render_plan_html.py +363 -76
- app.py +46 -105
UI/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from .render_employee_card import render_employee_card
|
| 2 |
from .render_plan_html import render_plan_html
|
|
|
|
| 3 |
|
| 4 |
-
__all__ = ["render_employee_card", "render_plan_html"]
|
|
|
|
| 1 |
from .render_employee_card import render_employee_card
|
| 2 |
from .render_plan_html import render_plan_html
|
| 3 |
+
from .app_interface import EastSyncInterface
|
| 4 |
|
| 5 |
+
__all__ = ["render_employee_card", "render_plan_html", "EastSyncInterface"]
|
UI/app_interface.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
from collections import deque
|
| 5 |
+
from threading import Lock
|
| 6 |
+
from typing import Any, Callable, Deque, Dict, Optional
|
| 7 |
+
|
| 8 |
+
import gradio as gr # type: ignore
|
| 9 |
+
|
| 10 |
+
from .render_plan_html import render_plan_html
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class EastSyncInterface:
|
| 14 |
+
"""
|
| 15 |
+
EASTSYNC TACTICAL INTERFACE
|
| 16 |
+
Aesthetic: ARC Raiders / Tactical / High Contrast / Warm Spectrum.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
SAMPLE_PROMPT = (
|
| 20 |
+
"PROJECT: Payment Gateway Migration (PCI-DSS Compliant).\n"
|
| 21 |
+
"SCOPE: Upgrade legacy Java backend to microservices architecture.\n"
|
| 22 |
+
"TEAM: Engineering Squad Alpha.\n"
|
| 23 |
+
"REQUIREMENT: Identify skill deficiencies in Cloud Security and Kubernetes."
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
self._action_log: Deque[str] = deque(maxlen=200)
|
| 28 |
+
self._action_log_lock = Lock()
|
| 29 |
+
self.init_message = (
|
| 30 |
+
'<div class="console-line">>> TACTICAL LINK ESTABLISHED. WAITING FOR ORDERS...</div>'
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
self._app_css = self._compose_css()
|
| 34 |
+
|
| 35 |
+
# ---------------------- HELPER METHODS ----------------------
|
| 36 |
+
|
| 37 |
+
def register_agent_action(self, action: str, args: Optional[Dict[str, Any]] = None):
|
| 38 |
+
import datetime
|
| 39 |
+
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
| 40 |
+
with self._action_log_lock:
|
| 41 |
+
msg = f'<span class="console-timestamp">{timestamp}</span> <span style="color:var(--arc-yellow)">>></span> {html.escape(str(action))}'
|
| 42 |
+
if args:
|
| 43 |
+
args_str = str(args)
|
| 44 |
+
if len(args_str) > 80:
|
| 45 |
+
args_str = args_str[:80] + "..."
|
| 46 |
+
msg += f' <span style="color:var(--text-dim)">:: {html.escape(args_str)}</span>'
|
| 47 |
+
self._action_log.appendleft(f'<div class="console-line">{msg}</div>')
|
| 48 |
+
|
| 49 |
+
def get_action_log_text(self) -> str:
|
| 50 |
+
with self._action_log_lock:
|
| 51 |
+
body = "".join(self._action_log) if self._action_log else self.init_message
|
| 52 |
+
return f'<div class="console-wrapper">{body}</div>'
|
| 53 |
+
|
| 54 |
+
def clear_action_log(self) -> str:
|
| 55 |
+
with self._action_log_lock:
|
| 56 |
+
self._action_log.clear()
|
| 57 |
+
return self.get_action_log_text()
|
| 58 |
+
|
| 59 |
+
def render_analysis_result(self, result: Any) -> str:
|
| 60 |
+
return render_plan_html(result)
|
| 61 |
+
|
| 62 |
+
def render_idle_state(self) -> str:
|
| 63 |
+
# The style is handled inside render_plan_html.py CSS for consistency,
|
| 64 |
+
# but we need to ensure this specific div uses the class that forces height.
|
| 65 |
+
return "<div class='sec-status-offline'>// PROVIDE MISSION PARAMETERS TO INITIATE ANALYSIS</div>"
|
| 66 |
+
|
| 67 |
+
def render_error_state(self, reason: str) -> str:
|
| 68 |
+
safe_reason = html.escape(reason)
|
| 69 |
+
return f"<div class='sec-terminal-text'>SYSTEM OFFLINE :: {safe_reason}</div>"
|
| 70 |
+
|
| 71 |
+
def reset_prompt_value(self) -> str:
|
| 72 |
+
return self.SAMPLE_PROMPT
|
| 73 |
+
|
| 74 |
+
# ---------------------- TACTICAL CSS ----------------------
|
| 75 |
+
|
| 76 |
+
def _token_css(self) -> str:
|
| 77 |
+
return """
|
| 78 |
+
:root {
|
| 79 |
+
/* ARC RAIDERS SPECTRUM PALETTE */
|
| 80 |
+
--arc-red: #FF2A2A;
|
| 81 |
+
--arc-orange: #FF7F00;
|
| 82 |
+
--arc-yellow: #FFD400;
|
| 83 |
+
--arc-green: #55FF00;
|
| 84 |
+
--arc-cyan: #00FFFF;
|
| 85 |
+
|
| 86 |
+
/* SURFACES */
|
| 87 |
+
--bg-void: #090B10; /* Deep Black/Navy */
|
| 88 |
+
--bg-panel: #12141A; /* Slightly lighter panel */
|
| 89 |
+
--bg-card: #181B24; /* Card background */
|
| 90 |
+
|
| 91 |
+
/* TEXT - IMPROVED CONTRAST */
|
| 92 |
+
--text-main: #FFFFFF; /* Pure White for readability */
|
| 93 |
+
--text-dim: #AABBC9; /* Lighter grey for secondary text */
|
| 94 |
+
|
| 95 |
+
/* BORDERS */
|
| 96 |
+
--border-dim: #2A303C;
|
| 97 |
+
--border-bright: #FF7F00;
|
| 98 |
+
|
| 99 |
+
/* FONTS */
|
| 100 |
+
--font-header: "Inter", "Segoe UI", sans-serif;
|
| 101 |
+
--font-mono: "JetBrains Mono", "Consolas", monospace;
|
| 102 |
+
}
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def _base_typography_css(self) -> str:
|
| 106 |
+
return """
|
| 107 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
| 108 |
+
|
| 109 |
+
body {
|
| 110 |
+
background-color: var(--bg-void);
|
| 111 |
+
color: var(--text-main);
|
| 112 |
+
font-family: var(--font-header);
|
| 113 |
+
font-size: 16px;
|
| 114 |
+
line-height: 1.5;
|
| 115 |
+
-webkit-font-smoothing: antialiased;
|
| 116 |
+
margin: 0;
|
| 117 |
+
padding: 0;
|
| 118 |
+
min-height: 100vh;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/* FORCE FULL HEIGHT ON GRADIO CONTAINERS */
|
| 122 |
+
.gradio-container {
|
| 123 |
+
max-width: 1920px !important;
|
| 124 |
+
padding: 0 !important;
|
| 125 |
+
min-height: 100vh !important;
|
| 126 |
+
background-color: var(--bg-void); /* Ensure bg extends even if content is short */
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* Fix for the prose/markdown wrapper messing up heights */
|
| 130 |
+
.prose {
|
| 131 |
+
max-width: none !important;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
h1, h2, h3, h4 {
|
| 135 |
+
font-family: var(--font-header);
|
| 136 |
+
letter-spacing: -0.02em;
|
| 137 |
+
font-weight: 800;
|
| 138 |
+
color: white;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* COMPONENT OVERRIDES */
|
| 142 |
+
.gr-button {
|
| 143 |
+
border-radius: 2px !important;
|
| 144 |
+
font-family: var(--font-header) !important;
|
| 145 |
+
font-weight: 700 !important;
|
| 146 |
+
text-transform: uppercase;
|
| 147 |
+
letter-spacing: 1px;
|
| 148 |
+
font-size: 14px !important;
|
| 149 |
+
padding: 10px 16px !important;
|
| 150 |
+
}
|
| 151 |
+
.gr-box, .gr-panel, .gr-group {
|
| 152 |
+
border-radius: 2px !important;
|
| 153 |
+
border: 1px solid var(--border-dim) !important;
|
| 154 |
+
background: var(--bg-panel) !important;
|
| 155 |
+
}
|
| 156 |
+
.gr-input, textarea {
|
| 157 |
+
background: #0D1017 !important;
|
| 158 |
+
border: 1px solid var(--border-dim) !important;
|
| 159 |
+
color: var(--text-main) !important;
|
| 160 |
+
font-family: var(--font-mono) !important;
|
| 161 |
+
font-size: 15px !important;
|
| 162 |
+
line-height: 1.6 !important;
|
| 163 |
+
}
|
| 164 |
+
.gr-form { background: transparent !important; }
|
| 165 |
+
.gr-block { background: transparent !important; border: none !important; }
|
| 166 |
+
|
| 167 |
+
span.svelte-1gfkn6j { font-size: 13px !important; font-weight: 600 !important; color: var(--arc-yellow) !important; }
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
def _components_css(self) -> str:
|
| 171 |
+
return """
|
| 172 |
+
/* --- TOP SPECTRUM STRIPE --- */
|
| 173 |
+
.status-bar-spectrum {
|
| 174 |
+
height: 6px;
|
| 175 |
+
width: 100%;
|
| 176 |
+
background: linear-gradient(90deg,
|
| 177 |
+
var(--arc-red) 0%,
|
| 178 |
+
var(--arc-orange) 25%,
|
| 179 |
+
var(--arc-yellow) 50%,
|
| 180 |
+
var(--arc-green) 75%,
|
| 181 |
+
var(--arc-cyan) 100%);
|
| 182 |
+
box-shadow: 0 2px 15px rgba(255, 127, 0, 0.3);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/* --- BUTTON VARIANTS --- */
|
| 186 |
+
.btn-tac-primary {
|
| 187 |
+
background: var(--arc-orange) !important;
|
| 188 |
+
color: #000 !important;
|
| 189 |
+
border: 1px solid var(--arc-orange) !important;
|
| 190 |
+
}
|
| 191 |
+
.btn-tac-primary:hover {
|
| 192 |
+
background: #FF9500 !important;
|
| 193 |
+
box-shadow: 0 0 15px rgba(255, 127, 0, 0.5);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.btn-tac-secondary {
|
| 197 |
+
background: transparent !important;
|
| 198 |
+
border: 1px solid var(--border-dim) !important;
|
| 199 |
+
color: var(--text-dim) !important;
|
| 200 |
+
}
|
| 201 |
+
.btn-tac-secondary:hover {
|
| 202 |
+
border-color: var(--text-main) !important;
|
| 203 |
+
color: var(--text-main) !important;
|
| 204 |
+
background: rgba(255,255,255,0.05) !important;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* --- LAYOUT PANELS --- */
|
| 208 |
+
/* Use fill_height=True in Gradio Blocks, but CSS reinforces it */
|
| 209 |
+
.main-container {
|
| 210 |
+
min-height: calc(100vh - 80px); /* Account for header height approx */
|
| 211 |
+
display: flex;
|
| 212 |
+
align-items: stretch;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.input-panel {
|
| 216 |
+
padding: 32px;
|
| 217 |
+
border-right: 1px solid var(--border-dim);
|
| 218 |
+
background: var(--bg-panel);
|
| 219 |
+
height: auto !important; /* Let it grow */
|
| 220 |
+
min-height: 100%;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.output-panel {
|
| 224 |
+
padding: 32px;
|
| 225 |
+
background: #0C0E14; /* Darker background for content */
|
| 226 |
+
height: auto !important;
|
| 227 |
+
min-height: 100%;
|
| 228 |
+
flex-grow: 1;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.ent-header-label {
|
| 232 |
+
font-size: 13px;
|
| 233 |
+
color: var(--arc-yellow);
|
| 234 |
+
text-transform: uppercase;
|
| 235 |
+
letter-spacing: 1.5px;
|
| 236 |
+
margin-bottom: 12px;
|
| 237 |
+
font-weight: 700;
|
| 238 |
+
display: flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
gap: 8px;
|
| 241 |
+
}
|
| 242 |
+
.ent-header-label::before {
|
| 243 |
+
content: "";
|
| 244 |
+
display: block;
|
| 245 |
+
width: 4px; height: 16px;
|
| 246 |
+
background: var(--arc-yellow);
|
| 247 |
+
box-shadow: 0 0 8px var(--arc-yellow);
|
| 248 |
+
}
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
def _console_css(self) -> str:
|
| 252 |
+
return """
|
| 253 |
+
.console-wrapper {
|
| 254 |
+
background: #08090D;
|
| 255 |
+
border: 1px solid var(--border-dim);
|
| 256 |
+
padding: 16px;
|
| 257 |
+
font-family: var(--font-mono);
|
| 258 |
+
font-size: 13px;
|
| 259 |
+
min-height: 300px;
|
| 260 |
+
max-height: 40vh;
|
| 261 |
+
overflow-y: auto;
|
| 262 |
+
color: var(--text-dim);
|
| 263 |
+
}
|
| 264 |
+
.console-line {
|
| 265 |
+
margin-bottom: 8px;
|
| 266 |
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
| 267 |
+
padding-bottom: 4px;
|
| 268 |
+
line-height: 1.4;
|
| 269 |
+
}
|
| 270 |
+
.console-timestamp { color: var(--arc-cyan); margin-right: 8px; font-weight:600; }
|
| 271 |
+
|
| 272 |
+
.console-wrapper::-webkit-scrollbar { width: 8px; }
|
| 273 |
+
.console-wrapper::-webkit-scrollbar-track { background: #08090D; }
|
| 274 |
+
.console-wrapper::-webkit-scrollbar-thumb { background: var(--border-dim); border-radius: 4px; }
|
| 275 |
+
"""
|
| 276 |
+
|
| 277 |
+
def _compose_css(self) -> str:
|
| 278 |
+
return "\n".join([
|
| 279 |
+
self._token_css(),
|
| 280 |
+
self._base_typography_css(),
|
| 281 |
+
self._components_css(),
|
| 282 |
+
self._console_css(),
|
| 283 |
+
])
|
| 284 |
+
|
| 285 |
+
# --- UI Builders ---
|
| 286 |
+
|
| 287 |
+
def _build_hero(self) -> str:
|
| 288 |
+
return """
|
| 289 |
+
<div style="margin-bottom: 0px;">
|
| 290 |
+
<div class="status-bar-spectrum"></div>
|
| 291 |
+
<div style="background: var(--bg-panel); padding: 24px 32px; display:flex; justify-content:space-between; align-items:center; border-bottom: 1px solid var(--border-dim); flex-wrap: wrap; gap: 20px;">
|
| 292 |
+
<div style="display:flex; align-items:center; gap: 16px;">
|
| 293 |
+
<div style="width:40px; height:40px; background: var(--arc-orange); border-radius: 2px; display:flex; align-items:center; justify-content:center; font-weight:800; color:black; font-size: 20px;">E</div>
|
| 294 |
+
<div>
|
| 295 |
+
<h1 style="margin:0; font-size: 28px; line-height:1; color:white;">EASTSYNC <span style="font-weight:400; color:var(--text-dim);">TACTICAL</span></h1>
|
| 296 |
+
<div style="color:var(--text-dim); font-size:13px; letter-spacing:1px; margin-top:4px;">WORKFORCE DEPLOYMENT SYSTEM</div>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
<div style="text-align:right; font-family:var(--font-mono); color:var(--text-dim); font-size: 12px;">
|
| 300 |
+
<div><span style="color:var(--arc-green)">●</span> SECURE CONNECTION</div>
|
| 301 |
+
<div style="color:var(--arc-cyan)">VER: 4.2.0-ARC</div>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
def build_interface(self, analyze_callback: Callable[[str], str]) -> gr.Blocks:
|
| 308 |
+
theme = gr.themes.Base(
|
| 309 |
+
primary_hue="orange",
|
| 310 |
+
neutral_hue="slate",
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# Use fill_height=True on Blocks to encourage full-screen layout
|
| 314 |
+
with gr.Blocks(theme=theme, css=self._app_css, title="EastSync Tactical", fill_height=True) as demo:
|
| 315 |
+
gr.HTML(self._build_hero())
|
| 316 |
+
|
| 317 |
+
with gr.Row(equal_height=True, elem_classes=["main-container"]):
|
| 318 |
+
|
| 319 |
+
# --- LEFT COLUMN: INPUTS ---
|
| 320 |
+
with gr.Column(scale=3, elem_classes=["input-panel"]):
|
| 321 |
+
gr.HTML("<div class='ent-header-label'>MISSION PARAMETERS</div>")
|
| 322 |
+
|
| 323 |
+
input_box = gr.TextArea(
|
| 324 |
+
label="MISSION SCOPE",
|
| 325 |
+
show_label=False,
|
| 326 |
+
value=self.SAMPLE_PROMPT,
|
| 327 |
+
lines=12,
|
| 328 |
+
placeholder="Define project constraints, required technologies, and team allocation..."
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
with gr.Row():
|
| 332 |
+
btn_run = gr.Button("INITIATE SCAN", elem_classes=["btn-tac-primary"])
|
| 333 |
+
|
| 334 |
+
with gr.Row():
|
| 335 |
+
btn_reset = gr.Button("RESET PARAMETERS", elem_classes=["btn-tac-secondary"])
|
| 336 |
+
|
| 337 |
+
gr.HTML("<div style='height:30px'></div>") # Flexible Spacer
|
| 338 |
+
|
| 339 |
+
gr.HTML("<div class='ent-header-label'>SYSTEM LOGS</div>")
|
| 340 |
+
console = gr.HTML(self.get_action_log_text())
|
| 341 |
+
|
| 342 |
+
# --- RIGHT COLUMN: OUTPUT ---
|
| 343 |
+
with gr.Column(scale=7, elem_classes=["output-panel"]):
|
| 344 |
+
output_display = gr.HTML(self.render_idle_state())
|
| 345 |
+
|
| 346 |
+
# --- Event Bindings ---
|
| 347 |
+
|
| 348 |
+
btn_run.click(self.clear_action_log, outputs=console, queue=False) \
|
| 349 |
+
.then(lambda: self.get_action_log_text(), outputs=console) \
|
| 350 |
+
.then(analyze_callback, inputs=input_box, outputs=output_display) \
|
| 351 |
+
.then(self.get_action_log_text, outputs=console)
|
| 352 |
+
|
| 353 |
+
btn_reset.click(self.reset_prompt_value, outputs=input_box)
|
| 354 |
+
|
| 355 |
+
# Live log updates
|
| 356 |
+
gr.Timer(2).tick(self.get_action_log_text, outputs=console)
|
| 357 |
+
|
| 358 |
+
return demo
|
UI/render_employee_card.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
|
|
|
|
|
| 1 |
import html
|
| 2 |
from typing import Any, Iterable, Mapping, Sequence
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
def _as_mapping(member: EmployeeTrainingPlan | Mapping[str, Any]) -> Mapping[str, Any]:
|
| 8 |
if isinstance(member, Mapping):
|
| 9 |
return member
|
|
|
|
| 10 |
if hasattr(member, "model_dump"):
|
| 11 |
return member.model_dump()
|
| 12 |
return {
|
|
@@ -23,19 +30,37 @@ def _render_skill_gaps(gaps: Sequence[Mapping[str, Any]] | None) -> str:
|
|
| 23 |
items = []
|
| 24 |
for gap in gaps:
|
| 25 |
skill = html.escape(str(gap.get("skill", "Unknown skill")))
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
items.append(
|
| 28 |
"<li>"
|
|
|
|
| 29 |
f'<span class="skill-name">{skill}</span>'
|
| 30 |
-
"
|
| 31 |
-
|
| 32 |
"</li>"
|
| 33 |
)
|
| 34 |
-
return (
|
| 35 |
-
'<ul class="skill-gap-list" role="list">'
|
| 36 |
-
f'{"".join(items)}'
|
| 37 |
-
"</ul>"
|
| 38 |
-
)
|
| 39 |
|
| 40 |
|
| 41 |
def _render_training_plan(resources: Iterable[Mapping[str, Any]] | None) -> str:
|
|
@@ -76,13 +101,9 @@ def _render_training_plan(resources: Iterable[Mapping[str, Any]] | None) -> str:
|
|
| 76 |
)
|
| 77 |
rows.append(f'<div class="resource-skills">{chips}</div>')
|
| 78 |
|
| 79 |
-
cards.append(f
|
| 80 |
|
| 81 |
-
return (
|
| 82 |
-
'<ul class="training-plan-list" role="list">'
|
| 83 |
-
f'{"".join(cards)}'
|
| 84 |
-
"</ul>"
|
| 85 |
-
)
|
| 86 |
|
| 87 |
|
| 88 |
def render_employee_card(member: EmployeeTrainingPlan | Mapping[str, Any]) -> str:
|
|
@@ -96,7 +117,7 @@ def render_employee_card(member: EmployeeTrainingPlan | Mapping[str, Any]) -> st
|
|
| 96 |
return f"""
|
| 97 |
<div class="employee-card">
|
| 98 |
<div class="employee-title">{name}</div>
|
| 99 |
-
<div class="employee-meta">
|
| 100 |
<div class="employee-section">
|
| 101 |
<span class="section-label">Skill Gap</span>
|
| 102 |
{skill_gap_html}
|
|
@@ -106,4 +127,4 @@ def render_employee_card(member: EmployeeTrainingPlan | Mapping[str, Any]) -> st
|
|
| 106 |
{training_plan_html}
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
-
"""
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import html
|
| 4 |
from typing import Any, Iterable, Mapping, Sequence
|
| 5 |
|
| 6 |
+
|
| 7 |
+
# Placeholder for models.EmployeeTrainingPlan if not provided
|
| 8 |
+
class EmployeeTrainingPlan:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
pass
|
| 11 |
|
| 12 |
|
| 13 |
def _as_mapping(member: EmployeeTrainingPlan | Mapping[str, Any]) -> Mapping[str, Any]:
|
| 14 |
if isinstance(member, Mapping):
|
| 15 |
return member
|
| 16 |
+
# Assuming the actual EmployeeTrainingPlan model has these attributes or model_dump
|
| 17 |
if hasattr(member, "model_dump"):
|
| 18 |
return member.model_dump()
|
| 19 |
return {
|
|
|
|
| 30 |
items = []
|
| 31 |
for gap in gaps:
|
| 32 |
skill = html.escape(str(gap.get("skill", "Unknown skill")))
|
| 33 |
+
raw_magnitude = gap.get("gap")
|
| 34 |
+
try:
|
| 35 |
+
magnitude_value = max(0.0, min(10.0, float(raw_magnitude)))
|
| 36 |
+
magnitude = f"{magnitude_value:.1f}"
|
| 37 |
+
except (TypeError, ValueError):
|
| 38 |
+
magnitude_value = None
|
| 39 |
+
magnitude = html.escape(str(raw_magnitude or "N/A"))
|
| 40 |
+
|
| 41 |
+
severity = "High"
|
| 42 |
+
if magnitude_value is not None:
|
| 43 |
+
if magnitude_value < 4:
|
| 44 |
+
severity = "Low"
|
| 45 |
+
elif magnitude_value < 7:
|
| 46 |
+
severity = "Medium"
|
| 47 |
+
|
| 48 |
+
# progress_pct is not used in the current HTML but kept for context if needed
|
| 49 |
+
# progress_pct = (
|
| 50 |
+
# f"{(magnitude_value or 0) / 10 * 100:.0f}"
|
| 51 |
+
# if magnitude_value is not None
|
| 52 |
+
# else "0"
|
| 53 |
+
# )
|
| 54 |
+
|
| 55 |
items.append(
|
| 56 |
"<li>"
|
| 57 |
+
f'<div class="skill-gap-item">'
|
| 58 |
f'<span class="skill-name">{skill}</span>'
|
| 59 |
+
f'<span class="gap-pill" data-severity="{severity.lower()}">Gap {magnitude}</span>'
|
| 60 |
+
"</div>"
|
| 61 |
"</li>"
|
| 62 |
)
|
| 63 |
+
return f'<ul class="skill-gap-list" role="list">{"".join(items)}</ul>'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def _render_training_plan(resources: Iterable[Mapping[str, Any]] | None) -> str:
|
|
|
|
| 101 |
)
|
| 102 |
rows.append(f'<div class="resource-skills">{chips}</div>')
|
| 103 |
|
| 104 |
+
cards.append(f'<li>{"".join(rows)}</li>')
|
| 105 |
|
| 106 |
+
return f'<ul class="training-plan-list" role="list">{"".join(cards)}</ul>'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
def render_employee_card(member: EmployeeTrainingPlan | Mapping[str, Any]) -> str:
|
|
|
|
| 117 |
return f"""
|
| 118 |
<div class="employee-card">
|
| 119 |
<div class="employee-title">{name}</div>
|
| 120 |
+
<div class="employee-meta">{role}</div>
|
| 121 |
<div class="employee-section">
|
| 122 |
<span class="section-label">Skill Gap</span>
|
| 123 |
{skill_gap_html}
|
|
|
|
| 127 |
{training_plan_html}
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
+
"""
|
UI/render_plan_html.py
CHANGED
|
@@ -1,87 +1,374 @@
|
|
| 1 |
-
from
|
| 2 |
-
|
| 3 |
import html
|
| 4 |
-
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def render_plan_html(result: Any) -> str:
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
else:
|
| 15 |
-
if not isinstance(raw, str):
|
| 16 |
-
raw = json.dumps(raw, default=str)
|
| 17 |
-
try:
|
| 18 |
-
data = json.loads(raw)
|
| 19 |
-
except json.JSONDecodeError:
|
| 20 |
-
escaped = html.escape(raw)
|
| 21 |
-
return f"<pre>{escaped}</pre>"
|
| 22 |
-
|
| 23 |
-
project_name = html.escape(str(data.get("Project Name", "Training Plan")))
|
| 24 |
-
team: List[Dict[str, Any]] = data.get("team", [])
|
| 25 |
-
cards = "".join(render_employee_card(member) for member in team)
|
| 26 |
-
if not cards:
|
| 27 |
-
cards = "<p>No team members were returned.</p>"
|
| 28 |
-
|
| 29 |
-
return f"""
|
| 30 |
<style>
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
.employee-grid {{
|
| 41 |
display: grid;
|
| 42 |
-
grid-template-columns: repeat(auto-
|
| 43 |
-
gap:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
}
|
| 58 |
-
.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
font-weight: 600;
|
|
|
|
| 70 |
text-transform: uppercase;
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
</style>
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
</div>
|
| 86 |
</div>
|
| 87 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import html
|
| 4 |
+
import json
|
| 5 |
+
from typing import Any
|
| 6 |
|
| 7 |
+
# ==========================================
|
| 8 |
+
# HELPER: TACTICAL REPORT RENDERER
|
| 9 |
+
# ==========================================
|
| 10 |
|
| 11 |
def render_plan_html(result: Any) -> str:
|
| 12 |
+
"""
|
| 13 |
+
Renders the analysis payload into a Tactical Gap Analysis Grid.
|
| 14 |
+
"""
|
| 15 |
+
# --- INTERNAL CSS FOR THE REPORT ---
|
| 16 |
+
# Moved to top to ensure availability in all return paths (loading/error/success)
|
| 17 |
+
css = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
<style>
|
| 19 |
+
/* FORCE ROBUST CONTAINER HEIGHT */
|
| 20 |
+
.sec-report-wrapper {
|
| 21 |
+
width: 100%;
|
| 22 |
+
min-height: 70vh; /* Force container to take up significant screen space */
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.sec-grid {
|
|
|
|
| 28 |
display: grid;
|
| 29 |
+
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); /* Wider cards for better readability */
|
| 30 |
+
gap: 20px;
|
| 31 |
+
padding: 10px 0;
|
| 32 |
+
flex-grow: 1; /* Allow grid to expand */
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* CARD STYLING */
|
| 36 |
+
.sec-card {
|
| 37 |
+
background-color: var(--bg-card);
|
| 38 |
+
border: 1px solid var(--border-dim);
|
| 39 |
+
border-left: 4px solid var(--arc-orange); /* Default tactical color */
|
| 40 |
+
display: flex;
|
| 41 |
+
flex-direction: column;
|
| 42 |
+
position: relative;
|
| 43 |
+
transition: border-color 0.2s;
|
| 44 |
+
}
|
| 45 |
+
.sec-card:hover {
|
| 46 |
+
border-color: var(--arc-yellow);
|
| 47 |
+
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* HEADER */
|
| 51 |
+
.sec-card-header {
|
| 52 |
+
background: rgba(255,255,255,0.03);
|
| 53 |
+
padding: 16px 20px;
|
| 54 |
+
border-bottom: 1px solid var(--border-dim);
|
| 55 |
+
display: flex;
|
| 56 |
+
justify-content: space-between;
|
| 57 |
+
align-items: center;
|
| 58 |
+
}
|
| 59 |
+
.sec-role-badge {
|
| 60 |
+
font-size: 12px;
|
| 61 |
+
font-weight: 700;
|
| 62 |
+
color: var(--text-dim);
|
| 63 |
+
letter-spacing: 1px;
|
| 64 |
+
text-transform: uppercase;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* BODY */
|
| 68 |
+
.sec-card-body {
|
| 69 |
+
padding: 20px;
|
| 70 |
+
flex-grow: 1;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.sec-name {
|
| 74 |
+
font-family: var(--font-header);
|
| 75 |
+
font-size: 20px; /* Larger Name */
|
| 76 |
+
font-weight: 700;
|
| 77 |
+
color: var(--text-main);
|
| 78 |
+
margin-bottom: 4px;
|
| 79 |
+
letter-spacing: 0.5px;
|
| 80 |
+
}
|
| 81 |
+
.sec-id-tag {
|
| 82 |
+
font-family: var(--font-mono);
|
| 83 |
+
font-size: 12px;
|
| 84 |
+
color: var(--arc-orange);
|
| 85 |
+
margin-bottom: 20px;
|
| 86 |
+
opacity: 0.8;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* SKILL GAPS (Binary State) */
|
| 90 |
+
.sec-skill-container {
|
| 91 |
+
background: rgba(255, 42, 42, 0.08); /* Red tint */
|
| 92 |
+
border: 1px dashed var(--arc-red);
|
| 93 |
+
padding: 12px 16px;
|
| 94 |
+
margin-bottom: 20px;
|
| 95 |
+
border-radius: 4px;
|
| 96 |
+
}
|
| 97 |
+
.sec-skill-label {
|
| 98 |
+
font-size: 12px;
|
| 99 |
+
color: var(--arc-red);
|
| 100 |
+
font-weight: 700;
|
| 101 |
+
text-transform: uppercase;
|
| 102 |
+
margin-bottom: 10px;
|
| 103 |
+
display: flex;
|
| 104 |
+
align-items: center;
|
| 105 |
+
gap: 8px;
|
| 106 |
+
}
|
| 107 |
+
.sec-skill-tags {
|
| 108 |
+
display: flex;
|
| 109 |
+
flex-wrap: wrap;
|
| 110 |
+
gap: 8px;
|
| 111 |
+
}
|
| 112 |
+
.sec-skill-tag {
|
| 113 |
+
font-family: var(--font-mono);
|
| 114 |
+
font-size: 12px;
|
| 115 |
+
background: var(--bg-void);
|
| 116 |
+
border: 1px solid var(--arc-red);
|
| 117 |
+
color: var(--text-main);
|
| 118 |
+
padding: 6px 10px;
|
| 119 |
+
display: flex;
|
| 120 |
+
align-items: center;
|
| 121 |
+
gap: 6px;
|
| 122 |
+
}
|
| 123 |
+
.sec-skill-icon {
|
| 124 |
+
width: 8px; height: 8px; background: var(--arc-red); border-radius: 50%;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* TRAINING PLAN */
|
| 128 |
+
.sec-plan-section {
|
| 129 |
+
border-top: 1px solid var(--border-dim);
|
| 130 |
+
padding-top: 16px;
|
| 131 |
+
}
|
| 132 |
+
.sec-section-title {
|
| 133 |
+
font-size: 12px;
|
| 134 |
+
color: var(--arc-yellow);
|
| 135 |
+
text-transform: uppercase;
|
| 136 |
+
letter-spacing: 1px;
|
| 137 |
+
margin-bottom: 12px;
|
| 138 |
+
font-weight: 700;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.sec-item {
|
| 142 |
+
display: flex;
|
| 143 |
+
background: var(--bg-panel);
|
| 144 |
+
border-left: 3px solid var(--text-dim);
|
| 145 |
+
margin-bottom: 10px;
|
| 146 |
+
padding: 12px;
|
| 147 |
+
align-items: flex-start;
|
| 148 |
+
}
|
| 149 |
+
/* Cost Indicators */
|
| 150 |
+
.inv-low { border-left-color: var(--arc-green); }
|
| 151 |
+
.inv-med { border-left-color: var(--arc-yellow); }
|
| 152 |
+
.inv-high { border-left-color: var(--arc-red); }
|
| 153 |
+
|
| 154 |
+
.sec-item-details { flex-grow: 1; }
|
| 155 |
+
.sec-item-title { font-size: 14px; color: #fff; font-weight: 600; margin-bottom: 4px; line-height: 1.4;}
|
| 156 |
+
.sec-item-meta { font-size: 12px; color: #99AAB5; font-family: var(--font-mono); }
|
| 157 |
+
|
| 158 |
+
/* STATS BAR */
|
| 159 |
+
.sec-stats-bar {
|
| 160 |
+
display: flex;
|
| 161 |
+
gap: 20px;
|
| 162 |
+
background: rgba(255,255,255,0.05);
|
| 163 |
+
border: 1px solid var(--border-dim);
|
| 164 |
+
padding: 12px 20px;
|
| 165 |
+
margin-bottom: 20px;
|
| 166 |
+
align-items: center;
|
| 167 |
+
}
|
| 168 |
+
.sec-stat-group {
|
| 169 |
+
display: flex;
|
| 170 |
+
flex-direction: column;
|
| 171 |
+
}
|
| 172 |
+
.sec-stat-label {
|
| 173 |
+
font-size: 11px;
|
| 174 |
+
color: var(--text-dim);
|
| 175 |
font-weight: 600;
|
| 176 |
+
letter-spacing: 0.5px;
|
| 177 |
text-transform: uppercase;
|
| 178 |
+
}
|
| 179 |
+
.sec-stat-value {
|
| 180 |
+
font-size: 18px;
|
| 181 |
+
color: var(--text-main);
|
| 182 |
+
font-weight: 700;
|
| 183 |
+
font-family: var(--font-mono);
|
| 184 |
+
}
|
| 185 |
+
.sec-divider {
|
| 186 |
+
width: 1px;
|
| 187 |
+
height: 24px;
|
| 188 |
+
background: var(--border-dim);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* LOADING / ERROR STATES */
|
| 192 |
+
.sec-status-offline {
|
| 193 |
+
color: var(--text-dim);
|
| 194 |
+
font-family: var(--font-mono);
|
| 195 |
+
font-size: 14px;
|
| 196 |
+
padding: 20px;
|
| 197 |
+
min-height: 70vh; /* Use VH to ensure it fills screen vertical space */
|
| 198 |
+
display: flex;
|
| 199 |
+
align-items: center;
|
| 200 |
+
justify-content: center;
|
| 201 |
+
background: rgba(255,255,255,0.01);
|
| 202 |
+
border: 1px dashed var(--border-dim);
|
| 203 |
+
flex-grow: 1;
|
| 204 |
+
}
|
| 205 |
+
.sec-terminal-text {
|
| 206 |
+
color: var(--arc-red);
|
| 207 |
+
font-family: var(--font-mono);
|
| 208 |
+
font-size: 14px;
|
| 209 |
+
padding: 20px;
|
| 210 |
+
min-height: 70vh; /* Consistent height for error states */
|
| 211 |
+
}
|
| 212 |
</style>
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
if result is None:
|
| 216 |
+
return f"{css}<div class='sec-report-wrapper'><div class='sec-status-offline'>// AWAITING DATA STREAM</div></div>"
|
| 217 |
+
|
| 218 |
+
# Ensure data is dict; handle string inputs gracefully
|
| 219 |
+
data = result
|
| 220 |
+
if not isinstance(data, dict):
|
| 221 |
+
try:
|
| 222 |
+
data = json.loads(str(result))
|
| 223 |
+
except (json.JSONDecodeError, TypeError):
|
| 224 |
+
# Fallback for non-JSON strings
|
| 225 |
+
return f"{css}<div class='sec-report-wrapper'><div class='sec-terminal-text'>{html.escape(str(result))}</div></div>"
|
| 226 |
+
|
| 227 |
+
project_name = html.escape(str(data.get("project_name", "UNNAMED INITIATIVE")).upper())
|
| 228 |
+
team = data.get("team", [])
|
| 229 |
+
|
| 230 |
+
# --- CALCULATE AGGREGATE STATS ---
|
| 231 |
+
total_cost = 0.0
|
| 232 |
+
max_duration = 0.0
|
| 233 |
+
|
| 234 |
+
for member in team:
|
| 235 |
+
member_cost = 0.0
|
| 236 |
+
member_duration = 0.0
|
| 237 |
+
for plan in member.get("training_plan", []):
|
| 238 |
+
try:
|
| 239 |
+
member_cost += float(plan.get("cost", 0))
|
| 240 |
+
member_duration += float(plan.get("duration_hours", 0))
|
| 241 |
+
except (ValueError, TypeError):
|
| 242 |
+
continue
|
| 243 |
+
|
| 244 |
+
total_cost += member_cost
|
| 245 |
+
# Parallel training assumption: Project duration is limited by the person with the longest plan
|
| 246 |
+
if member_duration > max_duration:
|
| 247 |
+
max_duration = member_duration
|
| 248 |
+
|
| 249 |
+
# --- BUILD HTML ---
|
| 250 |
+
html_parts = [css, "<div class='sec-report-wrapper'>"]
|
| 251 |
+
|
| 252 |
+
# --- MAIN HEADER ---
|
| 253 |
+
html_parts.append(f"""
|
| 254 |
+
<div style="margin-bottom: 16px; padding: 0 4px; display:flex; flex-wrap: wrap; justify-content:space-between; align-items:flex-end; border-bottom: 1px solid var(--border-dim); padding-bottom: 12px; gap: 10px;">
|
| 255 |
+
<div>
|
| 256 |
+
<h2 style="margin:0; color:white; font-size:24px; letter-spacing: 0.5px;">GAP ANALYSIS REPORT</h2>
|
| 257 |
+
<div style="color:var(--arc-yellow); font-family:var(--font-mono); font-size: 14px; margin-top:6px;">TARGET: {project_name}</div>
|
| 258 |
+
</div>
|
| 259 |
+
<div style="font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); text-align: right;">
|
| 260 |
+
<div>STATUS: <span style="color:var(--arc-green)">SECURE</span></div>
|
| 261 |
+
<div>UNITS: {len(team)}</div>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
""")
|
| 265 |
+
|
| 266 |
+
# --- OVERALL STATS BAR ---
|
| 267 |
+
html_parts.append(f"""
|
| 268 |
+
<div class="sec-stats-bar">
|
| 269 |
+
<div class="sec-stat-group">
|
| 270 |
+
<div class="sec-stat-label">Total Investment</div>
|
| 271 |
+
<div class="sec-stat-value" style="color: var(--arc-cyan);">${total_cost:,.2f}</div>
|
| 272 |
+
</div>
|
| 273 |
+
<div class="sec-divider"></div>
|
| 274 |
+
<div class="sec-stat-group">
|
| 275 |
+
<div class="sec-stat-label">Est. Completion</div>
|
| 276 |
+
<div class="sec-stat-value" style="color: var(--arc-orange);">{max_duration:.0f} HRS <span style="font-size:11px; color:var(--text-dim)">(PARALLEL)</span></div>
|
| 277 |
+
</div>
|
| 278 |
+
<div class="sec-divider"></div>
|
| 279 |
+
<div class="sec-stat-group">
|
| 280 |
+
<div class="sec-stat-label">Resource Count</div>
|
| 281 |
+
<div class="sec-stat-value">{len(team)}</div>
|
| 282 |
</div>
|
| 283 |
</div>
|
| 284 |
+
""")
|
| 285 |
+
|
| 286 |
+
html_parts.append("<div class='sec-grid'>")
|
| 287 |
+
|
| 288 |
+
for member in team:
|
| 289 |
+
name = html.escape(str(member.get("employee_id", "UNKNOWN")).upper())
|
| 290 |
+
role = html.escape(str(member.get("role", "UNASSIGNED")).upper())
|
| 291 |
+
|
| 292 |
+
# --- SKILL GAPS ---
|
| 293 |
+
skills_html = ""
|
| 294 |
+
gaps_list = member.get("skills_gaps", [])
|
| 295 |
+
|
| 296 |
+
if gaps_list:
|
| 297 |
+
skill_tags = ""
|
| 298 |
+
for gap_data in gaps_list:
|
| 299 |
+
skill_name = html.escape(gap_data.get("skill", "GENERIC"))
|
| 300 |
+
skill_tags += f"""
|
| 301 |
+
<div class="sec-skill-tag">
|
| 302 |
+
<div class="sec-skill-icon"></div>
|
| 303 |
+
{skill_name}
|
| 304 |
+
</div>
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
skills_html = f"""
|
| 308 |
+
<div class="sec-skill-container">
|
| 309 |
+
<div class="sec-skill-label">⚠ SKILL DEFICIENCY DETECTED</div>
|
| 310 |
+
<div class="sec-skill-tags">{skill_tags}</div>
|
| 311 |
+
</div>
|
| 312 |
+
"""
|
| 313 |
+
else:
|
| 314 |
+
skills_html = """
|
| 315 |
+
<div style="padding: 12px; border: 1px solid var(--arc-green); color: var(--arc-green); font-size: 12px; display:flex; align-items:center; gap:8px; margin-bottom:20px; border-radius:4px;">
|
| 316 |
+
<span>✔</span> READINESS VERIFIED. NO GAPS.
|
| 317 |
+
</div>
|
| 318 |
+
"""
|
| 319 |
+
|
| 320 |
+
# --- TRAINING PLAN ---
|
| 321 |
+
training_html = ""
|
| 322 |
+
for plan in member.get("training_plan", []):
|
| 323 |
+
title = html.escape(plan.get("title", "Training Module"))
|
| 324 |
+
cost = float(plan.get("cost", 0))
|
| 325 |
+
hours = float(plan.get("duration_hours", 0))
|
| 326 |
+
|
| 327 |
+
# Investment Level Logic
|
| 328 |
+
inv_class = "inv-low"
|
| 329 |
+
if cost > 50: inv_class = "inv-med"
|
| 330 |
+
if cost > 200: inv_class = "inv-high"
|
| 331 |
+
|
| 332 |
+
training_html += f"""
|
| 333 |
+
<div class="sec-item {inv_class}">
|
| 334 |
+
<div class="sec-item-details">
|
| 335 |
+
<div class="sec-item-title">{title}</div>
|
| 336 |
+
<div class="sec-item-meta">EST. COST: ${cost} // DURATION: {hours} HRS</div>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
"""
|
| 340 |
+
|
| 341 |
+
if not training_html and gaps_list:
|
| 342 |
+
training_html = "<div style='font-size:12px; color:var(--arc-red); padding: 4px 0;'>ACTION REQUIRED: GENERATE PLAN</div>"
|
| 343 |
+
elif not training_html:
|
| 344 |
+
training_html = "<div style='font-size:12px; color:var(--text-dim); padding: 4px 0;'>NO ACTION REQUIRED</div>"
|
| 345 |
+
|
| 346 |
+
# Assemble Card
|
| 347 |
+
emp_hash = f"ID-{hash(name) % 99999:05d}"
|
| 348 |
+
|
| 349 |
+
card_html = f"""
|
| 350 |
+
<div class="sec-card">
|
| 351 |
+
<div class="sec-card-header">
|
| 352 |
+
<div style="display:flex; align-items:center; gap:8px;">
|
| 353 |
+
<div style="width:8px; height:8px; background:var(--arc-orange);"></div>
|
| 354 |
+
<div class="sec-role-badge">{role}</div>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
<div class="sec-card-body">
|
| 358 |
+
<div class="sec-name">{name}</div>
|
| 359 |
+
<div class="sec-id-tag">{emp_hash}</div>
|
| 360 |
+
|
| 361 |
+
{skills_html}
|
| 362 |
+
|
| 363 |
+
<div class="sec-plan-section">
|
| 364 |
+
<div class="sec-section-title">REMEDIATION PROTOCOLS</div>
|
| 365 |
+
{training_html}
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
"""
|
| 370 |
+
html_parts.append(card_html)
|
| 371 |
+
|
| 372 |
+
html_parts.append("</div>")
|
| 373 |
+
html_parts.append("</div>") # Close sec-report-wrapper
|
| 374 |
+
return "".join(html_parts)
|
app.py
CHANGED
|
@@ -1,114 +1,55 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
_action_log.appendleft(f"<b>{action}</b><br>{formatted_args}")
|
| 43 |
-
else:
|
| 44 |
-
_action_log.appendleft(f"<b>{action}</b>")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def get_action_log_text():
|
| 48 |
-
"""Return the log wrapped in a scrollable console container."""
|
| 49 |
-
with _action_log_lock:
|
| 50 |
-
if not _action_log:
|
| 51 |
-
body = init_message
|
| 52 |
-
else:
|
| 53 |
-
body = "<br>".join(_action_log)
|
| 54 |
-
|
| 55 |
-
return f"""
|
| 56 |
-
<div style="
|
| 57 |
-
max-height: 300px;
|
| 58 |
-
overflow-y: auto;
|
| 59 |
-
background: #0d1117;
|
| 60 |
-
color: #f0f6fc;
|
| 61 |
-
padding: 10px;
|
| 62 |
-
font-family: monospace;
|
| 63 |
-
border-radius: 6px;
|
| 64 |
-
border: 1px solid #30363d;
|
| 65 |
-
white-space: pre-wrap;
|
| 66 |
-
">
|
| 67 |
-
{body}
|
| 68 |
-
</div>
|
| 69 |
-
"""
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def clear_action_log():
|
| 73 |
-
with _action_log_lock:
|
| 74 |
-
_action_log.clear()
|
| 75 |
-
return init_message
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def analyze_and_plan_interface(user_prompt: str):
|
| 79 |
-
result = orchestrator_agent.analyze_and_plan(user_prompt, register_agent_action)
|
| 80 |
-
return render_plan_html(result)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
with gr.Blocks(fill_height=True) as demo:
|
| 84 |
-
gr.Markdown("## EastSync AI · HR Skill-Gap Analysis")
|
| 85 |
-
|
| 86 |
-
with gr.Row():
|
| 87 |
-
inputs = gr.TextArea(
|
| 88 |
-
label="Describe the project or skill gap you want analyzed",
|
| 89 |
-
placeholder="e.g. Analyze project 2 and suggest training plans for John Doe and Daniel Tatar.",
|
| 90 |
-
lines=4,
|
| 91 |
)
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
action_feed = gr.HTML(
|
| 97 |
-
label="Agent Activity",
|
| 98 |
-
value=init_message,
|
| 99 |
-
)
|
| 100 |
-
|
| 101 |
-
(
|
| 102 |
-
btn.click(clear_action_log, outputs=action_feed, queue=False)
|
| 103 |
-
.then(analyze_and_plan_interface, inputs=inputs, outputs=outputs)
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
gr.Timer(value=3).tick(fn=get_action_log_text, outputs=action_feed)
|
| 107 |
|
| 108 |
|
| 109 |
def main():
|
|
|
|
| 110 |
demo.launch(share=True)
|
| 111 |
|
| 112 |
|
| 113 |
if __name__ == "__main__":
|
| 114 |
-
main()
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
try: # pragma: no cover - fallback when providers misconfigured
|
| 10 |
+
from agents.orchestrator_agent import OrchestratorAgent
|
| 11 |
+
except Exception as exc: # pylint: disable=broad-except
|
| 12 |
+
OrchestratorAgent = None # type: ignore
|
| 13 |
+
ORCHESTRATOR_IMPORT_ERROR = str(exc)
|
| 14 |
+
else:
|
| 15 |
+
ORCHESTRATOR_IMPORT_ERROR = None
|
| 16 |
+
|
| 17 |
+
from UI import EastSyncInterface
|
| 18 |
+
|
| 19 |
+
ui = EastSyncInterface()
|
| 20 |
+
|
| 21 |
+
if OrchestratorAgent is not None:
|
| 22 |
+
try:
|
| 23 |
+
orchestrator_agent: OrchestratorAgent | None = OrchestratorAgent()
|
| 24 |
+
orchestrator_error: str | None = None
|
| 25 |
+
except Exception as exc: # pragma: no cover - best-effort graceful fallback
|
| 26 |
+
orchestrator_agent = None
|
| 27 |
+
orchestrator_error = str(exc)
|
| 28 |
+
else:
|
| 29 |
+
orchestrator_agent = None
|
| 30 |
+
orchestrator_error = ORCHESTRATOR_IMPORT_ERROR or "Provider unavailable"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def analyze_and_plan_interface(user_prompt: str) -> str:
|
| 34 |
+
if orchestrator_agent is None:
|
| 35 |
+
message = orchestrator_error or "Agent unavailable"
|
| 36 |
+
ui.register_agent_action(
|
| 37 |
+
"System Offline",
|
| 38 |
+
{
|
| 39 |
+
"reason": message,
|
| 40 |
+
"prompt": user_prompt,
|
| 41 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
)
|
| 43 |
+
return ui.render_error_state(message)
|
| 44 |
|
| 45 |
+
result: Any = orchestrator_agent.analyze_and_plan(user_prompt, ui.register_agent_action)
|
| 46 |
+
return ui.render_analysis_result(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
def main():
|
| 50 |
+
demo = ui.build_interface(analyze_and_plan_interface)
|
| 51 |
demo.launch(share=True)
|
| 52 |
|
| 53 |
|
| 54 |
if __name__ == "__main__":
|
| 55 |
+
main()
|