feat(graph): metadata filters + facets (corpus graph PR A)
Adds legal-metadata filtering and the payload to color by it (foundation for the color-by selector in the analytics PR). Backend (web/graph_api.py, web/app.py) — read-only, G2: - GraphNode += court, date (ISO) — precedents carry them for filter/color-by. - build_corpus_graph += server-side WHERE filters (G5): court, precedent_level, chair, district, year_from, year_to (EXTRACT(YEAR FROM date)). Neighborhood query also selects court/date. - New GET /api/graph/facets (response_model GraphFacets, UI2) → distinct courts/levels/chairs/districts so the UI doesn't hardcode Hebrew strings. Frontend: - graph.ts: GraphNode += court/date; GraphFilters += the six params; buildParams; useGraphFacets() hook. - graph-filter-panel: an "advanced" Accordion with court/precedent_level/chair/ district Selects (from facets) + year-from/year-to Selects. - graph-view: new controls wired into filters; facets fetched and passed down. Verified read-only against the live DB (precedent_level=עליון&year_from=2015 filters correctly; facets populated: 36 courts / 3 levels / 19 chairs / 4 districts). web-ui build + lint pass. Invariants: G2 (SELECT-only via db.get_pool), G5 (filters server-side), UI2 (explicit response_models). api:types to be regenerated post-deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,12 @@
|
||||
* always on; halacha is Phase 2 and shown disabled to telegraph the roadmap).
|
||||
*/
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -18,18 +24,26 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { GraphFacets } from "@/lib/api/graph";
|
||||
|
||||
export type GraphControls = {
|
||||
practiceArea: string;
|
||||
source: string;
|
||||
minCitations: number;
|
||||
q: string;
|
||||
court: string;
|
||||
precedentLevel: string;
|
||||
chair: string;
|
||||
district: string;
|
||||
yearFrom: number;
|
||||
yearTo: number;
|
||||
showTopics: boolean;
|
||||
showPracticeAreas: boolean;
|
||||
showHalachot: boolean;
|
||||
};
|
||||
|
||||
const ALL = "__all__";
|
||||
const YEARS = Array.from({ length: 2026 - 1994 + 1 }, (_, i) => 2026 - i);
|
||||
|
||||
const PRACTICE_AREAS: { value: string; label: string }[] = [
|
||||
{ value: "rishuy_uvniya", label: "רישוי ובנייה" },
|
||||
@@ -48,9 +62,11 @@ const MIN_CITATIONS = [0, 1, 2, 3, 5];
|
||||
export function GraphFilterPanel({
|
||||
controls,
|
||||
onChange,
|
||||
facets,
|
||||
}: {
|
||||
controls: GraphControls;
|
||||
onChange: (patch: Partial<GraphControls>) => void;
|
||||
facets?: GraphFacets;
|
||||
}) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto">
|
||||
@@ -126,6 +142,52 @@ export function GraphFilterPanel({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Accordion type="single" collapsible className="border-0">
|
||||
<AccordionItem value="advanced" className="border-0">
|
||||
<AccordionTrigger className="py-1 text-xs text-ink-muted hover:no-underline">
|
||||
סינון מתקדם
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4 pt-2">
|
||||
<FacetSelect
|
||||
label="בית משפט / ערכאה"
|
||||
value={controls.court}
|
||||
options={facets?.courts ?? []}
|
||||
onChange={(v) => onChange({ court: v })}
|
||||
/>
|
||||
<FacetSelect
|
||||
label="דרגת סמכות"
|
||||
value={controls.precedentLevel}
|
||||
options={facets?.precedent_levels ?? []}
|
||||
onChange={(v) => onChange({ precedentLevel: v })}
|
||||
/>
|
||||
<FacetSelect
|
||||
label="יו״ר"
|
||||
value={controls.chair}
|
||||
options={facets?.chairs ?? []}
|
||||
onChange={(v) => onChange({ chair: v })}
|
||||
/>
|
||||
<FacetSelect
|
||||
label="מחוז"
|
||||
value={controls.district}
|
||||
options={facets?.districts ?? []}
|
||||
onChange={(v) => onChange({ district: v })}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<YearSelect
|
||||
label="משנה"
|
||||
value={controls.yearFrom}
|
||||
onChange={(v) => onChange({ yearFrom: v })}
|
||||
/>
|
||||
<YearSelect
|
||||
label="עד שנה"
|
||||
value={controls.yearTo}
|
||||
onChange={(v) => onChange({ yearTo: v })}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -152,6 +214,69 @@ export function GraphFilterPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function FacetSelect({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-ink-muted">{label}</Label>
|
||||
<Select value={value || ALL} onValueChange={(v) => onChange(v === ALL ? "" : v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL}>הכל</SelectItem>
|
||||
{options.map((o) => (
|
||||
<SelectItem key={o} value={o}>
|
||||
{o}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function YearSelect({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (v: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-ink-muted">{label}</Label>
|
||||
<Select
|
||||
value={value ? String(value) : ALL}
|
||||
onValueChange={(v) => onChange(v === ALL ? 0 : Number(v))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL}>—</SelectItem>
|
||||
{YEARS.map((y) => (
|
||||
<SelectItem key={y} value={String(y)}>
|
||||
{y}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleRow({
|
||||
label,
|
||||
checked,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type CorpusGraph,
|
||||
type GraphNode,
|
||||
useCorpusGraph,
|
||||
useGraphFacets,
|
||||
useNodeNeighborhood,
|
||||
} from "@/lib/api/graph";
|
||||
import {
|
||||
@@ -39,10 +40,17 @@ export function GraphView() {
|
||||
source: "",
|
||||
minCitations: 0,
|
||||
q: "",
|
||||
court: "",
|
||||
precedentLevel: "",
|
||||
chair: "",
|
||||
district: "",
|
||||
yearFrom: 0,
|
||||
yearTo: 0,
|
||||
showTopics: true,
|
||||
showPracticeAreas: true,
|
||||
showHalachot: false,
|
||||
});
|
||||
const facets = useGraphFacets().data;
|
||||
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
|
||||
const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
|
||||
|
||||
@@ -67,8 +75,26 @@ export function GraphView() {
|
||||
node_types: nodeTypes,
|
||||
limit: NODE_LIMIT,
|
||||
q: debouncedQ,
|
||||
court: controls.court,
|
||||
precedent_level: controls.precedentLevel,
|
||||
chair: controls.chair,
|
||||
district: controls.district,
|
||||
year_from: controls.yearFrom,
|
||||
year_to: controls.yearTo,
|
||||
}),
|
||||
[controls.practiceArea, controls.source, controls.minCitations, nodeTypes, debouncedQ],
|
||||
[
|
||||
controls.practiceArea,
|
||||
controls.source,
|
||||
controls.minCitations,
|
||||
controls.court,
|
||||
controls.precedentLevel,
|
||||
controls.chair,
|
||||
controls.district,
|
||||
controls.yearFrom,
|
||||
controls.yearTo,
|
||||
nodeTypes,
|
||||
debouncedQ,
|
||||
],
|
||||
);
|
||||
|
||||
const isFocused = !!focusNodeId;
|
||||
@@ -106,7 +132,7 @@ export function GraphView() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh-320px)] min-h-[560px]">
|
||||
<GraphFilterPanel controls={controls} onChange={onChange} />
|
||||
<GraphFilterPanel controls={controls} onChange={onChange} facets={facets} />
|
||||
|
||||
<div className="relative flex-1 rounded-lg border border-rule bg-surface overflow-hidden">
|
||||
{error ? (
|
||||
|
||||
Reference in New Issue
Block a user