From ce2bd6e72997835892b43ce4395b9c8e396d167e Mon Sep 17 00:00:00 2001 From: yydawx <195621032+yydawx@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:07:59 +0800 Subject: [PATCH] feat(dashboard): add text search to the Experience list The Methods page lets users search Skills but only browse Experiences, even though the family-scoped Artifact search added in #1862 covers the family. Load the Experience collection through POST /v1/scopes/{scope_id}/artifacts/experience/search when the Methods page carries a query, and adapt each hit to the exact-revision record shape the existing card, detail and paging path already uses. An empty query keeps the cursor-based browsing, and the result window follows the Skill search so a broad query cannot render hundreds of cards at once. Page links keep the kind and the query, the clear action resets the search position, and no-match and read failures stay separate with matching Chinese and English labels. Document the search entry point on the Dashboard page-API table. --- docs/en/development/dashboard.md | 2 +- docs/zh/development/dashboard.md | 2 +- src/powercontext/server/dashboard/api.py | 30 ++++- src/powercontext/server/dashboard/content.py | 7 +- .../server/dashboard/labels.en.json | 3 + src/powercontext/server/dashboard/labels.json | 3 + .../server/dashboard/pagination.py | 6 +- .../templates/components/search.html | 2 +- .../server/dashboard/templates/methods.html | 6 +- tests/test_dashboard.py | 115 ++++++++++++++++++ 10 files changed, 163 insertions(+), 13 deletions(-) diff --git a/docs/en/development/dashboard.md b/docs/en/development/dashboard.md index 4c54ac32e3..cf1338624f 100644 --- a/docs/en/development/dashboard.md +++ b/docs/en/development/dashboard.md @@ -159,7 +159,7 @@ Use this section to locate the implementation. The code and API specification de | Scope selection | `GET /v1/scopes`, `GET /v1/scopes/default`, `GET /v1/scopes/{scope_id}` | Treat defaults, explicit selection, hierarchy and readable scopes separately | | Memory collection and text | `POST /v1/atomic-memory/list`, `POST /v1/memory/search`, exact Atomic Memory revision and state reads | Preserve Atomic Memory identities, lifecycle filters, the selected search mode and hit evidence; return up to 50 matches. The search compatibility route resolves `auto` and maps `fts` to Atomic Memory `text` | | Handoff collection and text | `GET /v1/scopes/{scope_id}/artifacts/handoff` and exact revision reads | Retain cursors; list order does not imply chronological order | -| Experience collection and text | `GET /v1/scopes/{scope_id}/artifacts/experience`, `POST /v1/experience/get` | Dashboard provides paged browsing; see [Artifact search](../docs/workflows/search-artifacts.md) for Family relevance retrieval | +| Experience collection and text | `GET /v1/scopes/{scope_id}/artifacts/experience`, `POST /v1/experience/get`, `POST /v1/scopes/{scope_id}/artifacts/experience/search` | Dashboard provides paged browsing and Family relevance search; search covers approved active revisions only, returns up to 200 matches, and suggests a narrower query at that limit | | Skill collection and text | `POST /v1/skill/library`, `POST /v1/skill/get` | Library queries return up to 200 entries; suggest a narrower query at the limit and preserve source identities | | Source material | `GET /v1/scopes/{scope_id}/sources/{source_type}/{source_id}` | Verify the source's relationship to the record; `content` is currently the source type with readable text | | Usage | `POST /v1/stats` | Read the current scope with `exact` and use Server reporting periods and statistical definitions | diff --git a/docs/zh/development/dashboard.md b/docs/zh/development/dashboard.md index 349cf80ed2..ac1c70f14c 100644 --- a/docs/zh/development/dashboard.md +++ b/docs/zh/development/dashboard.md @@ -158,7 +158,7 @@ Jinja2 组织内容与页面结构,HTMX 处理导航和片段替换。只有 | 范围选择 | `GET /v1/scopes`、`GET /v1/scopes/default`、`GET /v1/scopes/{scope_id}` | 默认值、显式选择、父子关系和可读范围分别处理 | | 记忆目录与正文 | `POST /v1/atomic-memory/list`、`POST /v1/memory/search`、Atomic Memory 精确版本与状态读取 | 保留独立制品身份、生命周期筛选、搜索模式与命中依据;最多 50 条匹配结果。搜索兼容入口解析 `auto`,并把 `fts` 映射为 Atomic Memory 的 `text` | | 交接目录与正文 | `GET /v1/scopes/{scope_id}/artifacts/handoff` 及精确版本读取 | 保留游标;列表顺序不解释为时间顺序 | -| 经验目录与正文 | `GET /v1/scopes/{scope_id}/artifacts/experience`、`POST /v1/experience/get` | Dashboard 提供分页浏览;Family 相关性检索见 [Artifact 检索](../docs/workflows/search-artifacts.md) | +| 经验目录与正文 | `GET /v1/scopes/{scope_id}/artifacts/experience`、`POST /v1/experience/get`、`POST /v1/scopes/{scope_id}/artifacts/experience/search` | Dashboard 提供分页浏览与 Family 相关性搜索;搜索只覆盖 approved 且 active 的当前版本,最多返回 200 条,达到上限时提示缩小查询 | | 技能目录与正文 | `POST /v1/skill/library`、`POST /v1/skill/get` | 库检索最多 200 项,达到上限时提示缩小查询;保留来源身份 | | 原始材料 | `GET /v1/scopes/{scope_id}/sources/{source_type}/{source_id}` | 验证材料与记录的关联;当前可读取正文的来源类型为 `content` | | 用量 | `POST /v1/stats` | 以 `exact` 读取当前范围,使用服务端周期与统计口径 | diff --git a/src/powercontext/server/dashboard/api.py b/src/powercontext/server/dashboard/api.py index 6a51f66283..4b00a95627 100644 --- a/src/powercontext/server/dashboard/api.py +++ b/src/powercontext/server/dashboard/api.py @@ -47,6 +47,8 @@ "profile": ProfileContent, } +SEARCH_LIMIT = 200 + def segment(value: str) -> str: return quote(value, safe="") @@ -154,12 +156,22 @@ async def records( self, scope: str, family: str, *, cursor: str | None = None, limit: int = 12, query: str | None = None ) -> dict[str, Any]: if family == "skill": - maximum = 1 if limit == 1 else 200 + maximum = 1 if limit == 1 else SEARCH_LIMIT result = await self.read("/v1/skill/library", {"scope_id": scope, "limit": maximum, "query": query}) return { "items": [self.family_record(family, item) for item in result["skills"]], "next_cursor": None, - "search_limited": maximum == 200 and len(result["skills"]) == maximum, + "search_limited": maximum == SEARCH_LIMIT and len(result["skills"]) == maximum, + } + if family == "experience" and query: + result = await self.read( + f"/v1/scopes/{segment(scope)}/artifacts/{segment(family)}/search", + {"query": query, "limit": SEARCH_LIMIT}, + ) + return { + "items": [self.family_record(family, self.search_record(item)) for item in result["results"]], + "next_cursor": None, + "search_limited": len(result["results"]) == SEARCH_LIMIT, } url = httpx.URL(f"/v1/scopes/{segment(scope)}/artifacts/{family}", params={"limit": limit}) if cursor: @@ -179,6 +191,20 @@ async def load(item: dict[str, Any]) -> dict[str, Any]: "next_cursor": page["next_cursor"], } + @staticmethod + def search_record(item: dict[str, Any]) -> dict[str, Any]: + """Adapt one family-scoped search hit to the exact-revision record shape.""" + + lineage = item["lineage"] + return { + "content": item["content"], + "artifact": {"artifact_id": item["artifact_id"], "revision": item["revision"]}, + "source_refs": [ + {"name": source["source_type"], "source_id": source["source_id"]} for source in lineage["sources"] + ], + "artifact_refs": lineage["artifacts"], + } + async def topic_memory_search(self, scope: str, query: str) -> dict[str, Any]: return await self.read("/v1/topic-memory/search", {"scope_id": scope, "query": query}) diff --git a/src/powercontext/server/dashboard/content.py b/src/powercontext/server/dashboard/content.py index e72deb4257..6755c62b40 100644 --- a/src/powercontext/server/dashboard/content.py +++ b/src/powercontext/server/dashboard/content.py @@ -34,16 +34,17 @@ async def load_collection(api: DashboardAPI, request: Request, ctx: dict[str, An ctx["collection_page_size"] = PAGE_SIZE try: cursor = request.query_params.get(f"{family}_cursor" if page == "methods" else "cursor") + query = ctx["search_query"] if page == "methods" else None result = await api.records( scope, family, cursor=cursor, limit=1 if page == "home" else PAGE_SIZE, - query=ctx["search_query"] if family == "skill" and page == "methods" else None, + query=query, ) if page != "home": - if family == "skill": - window = list_page(result["items"], request.query_params.get("skill_page")) + if family == "skill" or query: + window = list_page(result["items"], request.query_params.get(f"{family}_page")) result["items"] = window["items"] result["pager"] = list_links(ctx, family, window) else: diff --git a/src/powercontext/server/dashboard/labels.en.json b/src/powercontext/server/dashboard/labels.en.json index 8b045c4a12..271f5d6712 100644 --- a/src/powercontext/server/dashboard/labels.en.json +++ b/src/powercontext/server/dashboard/labels.en.json @@ -224,8 +224,11 @@ "light": "Light", "dark": "Dark", "display_preferences": "Display Settings", + "search_experiences": "Search experiences", "search_skills": "Search skills", "search": "Search", + "experiences_limit": "Showing the first 200 experiences. Search to narrow the results.", + "experiences_no_match": "No matching experiences", "skills_limit": "Showing the first 200 skills. Search to narrow the results.", "skills_no_match": "No matching skills", "pagination": "Pagination", diff --git a/src/powercontext/server/dashboard/labels.json b/src/powercontext/server/dashboard/labels.json index 0eea5445dc..80876b9349 100644 --- a/src/powercontext/server/dashboard/labels.json +++ b/src/powercontext/server/dashboard/labels.json @@ -224,8 +224,11 @@ "light": "浅色", "dark": "深色", "display_preferences": "显示设置", + "search_experiences": "搜索经验", "search_skills": "搜索技能", "search": "搜索", + "experiences_limit": "显示前 200 条经验,可通过搜索缩小结果范围。", + "experiences_no_match": "没有匹配的经验", "skills_limit": "显示前 200 项技能,可通过搜索缩小结果范围。", "skills_no_match": "没有匹配的技能", "pagination": "分页", diff --git a/src/powercontext/server/dashboard/pagination.py b/src/powercontext/server/dashboard/pagination.py index d2d0eea5d9..f3e98aa60e 100644 --- a/src/powercontext/server/dashboard/pagination.py +++ b/src/powercontext/server/dashboard/pagination.py @@ -46,8 +46,10 @@ def list_links(ctx: dict[str, Any], family: str, window: dict[str, Any]) -> dict params: dict[str, Any] = {} if family == "notes": params.update(artifact=None, revision=None, notes_cursor=None, notes_history=None) - if family == "skill": - params["kind"] = "skill" + if family in {"skill", "experience"}: + params["kind"] = family + if family == "experience": + params.update(experience_cursor=None, experience_history=None) return { "page": window["page"], **{ diff --git a/src/powercontext/server/dashboard/templates/components/search.html b/src/powercontext/server/dashboard/templates/components/search.html index ed4c913db4..a00b0f9375 100644 --- a/src/powercontext/server/dashboard/templates/components/search.html +++ b/src/powercontext/server/dashboard/templates/components/search.html @@ -31,7 +31,7 @@ -
{{ t.skills_limit }}
{% endif %} +{{ search_form(t.search_experiences if method_kind == 'experience' else t.search_skills, 2000, method_kind) }} +{% if collection.get('search_limited') %}{{ t.experiences_limit if method_kind == 'experience' else t.skills_limit }}
{% endif %} {% from 'components/read-error.html' import read_error with context %} {% if errors.get(family) %}{{ read_error(errors[family], t[family]) }}{% endif %}