04/23 Enhance app functionalities 2
This commit is contained in:
+284
-4
@@ -217,9 +217,17 @@ class AiSummaryView(ttk.Frame):
|
||||
|
||||
self._build_criteria_panel()
|
||||
|
||||
pane = tk.PanedWindow(self, orient="horizontal",
|
||||
# Notebook — Analyze tab (existing layout) + History tab (new)
|
||||
self._nb = ttk.Notebook(self)
|
||||
self._nb.pack(fill="both", expand=True, pady=(0, 6))
|
||||
|
||||
# ── Analyze tab ───────────────────────────────────────────────────────
|
||||
analyze_tab = tk.Frame(self._nb, bg=C["bg"])
|
||||
self._nb.add(analyze_tab, text="✨ Analyze")
|
||||
|
||||
pane = tk.PanedWindow(analyze_tab, orient="horizontal",
|
||||
bg=C["border"], sashwidth=4, sashrelief="flat")
|
||||
pane.pack(fill="both", expand=True, pady=(0, 6))
|
||||
pane.pack(fill="both", expand=True)
|
||||
|
||||
left = tk.Frame(pane, bg=C["bg"])
|
||||
pane.add(left, minsize=260, width=300)
|
||||
@@ -229,6 +237,12 @@ class AiSummaryView(ttk.Frame):
|
||||
pane.add(right, minsize=350)
|
||||
self._build_output_panel(right)
|
||||
|
||||
# ── History tab ───────────────────────────────────────────────────────
|
||||
history_tab = tk.Frame(self._nb, bg=C["bg"])
|
||||
self._nb.add(history_tab, text="🕑 History")
|
||||
self._build_history_panel(history_tab)
|
||||
self._nb.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
||||
|
||||
status_bar = tk.Frame(self, bg=C["surface2"], pady=4)
|
||||
status_bar.pack(fill="x", side="bottom")
|
||||
tk.Label(status_bar, textvariable=self._status_var,
|
||||
@@ -858,12 +872,24 @@ class AiSummaryView(ttk.Frame):
|
||||
)
|
||||
full_output = header + result
|
||||
|
||||
self.after(0, lambda t=full_output, v=verdict: self._on_success(t, v))
|
||||
# Build artefacts for history persistence
|
||||
file_names_str = ", ".join(os.path.basename(fp) for fp in file_paths)
|
||||
criteria_snapshot = (
|
||||
"\n".join(
|
||||
f"{i+1}. {c['title']}: {c['description']}"
|
||||
for i, c in enumerate(active_criteria)
|
||||
) if active_criteria else None
|
||||
)
|
||||
|
||||
self.after(0, lambda t=full_output, v=verdict,
|
||||
fn=file_names_str, m=model, cs=criteria_snapshot:
|
||||
self._on_success(t, v, fn, m, cs))
|
||||
|
||||
except Exception as exc:
|
||||
self.after(0, lambda e=exc: self._on_error(e))
|
||||
|
||||
def _on_success(self, text: str, verdict):
|
||||
def _on_success(self, text: str, verdict, file_names: str,
|
||||
model: str, criteria_snapshot: str):
|
||||
self._stop_spinner()
|
||||
self._set_output_text(text)
|
||||
if verdict:
|
||||
@@ -876,6 +902,25 @@ class AiSummaryView(ttk.Frame):
|
||||
f"'{self.current_user.get('username')}' "
|
||||
f"({len(self._files)} file(s)). Verdict: {verdict or 'N/A'}."
|
||||
)
|
||||
# Persist to ai_analysis_log so the history panel can display it
|
||||
try:
|
||||
from models import save_ai_analysis
|
||||
save_ai_analysis(
|
||||
user_id=self.current_user["id"],
|
||||
file_names=file_names,
|
||||
model=model,
|
||||
verdict=verdict,
|
||||
criteria_snapshot=criteria_snapshot,
|
||||
summary_text=text,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[AI SUMMARY] Could not save analysis to history: {e}")
|
||||
# Refresh history panel if it exists (it is built lazily on tab switch)
|
||||
if hasattr(self, "_refresh_history"):
|
||||
try:
|
||||
self._refresh_history()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_error(self, exc: Exception):
|
||||
self._stop_spinner()
|
||||
@@ -904,6 +949,241 @@ class AiSummaryView(ttk.Frame):
|
||||
self._progress.pack_forget()
|
||||
self._run_btn.config(state="normal", text="✨ Analyze with AI")
|
||||
|
||||
# -- History panel --------------------------------------------------------
|
||||
|
||||
def _build_history_panel(self, parent):
|
||||
"""
|
||||
Build the analysis history tab.
|
||||
Admin: sees all users' analyses.
|
||||
User: sees only their own.
|
||||
Treeview columns: Date/Time, User (admin only), Files, Model, Verdict
|
||||
Double-click or View button restores the full result in the output panel.
|
||||
"""
|
||||
C = COLOURS
|
||||
|
||||
# ── Toolbar ───────────────────────────────────────────────────────────
|
||||
toolbar = tk.Frame(parent, bg=C["surface"], pady=8, padx=10)
|
||||
toolbar.pack(fill="x")
|
||||
|
||||
tk.Label(toolbar, text="📜 Analysis History",
|
||||
bg=C["surface"], fg=C["text"],
|
||||
font=FONT_BOLD).pack(side="left")
|
||||
|
||||
tk.Button(
|
||||
toolbar, text="🔍 View Result",
|
||||
command=self._history_view_selected,
|
||||
bg=C["accent"], fg=C["white"],
|
||||
activebackground=C["accent_hover"], activeforeground=C["white"],
|
||||
relief="flat", font=FONT_SMALL, cursor="hand2",
|
||||
padx=8, pady=4,
|
||||
).pack(side="right", padx=(4, 0))
|
||||
|
||||
tk.Button(
|
||||
toolbar, text="↻ Refresh",
|
||||
command=self._refresh_history,
|
||||
bg=C["surface2"], fg=C["text"],
|
||||
activebackground=C["accent"], activeforeground=C["white"],
|
||||
relief="flat", font=FONT_SMALL, cursor="hand2",
|
||||
padx=8, pady=4,
|
||||
).pack(side="right", padx=(4, 0))
|
||||
|
||||
# ── Verdict filter ────────────────────────────────────────────────────
|
||||
filter_frame = tk.Frame(parent, bg=C["surface"], padx=10, pady=4)
|
||||
filter_frame.pack(fill="x")
|
||||
|
||||
tk.Label(filter_frame, text="Filter by verdict:",
|
||||
bg=C["surface"], fg=C["text_dim"],
|
||||
font=FONT_SMALL).pack(side="left")
|
||||
|
||||
self._hist_filter_var = tk.StringVar(value="All")
|
||||
for label in ("All", "PURSUE", "PASS", "UNCLEAR", "— none —"):
|
||||
tk.Radiobutton(
|
||||
filter_frame, text=label,
|
||||
variable=self._hist_filter_var, value=label,
|
||||
command=self._apply_history_filter,
|
||||
bg=C["surface"], fg=C["text"],
|
||||
activebackground=C["surface"],
|
||||
activeforeground=C["accent"],
|
||||
selectcolor=C["surface2"],
|
||||
font=FONT_SMALL, cursor="hand2",
|
||||
).pack(side="left", padx=(8, 0))
|
||||
|
||||
# ── Treeview ──────────────────────────────────────────────────────────
|
||||
tree_frame = tk.Frame(parent, bg=C["bg"])
|
||||
tree_frame.pack(fill="both", expand=True, padx=6, pady=(4, 4))
|
||||
|
||||
if self._is_admin:
|
||||
cols = ("ID", "Date/Time", "User", "Files", "Model", "Verdict")
|
||||
widths = [40, 140, 100, 280, 160, 80]
|
||||
else:
|
||||
cols = ("ID", "Date/Time", "Files", "Model", "Verdict")
|
||||
widths = [40, 140, 360, 160, 80]
|
||||
|
||||
self._hist_tree = ttk.Treeview(
|
||||
tree_frame, columns=cols,
|
||||
show="headings", selectmode="browse",
|
||||
)
|
||||
for col, w in zip(cols, widths):
|
||||
self._hist_tree.heading(col, text=col)
|
||||
self._hist_tree.column(
|
||||
col, width=w,
|
||||
anchor="center" if col in ("ID", "Verdict") else "w",
|
||||
)
|
||||
|
||||
# Colour-code verdict rows
|
||||
self._hist_tree.tag_configure("PURSUE", foreground=C["success"])
|
||||
self._hist_tree.tag_configure("PASS", foreground=C["danger"])
|
||||
self._hist_tree.tag_configure("UNCLEAR", foreground=C["warning"])
|
||||
self._hist_tree.tag_configure("none", foreground=C["text_dim"])
|
||||
|
||||
vsb = ttk.Scrollbar(tree_frame, orient="vertical",
|
||||
command=self._hist_tree.yview)
|
||||
self._hist_tree.configure(yscrollcommand=vsb.set)
|
||||
vsb.pack(side="right", fill="y")
|
||||
self._hist_tree.pack(side="left", fill="both", expand=True)
|
||||
|
||||
self._hist_tree.bind("<Double-1>",
|
||||
lambda _: self._history_view_selected())
|
||||
|
||||
# ── Detail strip ─────────────────────────────────────────────────────
|
||||
detail_bar = tk.Frame(parent, bg=C["surface2"], pady=6, padx=10)
|
||||
detail_bar.pack(fill="x", side="bottom")
|
||||
self._hist_detail_lbl = tk.Label(
|
||||
detail_bar, text="Select a row to see details.",
|
||||
bg=C["surface2"], fg=C["text_dim"],
|
||||
font=FONT_SMALL, anchor="w", wraplength=900, justify="left",
|
||||
)
|
||||
self._hist_detail_lbl.pack(fill="x")
|
||||
self._hist_tree.bind("<<TreeviewSelect>>", self._on_history_select)
|
||||
|
||||
# Store all loaded rows for client-side filtering
|
||||
self._hist_all_rows = []
|
||||
|
||||
self._refresh_history()
|
||||
|
||||
def _refresh_history(self):
|
||||
"""Reload history from DB and repopulate the treeview."""
|
||||
try:
|
||||
from models import get_ai_analysis_history
|
||||
uid = None if self._is_admin else self.current_user["id"]
|
||||
self._hist_all_rows = get_ai_analysis_history(user_id=uid, limit=200)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not load AI analysis history: {e}")
|
||||
self._hist_all_rows = []
|
||||
self._apply_history_filter()
|
||||
|
||||
def _apply_history_filter(self):
|
||||
"""Re-populate the treeview using the active verdict filter."""
|
||||
self._hist_tree.delete(*self._hist_tree.get_children())
|
||||
verdict_filter = self._hist_filter_var.get()
|
||||
|
||||
for row in self._hist_all_rows:
|
||||
verdict = row.get("verdict") or ""
|
||||
# Map filter labels to DB values
|
||||
if verdict_filter == "All":
|
||||
pass
|
||||
elif verdict_filter == "— none —":
|
||||
if verdict:
|
||||
continue
|
||||
elif verdict != verdict_filter:
|
||||
continue
|
||||
|
||||
dt_str = str(row.get("analyzed_at", ""))[:16]
|
||||
files = (row.get("file_names") or "")[:60]
|
||||
if len(row.get("file_names") or "") > 60:
|
||||
files += "…"
|
||||
tag = verdict if verdict else "none"
|
||||
|
||||
if self._is_admin:
|
||||
values = (
|
||||
row["id"], dt_str,
|
||||
row.get("username") or "—",
|
||||
files, row.get("model") or "",
|
||||
verdict or "—",
|
||||
)
|
||||
else:
|
||||
values = (
|
||||
row["id"], dt_str,
|
||||
files, row.get("model") or "",
|
||||
verdict or "—",
|
||||
)
|
||||
|
||||
self._hist_tree.insert(
|
||||
"", "end", iid=str(row["id"]),
|
||||
tags=(tag,), values=values,
|
||||
)
|
||||
|
||||
def _on_history_select(self, event=None):
|
||||
"""Show a one-line detail strip when a history row is selected."""
|
||||
sel = self._hist_tree.selection()
|
||||
if not sel:
|
||||
return
|
||||
analysis_id = int(sel[0])
|
||||
try:
|
||||
from models import get_ai_analysis_detail
|
||||
detail = get_ai_analysis_detail(analysis_id)
|
||||
except Exception:
|
||||
return
|
||||
if not detail:
|
||||
return
|
||||
criteria_info = (
|
||||
f" | Criteria: {detail['criteria_snapshot'][:80]}…"
|
||||
if detail.get("criteria_snapshot") else
|
||||
" | No criteria evaluated"
|
||||
)
|
||||
self._hist_detail_lbl.config(
|
||||
text=(
|
||||
f"ID {detail['id']} | "
|
||||
f"{str(detail.get('analyzed_at', ''))[:16]} | "
|
||||
f"Model: {detail.get('model', '')} | "
|
||||
f"Verdict: {detail.get('verdict') or 'N/A'}"
|
||||
f"{criteria_info}"
|
||||
)
|
||||
)
|
||||
|
||||
def _history_view_selected(self):
|
||||
"""
|
||||
Load the selected history entry's full summary into the output panel
|
||||
and switch to the Analyze tab so the user can read it.
|
||||
"""
|
||||
sel = self._hist_tree.selection()
|
||||
if not sel:
|
||||
show_error("Please select an analysis to view.")
|
||||
return
|
||||
analysis_id = int(sel[0])
|
||||
try:
|
||||
from models import get_ai_analysis_detail
|
||||
detail = get_ai_analysis_detail(analysis_id)
|
||||
except Exception as e:
|
||||
show_error(f"Could not load analysis:\n{e}")
|
||||
return
|
||||
if not detail:
|
||||
show_error("Analysis record not found.")
|
||||
return
|
||||
|
||||
# Switch to Analyze tab
|
||||
self._nb.select(0)
|
||||
# Restore output
|
||||
self._set_output_text(detail["summary_text"])
|
||||
verdict = detail.get("verdict")
|
||||
if verdict:
|
||||
self._show_verdict_banner(verdict)
|
||||
else:
|
||||
self._hide_verdict_banner()
|
||||
self._set_status(
|
||||
f"Viewing history entry #{analysis_id} "
|
||||
f"({str(detail.get('analyzed_at', ''))[:16]})"
|
||||
)
|
||||
|
||||
def _on_tab_changed(self, event=None):
|
||||
"""Refresh history data whenever the user switches to the History tab."""
|
||||
try:
|
||||
current = self._nb.index(self._nb.select())
|
||||
if current == 1: # History tab is index 1
|
||||
self._refresh_history()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -- Status bar ------------------------------------------------------------
|
||||
|
||||
def _set_status(self, msg: str):
|
||||
|
||||
Reference in New Issue
Block a user