paudelapil commited on
Commit
4fe75bc
·
1 Parent(s): 5a14532

final commit

Browse files
ticketiq/rxconfig.py CHANGED
@@ -2,11 +2,7 @@ import reflex as rx
2
 
3
  config = rx.Config(
4
  app_name="ticketiq",
5
- plugins=[
6
- rx.plugins.TailwindV4Plugin(),
7
- ],
8
- frontend_path = "",
9
- backend_port = 7860,
10
- api_url="http://localhost:7860",
11
- disable_plugins = ["SitemapPlugin"]
12
  )
 
2
 
3
  config = rx.Config(
4
  app_name="ticketiq",
5
+ frontend_port=3000,
6
+ backend_port=8001,
7
+ api_url="http://localhost:8001",
 
 
 
 
8
  )
ticketiq/ticketiq/card.py CHANGED
@@ -75,38 +75,41 @@ def ticket_card(ticket: dict, show_similar: bool = True) -> rx.Component:
75
  margin_bottom="14px",
76
  ),
77
 
78
- # ── solution toggle ───────────────────────────────────────────────
79
- rx.box(
80
- rx.button(
81
- rx.icon(rx.cond(is_open, "chevron-up", "chevron-down"), size=12),
82
- rx.icon("zap", size=12),
83
- rx.text("Solution", font_size="12px", font_family=SANS),
84
- on_click=TicketState.toggle_expanded(tid),
85
- display="flex", align_items="center", gap="5px",
86
- background=rx.cond(is_open, ACCENT_BG, "transparent"),
87
- color=rx.cond(is_open, ACCENT, MUTED),
88
- border=rx.cond(is_open, f"1px solid {ACCENT}", f"1px solid {BORDER}"),
89
- border_radius="4px", padding="5px 10px",
90
- cursor="pointer", font_family=SANS,
91
- transition="all 0.15s",
92
- ),
93
- rx.cond(
94
- is_open,
95
- rx.box(
96
- rx.text(
97
- ticket["solution"],
98
- font_size="13px", line_height="1.75",
99
- color=TEXT, white_space="pre-wrap",
 
 
 
 
 
 
 
 
100
  ),
101
- margin_top="10px", padding="14px 16px",
102
- background="rgba(245,166,35,0.04)",
103
- border=f"1px solid {BORDER}",
104
- border_left=f"3px solid {ACCENT}",
105
- border_radius="4px",
106
  ),
107
- ),
108
- margin_bottom=rx.cond(
109
- show_similar & TicketState.has_similar_tickets, "14px", "0"
110
  ),
111
  ),
112
 
@@ -124,11 +127,16 @@ def ticket_card(ticket: dict, show_similar: bool = True) -> rx.Component:
124
  ),
125
 
126
  # ── card shell ────────────────────────────────────────────────────
 
127
  background=SURFACE,
128
- border=f"1px solid {BORDER}",
129
  border_radius=RADIUS,
130
  padding="20px",
131
  width="100%",
132
- _hover={"border_color": BORDER_HI},
133
- transition="border-color 0.2s",
 
 
 
 
134
  )
 
75
  margin_bottom="14px",
76
  ),
77
 
78
+ # ── solution toggle (only when solution is present) ──────────────
79
+ rx.cond(
80
+ ticket["solution"] != "",
81
+ rx.box(
82
+ rx.button(
83
+ rx.icon(rx.cond(is_open, "chevron-up", "chevron-down"), size=12),
84
+ rx.icon("zap", size=12),
85
+ rx.text("Solution", font_size="12px", font_family=SANS),
86
+ on_click=TicketState.toggle_expanded(tid),
87
+ display="flex", align_items="center", gap="5px",
88
+ background=rx.cond(is_open, ACCENT_BG, "transparent"),
89
+ color=rx.cond(is_open, ACCENT, MUTED),
90
+ border=rx.cond(is_open, f"1px solid {ACCENT}", f"1px solid {BORDER}"),
91
+ border_radius="4px", padding="5px 10px",
92
+ cursor="pointer", font_family=SANS,
93
+ transition="all 0.15s",
94
+ ),
95
+ rx.cond(
96
+ is_open,
97
+ rx.box(
98
+ rx.text(
99
+ ticket["solution"],
100
+ font_size="13px", line_height="1.75",
101
+ color=TEXT, white_space="pre-wrap",
102
+ ),
103
+ margin_top="10px", padding="14px 16px",
104
+ background="rgba(245,166,35,0.04)",
105
+ border=f"1px solid {BORDER}",
106
+ border_left=f"3px solid {ACCENT}",
107
+ border_radius="4px",
108
  ),
 
 
 
 
 
109
  ),
110
+ margin_bottom=rx.cond(
111
+ show_similar & TicketState.has_similar_tickets, "14px", "0"
112
+ ),
113
  ),
114
  ),
115
 
 
127
  ),
128
 
129
  # ── card shell ────────────────────────────────────────────────────
130
+ # ── card shell ────────────────────────────────────────────────────
131
  background=SURFACE,
132
+ border=f"2px solid {rx.cond(rx.color_mode == 'dark', '#4a4a5e', '#c0c0cc')}",
133
  border_radius=RADIUS,
134
  padding="20px",
135
  width="100%",
136
+ box_shadow="0 2px 8px rgba(0,0,0,0.15)",
137
+ _hover={
138
+ "border_color": ACCENT,
139
+ "box_shadow": f"0 4px 16px {ACCENT_BG}",
140
+ },
141
+ transition="all 0.2s ease",
142
  )
ticketiq/ticketiq/modal.py CHANGED
@@ -40,12 +40,12 @@ def ticket_modal() -> rx.Component:
40
  return rx.cond(
41
  TicketState.modal_open,
42
  rx.box(
43
- # ── backdrop ────────────────────────────────────────────────
44
  rx.box(
45
  on_click=TicketState.close_modal,
46
  position="fixed", top="0", left="0",
47
  width="100vw", height="100vh",
48
- background=OVERLAY,
49
  z_index="40",
50
  ),
51
 
@@ -60,25 +60,25 @@ def ticket_modal() -> rx.Component:
60
  ),
61
  rx.text(
62
  "Ticket Detail",
63
- font_size="15px", font_weight="600", color=TEXT,
64
  ),
65
  gap="2px", align="start",
66
  ),
67
  rx.spacer(),
68
  rx.icon_button(
69
- rx.icon("x", size=16),
70
  on_click=TicketState.close_modal,
71
  background="transparent",
72
- border=f"1px solid {BORDER}",
73
- border_radius="6px",
74
  color=MUTED,
75
  cursor="pointer",
76
  size="2",
77
  variant="ghost",
78
  ),
79
  align="center",
80
- padding="20px 24px",
81
- border_bottom=f"1px solid {BORDER}",
82
  ),
83
 
84
  # scrollable body
@@ -88,10 +88,10 @@ def ticket_modal() -> rx.Component:
88
  section_label("DESCRIPTION"),
89
  rx.text(
90
  t["description"],
91
- font_size="13px", line_height="1.7",
92
  color=TEXT, white_space="pre-wrap",
93
  ),
94
- margin_bottom="20px",
95
  ),
96
 
97
  # label + department
@@ -99,17 +99,17 @@ def ticket_modal() -> rx.Component:
99
  section_label("CATEGORY"),
100
  mono(
101
  t["label"],
102
- font_size="12px", color=ACCENT,
103
  letter_spacing="0.02em",
104
  ),
105
  rx.cond(
106
  t["department"] != "Uncategorised",
107
  rx.text(
108
  t["department"],
109
- font_size="12px", color=MUTED, margin_top="2px",
110
  ),
111
  ),
112
- margin_bottom="20px",
113
  ),
114
 
115
  # badges + confidence
@@ -117,12 +117,12 @@ def ticket_modal() -> rx.Component:
117
  priority_badge(t["priority"]),
118
  source_badge(t["source"]),
119
  gap="8px",
120
- margin_bottom="12px",
121
  ),
122
  rx.box(
123
  rx.text("Confidence", font_size="11px", color=MUTED, margin_bottom="4px"),
124
  confidence_bar(t["confidence"]),
125
- margin_bottom="20px",
126
  ),
127
 
128
  # solution
@@ -161,7 +161,7 @@ def ticket_modal() -> rx.Component:
161
  border_left=f"3px solid {ACCENT}",
162
  border_radius="4px",
163
  ),
164
- margin_bottom="20px",
165
  ),
166
 
167
  # similar tickets
@@ -173,23 +173,27 @@ def ticket_modal() -> rx.Component:
173
  ),
174
  ),
175
 
176
- padding="20px 24px",
177
  overflow_y="auto",
178
  flex="1",
179
  ),
180
 
181
- # panel shell
182
  position="fixed",
183
  top="0", right="0",
184
- width=PANEL_W,
185
  max_width="95vw",
186
  height="100vh",
187
- background=SURFACE,
188
- border_left=f"1px solid {BORDER_HI}",
189
  z_index="50",
190
  display="flex",
191
  flex_direction="column",
192
- box_shadow="-8px 0 32px rgba(0,0,0,0.4)",
 
 
 
 
193
  ),
194
  ),
195
  )
 
40
  return rx.cond(
41
  TicketState.modal_open,
42
  rx.box(
43
+ # ── backdrop (darker overlay) ────────────────────────────────
44
  rx.box(
45
  on_click=TicketState.close_modal,
46
  position="fixed", top="0", left="0",
47
  width="100vw", height="100vh",
48
+ background="rgba(0,0,0,0.7)",
49
  z_index="40",
50
  ),
51
 
 
60
  ),
61
  rx.text(
62
  "Ticket Detail",
63
+ font_size="16px", font_weight="600", color=TEXT,
64
  ),
65
  gap="2px", align="start",
66
  ),
67
  rx.spacer(),
68
  rx.icon_button(
69
+ rx.icon("x", size=18),
70
  on_click=TicketState.close_modal,
71
  background="transparent",
72
+ border=f"1px solid {BORDER_HI}",
73
+ border_radius="8px",
74
  color=MUTED,
75
  cursor="pointer",
76
  size="2",
77
  variant="ghost",
78
  ),
79
  align="center",
80
+ padding="24px",
81
+ border_bottom=f"1px solid {BORDER_HI}",
82
  ),
83
 
84
  # scrollable body
 
88
  section_label("DESCRIPTION"),
89
  rx.text(
90
  t["description"],
91
+ font_size="14px", line_height="1.7",
92
  color=TEXT, white_space="pre-wrap",
93
  ),
94
+ margin_bottom="24px",
95
  ),
96
 
97
  # label + department
 
99
  section_label("CATEGORY"),
100
  mono(
101
  t["label"],
102
+ font_size="13px", color=ACCENT,
103
  letter_spacing="0.02em",
104
  ),
105
  rx.cond(
106
  t["department"] != "Uncategorised",
107
  rx.text(
108
  t["department"],
109
+ font_size="13px", color=MUTED, margin_top="4px",
110
  ),
111
  ),
112
+ margin_bottom="24px",
113
  ),
114
 
115
  # badges + confidence
 
117
  priority_badge(t["priority"]),
118
  source_badge(t["source"]),
119
  gap="8px",
120
+ margin_bottom="16px",
121
  ),
122
  rx.box(
123
  rx.text("Confidence", font_size="11px", color=MUTED, margin_bottom="4px"),
124
  confidence_bar(t["confidence"]),
125
+ margin_bottom="24px",
126
  ),
127
 
128
  # solution
 
161
  border_left=f"3px solid {ACCENT}",
162
  border_radius="4px",
163
  ),
164
+ margin_bottom="24px",
165
  ),
166
 
167
  # similar tickets
 
173
  ),
174
  ),
175
 
176
+ padding="24px",
177
  overflow_y="auto",
178
  flex="1",
179
  ),
180
 
181
+ # panel shell – now with high‑contrast background and bold left edge
182
  position="fixed",
183
  top="0", right="0",
184
+ width="540px",
185
  max_width="95vw",
186
  height="100vh",
187
+ background=rx.cond(rx.color_mode == "dark", "#262630", "#f5f6fa"),
188
+ border_left=f"3px solid {rx.cond(rx.color_mode == 'dark', '#5a5a70', '#a0a0b0')}",
189
  z_index="50",
190
  display="flex",
191
  flex_direction="column",
192
+ box_shadow=rx.cond(
193
+ rx.color_mode == "dark",
194
+ "-12px 0 40px rgba(255,255,255,0.05), 0 0 0 1px rgba(255,255,255,0.05)",
195
+ "-12px 0 40px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.1)",
196
+ ),
197
  ),
198
  ),
199
  )
ticketiq/ticketiq/pages/playground.py CHANGED
@@ -4,6 +4,37 @@ from ..ui import (
4
  mono, ACCENT, BORDER, MUTED, TEXT, SURFACE, SANS, RADIUS, section_label
5
  )
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def embedding_explorer() -> rx.Component:
8
  return rx.card(
9
  rx.vstack(
@@ -18,6 +49,7 @@ def embedding_explorer() -> rx.Component:
18
  rx.button("Probe", on_click=PlaygroundState.probe),
19
  rx.cond(
20
  PlaygroundState.probe_results.length() > 0,
 
21
  rx.table.root(
22
  rx.table.header(
23
  rx.table.row(
 
4
  mono, ACCENT, BORDER, MUTED, TEXT, SURFACE, SANS, RADIUS, section_label
5
  )
6
 
7
+ def network_graph_card() -> rx.Component:
8
+ return rx.box(
9
+ # Header + button (outside the iframe)
10
+ rx.vstack(
11
+ section_label("SIMILAR TICKETS NETWORK (DRAGGABLE)"),
12
+ rx.button("Show Network", on_click=PlaygroundState.show_network),
13
+ spacing="3",
14
+ margin_bottom="8px",
15
+ ),
16
+ # Iframe in a dedicated container that forces 100% width
17
+ rx.cond(
18
+ PlaygroundState.network_url != "",
19
+ rx.box(
20
+ rx.html(
21
+ f'<iframe src="{PlaygroundState.network_url}" '
22
+ 'style="width:100%; height:520px; border:none; display:block;"></iframe>'
23
+ ),
24
+ width="100%",
25
+ # This ensures the box doesn't shrink
26
+ flex="1",
27
+ ),
28
+ ),
29
+ # Card shell
30
+ width="100%",
31
+ background=SURFACE,
32
+ border=f"1px solid {BORDER}",
33
+ border_radius=RADIUS,
34
+ padding="12px",
35
+ margin_top="16px",
36
+ )
37
+
38
  def embedding_explorer() -> rx.Component:
39
  return rx.card(
40
  rx.vstack(
 
49
  rx.button("Probe", on_click=PlaygroundState.probe),
50
  rx.cond(
51
  PlaygroundState.probe_results.length() > 0,
52
+ network_graph_card(),
53
  rx.table.root(
54
  rx.table.header(
55
  rx.table.row(
ticketiq/ticketiq/pages/stream_page.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import reflex as rx
2
+ from ..state import StreamState
3
+ from ..ui import (
4
+ ACCENT, BORDER, MUTED, TEXT, SURFACE, SANS, RADIUS, LOW_C,
5
+ )
6
+
7
+ def stream_page() -> rx.Component:
8
+ return rx.box(
9
+ # Live indicator
10
+ rx.hstack(
11
+ rx.box(
12
+ width="8px", height="8px", border_radius="50%", background=LOW_C,
13
+ style={"animation": "pulse 1.5s infinite"},
14
+ ),
15
+ rx.text("LIVE", font_size="24px", font_weight="bold", color=ACCENT),
16
+ rx.text("●", font_size="14px", color=LOW_C),
17
+ rx.text(
18
+ StreamState.live_tickets.length().to_string() + " tickets",
19
+ font_size="14px", color=MUTED,
20
+ ),
21
+ gap="8px", align="center", margin_bottom="24px",
22
+ ),
23
+
24
+ # Feed – rendered as HTML (no foreach)
25
+ rx.vstack(
26
+ rx.cond(
27
+ StreamState.live_tickets.length() == 0,
28
+ rx.text("Waiting for tickets…", font_size="14px", color=MUTED, padding="40px"),
29
+ rx.html(StreamState.feed_html),
30
+ ),
31
+ width="100%",
32
+ ),
33
+
34
+ # Animations
35
+ rx.el.style("""
36
+ @keyframes slideIn {
37
+ from { opacity: 0; transform: translateY(-20px); }
38
+ to { opacity: 1; transform: translateY(0); }
39
+ }
40
+ @keyframes pulse {
41
+ 0%, 100% { opacity: 1; }
42
+ 50% { opacity: 0.4; }
43
+ }
44
+ """),
45
+
46
+ max_width="900px", margin="0 auto",
47
+ padding="30px 24px", width="100%",
48
+ on_mount=StreamState.start_stream,
49
+ on_unmount=StreamState.stop_stream,
50
+ )
ticketiq/ticketiq/state.py CHANGED
@@ -1,8 +1,17 @@
1
- import httpx
2
  import reflex as rx
 
3
 
4
- API_BASE = "https://paudelapil-tech-triage.hf.space/api/v1/tickets"
5
- API_BASE_V1 = "https://paudelapil-tech-triage.hf.space/api/v1"
 
 
 
 
 
 
 
 
6
  TIMEOUT = 60.0
7
  PAGE_SIZE = 12
8
 
@@ -434,6 +443,7 @@ class PlaygroundState(rx.State):
434
  batch_text: str = ""
435
  batch_results: list[dict] = []
436
 
 
437
  def set_probe_text(self, value: str):
438
  self.probe_text = value
439
 
@@ -483,4 +493,90 @@ class PlaygroundState(rx.State):
483
  json={"texts": lines}
484
  )
485
  data = resp.json()
486
- self.batch_results = data["results"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx, asyncio
2
  import reflex as rx
3
+ import time, json
4
 
5
+
6
+
7
+ from .ui import (
8
+ mono, ACCENT, CHART_BLUE,
9
+ ACCENT, BORDER, MUTED, TEXT, SURFACE, SANS, MONO, RADIUS,
10
+ HIGH, MED, LOW_C, HIGH_BG, MED_BG, LOW_BG,
11
+ )
12
+
13
+ API_BASE = "http://localhost:8000/api/v1/tickets"
14
+ API_BASE_V1 = "http://localhost:8000/api/v1"
15
  TIMEOUT = 60.0
16
  PAGE_SIZE = 12
17
 
 
443
  batch_text: str = ""
444
  batch_results: list[dict] = []
445
 
446
+
447
  def set_probe_text(self, value: str):
448
  self.probe_text = value
449
 
 
493
  json={"texts": lines}
494
  )
495
  data = resp.json()
496
+ self.batch_results = data["results"]
497
+
498
+ network_url: str = ""
499
+
500
+ async def show_network(self):
501
+ if not self.probe_text.strip():
502
+ return
503
+ async with httpx.AsyncClient(timeout=120) as client:
504
+ resp = await client.post(
505
+ f"{API_BASE_V1}/dev/probe-tickets-graph",
506
+ json={"text": self.probe_text.strip()}
507
+ )
508
+ data = resp.json()
509
+ self.network_url = data["url"]
510
+
511
+
512
+ class ThemeState(rx.State):
513
+ theme: str = "dark"
514
+
515
+ def toggle(self):
516
+ self.theme = "light" if self.theme == "dark" else "dark"
517
+
518
+ @rx.var
519
+ def opposite(self) -> str:
520
+ return "light" if self.theme == "dark" else "dark"
521
+
522
+
523
+ class StreamState(rx.State):
524
+ live_tickets: list[dict] = []
525
+ last_id: int = 0
526
+ streaming: bool = False
527
+
528
+ async def start_stream(self):
529
+ self.streaming = True
530
+ self.last_id = 0
531
+ self.live_tickets = []
532
+ return StreamState.poll
533
+
534
+ async def poll(self):
535
+ if not self.streaming:
536
+ return
537
+ try:
538
+ async with httpx.AsyncClient(timeout=10) as client:
539
+ r = await client.get(
540
+ f"{API_BASE_V1}/stream/latest",
541
+ params={"since_id": self.last_id, "limit": 20},
542
+ )
543
+ data = r.json()
544
+ new_tickets = data.get("tickets", [])
545
+ if new_tickets:
546
+ self.live_tickets = new_tickets + self.live_tickets
547
+ self.last_id = new_tickets[0]["id"]
548
+ self.live_tickets = self.live_tickets[:50]
549
+ except Exception:
550
+ pass
551
+ await asyncio.sleep(3)
552
+ return StreamState.poll
553
+
554
+ async def stop_stream(self):
555
+ self.streaming = False
556
+ self.live_tickets = []
557
+ self.last_id = 0
558
+
559
+ @rx.var
560
+ def feed_html(self) -> str:
561
+ """Render the live ticket feed as an HTML string."""
562
+ if not self.live_tickets:
563
+ return ""
564
+ rows = []
565
+ for t in self.live_tickets:
566
+ p = t.get("priority", "low").lower()
567
+ color = {"high": "#ef4444", "medium": "#f59e0b", "low": "#22c55e"}.get(p, "#888")
568
+ bg = {"high": "rgba(239,68,68,0.10)", "medium": "rgba(245,158,11,0.10)", "low": "rgba(34,197,94,0.10)"}.get(p, "transparent")
569
+ rows.append(f"""<div style="background:{bg}; border-left:3px solid {color};
570
+ padding:10px 14px; margin-bottom:6px; border-radius:4px;
571
+ display:flex; align-items:flex-start; gap:10px; animation:slideIn 0.4s ease-out;">
572
+ <div style="width:10px;height:10px;border-radius:50%;background:{color};flex-shrink:0;margin-top:4px;"></div>
573
+ <div style="font-family:monospace;font-size:11px;font-weight:bold;color:{color};min-width:50px;">
574
+ {t.get('priority','?').upper()}</div>
575
+ <div style="display:flex;flex-direction:column;gap:2px;">
576
+ <div style="font-size:13px;color:#d0d0d8;line-height:1.4;">{t.get('description','')}</div>
577
+ <div style="font-size:11px;color:#9090a8;">
578
+ {t.get('label','')} · {t.get('department','')} · conf: {t.get('confidence',0):.2f} · {t.get('source','')}
579
+ </div>
580
+ </div>
581
+ </div>""")
582
+ return "".join(rows)
ticketiq/ticketiq/stats.py CHANGED
@@ -7,11 +7,10 @@ from .ui import (
7
  HIGH, HIGH_BG, LOW_C, LOW_BG, MED, MED_BG, SANS, MONO, RADIUS,
8
  )
9
 
10
- # Recharts palette
11
  PIE_COLORS = [ACCENT, "#60a5fa", LOW_C, "#a78bfa", HIGH, "#f472b6", "#34d399"]
12
 
13
-
14
- # ── Stat card ─────────────────────────────────────────────────────────────────
15
 
16
  def stat_card(label: str, value: rx.Var, sub: str = "") -> rx.Component:
17
  return rx.box(
@@ -29,7 +28,7 @@ def stat_card(label: str, value: rx.Var, sub: str = "") -> rx.Component:
29
  )
30
 
31
 
32
- # ── Priority donut ────────────────────────────────────────────────────────────
33
 
34
  def priority_donut() -> rx.Component:
35
  stats = TicketState.stats
@@ -86,7 +85,7 @@ def priority_donut() -> rx.Component:
86
  )
87
 
88
 
89
- # ── Source donut ──────────────────────────────────────────────────────────────
90
 
91
  def source_donut() -> rx.Component:
92
  stats = TicketState.stats
@@ -136,7 +135,7 @@ def source_donut() -> rx.Component:
136
  )
137
 
138
 
139
- # ── Top labels bar chart ──────────────────────────────────────────────────────
140
 
141
  def top_labels_chart() -> rx.Component:
142
  return rx.box(
@@ -144,18 +143,18 @@ def top_labels_chart() -> rx.Component:
144
  rx.recharts.bar_chart(
145
  rx.recharts.bar(
146
  data_key="count",
147
- fill="#f59e0b",
148
  radius=[0, 4, 4, 0],
149
  ),
150
  rx.recharts.x_axis(
151
  type_="number",
152
- tick={"fontSize": 10, "fill": MUTED, "fontFamily": MONO},
153
  ),
154
  rx.recharts.y_axis(
155
  data_key="label",
156
  type_="category",
157
- tick={"fontSize": 10, "fill": MUTED, "fontFamily": MONO},
158
- width=200, # wider for longer labels
159
  ),
160
  rx.recharts.cartesian_grid(
161
  stroke_dasharray="3 3",
@@ -170,7 +169,7 @@ def top_labels_chart() -> rx.Component:
170
  "fontSize": "12px",
171
  "color": TEXT,
172
  },
173
- cursor={"fill": "rgba(245,158,11,0.06)"},
174
  ),
175
  layout="vertical",
176
  data=TicketState.stats_top_labels,
@@ -186,7 +185,7 @@ def top_labels_chart() -> rx.Component:
186
  )
187
 
188
 
189
- # ── Department bar chart ──────────────────────────────────────────────────────
190
 
191
  def dept_chart() -> rx.Component:
192
  return rx.box(
@@ -194,17 +193,17 @@ def dept_chart() -> rx.Component:
194
  rx.recharts.bar_chart(
195
  rx.recharts.bar(
196
  data_key="count",
197
- fill="#60a5fa",
198
  radius=[0, 4, 4, 0],
199
  ),
200
  rx.recharts.x_axis(
201
  type_="number",
202
- tick={"fontSize": 10, "fill": MUTED, "fontFamily": MONO},
203
  ),
204
  rx.recharts.y_axis(
205
  data_key="department",
206
  type_="category",
207
- tick={"fontSize": 10, "fill": MUTED, "fontFamily": MONO},
208
  width=180,
209
  ),
210
  rx.recharts.cartesian_grid(
@@ -220,9 +219,9 @@ def dept_chart() -> rx.Component:
220
  "fontSize": "12px",
221
  "color": TEXT,
222
  },
223
- cursor={"fill": "rgba(96,165,250,0.06)"},
224
  ),
225
- layout="vertical", # ← horizontal bars
226
  data=TicketState.stats_department_breakdown,
227
  width="100%",
228
  height=400,
@@ -235,6 +234,7 @@ def dept_chart() -> rx.Component:
235
  width="100%",
236
  )
237
 
 
238
  def uncategorised_card() -> rx.Component:
239
  return rx.box(
240
  rx.text("Uncategorised", font_size="12px", color=MUTED),
@@ -253,7 +253,8 @@ def uncategorised_card() -> rx.Component:
253
  min_width="120px",
254
  )
255
 
256
- # ── Stats page ────────────────────────────────────────────────────────────────
 
257
 
258
  def stats_page() -> rx.Component:
259
  s = TicketState.stats
@@ -347,5 +348,4 @@ def stats_page() -> rx.Component:
347
  max_width="900px", margin="0 auto",
348
  padding="40px 24px", width="100%",
349
  on_mount=TicketState.load_stats,
350
- )
351
-
 
7
  HIGH, HIGH_BG, LOW_C, LOW_BG, MED, MED_BG, SANS, MONO, RADIUS,
8
  )
9
 
10
+ # PIE_COLORS – using CSS variables works fine here because they're passed directly as fill values
11
  PIE_COLORS = [ACCENT, "#60a5fa", LOW_C, "#a78bfa", HIGH, "#f472b6", "#34d399"]
12
 
13
+ # ── Stat card (unchanged) ────────────────────────────────────────────────────
 
14
 
15
  def stat_card(label: str, value: rx.Var, sub: str = "") -> rx.Component:
16
  return rx.box(
 
28
  )
29
 
30
 
31
+ # ── Priority donut (unchanged) ──────────────────────────────────────────────
32
 
33
  def priority_donut() -> rx.Component:
34
  stats = TicketState.stats
 
85
  )
86
 
87
 
88
+ # ── Source donut (unchanged) ────────────────────────────────────────────────
89
 
90
  def source_donut() -> rx.Component:
91
  stats = TicketState.stats
 
135
  )
136
 
137
 
138
+ # ── Top labels bar chart – FIXED TICK COLORS ────────────────────────────────
139
 
140
  def top_labels_chart() -> rx.Component:
141
  return rx.box(
 
143
  rx.recharts.bar_chart(
144
  rx.recharts.bar(
145
  data_key="count",
146
+ fill=rx.cond(rx.color_mode == "dark", "#f59e0b", "#d97706"),
147
  radius=[0, 4, 4, 0],
148
  ),
149
  rx.recharts.x_axis(
150
  type_="number",
151
+ tick={"fontSize": 10, "fill": rx.cond(rx.color_mode == "dark", "#ccc", "#555"), "fontFamily": MONO},
152
  ),
153
  rx.recharts.y_axis(
154
  data_key="label",
155
  type_="category",
156
+ tick={"fontSize": 10, "fill": rx.cond(rx.color_mode == "dark", "#ccc", "#555"), "fontFamily": MONO},
157
+ width=200,
158
  ),
159
  rx.recharts.cartesian_grid(
160
  stroke_dasharray="3 3",
 
169
  "fontSize": "12px",
170
  "color": TEXT,
171
  },
172
+ cursor={"fill": rx.cond(rx.color_mode == "dark", "rgba(245,158,11,0.06)", "rgba(217,119,6,0.06)")},
173
  ),
174
  layout="vertical",
175
  data=TicketState.stats_top_labels,
 
185
  )
186
 
187
 
188
+ # ── Department bar chart – FIXED TICK COLORS ────────────────────────────────
189
 
190
  def dept_chart() -> rx.Component:
191
  return rx.box(
 
193
  rx.recharts.bar_chart(
194
  rx.recharts.bar(
195
  data_key="count",
196
+ fill=rx.cond(rx.color_mode == "dark", "#60a5fa", "#2563eb"),
197
  radius=[0, 4, 4, 0],
198
  ),
199
  rx.recharts.x_axis(
200
  type_="number",
201
+ tick={"fontSize": 10, "fill": rx.cond(rx.color_mode == "dark", "#ccc", "#555"), "fontFamily": MONO},
202
  ),
203
  rx.recharts.y_axis(
204
  data_key="department",
205
  type_="category",
206
+ tick={"fontSize": 10, "fill": rx.cond(rx.color_mode == "dark", "#ccc", "#555"), "fontFamily": MONO},
207
  width=180,
208
  ),
209
  rx.recharts.cartesian_grid(
 
219
  "fontSize": "12px",
220
  "color": TEXT,
221
  },
222
+ cursor={"fill": rx.cond(rx.color_mode == "dark", "rgba(96,165,250,0.06)", "rgba(37,99,235,0.06)")},
223
  ),
224
+ layout="vertical",
225
  data=TicketState.stats_department_breakdown,
226
  width="100%",
227
  height=400,
 
234
  width="100%",
235
  )
236
 
237
+
238
  def uncategorised_card() -> rx.Component:
239
  return rx.box(
240
  rx.text("Uncategorised", font_size="12px", color=MUTED),
 
253
  min_width="120px",
254
  )
255
 
256
+
257
+ # ── Stats page (unchanged) ──────────────────────────────────────────────────
258
 
259
  def stats_page() -> rx.Component:
260
  s = TicketState.stats
 
348
  max_width="900px", margin="0 auto",
349
  padding="40px 24px", width="100%",
350
  on_mount=TicketState.load_stats,
351
+ )
 
ticketiq/ticketiq/sth.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ test_tickets = {
2
+ "The `cert-manager` Order resource for `api.fintech.com` is stuck in 'pending' because the ACME HTTP01 challenge is returning a 404 from the ingress controller after a recent nginx config change.",
3
+
4
+ "Our `grafana` dashboard for payment latency shows a gap between 02:00 and 04:00 UTC because the `prometheus` retention policy was shortened to 30 days without updating the dashboard query range.",
5
+
6
+ "The `mlflow` experiment tracking server is returning '500 Internal Server Error' when logging parameters larger than 64KB because the `nginx` reverse proxy has `client_max_body_size` set to 64k.",
7
+
8
+ "A `terraform apply` on the staging environment is trying to destroy and recreate the `aws_elasticache_replication_group` because the `snapshot_window` parameter was removed from the module and the state file sees a drift.",
9
+
10
+ "The `sonarqube` scanner in the CI pipeline fails with 'Project not found' because the project key was renamed from `fintech-payment` to `fintech.payment` in the UI but the `sonar-project.properties` file was not updated.",
11
+
12
+ "Our `karpenter` provisioner is scaling down GPU nodes too aggressively during off-peak hours, causing model inference pods to be evicted mid-request and resulting in 503 errors for the risk scoring API.",
13
+
14
+ "The `datadog` agent on `us-east-1` EKS nodes is emitting duplicate `kubernetes_state.*` metrics because the `kube-state-metrics` service is being scraped by both the cluster agent and the node agent simultaneously.",
15
+
16
+ "The `vault` audit log is full of 'permission denied' errors for the `payment-service` role because the policy attached to the role was changed to require `sudo` for `transit/decrypt` operations after a security audit.",
17
+
18
+ "The `next.js` build in the CI pipeline fails with 'Error: EMFILE: too many open files' because the build process opens more than 1024 file handles simultaneously and the default `ulimit` on the runner is too low.",
19
+
20
+ "The `timescaledb` continuous aggregate for `hourly_trade_volume` stopped refreshing after the source hypertable was renamed from `trades` to `trade_events` and the materialization job is referencing the old name.",
21
+ }
ticketiq/ticketiq/ticketiq.py CHANGED
@@ -10,11 +10,21 @@ from ticketiq.search import search_page
10
  from ticketiq.stats import stats_page
11
  from .pages.trends import trends_page
12
  from .pages.playground import playground_page
 
13
  from ticketiq.ui import (
14
  BG, SURFACE, BORDER, BORDER_HI, ACCENT, ACCENT_BG,
15
  MUTED, TEXT, SANS, MONO, RADIUS,
16
  )
17
 
 
 
 
 
 
 
 
 
 
18
  GOOGLE_FONTS = (
19
  "https://fonts.googleapis.com/css2?"
20
  "family=JetBrains+Mono:wght@400;500;700"
@@ -29,6 +39,7 @@ NAV = [
29
  ("stats", "bar-chart-2", "Stats"),
30
  ("trends", "trending-up", "Trends"),
31
  ("playground", "flask-conical", "Playground"),
 
32
 
33
  ]
34
 
@@ -68,7 +79,7 @@ def sidebar() -> rx.Component:
68
  # logo
69
  rx.hstack(
70
  rx.box(
71
- rx.icon("cpu", size=16, color="#0a0a0b"),
72
  width="30px", height="30px",
73
  background=ACCENT, border_radius="6px",
74
  display="flex", align_items="center", justify_content="center",
@@ -92,8 +103,20 @@ def sidebar() -> rx.Component:
92
  padding="0 10px",
93
  ),
94
 
95
- # footer
96
  rx.box(
 
 
 
 
 
 
 
 
 
 
 
 
97
  rx.text(
98
  "BGE-M3 · HDBSCAN · XGBoost · Groq",
99
  font_size="10px", color=MUTED, font_family=MONO,
@@ -128,6 +151,7 @@ def page_body() -> rx.Component:
128
  ("stats", stats_page()),
129
  ("trends", trends_page()),
130
  ("playground", playground_page()),
 
131
  submit_page(),
132
  )
133
 
@@ -136,9 +160,60 @@ def page_body() -> rx.Component:
136
 
137
  def index() -> rx.Component:
138
  return rx.box(
139
- # inject Google Fonts
140
  rx.script(src=GOOGLE_FONTS),
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  rx.hstack(
143
  sidebar(),
144
  rx.box(
@@ -152,7 +227,7 @@ def index() -> rx.Component:
152
  width="100%",
153
  ),
154
 
155
- # global styles
156
  font_family=SANS,
157
  background=BG,
158
  color=TEXT,
@@ -166,8 +241,6 @@ def index() -> rx.Component:
166
  ),
167
  },
168
  )
169
-
170
-
171
  # ── App ───────────────────────────────────────────────────────────────────────
172
 
173
  app = rx.App(
 
10
  from ticketiq.stats import stats_page
11
  from .pages.trends import trends_page
12
  from .pages.playground import playground_page
13
+ from .pages.stream_page import stream_page
14
  from ticketiq.ui import (
15
  BG, SURFACE, BORDER, BORDER_HI, ACCENT, ACCENT_BG,
16
  MUTED, TEXT, SANS, MONO, RADIUS,
17
  )
18
 
19
+
20
+ app = rx.App(
21
+ style={"font_family": "Inter, sans-serif"},
22
+ theme=rx.theme(appearance="light", accent_color="blue"),
23
+ stylesheets=[
24
+ "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=DM+Sans:wght@300;400;500;600&display=swap",
25
+ ],
26
+ )
27
+
28
  GOOGLE_FONTS = (
29
  "https://fonts.googleapis.com/css2?"
30
  "family=JetBrains+Mono:wght@400;500;700"
 
39
  ("stats", "bar-chart-2", "Stats"),
40
  ("trends", "trending-up", "Trends"),
41
  ("playground", "flask-conical", "Playground"),
42
+ ("stream", "radio", "Live"),
43
 
44
  ]
45
 
 
79
  # logo
80
  rx.hstack(
81
  rx.box(
82
+ rx.icon("cpu", size=16, color=TEXT),
83
  width="30px", height="30px",
84
  background=ACCENT, border_radius="6px",
85
  display="flex", align_items="center", justify_content="center",
 
103
  padding="0 10px",
104
  ),
105
 
106
+ # footer (with toggle button)
107
  rx.box(
108
+ rx.icon_button(
109
+ rx.cond(rx.color_mode == "dark", rx.icon("sun", size=16), rx.icon("moon", size=16)),
110
+ on_click=rx.toggle_color_mode,
111
+ background="transparent",
112
+ border=f"1px solid {BORDER}",
113
+ border_radius="6px",
114
+ color=MUTED,
115
+ cursor="pointer",
116
+ size="2",
117
+ variant="ghost",
118
+ margin_bottom="10px",
119
+ ),
120
  rx.text(
121
  "BGE-M3 · HDBSCAN · XGBoost · Groq",
122
  font_size="10px", color=MUTED, font_family=MONO,
 
151
  ("stats", stats_page()),
152
  ("trends", trends_page()),
153
  ("playground", playground_page()),
154
+ ("stream", stream_page()),
155
  submit_page(),
156
  )
157
 
 
160
 
161
  def index() -> rx.Component:
162
  return rx.box(
163
+ # Inject Google Fonts
164
  rx.script(src=GOOGLE_FONTS),
165
 
166
+ # Embedded theme styles (dark / light + chart colours)
167
+ rx.el.style("""
168
+ html[data-theme='dark'] {
169
+ --bg: #1a1a20;
170
+ --surface: #242430;
171
+ --border: #343440;
172
+ --border-hi: #4a4a5a;
173
+ --text: #e0e0e8;
174
+ --muted: #7a7a90;
175
+ --accent: #818cf8;
176
+ --accent-bg: rgba(129,140,248,0.12);
177
+ --high: #f87171;
178
+ --high-bg: rgba(248,113,113,0.12);
179
+ --med: #fbbf24;
180
+ --med-bg: rgba(251,191,36,0.12);
181
+ --low: #34d399;
182
+ --low-bg: rgba(52,211,153,0.12);
183
+ --chart-blue: #60a5fa;
184
+ --chart-orange: #f59e0b;
185
+ --chart-purple: #a78bfa;
186
+ --chart-pink: #f472b6;
187
+ --chart-green: #34d399;
188
+ --chart-blue-bg: rgba(96,165,250,0.06);
189
+ --chart-orange-bg: rgba(245,158,11,0.06);
190
+ }
191
+ html[data-theme='light'] {
192
+ --bg: #f8f9fa;
193
+ --surface: #ffffff;
194
+ --border: #dee2e6;
195
+ --border-hi: #adb5bd;
196
+ --text: #212529;
197
+ --muted: #6c757d;
198
+ --accent: #0d6efd;
199
+ --accent-bg: rgba(13,110,253,0.10);
200
+ --high: #dc3545;
201
+ --high-bg: rgba(220,53,69,0.08);
202
+ --med: #fd7e14;
203
+ --med-bg: rgba(253,126,20,0.08);
204
+ --low: #198754;
205
+ --low-bg: rgba(25,135,84,0.08);
206
+ --chart-blue: #60a5fa;
207
+ --chart-orange: #f59e0b;
208
+ --chart-purple: #a78bfa;
209
+ --chart-pink: #f472b6;
210
+ --chart-green: #34d399;
211
+ --chart-blue-bg: rgba(96,165,250,0.06);
212
+ --chart-orange-bg: rgba(245,158,11,0.06);
213
+ }
214
+ """),
215
+
216
+ # Sidebar + main content
217
  rx.hstack(
218
  sidebar(),
219
  rx.box(
 
227
  width="100%",
228
  ),
229
 
230
+ # Global styles
231
  font_family=SANS,
232
  background=BG,
233
  color=TEXT,
 
241
  ),
242
  },
243
  )
 
 
244
  # ── App ───────────────────────────────────────────────────────────────────────
245
 
246
  app = rx.App(
ticketiq/ticketiq/ui.py CHANGED
@@ -2,21 +2,31 @@
2
  import reflex as rx
3
 
4
  # ── palette ───────────────────────────────────────────────────────────────────
5
- BG = "#0a0a0b"
6
- SURFACE = "#141418"
7
- BORDER = "#2a2a36"
8
- BORDER_HI= "#2e2e38"
9
- TEXT = "#f0f0f5"
10
- MUTED = "#9090a8"
11
- ACCENT = "#f5a623"
12
- ACCENT_BG= "rgba(245,166,35,0.10)"
13
-
14
- HIGH = "#ef4444"
15
- HIGH_BG = "rgba(239,68,68,0.10)"
16
- MED = "#f5a623"
17
- MED_BG = "rgba(245,166,35,0.10)"
18
- LOW_C = "#22c55e"
19
- LOW_BG = "rgba(34,197,94,0.10)"
 
 
 
 
 
 
 
 
 
 
20
 
21
  MONO = "'JetBrains Mono', monospace"
22
  SANS = "'DM Sans', sans-serif"
 
2
  import reflex as rx
3
 
4
  # ── palette ───────────────────────────────────────────────────────────────────
5
+ import reflex as rx
6
+
7
+ # ── palette ───────────────────────────────────────────────────────────────────
8
+ BG = "var(--bg)"
9
+ SURFACE = "var(--surface)"
10
+ BORDER = "var(--border)"
11
+ BORDER_HI = "var(--border-hi)"
12
+ TEXT = "var(--text)"
13
+ MUTED = "var(--muted)"
14
+ ACCENT = "var(--accent)"
15
+ ACCENT_BG = "var(--accent-bg)"
16
+ HIGH = "var(--high)"
17
+ HIGH_BG = "var(--high-bg)"
18
+ MED = "var(--med)"
19
+ MED_BG = "var(--med-bg)"
20
+ LOW_C = "var(--low)"
21
+ LOW_BG = "var(--low-bg)"
22
+
23
+ CHART_BLUE = "var(--chart-blue)"
24
+ CHART_ORANGE = "var(--chart-orange)"
25
+ CHART_PURPLE = "var(--chart-purple)"
26
+ CHART_PINK = "var(--chart-pink)"
27
+ CHART_GREEN = "var(--chart-green)"
28
+ CHART_BLUE_BG = "var(--chart-blue-bg)"
29
+ CHART_ORANGE_BG = "var(--chart-orange-bg)"
30
 
31
  MONO = "'JetBrains Mono', monospace"
32
  SANS = "'DM Sans', sans-serif"