Files
HUANG Cheng ce5a7505f0 fix(chat): in-chat preview for knowledge citations (#634) (#642)
* feat(knowledge): include document path in citation markers

* feat(chat): in-chat preview for knowledge citations (#634)

Open Sources chips in a chat modal (rich with text fallback), show KB/path
in the tooltip, and deep-link View in knowledge base to the right folder.

* polish(chat): citation preview UX — tooltip, nav, download, path

Only show tooltip when nested path or truncation; match KB modal width
for markdown; hide download without original; prev/next + arrow keys;
scroll-to-top for long MD; resolve path/has_original from document list.

* polish(dashboard): open citation preview and expert file edit as right drawers

Align chat knowledge citation preview and expert workspace file edit with
the knowledge-base text editor: placement=right, shared width, footer actions.

* polish(chat): put knowledge base name inline in citation drawer title

Show filename · kbName on one line; keep nested folder path as the only subtitle.

* refactor(chat): open knowledge citations in the shared dock panel

Match “edited files” behavior: Sources chips open knowledge tabs in the
chat dock instead of a separate Ant Design drawer.

* fix(dashboard): keep nearby PDF pages mounted to avoid blank scroll canvases

Windowed react-pdf unmount/remount left blank pages while scrolling; keep
pages sticky around the current page and tighten citation dock flex height.
2026-09-10 17:47:22 +08:00

96 lines
2.4 KiB
Python

"""Unit tests for knowledge citation markers."""
from __future__ import annotations
from types import SimpleNamespace
from octop.infra.knowledge.citations import (
CITATIONS_MARKER_PREFIX,
append_citations_marker,
citations_from_ranked,
strip_citations_marker,
)
def test_citations_from_ranked_dedupes_documents() -> None:
base = SimpleNamespace(id="kb1", name="Policies")
doc_a = SimpleNamespace(id="d1", filename="a.md", path="notes/a.md")
doc_b = SimpleNamespace(id="d2", filename="b.md", path="b.md")
hit = SimpleNamespace()
ranked = [
(base, hit, doc_a),
(base, hit, doc_a),
(base, hit, doc_b),
]
assert citations_from_ranked(ranked) == [
{
"kb_id": "kb1",
"kb_name": "Policies",
"doc_id": "d1",
"filename": "a.md",
"path": "notes/a.md",
},
{
"kb_id": "kb1",
"kb_name": "Policies",
"doc_id": "d2",
"filename": "b.md",
"path": "b.md",
},
]
def test_append_citations_marker() -> None:
text = append_citations_marker(
"passages here",
[
{
"kb_id": "kb",
"kb_name": "KB",
"doc_id": "d1",
"filename": "doc.md",
"path": "doc.md",
}
],
)
assert text.startswith("passages here\n\n" + CITATIONS_MARKER_PREFIX)
assert '"filename":"doc.md"' in text
assert '"path":"doc.md"' in text
assert text.endswith("-->")
def test_append_citations_marker_skips_empty() -> None:
assert (
append_citations_marker(
"",
[
{
"kb_id": "k",
"kb_name": "n",
"doc_id": "d",
"filename": "f",
"path": "f",
}
],
)
== ""
)
assert append_citations_marker("text", []) == "text"
def test_strip_citations_marker() -> None:
raw = append_citations_marker(
"passages here",
[
{
"kb_id": "kb",
"kb_name": "KB",
"doc_id": "d1",
"filename": "doc.md",
"path": "notes/doc.md",
}
],
)
assert strip_citations_marker(raw) == "passages here"
assert strip_citations_marker("plain") == "plain"