UXS-016: Query Workspace (operator viewer)¶
- Status: Draft
- Authors: Podcast Scraper Team
- Parent UXS: UXS-001: GI/KG Viewer — shared tokens, typography, layout, states
- Related PRDs:
- PRD-045: Search v3 — Query Workspace
- Related RFCs:
- RFC-107 — technical design
- RFC-094 — shipped
activeSearchContext+PanelRetrievalStore(composed with, not replaced) - RFC-088 — QueryEnricher (Enriched Answer source)
- RFC-093 —
build_briefing_pack(Compare operator source) - RFC-090 — hybrid retrieval
- RFC-092 — intent taxonomy
- Related ADRs:
- ADR-108 —
topic_consensus(Consensus operator source) - ADR-125 — saved-queries persistence
- Related UX specs:
- UXS-005 — retains compact-launcher role after Workspace lands
- UXS-008 — Enriched Answer surface (heroified here)
- Shell IA: VIEWER_IA.md — Search as 5th main tab
Summary¶
The Query Workspace is the operator viewer's Search main tab (5th tab: Digest · Library · Search · Graph · Dashboard). Full-width main-area surface. Hosts the merged Search + Explore query UI, the enriched-answer hero, result-set operators (cluster / on-graph / timeline / compare / consensus), and the saved-queries sidebar (via LeftPanel). Also the target of the shell-wide Cmd-K palette's "Open in Workspace" action and every rail's "Search inside this X" launcher.
Detail per-surface is in UXS-005 (Revised in §S4-shell: compact launcher retired; LeftPanel hosts Saved + Recent only on all tabs) and UXS-008 (Enriched Answer visual contract, promoted from Advanced-dialog-gated to hero placement here).
Placement¶
Full-width main area (w-full, no w-72 constraint). Right subject rail persistence rule from VIEWER_IA.md holds: opening the Search tab does NOT clear the current subject. Subject rail sits to the right of the Workspace as on any other main tab.
Revised in §S4-shell (2026-07-20): The LeftPanel is visible on ALL main tabs (Digest / Library / Search / Graph / Dashboard). On the Search tab, it hosts the Saved + Recent query sections (collapsible right-edge pattern, §Sidebar below). On other tabs, it remains visible for these sections, supporting the compact-launcher patterns those tabs use — see UXS-005 for the launcher integration. The compact SearchPanel launcher that previously occupied the LeftPanel on non-Search tabs has been retired; all query surfaces now route through the Workspace (Search tab) or the Cmd-K palette.
Component tree¶
SearchTab.vue [w-full main area]
├─ WorkspaceHeader.vue
│ ├─ QueryField.vue input; Preset dropdown; Enter=submit
│ ├─ IntentChip.vue search-query-type (RFC-092 label)
│ └─ SearchFilterBar.vue Since | Top-k | Doc types | Topic contains | Speaker contains | Min confidence | Grounded only | Enriched | More (feed / embedding model)
├─ EnrichedAnswerHero.vue UXS-008 contract; visible when enriched_search_available and enrich_results=true
├─ ResultSetOperatorBar.vue Cluster | On graph | Timeline | Compare | Consensus
└─ WorkspaceResults.vue scrollable
├─ ResultCard.vue tier badge (Insight / Transcript / Reference) + compound "+ insight" badge
├─ CompoundCard.vue when hit has `lifted` (RFC-072 KL1)
└─ ClusterGroupCard.vue when operator === 'cluster'
Revised in §S4-shell: WorkspaceSidebar.vue is now hosted in the LeftPanel (not as a child of SearchTab), supporting the pattern that LeftPanel is visible on all main tabs. See §Sidebar below for details.
Header¶
WorkspaceHeader sits at the top of the Workspace. Rows:
- Query row:
QueryField.vue(min-w-0 flex-1, sm text, Enter = submit, Shift+Enter = newline, IME-safe — same rules as UXS-005). Preset dropdown to the left of the field (Preset: <label> ▾when active;Preset ▾default). Search button + Clear button to the right. - Intent row:
IntentChip.vuewhen the response carriesquery_type(RFC-092 label — Entity lookup / Raw evidence / Temporal tracking / Cross-show synthesis / Semantic). Muted; transparency-only.data-testid="search-query-type"(unchanged from UXS-005). - Filter chip bar:
SearchFilterBar.vue. Chips left-to-right: Since (search-chip-since), Top-k (search-chip-topk, default 10), Doc types (search-chip-doctypes), Topic contains (search-chip-topic-contains, new — from merged Explore), Speaker contains (search-chip-speaker-contains, new), Min confidence (search-chip-min-confidence, new), Grounded only (search-chip-grounded-only, new), Enriched (search-chip-enriched, when enrichment configured), More (search-chip-more— hosts low-traffic fields: feed, embedding model, merge-duplicate-KG-surfaces). Each chip's label switches fromLabel ▾(default) toLabel: detail ▾(active); More showsMore: Nreflecting non-default field count.
The Enriched answers toggle previously in the Advanced dialog (UXS-005) is retired here — the Enriched filter chip replaces it.
Enriched Answer hero¶
EnrichedAnswerHero.vue sits between the header and the operator bar. Visible when enriched_search_available: true from /api/health AND the request was made with enrich_results=true (shipped in RFC-088 chunk 5).
Contract inherits UXS-008 in full: gi domain token border/tint, "AI-generated / grounded" badge, synthesized answer with clickable speaker names (opening Person Landing) + topic tags (opening Topic Entity View), Sources section (collapsible, default 3, "Show all N" toggle), grounded source count indicator ("Based on N grounded insights"), provider attribution ("Synthesised by … "), source-to-result linking (Used in answer chip on the matching hit card).
Degradation follows UXS-008: hidden when no grounded insights lifted; muted error state on provider failure; skeleton on latency > 5 s; hidden entirely when enrichment not configured. The hero is not rendered by the Cmd-K palette or the LeftPanel launcher (bounded cost).
Result-set operator bar¶
ResultSetOperatorBar.vue sits between the hero and the results. One button per operator; the active operator has aria-pressed.
| Operator | data-testid |
Effect |
|---|---|---|
| Cluster | search-op-cluster |
Server groups hits via insight_clusters / theme_clusters (RFC-107 §6); UI renders ClusterGroupCard. Fetch top_k * 3 when active. |
| On graph | search-op-graph |
Union bbox of derived node ids → graph camera set-focus (graphNavigation.focusSet). Camera-fit invariant preserved. |
| Timeline | search-op-timeline |
Client bucket by publish_date month → SubjectTimelineChart; bucket-click filters WorkspaceResults client-side. |
| Compare | search-op-compare |
Enabled when ≥ 2 subject types are present in the hit set. Opens a 2-column view sourced from build_briefing_pack(query, query_type, results, canonical_entity, max_tokens) per side (RFC-093 shipped API). Judge summary muted below when available. |
| Consensus | search-op-consensus |
Enabled when at least one Topic is present. Surfaces topic_consensus enricher output (ADR-108; shipped 0.91 precision on prod-v2). Cross-speaker corroboration pairs (not contradictions — CONTRADICTS edges are v3+, out of scope). |
Operator bar aria-role="toolbar"; buttons aria-role="button" with visible focus ring. Toolbar disappears when there are 0 hits.
Results¶
WorkspaceResults.vue renders the muted "N results" / "1 result" line (unchanged from UXS-005), the optional Lift: applied / transcript rows line (from RFC-072 KL1 lift stats), and the hit list.
Each ResultCard:
- Tier badge (
search-result-tier, from UXS-005 PRD-033 FR1.1): Insight (primary) / Transcript (success) / Reference (muted), fromsource_tier. - Compound
+ insightbadge (search-result-compound) when the hit is a transcript-lifted-to-insight compound (liftedblock, RFC-072 KL1). - Actions:
G(graph focus),L(Library episode) — same rules as UXS-005 (L requiressource_metadata_relative_path+ healthy API);E(episode id chip, informational). When KG-surface merged, onlyGshows. - Lifted GI insight region (when
liftedpresent, UXS-005-inherited): linked insight id/text, speaker/topic labels, quote time range. Includes theNo speaker detectedmuted line (GI_QUOTE_SPEAKER_UNAVAILABLE_HINT, #541) whenlifted.quotehas timestamps but no speaker label. - Supporting quotes (collapsible, UXS-005-inherited): same
No speaker detectedtreatment when speaker missing.
ClusterGroupCard (when operator === 'cluster'):
- Header: cluster shared entity + member count.
- Expandable body:
ResultCards for each member, indented. data-testid="search-cluster-group"on the header.
Sidebar — Saved + Recent (USERPREFS-1)¶
Revised in §S4-shell (2026-07-20): LeftPanel hosts two collapsible sections:
- Saved queries — reads
search.savedQueriesfrom USERPREFS-1. Each row:name, muted preview ofq. Click = switch to Search tab + re-run withfilters+operatorre-applied. Trash icon on hover deletes. Header testid:left-panel-saved-queries. - Recent queries — reads
search.recentQueries(last 20, USERPREFS-1). Each row:qtruncated. Click = switch to Search tab + re-run. Header testid:left-panel-recent-queries.
Empty states: left-panel-saved-empty (Saved section, no items), left-panel-recent-empty (Recent section, no items). List containers: left-panel-saved-list, left-panel-recent-list.
Both sections hide entirely (no error banner) when useUserPreferencesStore.available === false (offline / unauth) — matches USERPREFS-1's silent-degrade rule.
Save affordance: after a successful search in the Workspace, a Save this query button in the header opens a mini-dialog for name. Persists via useSavedQueriesStore.save(name, request) → USERPREFS-1 PATCH.
Retired testids (was workspace-sidebar-*): workspace-sidebar-saved, workspace-sidebar-recent.
Active-search-context integration¶
Every successful Workspace search publishes to useActiveSearchContextStore (RFC-094 OQ-2, shipped) with { q, filters, scope }. LibraryView and GraphCanvas consumers read this today and weight rows / nodes accordingly. Rail launchers (see below) also publish to the same store on activation. No new store; no changes to consumers.
Rail search launchers¶
Each subject rail carries a "Search inside this X" affordance in its header (or context menu for Graph nodes). Activating the launcher:
- Publishes the scope to
useActiveSearchContextStore. - Switches the main tab to Search.
- Focuses
QueryFieldwith the scope filter chip visible + the current subject's canonical name pre-filled in a scope hint (not the query text — the user still types their question).
| Rail | Testid | Scope pre-fill |
|---|---|---|
EpisodeDetailPanel |
rail-search-in-episode |
{episode: metadata_relative_path} |
Node view (Topic) / TopicEntityView |
rail-search-in-topic |
{topic: id} |
Node view (Person) / PersonLandingView |
rail-search-in-person |
{person: id} |
ShowRailPanel |
rail-search-in-show |
{feed: feed_id} |
GraphNodeRailPanel context menu |
rail-search-in-selection |
{selection: {node_ids: [selected ids]}} — server OR-merges each node's natural scope; no kg_proximity (RFC-091 rejected). |
The rail's existing content is not replaced — this is search-in-addition (e.g. positions_of(person) in PersonLanding stays).
In-episode search¶
EpisodeDetailPanel grows a compact QueryField below its header, above the sections. Queries the episode-scoped app_episodes search endpoint. Hits render inline (no Workspace navigation), each with a jump-to-moment. data-testid="episode-inline-search-field" / episode-inline-search-results. An "Open in Workspace" chip escape-hatches to the full Workspace with the episode scope pre-filled.
Command palette (shell-wide)¶
Not in the Workspace itself — see UXS-005 for the compact-launcher role and RFC-107 §4 for the shell-wide palette. The palette's Open in Workspace action routes here.
Empty states¶
- No corpus configured: full-width muted card — "Set a corpus path in the status bar to enable search." (Same copy family as UXS-005.)
data-testid="workspace-no-corpus". - API down: same as UXS-005 (
Search is unavailable right now). - Query field empty on tab arrival: sidebar renders (Saved + Recent when available); results area shows a muted centred hint "Type a question and press Enter." No skeleton, no operator bar.
- Zero results: muted "No results for
<q>" line, retained filter chip bar so the user can widen; operator bar hides.
Accessibility¶
- Workspace
role="region"aria-label="Search workspace". - Header
role="search"(the query form is the primary form). - Operator bar
role="toolbar"; each buttonaria-pressedreflects active operator. - Enriched Answer hero inherits UXS-008 accessibility.
- Sidebar sections use
aria-label="Saved queries"/"Recent queries"; each item is arole="button"with visible focus ring and Enter/Space activation. - Keyboard:
/andCmd-Kopen the shell-wide palette (RFC-107 §4); inside the WorkspaceTaborder = query field → filter chips → operator bar → first result → sidebar. - All new controls carry
data-testidfor the E2E surface map;aria-*for screen readers.
Tokens¶
Inherits UXS-001 tokens. New surface-level rules:
- Workspace background:
canvas. - Workspace card padding: standard (matches UXS-001 card padding).
- Hero panel:
surfacebackground +gileft border (4 px solid, inherited from UXS-008). - Sidebar background:
surface; section headerslp-section(small-caps). - Operator bar:
surfacebackground,borderdivider.
E2E contract¶
New visible labels and selectors require updates to E2E surface map before or with implementation. Key surfaces:
SearchTab.vue(workspace-root)- Header (
workspace-header,search-query-type,search-filter-bar,search-chip-*) - Enriched Answer hero (inherits UXS-008 selectors)
- Operator bar (
search-op-cluster,search-op-graph,search-op-timeline,search-op-compare,search-op-consensus) - Results (
search-results,search-result-tier,search-result-compound,search-cluster-group) - Sidebar (
workspace-sidebar-saved,workspace-sidebar-recent,workspace-save-button,workspace-save-dialog) - Rail launchers (
rail-search-in-episode,-topic,-person,-show,-selection) - In-episode search (
episode-inline-search-field,episode-inline-search-results)
Retired (was UXS-005 / VIEWER_IA §Left panel): left-panel-enter-explore, left-panel-back-search.
Playwright coverage: per-surface Tier-1 mocked specs + Tier-2 production-shaped specs (RFC-107 §T1); Tier-3 real-corpus spec web/gi-kg-viewer/e2e/validation/search-real-corpus.spec.ts.
Revision history¶
| Date | Change |
|---|---|
| 2026-07-20 | §S4-shell revision — LeftPanel visible on all tabs; Saved + Recent hosted in LeftPanel, not SearchTab; compact launcher retired. See 4d13dce9. |
| 2026-07-20 | Initial draft — Query Workspace + operator bar + sidebar + hero placement (PRD-045 / RFC-107) |
§S4-shell revision (2026-07-20)¶
Compact SearchPanel launcher retired. The shell pivot shipped in 4d13dce9 changed the Workspace shape:
- LeftPanel is now visible on ALL main tabs (Digest / Library / Search / Graph / Dashboard), not hidden on Search.
- Saved + Recent query sections now live in LeftPanel (via collapsible pattern), not as children of SearchTab via
WorkspaceSidebar.vue. - The compact query launcher that previously occupied LeftPanel on non-Search tabs has been retired.
- All Saved + Recent data still flows from USERPREFS-1 keys
search.savedQueriesandsearch.recentQueries(unchanged). - Rows emit
apply-query→ App switches to Search main tab + runs. This preserves the UX intent (recent/saved lead to Workspace navigation) while co-locating Saved + Recent in one place. - Keyboard:
focusSearchinuseViewerKeyboardmade optional;/andCmd-Kboth open the palette. - Vertical rail "Search / Explore" button relabeled "Saved queries".
- New testids reflect LeftPanel location:
left-panel-saved-queries,left-panel-saved-list,left-panel-saved-empty,left-panel-recent-list,left-panel-recent-empty.