vedana-backoffice 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vedana_backoffice/Caddyfile +17 -0
- vedana_backoffice/__init__.py +0 -0
- vedana_backoffice/components/__init__.py +0 -0
- vedana_backoffice/components/etl_graph.py +132 -0
- vedana_backoffice/components/ui_chat.py +236 -0
- vedana_backoffice/graph/__init__.py +0 -0
- vedana_backoffice/graph/build.py +169 -0
- vedana_backoffice/pages/__init__.py +0 -0
- vedana_backoffice/pages/chat.py +204 -0
- vedana_backoffice/pages/etl.py +353 -0
- vedana_backoffice/pages/eval.py +1006 -0
- vedana_backoffice/pages/jims_thread_list_page.py +894 -0
- vedana_backoffice/pages/main_dashboard.py +483 -0
- vedana_backoffice/py.typed +0 -0
- vedana_backoffice/start_services.py +39 -0
- vedana_backoffice/state.py +0 -0
- vedana_backoffice/states/__init__.py +0 -0
- vedana_backoffice/states/chat.py +368 -0
- vedana_backoffice/states/common.py +66 -0
- vedana_backoffice/states/etl.py +1590 -0
- vedana_backoffice/states/eval.py +1940 -0
- vedana_backoffice/states/jims.py +508 -0
- vedana_backoffice/states/main_dashboard.py +757 -0
- vedana_backoffice/ui.py +115 -0
- vedana_backoffice/util.py +71 -0
- vedana_backoffice/vedana_backoffice.py +23 -0
- vedana_backoffice-0.1.0.dist-info/METADATA +10 -0
- vedana_backoffice-0.1.0.dist-info/RECORD +30 -0
- vedana_backoffice-0.1.0.dist-info/WHEEL +4 -0
- vedana_backoffice-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import pprint
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import orjson as json
|
|
8
|
+
import reflex as rx
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from jims_core.db import ThreadEventDB
|
|
11
|
+
from jims_core.util import uuid7
|
|
12
|
+
|
|
13
|
+
from vedana_backoffice.states.common import get_vedana_app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ThreadEventVis:
|
|
18
|
+
event_id: str
|
|
19
|
+
created_at: datetime
|
|
20
|
+
created_at_str: str
|
|
21
|
+
event_type: str
|
|
22
|
+
role: str
|
|
23
|
+
content: str
|
|
24
|
+
tags: list[str]
|
|
25
|
+
event_data_str: str
|
|
26
|
+
technical_vts_queries: list[str]
|
|
27
|
+
technical_cypher_queries: list[str]
|
|
28
|
+
technical_models: list[tuple[str, str]]
|
|
29
|
+
vts_str: str
|
|
30
|
+
cypher_str: str
|
|
31
|
+
models_str: str
|
|
32
|
+
has_technical_info: bool
|
|
33
|
+
has_vts: bool
|
|
34
|
+
has_cypher: bool
|
|
35
|
+
has_models: bool
|
|
36
|
+
# Aggregated annotations from jims.backoffice.* events
|
|
37
|
+
visible_tags: list[str] = field(default_factory=list)
|
|
38
|
+
feedback_comments: list[dict[str, Any]] = field(default_factory=list)
|
|
39
|
+
generic_meta: bool = False
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(cls, event_id: Any, created_at: datetime, event_type: str, event_data: dict) -> "ThreadEventVis":
|
|
43
|
+
# Parse technical_info if present
|
|
44
|
+
tech: dict = event_data.get("technical_info", {})
|
|
45
|
+
has_technical_info = bool(tech)
|
|
46
|
+
|
|
47
|
+
# Extract message-like fields
|
|
48
|
+
# Role: Only comm.user_message is user; all others assistant.
|
|
49
|
+
role = "user" if event_type == "comm.user_message" else "assistant"
|
|
50
|
+
content: str = event_data.get("content", "")
|
|
51
|
+
# tags may be stored in event_data["tags"] as list[str]
|
|
52
|
+
tags_value = event_data.get("tags") # todo check fmt
|
|
53
|
+
tags: list[str] = list(tags_value or []) if isinstance(tags_value, (list, tuple)) else []
|
|
54
|
+
|
|
55
|
+
vts_queries: list[str] = list(tech.get("vts_queries", []) or [])
|
|
56
|
+
cypher_queries: list[str] = list(tech.get("cypher_queries", []) or [])
|
|
57
|
+
|
|
58
|
+
model_stats = tech.get("model_stats", {}) or tech.get("model_used", {}) or {}
|
|
59
|
+
models_list: list[tuple[str, str]] = []
|
|
60
|
+
try:
|
|
61
|
+
# If nested dict like {model: {...}} flatten to stringified value
|
|
62
|
+
for mk, mv in model_stats.items() if isinstance(model_stats, dict) else []:
|
|
63
|
+
try:
|
|
64
|
+
models_list.append((f'"{mk}"', json.dumps(mv, option=json.OPT_INDENT_2).decode()))
|
|
65
|
+
except Exception:
|
|
66
|
+
models_list.append((f'"{mk}"', str(mv)))
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
vts_str = "\n".join(vts_queries)
|
|
71
|
+
cypher_str = "\n".join([pprint.pformat(x)[1:-1].replace("'", "") for x in cypher_queries]) # format to fit
|
|
72
|
+
models_str = "\n".join([f"{k}: {v}" for k, v in models_list])
|
|
73
|
+
|
|
74
|
+
# Show meta (event_data) for events that are NOT comm.* and NOT rag.query_processed
|
|
75
|
+
generic_meta = False
|
|
76
|
+
if not event_type.startswith("comm.") and event_type != "rag.query_processed":
|
|
77
|
+
generic_meta = True
|
|
78
|
+
|
|
79
|
+
return cls(
|
|
80
|
+
event_id=str(event_id),
|
|
81
|
+
created_at=created_at.replace(microsecond=0),
|
|
82
|
+
created_at_str=datetime.strftime(created_at, "%Y-%m-%d %H:%M:%S"),
|
|
83
|
+
event_type=event_type,
|
|
84
|
+
role=role,
|
|
85
|
+
content=content,
|
|
86
|
+
tags=tags,
|
|
87
|
+
event_data_str=json.dumps(event_data, option=json.OPT_INDENT_2).decode(),
|
|
88
|
+
technical_vts_queries=vts_queries,
|
|
89
|
+
technical_cypher_queries=cypher_queries,
|
|
90
|
+
technical_models=models_list,
|
|
91
|
+
vts_str=vts_str,
|
|
92
|
+
cypher_str=cypher_str,
|
|
93
|
+
models_str=models_str,
|
|
94
|
+
has_technical_info=has_technical_info,
|
|
95
|
+
has_vts=bool(vts_queries),
|
|
96
|
+
has_cypher=bool(cypher_queries),
|
|
97
|
+
has_models=bool(models_list),
|
|
98
|
+
visible_tags=list(tags),
|
|
99
|
+
feedback_comments=[],
|
|
100
|
+
generic_meta=generic_meta,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ThreadViewState(rx.State):
|
|
105
|
+
loading: bool = True
|
|
106
|
+
events: list[ThreadEventVis] = []
|
|
107
|
+
new_tag_text: str = ""
|
|
108
|
+
note_text: str = ""
|
|
109
|
+
note_severity: str = "Low"
|
|
110
|
+
note_text_by_event: dict[str, str] = {}
|
|
111
|
+
note_severity_by_event: dict[str, str] = {}
|
|
112
|
+
selected_thread_id: str = ""
|
|
113
|
+
expanded_event_id: str = ""
|
|
114
|
+
tag_dialog_open_for_event: str = ""
|
|
115
|
+
selected_tags_for_event: dict[str, list[str]] = {}
|
|
116
|
+
new_tag_text_for_event: dict[str, str] = {}
|
|
117
|
+
available_tags: list[str] = []
|
|
118
|
+
|
|
119
|
+
async def _reload(self) -> None:
|
|
120
|
+
vedana_app = await get_vedana_app()
|
|
121
|
+
|
|
122
|
+
async with vedana_app.sessionmaker() as session:
|
|
123
|
+
stmt = (
|
|
124
|
+
sa.select(ThreadEventDB)
|
|
125
|
+
.where(
|
|
126
|
+
ThreadEventDB.thread_id == self.selected_thread_id,
|
|
127
|
+
)
|
|
128
|
+
.order_by(ThreadEventDB.created_at.asc())
|
|
129
|
+
)
|
|
130
|
+
all_events = (await session.execute(stmt)).scalars().all()
|
|
131
|
+
|
|
132
|
+
# Split base convo events vs backoffice annotations
|
|
133
|
+
base_events: list[Any] = []
|
|
134
|
+
backoffice_events: list[Any] = []
|
|
135
|
+
for ev in all_events:
|
|
136
|
+
etype = str(getattr(ev, "event_type", ""))
|
|
137
|
+
if etype.startswith("jims.backoffice."):
|
|
138
|
+
backoffice_events.append(ev)
|
|
139
|
+
elif etype.startswith("jims."):
|
|
140
|
+
# ignore other jims.* noise
|
|
141
|
+
continue
|
|
142
|
+
else:
|
|
143
|
+
base_events.append(ev)
|
|
144
|
+
|
|
145
|
+
# Prepare aggregations
|
|
146
|
+
# 1) Tags per original event
|
|
147
|
+
tags_by_event: dict[str, set[str]] = {}
|
|
148
|
+
for ev in base_events:
|
|
149
|
+
eid = str(getattr(ev, "event_id", ""))
|
|
150
|
+
try:
|
|
151
|
+
base_tags = getattr(ev, "event_data", {}).get("tags") or []
|
|
152
|
+
tags_by_event[eid] = set([str(t) for t in base_tags])
|
|
153
|
+
except Exception:
|
|
154
|
+
tags_by_event[eid] = set()
|
|
155
|
+
|
|
156
|
+
# Apply tag add/remove in chronological order
|
|
157
|
+
for ev in backoffice_events:
|
|
158
|
+
etype = str(getattr(ev, "event_type", ""))
|
|
159
|
+
edata = dict(getattr(ev, "event_data", {}) or {})
|
|
160
|
+
if etype == "jims.backoffice.tag_added":
|
|
161
|
+
tid = str(edata.get("target_event_id", ""))
|
|
162
|
+
tag = str(edata.get("tag", "")).strip()
|
|
163
|
+
if tid:
|
|
164
|
+
tags_by_event.setdefault(tid, set()).add(tag)
|
|
165
|
+
elif etype == "jims.backoffice.tag_removed":
|
|
166
|
+
tid = str(edata.get("target_event_id", ""))
|
|
167
|
+
tag = str(edata.get("tag", "")).strip()
|
|
168
|
+
if tid and tag:
|
|
169
|
+
try:
|
|
170
|
+
tags_by_event.setdefault(tid, set()).discard(tag)
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
# 2) Comments per original event + status mapping
|
|
175
|
+
comments_by_event: dict[str, list[dict[str, Any]]] = {}
|
|
176
|
+
# status by comment id (event_id of feedback)
|
|
177
|
+
comment_status: dict[str, str] = {}
|
|
178
|
+
for ev in backoffice_events:
|
|
179
|
+
etype = str(getattr(ev, "event_type", ""))
|
|
180
|
+
if etype == "jims.backoffice.feedback":
|
|
181
|
+
edata = dict(getattr(ev, "event_data", {}) or {})
|
|
182
|
+
target = str(edata.get("event_id", ""))
|
|
183
|
+
if not target:
|
|
184
|
+
continue
|
|
185
|
+
note_text = str(edata.get("note", ""))
|
|
186
|
+
severity = str(edata.get("severity", "Low"))
|
|
187
|
+
created_at = getattr(ev, "created_at", datetime.utcnow()).replace(microsecond=0)
|
|
188
|
+
comments_by_event.setdefault(target, []).append(
|
|
189
|
+
{
|
|
190
|
+
"id": str(getattr(ev, "event_id", "")),
|
|
191
|
+
"note": note_text,
|
|
192
|
+
"severity": severity,
|
|
193
|
+
"created_at": datetime.strftime(created_at, "%Y-%m-%d %H:%M:%S"),
|
|
194
|
+
"status": "open",
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
elif etype in ("jims.backoffice.comment_resolved", "jims.backoffice.comment_closed"):
|
|
198
|
+
ed = dict(getattr(ev, "event_data", {}) or {})
|
|
199
|
+
cid = str(ed.get("comment_id", ""))
|
|
200
|
+
if not cid:
|
|
201
|
+
continue
|
|
202
|
+
comment_status[cid] = "resolved" if etype.endswith("comment_resolved") else "closed"
|
|
203
|
+
|
|
204
|
+
# Convert base events into visual items and attach aggregations
|
|
205
|
+
ev_items: list[ThreadEventVis] = []
|
|
206
|
+
for bev in base_events:
|
|
207
|
+
item = ThreadEventVis.create(
|
|
208
|
+
event_id=bev.event_id,
|
|
209
|
+
created_at=bev.created_at,
|
|
210
|
+
event_type=bev.event_type,
|
|
211
|
+
event_data=bev.event_data,
|
|
212
|
+
)
|
|
213
|
+
eid = item.event_id
|
|
214
|
+
try:
|
|
215
|
+
item.visible_tags = sorted(list(tags_by_event.get(eid, set())))
|
|
216
|
+
except Exception:
|
|
217
|
+
item.visible_tags = list(item.tags or [])
|
|
218
|
+
try:
|
|
219
|
+
cmts = []
|
|
220
|
+
for c in comments_by_event.get(eid, []) or []:
|
|
221
|
+
c = dict(c)
|
|
222
|
+
cid = str(c.get("id", ""))
|
|
223
|
+
if cid in comment_status:
|
|
224
|
+
c["status"] = comment_status[cid]
|
|
225
|
+
cmts.append(c)
|
|
226
|
+
item.feedback_comments = cmts
|
|
227
|
+
except Exception:
|
|
228
|
+
item.feedback_comments = []
|
|
229
|
+
ev_items.append(item)
|
|
230
|
+
|
|
231
|
+
# Present in chronological order as originally shown (created_at asc)
|
|
232
|
+
self.events = ev_items
|
|
233
|
+
|
|
234
|
+
# Collect all available tags from all threads
|
|
235
|
+
all_tags: set[str] = set()
|
|
236
|
+
# From current thread
|
|
237
|
+
for tags_set in tags_by_event.values():
|
|
238
|
+
all_tags.update(tags_set)
|
|
239
|
+
|
|
240
|
+
# From all threads in the database
|
|
241
|
+
try:
|
|
242
|
+
async with vedana_app.sessionmaker() as session:
|
|
243
|
+
# Query all tag_added events to get all tags ever used
|
|
244
|
+
tag_stmt = sa.select(ThreadEventDB.event_data).where(
|
|
245
|
+
ThreadEventDB.event_type == "jims.backoffice.tag_added"
|
|
246
|
+
)
|
|
247
|
+
tag_results = (await session.execute(tag_stmt)).scalars().all()
|
|
248
|
+
for edata in tag_results:
|
|
249
|
+
try:
|
|
250
|
+
ed = dict(edata or {})
|
|
251
|
+
tag = str(ed.get("tag", "")).strip()
|
|
252
|
+
if tag:
|
|
253
|
+
all_tags.add(tag)
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
self.available_tags = sorted(list(all_tags))
|
|
260
|
+
self.loading = False
|
|
261
|
+
|
|
262
|
+
@rx.event
|
|
263
|
+
async def get_data(self):
|
|
264
|
+
await self._reload()
|
|
265
|
+
|
|
266
|
+
@rx.event
|
|
267
|
+
async def select_thread(self, thread_id: str) -> None:
|
|
268
|
+
self.selected_thread_id = thread_id
|
|
269
|
+
await self._reload()
|
|
270
|
+
|
|
271
|
+
# UI field updates
|
|
272
|
+
@rx.event
|
|
273
|
+
def set_new_tag_text(self, value: str) -> None:
|
|
274
|
+
self.new_tag_text = value
|
|
275
|
+
|
|
276
|
+
@rx.event
|
|
277
|
+
def set_note_text(self, value: str) -> None:
|
|
278
|
+
self.note_text = value
|
|
279
|
+
|
|
280
|
+
@rx.event
|
|
281
|
+
def set_note_severity(self, value: str) -> None:
|
|
282
|
+
self.note_severity = value
|
|
283
|
+
|
|
284
|
+
@rx.event
|
|
285
|
+
def toggle_details(self, event_id: str) -> None:
|
|
286
|
+
self.expanded_event_id = "" if self.expanded_event_id == event_id else event_id
|
|
287
|
+
|
|
288
|
+
# Per-message note editing
|
|
289
|
+
# todo check if necessary or just keep jims sending only
|
|
290
|
+
@rx.event
|
|
291
|
+
def set_note_text_for(self, value: str, event_id: str) -> None:
|
|
292
|
+
self.note_text_by_event[event_id] = value
|
|
293
|
+
|
|
294
|
+
@rx.event
|
|
295
|
+
def set_note_severity_for(self, value: str, event_id: str) -> None:
|
|
296
|
+
self.note_severity_by_event[event_id] = value
|
|
297
|
+
|
|
298
|
+
# Tag dialog management
|
|
299
|
+
@rx.event
|
|
300
|
+
def open_tag_dialog(self, event_id: str) -> None:
|
|
301
|
+
"""Open tag dialog for a specific event and initialize selected tags with current tags."""
|
|
302
|
+
self.tag_dialog_open_for_event = event_id
|
|
303
|
+
# Initialize selected tags with current visible tags for this event
|
|
304
|
+
current_event = next((e for e in self.events if e.event_id == event_id), None)
|
|
305
|
+
if current_event:
|
|
306
|
+
self.selected_tags_for_event[event_id] = list(current_event.visible_tags or [])
|
|
307
|
+
else:
|
|
308
|
+
self.selected_tags_for_event[event_id] = []
|
|
309
|
+
|
|
310
|
+
@rx.event
|
|
311
|
+
def close_tag_dialog(self) -> None:
|
|
312
|
+
"""Close tag dialog and clear temporary state."""
|
|
313
|
+
event_id = self.tag_dialog_open_for_event
|
|
314
|
+
self.tag_dialog_open_for_event = ""
|
|
315
|
+
# Optionally clear temporary state
|
|
316
|
+
if event_id in self.new_tag_text_for_event:
|
|
317
|
+
del self.new_tag_text_for_event[event_id]
|
|
318
|
+
|
|
319
|
+
@rx.event
|
|
320
|
+
def handle_tag_dialog_open_change(self, is_open: bool) -> None:
|
|
321
|
+
"""Handle dialog open/close state changes."""
|
|
322
|
+
if not is_open:
|
|
323
|
+
self.close_tag_dialog() # type: ignore[operator]
|
|
324
|
+
|
|
325
|
+
@rx.event
|
|
326
|
+
def set_new_tag_text_for_event(self, value: str, event_id: str) -> None:
|
|
327
|
+
"""Set new tag text for a specific event."""
|
|
328
|
+
self.new_tag_text_for_event[event_id] = value
|
|
329
|
+
|
|
330
|
+
@rx.event
|
|
331
|
+
def toggle_tag_selection_for_event(self, tag: str, event_id: str, checked: bool) -> None:
|
|
332
|
+
"""Toggle tag selection for a specific event."""
|
|
333
|
+
selected = self.selected_tags_for_event.get(event_id, [])
|
|
334
|
+
if checked:
|
|
335
|
+
if tag not in selected:
|
|
336
|
+
self.selected_tags_for_event[event_id] = [*selected, tag]
|
|
337
|
+
else:
|
|
338
|
+
self.selected_tags_for_event[event_id] = [t for t in selected if t != tag]
|
|
339
|
+
|
|
340
|
+
@rx.event
|
|
341
|
+
async def add_new_tag_to_available(self, event_id: str) -> None:
|
|
342
|
+
"""Add a new tag to available tags list."""
|
|
343
|
+
new_tag = (self.new_tag_text_for_event.get(event_id) or "").strip()
|
|
344
|
+
if new_tag and new_tag not in self.available_tags:
|
|
345
|
+
self.available_tags = sorted([*self.available_tags, new_tag])
|
|
346
|
+
# Also add to selected tags for this event
|
|
347
|
+
selected = self.selected_tags_for_event.get(event_id, [])
|
|
348
|
+
if new_tag not in selected:
|
|
349
|
+
self.selected_tags_for_event[event_id] = [*selected, new_tag]
|
|
350
|
+
# Clear the input
|
|
351
|
+
self.new_tag_text_for_event[event_id] = ""
|
|
352
|
+
|
|
353
|
+
@rx.event
|
|
354
|
+
async def apply_tags_to_event(self, event_id: str):
|
|
355
|
+
"""Apply selected tags to an event by adding/removing tags as needed."""
|
|
356
|
+
current_event = next((e for e in self.events if e.event_id == event_id), None)
|
|
357
|
+
if not current_event:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
current_tags = set(current_event.visible_tags or [])
|
|
361
|
+
selected_tags = set(self.selected_tags_for_event.get(event_id, []))
|
|
362
|
+
|
|
363
|
+
tags_to_add = selected_tags - current_tags
|
|
364
|
+
tags_to_remove = current_tags - selected_tags
|
|
365
|
+
|
|
366
|
+
vedana_app = await get_vedana_app()
|
|
367
|
+
async with vedana_app.sessionmaker() as session:
|
|
368
|
+
thread_uuid = (
|
|
369
|
+
UUID(self.selected_thread_id) if isinstance(self.selected_thread_id, str) else self.selected_thread_id
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
for tag in tags_to_add:
|
|
373
|
+
tag_event = ThreadEventDB(
|
|
374
|
+
thread_id=thread_uuid,
|
|
375
|
+
event_id=uuid7(),
|
|
376
|
+
event_type="jims.backoffice.tag_added",
|
|
377
|
+
event_data={"target_event_id": event_id, "tag": tag},
|
|
378
|
+
)
|
|
379
|
+
session.add(tag_event)
|
|
380
|
+
await session.flush()
|
|
381
|
+
|
|
382
|
+
for tag in tags_to_remove:
|
|
383
|
+
tag_event = ThreadEventDB(
|
|
384
|
+
thread_id=thread_uuid,
|
|
385
|
+
event_id=uuid7(),
|
|
386
|
+
event_type="jims.backoffice.tag_removed",
|
|
387
|
+
event_data={"target_event_id": event_id, "tag": tag},
|
|
388
|
+
)
|
|
389
|
+
session.add(tag_event)
|
|
390
|
+
await session.flush()
|
|
391
|
+
|
|
392
|
+
await session.commit()
|
|
393
|
+
|
|
394
|
+
# Close dialog and reload
|
|
395
|
+
self.tag_dialog_open_for_event = ""
|
|
396
|
+
await self._reload()
|
|
397
|
+
try:
|
|
398
|
+
# local import to avoid cycles
|
|
399
|
+
from vedana_backoffice.pages.jims_thread_list_page import ThreadListState
|
|
400
|
+
|
|
401
|
+
yield ThreadListState.get_data() # type: ignore[operator]
|
|
402
|
+
except Exception:
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
@rx.event
|
|
406
|
+
async def remove_tag(self, event_id: str, tag: str):
|
|
407
|
+
vedana_app = await get_vedana_app()
|
|
408
|
+
async with vedana_app.sessionmaker() as session:
|
|
409
|
+
tag_event = ThreadEventDB(
|
|
410
|
+
thread_id=self.selected_thread_id,
|
|
411
|
+
event_id=uuid7(),
|
|
412
|
+
event_type="jims.backoffice.tag_removed",
|
|
413
|
+
event_data={"target_event_id": event_id, "tag": tag},
|
|
414
|
+
)
|
|
415
|
+
session.add(tag_event)
|
|
416
|
+
await session.commit()
|
|
417
|
+
await self._reload()
|
|
418
|
+
try:
|
|
419
|
+
from vedana_backoffice.pages.jims_thread_list_page import ThreadListState # local import to avoid cycles
|
|
420
|
+
|
|
421
|
+
yield ThreadListState.get_data() # type: ignore[operator]
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
@rx.event
|
|
426
|
+
async def submit_note_for(self, event_id: str):
|
|
427
|
+
text = (self.note_text_by_event.get(event_id) or "").strip()
|
|
428
|
+
if not text:
|
|
429
|
+
return
|
|
430
|
+
# Collect current tags from the target event if present
|
|
431
|
+
try:
|
|
432
|
+
target = next((e for e in self.events if e.event_id == event_id), None)
|
|
433
|
+
tags_list = list(getattr(target, "tags", []) or []) if target is not None else []
|
|
434
|
+
except Exception:
|
|
435
|
+
tags_list = []
|
|
436
|
+
vedana_app = await get_vedana_app()
|
|
437
|
+
async with vedana_app.sessionmaker() as session:
|
|
438
|
+
severity_val = self.note_severity_by_event.get(event_id, self.note_severity or "Low") # todo check
|
|
439
|
+
note_event = ThreadEventDB(
|
|
440
|
+
thread_id=self.selected_thread_id,
|
|
441
|
+
event_id=uuid7(),
|
|
442
|
+
event_type="jims.backoffice.feedback",
|
|
443
|
+
event_data={
|
|
444
|
+
"event_id": event_id,
|
|
445
|
+
"tags": tags_list,
|
|
446
|
+
"note": text,
|
|
447
|
+
"severity": severity_val,
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
session.add(note_event)
|
|
451
|
+
await session.commit()
|
|
452
|
+
try:
|
|
453
|
+
del self.note_text_by_event[event_id]
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
try:
|
|
457
|
+
del self.note_severity_by_event[event_id]
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
await self._reload()
|
|
461
|
+
try:
|
|
462
|
+
# todo check
|
|
463
|
+
from vedana_backoffice.pages.jims_thread_list_page import ThreadListState # local import
|
|
464
|
+
|
|
465
|
+
yield ThreadListState.get_data() # type: ignore[operator]
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
# --- Comment status actions ---
|
|
470
|
+
@rx.event
|
|
471
|
+
async def mark_comment_resolved(self, comment_id: str):
|
|
472
|
+
vedana_app = await get_vedana_app()
|
|
473
|
+
async with vedana_app.sessionmaker() as session:
|
|
474
|
+
ev = ThreadEventDB(
|
|
475
|
+
thread_id=self.selected_thread_id,
|
|
476
|
+
event_id=uuid7(),
|
|
477
|
+
event_type="jims.backoffice.comment_resolved",
|
|
478
|
+
event_data={"comment_id": comment_id},
|
|
479
|
+
)
|
|
480
|
+
session.add(ev)
|
|
481
|
+
await session.commit()
|
|
482
|
+
await self._reload()
|
|
483
|
+
try:
|
|
484
|
+
from vedana_backoffice.pages.jims_thread_list_page import ThreadListState
|
|
485
|
+
|
|
486
|
+
yield ThreadListState.get_data() # type: ignore[operator]
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
@rx.event
|
|
491
|
+
async def mark_comment_closed(self, comment_id: str):
|
|
492
|
+
vedana_app = await get_vedana_app()
|
|
493
|
+
async with vedana_app.sessionmaker() as session:
|
|
494
|
+
ev = ThreadEventDB(
|
|
495
|
+
thread_id=self.selected_thread_id,
|
|
496
|
+
event_id=uuid7(),
|
|
497
|
+
event_type="jims.backoffice.comment_closed",
|
|
498
|
+
event_data={"comment_id": comment_id},
|
|
499
|
+
)
|
|
500
|
+
session.add(ev)
|
|
501
|
+
await session.commit()
|
|
502
|
+
await self._reload()
|
|
503
|
+
try:
|
|
504
|
+
from vedana_backoffice.pages.jims_thread_list_page import ThreadListState
|
|
505
|
+
|
|
506
|
+
yield ThreadListState.get_data() # type: ignore[operator]
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|