=== TASK 5 REVIEW PACKAGE ===
def _render_kb_slices_tab(files: list):
    """切片详情：本地 sidecar 扁平化列表，支持文档筛选、切片 ID 搜索与分页。"""
    page_size = 10
    doc_label = {"": tr("KB All Documents")}
    all_slices = []
    for item in files:
        doc_id = item.get("doc_id") or ""
        filename = item.get("filename") or item.get("title") or doc_id or "?"
        if doc_id:
            doc_label[doc_id] = filename
        for sl in item.get("slices") or []:
            row = dict(sl)
            row["doc_id"] = row.get("doc_id") or doc_id
            row["filename"] = filename
            all_slices.append(row)

    doc_ids = [""] + [d for d in doc_label.keys() if d]
    desired = st.session_state.get("kb_slice_filter_doc") or ""
    if desired not in doc_label:
        desired = ""
    last_applied = st.session_state.get("kb_slice_filter_applied")
    if desired != last_applied:
        st.session_state["kb_slice_doc_select"] = desired
        st.session_state["kb_slice_filter_applied"] = desired
        st.session_state["kb_slice_page"] = 1
    elif "kb_slice_doc_select" not in st.session_state:
        st.session_state["kb_slice_doc_select"] = desired

    t1, t2, t3 = st.columns([1, 2, 2])
    with t1:
        total_placeholder = st.empty()
    with t2:
        selected_doc = st.selectbox(
            tr("KB All Documents"),
            options=doc_ids,
            format_func=lambda d: doc_label.get(d, d),
            key="kb_slice_doc_select",
            label_visibility="collapsed",
        )
    with t3:
        slice_id_q = st.text_input(
            tr("KB Search Slice Id"),
            key="kb_slice_id_filter",
            placeholder=tr("KB Search Slice Id"),
            label_visibility="collapsed",
        )

    if selected_doc != st.session_state.get("kb_slice_filter_doc"):
        st.session_state["kb_slice_filter_doc"] = selected_doc
        st.session_state["kb_slice_filter_applied"] = selected_doc
        st.session_state["kb_slice_page"] = 1

    filtered = all_slices
    if selected_doc:
        filtered = [s for s in filtered if s.get("doc_id") == selected_doc]
    needle = (slice_id_q or "").strip().lower()
    if needle:
        filtered = [
            s
            for s in filtered
            if needle in str(s.get("point_id") or "").lower()
        ]

    total = len(filtered)
    total_placeholder.caption(tr("KB Total Slices").format(n=total))

    total_pages = max(1, (total + page_size - 1) // page_size)
    page = int(st.session_state.get("kb_slice_page") or 1)
    if page > total_pages:
        page = total_pages
        st.session_state["kb_slice_page"] = page
    if page < 1:
        page = 1
        st.session_state["kb_slice_page"] = page

    start = (page - 1) * page_size
    page_slices = filtered[start : start + page_size]

    nav1, nav2, nav3 = st.columns([1, 2, 1])
    with nav1:
        if st.button(
            tr("KB Prev Page"),
            key="kb_slice_prev",
            disabled=page <= 1,
            width="stretch",
        ):
            st.session_state["kb_slice_page"] = page - 1
            st.rerun()
    with nav2:
        st.caption(tr("KB Page").format(page=page))
    with nav3:
        if st.button(
            tr("KB Next Page"),
            key="kb_slice_next",
            disabled=page >= total_pages,
            width="stretch",
        ):
            st.session_state["kb_slice_page"] = page + 1
            st.rerun()

    if not page_slices:
        st.info(tr("No Knowledge Base Videos"))
        return

    for idx, sl in enumerate(page_slices):
        img_col, meta_col = st.columns([1, 2])
        with img_col:
            keyframe = sl.get("keyframe_url") or ""
            if keyframe:
                try:
                    st.image(keyframe, use_container_width=True)
                except Exception:
                    pass
        with meta_col:
            start_ms = sl.get("start_ms")
            end_ms = sl.get("end_ms")
            st.markdown(
                f"**{_kb_format_ms(start_ms)} — {_kb_format_ms(end_ms)}**"
            )
            summary = (sl.get("summary") or "").strip()
            st.write(summary if summary else tr("KB No Speech"))
            st.caption(sl.get("filename") or "")
            point_id = sl.get("point_id") or ""
            if point_id:
                st.caption(tr("KB Slice Id"))
                st.code(point_id, language=None)
        if idx < len(page_slices) - 1:
            st.divider()


def _render_kb_search_tab(api_base: str, files: list):
    """知识检索：调用远程 POST /search，展示带分数的召回结果。"""
    import time as _time

    _ = files  # 仅远程检索，不走本地全文

    q = st.text_input(
        tr("KB Search Placeholder"),
        key="kb_search_q",
        placeholder=tr("KB Search Placeholder"),
    )
    c1, c2 = st.columns([1, 3])
    with c1:
        limit = st.number_input(
            "limit",
            min_value=1,
            max_value=50,
            value=10,
            key="kb_search_limit",
        )
    with c2:
        run = st.button(tr("KB Run Search"), type="primary", width="stretch")

    if run and (q or "").strip():
        t0 = _time.time()
        try:
            r = requests.post(
                f"{api_base}/search",
                json={"query": q.strip(), "limit": int(limit)},
                timeout=60,
            )
            elapsed = _time.time() - t0
            if r.status_code == 200:
                data = r.json().get("data") or {}
                hits = data.get("hits") or []
                st.session_state["kb_search_hits"] = hits
                st.session_state["kb_search_count"] = data.get("count", len(hits))
                st.session_state["kb_search_elapsed"] = elapsed
                st.session_state["kb_search_query"] = q.strip()
            else:
                st.error(r.text)
                st.session_state["kb_search_hits"] = []
                st.session_state["kb_search_count"] = 0
                st.session_state["kb_search_elapsed"] = elapsed
        except Exception as exc:
            st.error(str(exc))
            st.session_state["kb_search_hits"] = []
            st.session_state["kb_search_count"] = 0
            st.session_state["kb_search_elapsed"] = _time.time() - t0

    hits = st.session_state.get("kb_search_hits")
    if hits is None:
        return

    st.subheader(tr("KB Search Results"))
    st.caption(
        tr("KB Search Meta").format(
            count=st.session_state.get("kb_search_count", len(hits)),
            seconds=f"{st.session_state.get('kb_search_elapsed', 0):.2f}",
        )
    )
    if not hits:
        st.info(tr("No Knowledge Base Videos"))
        return

    for n, hit in enumerate(hits, start=1):
        img_col, meta_col = st.columns([1, 2])
        with img_col:
            keyframe = hit.get("keyframe_url") or ""
            if keyframe:
                try:
                    st.image(keyframe, use_container_width=True)
                except Exception:
                    pass
        with meta_col:
            st.markdown(
                f"**{_kb_format_ms(hit.get('start_ms'))} — "
                f"{_kb_format_ms(hit.get('end_ms'))}**"
            )
            summary = (hit.get("summary") or "").strip()
            st.write(summary if summary else tr("KB No Speech"))
            if hit.get("filename"):
                st.caption(hit["filename"])
            score = hit.get("score")
            if score is not None:
                try:
                    score_txt = f"{float(score):.4f}"
                except (TypeError, ValueError):
                    score_txt = str(score)
                st.caption(tr("KB Recall Score").format(score=score_txt, n=n))
            point_id = hit.get("point_id") or ""
            if point_id:
                st.caption(tr("KB Slice Id"))
                st.code(point_id, language=None)
        if n < len(hits):
            st.divider()




=== test file assertions ===
import json
import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent.parent))


class TestVolcanoKBWebUI(unittest.TestCase):
    def _main_py_text(self):
        return (Path(__file__).parent.parent.parent / "webui" / "Main.py").read_text(
            encoding="utf-8"
        )

    def test_video_sources_includes_volcano_kb(self):
        text = self._main_py_text()
        self.assertIn('"volcano_kb"', text)
        self.assertIn('tr("Volcano Knowledge Base")', text)

    def test_validation_accepts_volcano_kb(self):
        text = self._main_py_text()
        self.assertIn(
            '"pexels", "pixabay", "coverr", "local", "volcano_kb"', text
        )

    def test_kb_view_function_defined(self):
        text = self._main_py_text()
        self.assertIn("def _render_volcano_kb_view():", text)
        self.assertIn("def _render_main_nav()", text)
        self.assertIn("volcano_kb_service.is_enabled()", text)
        self.assertIn('key="main_nav"', text)
        self.assertIn('initial_sidebar_state="collapsed"', text)
        self.assertNotIn("st.sidebar.container(key=\"main_nav\")", text)
        self.assertNotIn("st.sidebar.radio(", text)

    def test_kb_tab_session_contract(self):
        text = self._main_py_text()
        self.assertIn('tab_ids = ["documents", "slices", "search", "qa"]', text)
        for tab_id in ("documents", "slices", "search", "qa"):
            self.assertIn(f'"{tab_id}"', text)
        for session_key in ("kb_force_slices", "kb_slice_filter_doc", "kb_console_tab"):
            self.assertIn(f'"{session_key}"', text)
        self.assertIn('key="kb_console_tab"', text)
        self.assertIn("st.session_state.pop(\"kb_force_slices\"", text)
        for fn in (
            "def _render_kb_documents_tab",
            "def _kb_format_ms",
            "def _kb_status_label",
            "def _render_kb_slices_tab",
            "def _render_kb_search_tab",
        ):
            self.assertIn(fn, text)
        self.assertIn('if choice == "documents":', text)
        self.assertIn('elif choice == "slices":', text)
        self.assertIn('elif choice == "search":', text)
        self.assertIn('st.info(tr("KB QA Coming Soon"))', text)
        for i18n_key in (
            "KB Tab Documents",
            "KB Tab Slices",
            "KB Tab Search",
            "KB Tab QA",
            "KB View Slices",
            "KB QA Coming Soon",
        ):
            self.assertIn(f'tr("{i18n_key}")', text)

    def test_zh_json_has_keys(self):
        data = json.loads(
            (Path(__file__).parent.parent.parent / "webui" / "i18n" / "zh.json").read_text(
                encoding="utf-8"
            )
        )
        table = data.get("Translation", data)
        for key in [
            "Volcano Knowledge Base",
            "Upload Videos",
            "Upload and Slice",
            "Uploaded Videos",
            "Volcano KB Not Configured",
            "View",
            "Generate",
            "Retry",
            "Delete",
            "KB Tab Documents",
            "KB Tab Slices",
            "KB Tab Search",
            "KB Tab QA",
            "KB QA Coming Soon",
            "KB View Slices",
            "KB Upload Document",
            "KB Search Filename",
        ]:
            self.assertIn(key, table)

    def test_en_json_has_keys(self):
        data = json.loads(
            (Path(__file__).parent.parent.parent / "webui" / "i18n" / "en.json").read_text(
                encoding="utf-8"
            )
        )
        table = data.get("Translation", data)
        for key in [
            "Volcano Knowledge Base",
            "Volcano KB Not Configured",
            "Upload and Slice",
            "KB Tab Documents",
            "KB Tab Slices",
            "KB Tab Search",
            "KB Tab QA",
            "KB QA Coming Soon",
            "KB View Slices",
        ]:
            self.assertIn(key, table)

    def test_slices_tab_pagination_and_empty_summary(self):
        text = self._main_py_text()
        self.assertIn("def _render_kb_slices_tab(files: list):", text)
        self.assertIn("page_size = 10", text)
        self.assertIn('tr("KB Prev Page")', text)
        self.assertIn('tr("KB Next Page")', text)
        self.assertIn('tr("KB Page")', text)
        self.assertIn('tr("KB Total Slices")', text)
        self.assertIn('tr("KB All Documents")', text)
        self.assertIn('tr("KB Search Slice Id")', text)
        self.assertIn('tr("KB No Speech")', text)
        self.assertIn('st.session_state.get("kb_slice_filter_doc")', text)
        self.assertIn("_kb_format_ms", text)

    def test_search_tab_uses_remote_endpoint(self):
        text = self._main_py_text()
        self.assertIn("def _render_kb_search_tab(api_base: str, files: list):", text)
        self.assertIn('f"{api_base}/search"', text)
        self.assertIn('tr("KB Run Search")', text)
        self.assertIn('tr("KB Search Results")', text)
        self.assertIn('tr("KB Search Meta")', text)
        self.assertIn('tr("KB Recall Score")', text)
        self.assertIn('tr("KB Search Placeholder")', text)
        self.assertIn('"query"', text)
        self.assertIn('"limit"', text)
        self.assertIn("kb_search_hits", text)
        self.assertIn("kb_search_elapsed", text)


if __name__ == "__main__":
    unittest.main()
