"use client"; import { useState } from "react"; import type { CategoryField, ChartConfig, ChartType, MetricField, SeriesGroupBy, TimeBin, TimeField, XAxisField, YMetric, } from "../../lib/chartConfig"; import { emptyRoot, parseRoot, stringifyRoot, type GroupNode, } from "../../lib/searchQuery"; import type { GroupState, LeafState } from "../../lib/searchEval"; import QueryBuilder from "../QueryBuilder"; const sel = "rounded-md border border-border bg-card px-2 py-1 text-sm"; const lbl = "flex flex-col gap-1 text-xs font-medium text-muted-foreground"; // The chart editor has no live search evaluation, so the builder renders without // per-leaf/group count badges. const NO_LEAF_STATES: ReadonlyMap = new Map(); const NO_GROUP_STATES: ReadonlyMap = new Map(); const METRIC_FIELDS: { value: MetricField; label: string }[] = [ { value: "duration", label: "Duration" }, { value: "viewCount", label: "Views" }, { value: "likeCount", label: "Likes" }, { value: "commentCount", label: "Comments" }, { value: "channelFollowerCount", label: "Followers" }, { value: "cueCount", label: "Cue count" }, { value: "coverage", label: "Transcript coverage" }, ]; export function ChartConfigEditor({ initial, onSave, onCancel, }: { initial: ChartConfig; onSave: (c: ChartConfig) => void; onCancel: () => void; }) { const [draft, setDraft] = useState(initial); const [mode, setMode] = useState<"metadata" | "search">( initial.search ? "search" : "metadata", ); // The full query builder owns the chart's search tree while editing. const [searchRoot, setSearchRoot] = useState( () => parseRoot(initial.search?.queryTree ?? "") ?? emptyRoot(), ); const patch = (p: Partial) => setDraft((d) => ({ ...d, ...p })); const patchX = (x: XAxisField) => patch({ x }); const patchY = (y: YMetric) => patch({ y }); const save = () => { let search: ChartConfig["search"]; if (mode === "search") { // Keep the search block even with an empty query, so the card prompts the // user to fill one in rather than silently reverting to a metadata chart. search = { queryTree: stringifyRoot(searchRoot), metric: draft.search?.metric ?? "matchedVideos", }; } else { search = undefined; } onSave({ ...draft, search }); }; return (
Data source
{(["metadata", "search"] as const).map((m) => ( ))}
X axis {draft.x.kind === "time" && ( <> )} {draft.x.kind === "category" && ( )} {draft.x.kind === "numericBucket" && ( )}
{mode === "metadata" && (
Y metric {draft.y.agg !== "count" && ( )}
)} {mode === "search" && (
Search

Build the query with any number of layers (AND/OR/NOT). Each matched video counts once, or by its hit count. The X axis, grouping, and filters above still apply.

)}
); }