| import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from "react";
|
|
|
| import type { QueryOpenDetail } from "../apiContract";
|
| import { QUERY_OPEN_EVENT } from "../apiContract";
|
|
|
|
|
|
|
|
|
| const CustomerGrid = lazy(() => import("../customer-grid/CustomerGrid"));
|
| import { OverlayProvider } from "../customer-grid/OverlaySurface";
|
| import { gridScopeFor } from "../shell/dbFrame";
|
| import { databaseEntries } from "../shell/nav";
|
| import type { NavEntry } from "../shell/nav";
|
| import { bindingFor, deleteQuery, fetchQueries, QUERY_BINDING_EVENT } from "./queryApi";
|
| import type { QueryCitation, SavedQuery } from "./queryApi";
|
| import { QueryEmpty, queryGroups, QueryRail } from "./queryParts";
|
| import type { QueryGroup } from "./queryParts";
|
| import "./query.css";
|
|
|
| export interface QueryPageProps {
|
| granted: NavEntry[];
|
| |
| |
| |
| |
| |
|
|
| hostedRail?: boolean;
|
|
|
| refreshToken?: number;
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| selectedId?: string;
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| export default function QueryPage({ granted, hostedRail, refreshToken, selectedId }: QueryPageProps) {
|
| const [views, setViews] = useState<SavedQuery[]>([]);
|
| const [citations, setCitations] = useState<QueryCitation[]>([]);
|
| const [activeId, setActiveId] = useState("");
|
| const [loaded, setLoaded] = useState(false);
|
| const entries = databaseEntries(granted);
|
| const entryOf = (key: string) => entries.find((entry) => entry.key === key);
|
| const known = views.filter((view) => entryOf(view.source.database));
|
| const active = known.find((view) => view.id === activeId) ?? known[0] ?? null;
|
|
|
| const groups = useMemo<QueryGroup[]>(() => queryGroups(views, entries), [views, entries]);
|
|
|
| const reload = useCallback(async () => {
|
| const result = await fetchQueries();
|
| setLoaded(true);
|
| if (result.ok) {
|
| setViews(result.value.views);
|
| setCitations(result.value.citations);
|
| }
|
| }, []);
|
|
|
| useEffect(() => { void reload(); }, [reload]);
|
|
|
|
|
| useEffect(() => { if (refreshToken) void reload(); }, [refreshToken, reload]);
|
|
|
| |
| |
| |
|
|
| useEffect(() => { if (selectedId) setActiveId(selectedId); }, [selectedId]);
|
|
|
| useEffect(() => {
|
| if (known.length && !known.some((view) => view.id === activeId)) setActiveId(known[0].id);
|
| }, [known, activeId]);
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const binding = useMemo(
|
| () => (active ? bindingFor(active, citations) : undefined), [active, citations]);
|
|
|
| useEffect(() => {
|
| if (!binding) return;
|
|
|
|
|
|
|
| window.dispatchEvent(new CustomEvent(QUERY_BINDING_EVENT, { detail: binding }));
|
| }, [binding]);
|
|
|
| useEffect(() => {
|
| const onOpen = (event: Event) => {
|
| const qid = (event as CustomEvent<QueryOpenDetail>).detail?.qid;
|
| if (!qid) return;
|
|
|
|
|
|
|
|
|
| setActiveId(qid);
|
| if (!views.some((view) => view.id === qid)) void reload();
|
| };
|
| window.addEventListener(QUERY_OPEN_EVENT, onOpen);
|
| return () => window.removeEventListener(QUERY_OPEN_EVENT, onOpen);
|
| }, [views, reload]);
|
|
|
|
|
| const upsert = useCallback((view: SavedQuery) => {
|
| setViews((current) => [view, ...current.filter((row) => row.id !== view.id)]);
|
| }, []);
|
|
|
| const remove = useCallback(async (qid: string) => {
|
| const result = await deleteQuery(qid);
|
| if (!result.ok) return;
|
| setViews((current) => current.filter((view) => view.id !== qid));
|
| setActiveId((current) => (current === qid ? "" : current));
|
| }, []);
|
|
|
| if (!active) return <QueryEmpty loaded={loaded} unnamed={views.length - known.length} />;
|
| |
| |
| |
| |
|
|
| return (
|
| <div className="shell-db-frame">
|
| {/*
|
| ββ WAVE 36 Β· W36-T67 (owner item 7) β THE UNIVERSAL DATABASE HEADER IS UNMOUNTED HERE,
|
| AND ON THIS SURFACE IT WAS ALREADY A SECOND COPY OF SOMETHING VISIBLE.
|
| β THE COMPONENT IS NOT NAMED ANYWHERE IN THIS FILE ANY MORE, DELIBERATELY. F's W36-T51
|
| deletes it and finds its last references by searching; a comment carrying the symbol is
|
| a hit that costs somebody a read, and this very file already warns about prose becoming
|
| its own marker one block below [[prose-that-becomes-its-own-marker]].
|
|
|
| β ITEM 7'S OWN REASONING DOES NOT TRANSFER TO A QUERY, which is why this needed its own
|
| ticket rather than following the database one. Owner: the header is *"a waste of white
|
| space. User should be able to tell what database they are in just by seeing the first
|
| Unique ID field."* A Query has no Unique ID column of its own to lean on.
|
| β WHAT IT LEANS ON INSTEAD IS ALREADY ON SCREEN, and it is richer than the band that went:
|
| `queryGroups` groups the rail BY SOURCE DATABASE and heads each group with that database's
|
| label AND its icon β owner item 3 of 2026-08-15, *"each View correspond to the relevant
|
| database"*. So `label={active.source.label}` was the same fact, painted twice, one of them
|
| costing a full-width band. The `hostedRail` case loses nothing either: the Assistant hosts
|
| the SAME `QueryRail` component, grouping included.
|
| β THE ONE THING THAT WAS NOT DUPLICATED IS THE HEADING ITSELF. That header rendered an
|
| `<h1>` its own comment called "the first time the work surface has NAMED itself"; deleting
|
| it outright would leave this page with no heading in the accessibility tree at all. So the
|
| name survives as an `h1` that costs NO vertical space, which is the half of the owner's
|
| instruction this surface can honour exactly. `flex: 1 1 auto` on `.shell-grid-host` hands
|
| the reclaimed band to the grid with no CSS change.
|
| */}
|
| <h1 className="lp-sr-only">
|
| {active.name || "Query"} on {active.source.label}
|
| </h1>
|
| <div className="shell-grid-host">
|
| <div className="qy-workspace">
|
| {hostedRail ? null : (
|
| <QueryRail groups={groups} activeId={active.id} onSelect={setActiveId}
|
| onDelete={(id) => void remove(id)} onChanged={upsert} />
|
| )}
|
| {/* β NO PROVENANCE ROW HERE (W35-T21, owner item 4 / R2). The "Sources" disclosure and
|
| the snapshot line it opened are deleted; a Query view is a database view now, and a
|
| database view carries no banner. The citations still travel β `binding` above is
|
| built from this artefact AND the fetched citation list β and the reader still SEES
|
| them in the chat, under the answer that used them (`AssistantPage::CitationLine`).
|
| β Do NOT paste that builder call into a comment: `verify_grid_ux.py`'s NC mutates the
|
| FIRST occurrence of it, so a second copy in prose keeps the scan green over a page
|
| that stopped binding citations [[prose-that-becomes-its-own-marker]]. */}
|
| <div className="qy-surface">
|
| <div className="qy-grid-host">
|
| <OverlayProvider>
|
| <Suspense fallback={<div className="lp-boot" aria-hidden="true" />}>
|
| <CustomerGrid key={gridScopeFor(active.source.database)}
|
| scope={gridScopeFor(active.source.database)} queryBinding={binding} />
|
| </Suspense>
|
| </OverlayProvider>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| );
|
| }
|
| |