pixel-pirat3 commited on
Commit
654175d
·
1 Parent(s): 5099f8f

Add ARC UI (#11)

Browse files
Files changed (5) hide show
  1. UI/__init__.py +2 -1
  2. UI/app_interface.py +358 -0
  3. UI/render_employee_card.py +38 -17
  4. UI/render_plan_html.py +363 -76
  5. 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
- from models import EmployeeTrainingPlan
 
 
 
 
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
- magnitude = html.escape(str(gap.get("gap", "N/A")))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  items.append(
28
  "<li>"
 
29
  f'<span class="skill-name">{skill}</span>'
30
- "&nbsp;"
31
- f'<span class="gap-pill">Gap {magnitude}</span>'
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"<li><div class=\"resource-card\">{''.join(rows)}</div></li>")
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">Role: {role}</div>
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 .render_employee_card import render_employee_card
2
- import json
3
  import html
4
- from typing import Any, Dict, List
 
5
 
 
 
 
6
 
7
  def render_plan_html(result: Any) -> str:
8
- if result is None:
9
- return "<p>No analysis result available.</p>"
10
-
11
- raw = result
12
- if isinstance(result, dict):
13
- data = result
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
- .report-container {{
32
- font-family: Inter, "Helvetica Neue", Arial, sans-serif;
33
- color: #0f172a;
34
- }}
35
- .project-title {{
36
- font-size: 1.8rem;
37
- font-weight: 700;
38
- margin-bottom: 1rem;
39
- }}
40
- .employee-grid {{
41
  display: grid;
42
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
43
- gap: 1rem;
44
- }}
45
- .employee-card {{
46
- background: linear-gradient(180deg, #027171 0%, #013737 100%);
47
- border-radius: 16px;
48
- padding: 1rem;
49
- box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
50
- border: 1px solid #e2e8f0;
51
- }}
52
- .employee-title {{
53
- font-size: 1.2rem;
54
- font-weight: 600;
55
- margin-bottom: 0.25rem;
56
- color: #0f172a;
57
- }}
58
- .employee-meta {{
59
- font-size: 0.95rem;
60
- color: #475569;
61
- margin-bottom: 0.75rem;
62
- }}
63
- .employee-section {{
64
- margin-bottom: 0.75rem;
65
- }}
66
- .section-label {{
67
- display: inline-block;
68
- font-size: 0.8rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  font-weight: 600;
 
70
  text-transform: uppercase;
71
- letter-spacing: 0.08rem;
72
- color: #6366f1;
73
- margin-bottom: 0.25rem;
74
- }}
75
- .employee-section p {{
76
- margin: 0;
77
- line-height: 1.4;
78
- font-size: 0.95rem;
79
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </style>
81
- <div class="report-container">
82
- <div class="project-title">Project: {project_name}</div>
83
- <div class="employee-grid">
84
- {cards}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- import gradio as gr # type: ignore
2
- from collections import deque
3
- from threading import Lock
4
- from typing import Optional
5
-
6
- from agents.orchestrator_agent import OrchestratorAgent
7
- from UI import render_plan_html
8
-
9
-
10
- init_message = "Awaiting agent activity..."
11
-
12
- orchestrator_agent = OrchestratorAgent()
13
- _action_log: deque[str] = deque(maxlen=200)
14
- _action_log_lock = Lock()
15
-
16
-
17
- def register_agent_action(action: str, args: Optional[str] = None):
18
- """Record agent actions with pretty HTML formatting."""
19
- with _action_log_lock:
20
- if args is not None:
21
- import json
22
- try:
23
- if isinstance(args, dict):
24
- pretty_args = json.dumps(args, indent=2)
25
- else:
26
- pretty_args = json.dumps(json.loads(args), indent=2)
27
- except Exception:
28
- pretty_args = str(args)
29
-
30
- formatted_args = f"""
31
- <pre style="
32
- background:#161b22;
33
- color:#c9d1d9;
34
- padding:6px;
35
- border-radius:4px;
36
- margin:4px 0 0 0;
37
- font-family:monospace;
38
- white-space: pre-wrap;
39
- ">{pretty_args}</pre>
40
- """.strip()
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
- btn = gr.Button("Analyze and Plan", variant="primary")
94
- outputs = gr.HTML(label="Training Plan")
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()