Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gknowledge/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ class ReviewItem:
source_id: str = ""
created_at: float = field(default_factory=now)

@property
def is_question(self) -> bool:
return self.action == ReviewAction.DEEP_RESEARCH

def to_row(self) -> dict[str, Any]:
return {
"id": self.id,
Expand Down
107 changes: 91 additions & 16 deletions gknowledge/ui/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,29 +620,94 @@ def _build_review_pane(self):

def _on_review_selected(self, _list, row):
item = getattr(row, "item", None)
if not item:
if not item or not self.project:
return
self._show_detail(self.review_stack, self.review_detail,
f"# {item.title}\n\n_{item.action.value}_\n\n{item.detail}")
source = self.project.store.get_source(item.source_id) if item.source_id else None
source_section = "\n\n## Source\n"
if source:
source_section += (
f"**{source.title}**\n\n"
f"- File: `{source.rel_path}`\n"
f"- Status: {source.status.value}"
)
source_page = next(
(page for page in self.project.store.pages_for_source(source.id)
if page.kind == PageKind.SOURCE),
None,
)
if source_page and source_page.summary:
source_section += f"\n\n{source_page.summary}"
else:
source_section += "_The original source is no longer available._"
query_section = ""
if item.queries:
query_section = "\n\n## Questions / research prompts\n" + "\n".join(
f"- {query}" for query in item.queries
)
detail = (
f"# {item.title}\n\n"
f"**Type:** {item.action.value}\n\n"
f"{item.detail}"
f"{query_section}"
f"{source_section}"
)
self._show_detail(self.review_stack, self.review_detail, detail)

def _refresh_reviews(self):
_clear_listbox(self.review_list)
if not self.project:
return
items = self.project.store.list_reviews(status="open")
if not items:
self.review_list.append(_placeholder_row("No items awaiting review."))
return
for item in items:
row = Adw.ActionRow(
activatable=True,
title=GLib.markup_escape_text(item.title),
subtitle=f"{item.action.value} · {item.detail}")
btn = Gtk.Button(label="Resolve", valign=Gtk.Align.CENTER)
btn.connect("clicked", self._on_resolve, item.id)
row.add_suffix(btn)
row.item = item
self.review_list.append(row)
questions = [item for item in items if item.is_question]
needs_review = [item for item in items if not item.is_question]

self.review_list.append(_section_header_row("Questions to explore"))
if questions:
for item in questions:
row = Adw.ActionRow(
activatable=True,
title=GLib.markup_escape_text(item.title),
subtitle=item.detail,
)
ask_btn = Gtk.Button(label="Ask", valign=Gtk.Align.CENTER)
ask_btn.add_css_class("suggested-action")
ask_btn.connect("clicked", self._on_ask_review, item)
dismiss_btn = Gtk.Button(label="Dismiss", valign=Gtk.Align.CENTER)
dismiss_btn.connect("clicked", self._on_dismiss_review, item.id)
row.add_suffix(ask_btn)
row.add_suffix(dismiss_btn)
row.item = item
self.review_list.append(row)
else:
self.review_list.append(_placeholder_row("No open questions."))

self.review_list.append(_section_header_row("Needs review"))
if needs_review:
for item in needs_review:
row = Adw.ActionRow(
activatable=True,
title=GLib.markup_escape_text(item.title),
subtitle=f"{item.action.value} · {item.detail}",
)
btn = Gtk.Button(label="Resolve", valign=Gtk.Align.CENTER)
btn.connect("clicked", self._on_resolve, item.id)
row.add_suffix(btn)
row.item = item
self.review_list.append(row)
else:
self.review_list.append(_placeholder_row("Nothing needs review."))

def _on_ask_review(self, _btn, item):
question = item.queries[0] if item.queries else item.title
self.query_entry.set_text(question)
self.view_stack.set_visible_child_name("assistant")
self._on_ask()

def _on_dismiss_review(self, _btn, review_id):
self.project.store.resolve_review(review_id)
self._refresh_reviews()
self._update_counts()
self.toast("Question dismissed")

def _on_resolve(self, _btn, review_id):
self.project.store.resolve_review(review_id)
Expand Down Expand Up @@ -844,6 +909,16 @@ def _clear_box(box: Gtk.Box):
box.remove(child)


def _section_header_row(text: str) -> Gtk.ListBoxRow:
row = Gtk.ListBoxRow(activatable=False, selectable=False)
label = Gtk.Label(label=text, xalign=0, margin_start=12,
margin_top=14, margin_bottom=6)
label.add_css_class("heading")
label.add_css_class("dim-label")
row.set_child(label)
return row


def _placeholder_row(text: str) -> Gtk.ListBoxRow:
row = Gtk.ListBoxRow(activatable=False, selectable=False)
row.set_child(Gtk.Label(label=text, css_classes=["dim-label"],
Expand Down
19 changes: 18 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from gknowledge.core.ingestion import IngestionManager
from gknowledge.core.knowledge import KnowledgeWriter
from gknowledge.core.lint import ChangeDetector, stale_sources
from gknowledge.core.models import Page, PageKind, Source, SourceStatus
from gknowledge.core.models import (
Page, PageKind, ReviewAction, ReviewItem, Source, SourceStatus,
)
from gknowledge.core.project import Project, Settings
from gknowledge.core.query import QueryEngine
from gknowledge.providers import build_provider
Expand All @@ -32,6 +34,21 @@ def make_project(tmp: Path) -> Project:
return proj


def test_review_items_separate_questions_from_review_tasks():
question = ReviewItem(
title="What caused the change?",
action=ReviewAction.DEEP_RESEARCH,
queries=["What caused the change?"],
)
review_task = ReviewItem(
title="Check the proposed page",
action=ReviewAction.CREATE_PAGE,
)

assert question.is_question
assert not review_task.is_question


def _write_minimal_epub(path: Path, chapter: str) -> None:
"""Build the smallest epub a real reader would accept: no binary fixture."""
container = (
Expand Down
Loading