@malloy-publisher/sdk 0.0.199 → 0.0.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/ServerProvider-BSyxB6sf.cjs.js +1 -0
  2. package/dist/{ServerProvider-BuM1usxf.es.js → ServerProvider-DSnbMlP3.es.js} +330 -470
  3. package/dist/client/api.d.ts +21 -121
  4. package/dist/client/index.cjs.js +1 -1
  5. package/dist/client/index.es.js +1 -1
  6. package/dist/components/Connections/EditConnectionDialog.d.ts +2 -1
  7. package/dist/components/given/GivenInput.d.ts +16 -0
  8. package/dist/components/given/GivensPanel.d.ts +18 -0
  9. package/dist/components/given/index.d.ts +2 -0
  10. package/dist/{core-DfcpQGVP.es.js → core-B3IQNPBD.es.js} +1 -1
  11. package/dist/{core-yDgxkpo0.cjs.js → core-GkjltsUf.cjs.js} +1 -1
  12. package/dist/hooks/index.d.ts +2 -0
  13. package/dist/hooks/useGivensForm.d.ts +33 -0
  14. package/dist/hooks/useModelGivens.d.ts +10 -0
  15. package/dist/index-BpoC5QHF.cjs.js +229 -0
  16. package/dist/{index-Y4ooZDYA.es.js → index-uW-ZBpF2.es.js} +26102 -25626
  17. package/dist/index.cjs.js +1 -1
  18. package/dist/index.es.js +28 -26
  19. package/package.json +1 -1
  20. package/src/components/Connections/DeleteConnectionDialog.tsx +10 -3
  21. package/src/components/Connections/EditConnectionDialog.tsx +11 -3
  22. package/src/components/Environment/Connections.tsx +388 -0
  23. package/src/components/Environment/Environment.tsx +7 -2
  24. package/src/components/Notebook/Notebook.tsx +81 -23
  25. package/src/components/Package/Package.tsx +0 -7
  26. package/src/components/given/GivenInput.tsx +190 -0
  27. package/src/components/given/GivensPanel.tsx +85 -0
  28. package/src/components/given/index.ts +2 -0
  29. package/src/hooks/index.ts +8 -0
  30. package/src/hooks/useGivensForm.ts +110 -0
  31. package/src/hooks/useModelGivens.ts +26 -0
  32. package/dist/ServerProvider-C_Mnvmgc.cjs.js +0 -1
  33. package/dist/index-CMA8U4-B.cjs.js +0 -228
  34. package/src/components/Package/Connections.tsx +0 -373
  35. /package/dist/components/{Package → Environment}/Connections.d.ts +0 -0
@@ -11,7 +11,10 @@ import {
11
11
  FilterSelection,
12
12
  useDimensionFilters,
13
13
  } from "../../hooks/useDimensionFilters";
14
+ import { GivenValue, useGivensForm } from "../../hooks/useGivensForm";
15
+ import { useModelGivens } from "../../hooks/useModelGivens";
14
16
  import { useQueryWithApiError } from "../../hooks/useQueryWithApiError";
17
+ import { parseResourceUri } from "../../utils/formatting";
15
18
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
16
19
  import { DimensionFilter, RetrievalFunction } from "../filter/DimensionFilter";
17
20
  import {
@@ -19,8 +22,7 @@ import {
19
22
  parseAllSourceInfos,
20
23
  parseNotebookFilterAnnotation,
21
24
  } from "../filter/utils";
22
-
23
- import { parseResourceUri } from "../../utils/formatting";
25
+ import { GivensPanel } from "../given";
24
26
  import { Loading } from "../Loading";
25
27
  import { useServer } from "../ServerProvider";
26
28
  import { CleanNotebookContainer, CleanNotebookSection } from "../styles";
@@ -216,6 +218,14 @@ export default function Notebook({
216
218
  [filterStates, getActiveFilters],
217
219
  );
218
220
 
221
+ // Extract model-level givens declared via `given:` and manage user overrides
222
+ const declaredGivens = useModelGivens(notebook);
223
+ const {
224
+ givenValues,
225
+ updateGiven,
226
+ clearAll: clearAllGivens,
227
+ } = useGivensForm(declaredGivens);
228
+
219
229
  // Create a map of dimension key -> source name for quick lookup (used by filter UI)
220
230
  const _dimensionToSourceMap = useMemo(() => {
221
231
  const map = new Map<string, string>();
@@ -273,11 +283,40 @@ export default function Notebook({
273
283
  [],
274
284
  );
275
285
 
286
+ /**
287
+ * Serialize given-value overrides into the JSON-encoded string the server
288
+ * expects on the notebook-cell GET endpoint's `givens` query param.
289
+ * Date values are rendered as YYYY-MM-DD; everything else passes through
290
+ * the standard JSON encoder.
291
+ */
292
+ const buildGivens = useCallback(
293
+ (values: Map<string, GivenValue>): string | undefined => {
294
+ if (values.size === 0) return undefined;
295
+ const encode = (v: GivenValue): unknown => {
296
+ if (v instanceof Date) return v.toISOString().slice(0, 10);
297
+ if (Array.isArray(v)) return v.map((item) => encode(item));
298
+ return v;
299
+ };
300
+ const params: Record<string, unknown> = {};
301
+ values.forEach((value, name) => {
302
+ if (value === null || value === undefined) return;
303
+ params[name] = encode(value);
304
+ });
305
+ return Object.keys(params).length > 0
306
+ ? JSON.stringify(params)
307
+ : undefined;
308
+ },
309
+ [],
310
+ );
311
+
276
312
  // Unified cell execution function
277
- // Executes all notebook cells, passing server-side filter params when available
313
+ // Executes all notebook cells, passing server-side filter params and givens
278
314
  // Runs up to 4 requests in parallel for better performance
279
315
  const executeCells = useCallback(
280
- async (filtersToApply: FilterSelection[] = []) => {
316
+ async (
317
+ filtersToApply: FilterSelection[] = [],
318
+ givensToApply: Map<string, GivenValue> = new Map(),
319
+ ) => {
281
320
  if (!isSuccess || !notebook?.notebookCells) return;
282
321
 
283
322
  // Initialize or reset cells
@@ -297,6 +336,7 @@ export default function Notebook({
297
336
  const filterParams = useServerFilters
298
337
  ? buildFilterParams(filtersToApply)
299
338
  : undefined;
339
+ const givensParam = buildGivens(givensToApply);
300
340
 
301
341
  try {
302
342
  // Build execution tasks for code cells
@@ -313,7 +353,7 @@ export default function Notebook({
313
353
 
314
354
  const executeCell = async () => {
315
355
  try {
316
- // Use notebook cell execution API with optional filter_params
356
+ // Use notebook cell execution API with optional filter_params and givens
317
357
  const response =
318
358
  await apiClients.notebooks.executeNotebookCell(
319
359
  environmentName,
@@ -322,6 +362,8 @@ export default function Notebook({
322
362
  cellIndex,
323
363
  versionId,
324
364
  filterParams,
365
+ undefined,
366
+ givensParam,
325
367
  );
326
368
 
327
369
  const executedCell = response.data;
@@ -381,6 +423,7 @@ export default function Notebook({
381
423
  notebook,
382
424
  useServerFilters,
383
425
  buildFilterParams,
426
+ buildGivens,
384
427
  environmentName,
385
428
  packageName,
386
429
  notebookPath,
@@ -389,45 +432,52 @@ export default function Notebook({
389
432
  ],
390
433
  );
391
434
 
392
- // Execute cells when notebook is loaded (no filters initially)
435
+ // Execute cells when notebook is loaded (no filters or givens initially)
393
436
  useEffect(() => {
394
437
  if (!isSuccess || !notebook?.notebookCells) return;
395
- executeCells([]);
438
+ executeCells([], new Map());
396
439
  }, [isSuccess, notebook, executeCells]);
397
440
 
398
- // Re-execute when filters change
399
- // Track previous activeFilters to detect actual changes (not just reference changes)
400
- const prevActiveFiltersRef = useRef<string>("");
441
+ // Re-execute when filters or givens change
442
+ // Track previous input shape to detect actual value changes (not just reference changes)
443
+ const prevInputsRef = useRef<string>("");
401
444
 
402
445
  useEffect(() => {
403
- // Serialize activeFilters to detect actual value changes
404
- const serialized = JSON.stringify(
405
- activeFilters.map((f) => ({
446
+ // Serialize activeFilters + givenValues to detect actual value changes
447
+ const serialized = JSON.stringify({
448
+ filters: activeFilters.map((f) => ({
406
449
  dim: f.dimensionName,
407
450
  type: f.matchType,
408
451
  val: f.value,
409
452
  val2: f.value2,
410
453
  })),
411
- );
454
+ givens: Array.from(givenValues.entries()).sort(([a], [b]) =>
455
+ a.localeCompare(b),
456
+ ),
457
+ });
412
458
 
413
- // Skip if no actual change or if this is the initial empty state
414
- if (serialized === prevActiveFiltersRef.current) {
459
+ // Skip if no actual change
460
+ if (serialized === prevInputsRef.current) {
415
461
  return;
416
462
  }
417
463
 
418
- // Skip the initial render (when prevActiveFiltersRef is empty and filters are also empty)
419
- if (prevActiveFiltersRef.current === "" && activeFilters.length === 0) {
420
- prevActiveFiltersRef.current = serialized;
464
+ // Skip the initial render when both inputs are empty
465
+ if (
466
+ prevInputsRef.current === "" &&
467
+ activeFilters.length === 0 &&
468
+ givenValues.size === 0
469
+ ) {
470
+ prevInputsRef.current = serialized;
421
471
  return;
422
472
  }
423
473
 
424
- prevActiveFiltersRef.current = serialized;
474
+ prevInputsRef.current = serialized;
425
475
 
426
- // Re-execute with current filters (or no filters if cleared)
476
+ // Re-execute with current filters and givens (or empty if cleared)
427
477
  if (!isExecuting) {
428
- executeCells(activeFilters);
478
+ executeCells(activeFilters, givenValues);
429
479
  }
430
- }, [activeFilters, isExecuting, executeCells]);
480
+ }, [activeFilters, givenValues, isExecuting, executeCells]);
431
481
 
432
482
  // Handle filter change using composite key
433
483
  const handleFilterChange = useCallback(
@@ -447,6 +497,14 @@ export default function Notebook({
447
497
  <CleanNotebookContainer>
448
498
  <CleanNotebookSection>
449
499
  <Stack spacing={3} component="section">
500
+ {/* Givens Panel — runtime parameters declared via `given:` */}
501
+ <GivensPanel
502
+ givens={declaredGivens}
503
+ values={givenValues}
504
+ onChange={updateGiven}
505
+ onClearAll={clearAllGivens}
506
+ />
507
+
450
508
  {/* Filter Panel */}
451
509
  {dimensionSpecs.length > 0 && filterValuesData && (
452
510
  <Paper
@@ -27,9 +27,6 @@ import { useServer } from "../ServerProvider";
27
27
  import { encodeResourceUri, parseResourceUri } from "../../utils/formatting";
28
28
  import { MALLOY_BRAND, MONO_FONT_FAMILY } from "../styles";
29
29
  import ContentTypeIcon from "./ContentTypeIcon";
30
- // TODO(redesign-followup): port the Connections section into the redesigned
31
- // flat-row aesthetic (currently rendered with its original card/table styling).
32
- import Connections from "./Connections";
33
30
 
34
31
  const README_NOTEBOOK = "README.malloynb";
35
32
 
@@ -213,10 +210,6 @@ export default function Package({
213
210
  {databases.length === 0 && <EmptyRow label="No data files" />}
214
211
  </PackageSection>
215
212
 
216
- <Box sx={{ mb: 4 }}>
217
- <Connections resourceUri={resourceUri} />
218
- </Box>
219
-
220
213
  {hasReadme && (
221
214
  <Box sx={{ mt: 6 }}>
222
215
  <Notebook
@@ -0,0 +1,190 @@
1
+ import ClearIcon from "@mui/icons-material/Clear";
2
+ import {
3
+ Autocomplete,
4
+ Checkbox,
5
+ FormControlLabel,
6
+ IconButton,
7
+ InputAdornment,
8
+ TextField,
9
+ } from "@mui/material";
10
+ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
11
+ import { DatePicker } from "@mui/x-date-pickers/DatePicker";
12
+ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
13
+ import dayjs from "dayjs";
14
+ import utc from "dayjs/plugin/utc";
15
+ import { Given } from "../../client";
16
+ import { GivenValue } from "../../hooks/useGivensForm";
17
+
18
+ dayjs.extend(utc);
19
+
20
+ export interface GivenInputProps {
21
+ given: Given;
22
+ value: GivenValue | undefined;
23
+ onChange: (next: GivenValue) => void;
24
+ }
25
+
26
+ /**
27
+ * Distill a given's `#(...)` annotation list into helper text for the UI.
28
+ * If an annotation includes `description="..."` (a Malloy convention), the
29
+ * quoted value is surfaced verbatim. Otherwise the annotation contents
30
+ * inside `#(...)` are joined as-is so model authors still see something
31
+ * recognizable. Returns undefined when nothing is renderable.
32
+ */
33
+ function annotationHelperText(given: Given): string | undefined {
34
+ const visible = (given.annotations ?? []).filter((a) =>
35
+ a.trim().startsWith("#("),
36
+ );
37
+ if (visible.length === 0) return undefined;
38
+
39
+ const rendered: string[] = [];
40
+ for (const raw of visible) {
41
+ const trimmed = raw.trim();
42
+ const descriptionMatch = trimmed.match(/description="([^"]*)"/);
43
+ if (descriptionMatch) {
44
+ rendered.push(descriptionMatch[1]);
45
+ continue;
46
+ }
47
+ // Strip leading `#(` and trailing `)`, then push the inner content
48
+ const inner = trimmed
49
+ .replace(/^#\(/, "")
50
+ .replace(/\)\s*$/, "")
51
+ .trim();
52
+ if (inner) rendered.push(inner);
53
+ }
54
+ return rendered.length > 0 ? rendered.join("\n") : undefined;
55
+ }
56
+
57
+ /**
58
+ * Renders an input widget appropriate for the declared given type.
59
+ * Unknown / unrecognized types fall back to a plain text input.
60
+ *
61
+ * For text-based inputs (string, number, filter, default), a clear (×)
62
+ * adornment appears when the field has a value. DatePicker, Checkbox, and
63
+ * multi-Autocomplete have their own native clear affordances.
64
+ */
65
+ export function GivenInput({ given, value, onChange }: GivenInputProps) {
66
+ const label = given.name ?? "";
67
+ const type = given.type ?? "string";
68
+ const helperText = annotationHelperText(given);
69
+
70
+ if (type === "boolean") {
71
+ const checked = value === true;
72
+ // Checkbox wrapped in FormControlLabel — no helperText slot available.
73
+ return (
74
+ <FormControlLabel
75
+ control={
76
+ <Checkbox
77
+ checked={checked}
78
+ onChange={(e) => onChange(e.target.checked)}
79
+ />
80
+ }
81
+ label={label}
82
+ />
83
+ );
84
+ }
85
+
86
+ if (type === "number") {
87
+ const num = typeof value === "number" ? value : "";
88
+ return (
89
+ <TextField
90
+ label={label}
91
+ type="number"
92
+ value={num}
93
+ onChange={(e) => {
94
+ const v = e.target.value;
95
+ onChange(v === "" ? null : Number(v));
96
+ }}
97
+ helperText={helperText}
98
+ slotProps={{
99
+ input: {
100
+ endAdornment: num !== "" && (
101
+ <ClearAdornment onClear={() => onChange(null)} />
102
+ ),
103
+ },
104
+ }}
105
+ fullWidth
106
+ size="small"
107
+ />
108
+ );
109
+ }
110
+
111
+ if (type === "date" || type === "timestamp" || type === "timestamptz") {
112
+ const dateValue = value instanceof Date ? dayjs.utc(value) : null;
113
+ return (
114
+ <LocalizationProvider dateAdapter={AdapterDayjs}>
115
+ <DatePicker
116
+ label={label}
117
+ value={dateValue}
118
+ onChange={(next) => onChange(next ? next.toDate() : null)}
119
+ slotProps={{
120
+ textField: { fullWidth: true, size: "small", helperText },
121
+ field: { clearable: true, onClear: () => onChange(null) },
122
+ }}
123
+ />
124
+ </LocalizationProvider>
125
+ );
126
+ }
127
+
128
+ if (type.startsWith("array<")) {
129
+ const list = Array.isArray(value) ? value.map(String) : [];
130
+ return (
131
+ <Autocomplete
132
+ multiple
133
+ freeSolo
134
+ options={[]}
135
+ value={list}
136
+ onChange={(_event, next) =>
137
+ onChange(next.length === 0 ? null : (next as string[]))
138
+ }
139
+ renderInput={(params) => (
140
+ <TextField
141
+ {...params}
142
+ label={label}
143
+ size="small"
144
+ helperText={helperText}
145
+ />
146
+ )}
147
+ fullWidth
148
+ />
149
+ );
150
+ }
151
+
152
+ // Default: string, filter<...>, or unknown types — plain text input
153
+ const str = typeof value === "string" ? value : "";
154
+ return (
155
+ <TextField
156
+ label={label}
157
+ value={str}
158
+ onChange={(e) => {
159
+ const v = e.target.value;
160
+ onChange(v === "" ? null : v);
161
+ }}
162
+ placeholder={type.startsWith("filter<") ? type : undefined}
163
+ helperText={helperText}
164
+ slotProps={{
165
+ input: {
166
+ endAdornment: str !== "" && (
167
+ <ClearAdornment onClear={() => onChange(null)} />
168
+ ),
169
+ },
170
+ }}
171
+ fullWidth
172
+ size="small"
173
+ />
174
+ );
175
+ }
176
+
177
+ function ClearAdornment({ onClear }: { onClear: () => void }) {
178
+ return (
179
+ <InputAdornment position="end">
180
+ <IconButton
181
+ size="small"
182
+ aria-label="clear value"
183
+ onClick={onClear}
184
+ edge="end"
185
+ >
186
+ <ClearIcon fontSize="small" />
187
+ </IconButton>
188
+ </InputAdornment>
189
+ );
190
+ }
@@ -0,0 +1,85 @@
1
+ import { Box, Button, Paper, Stack, Typography } from "@mui/material";
2
+ import { Given } from "../../client";
3
+ import { GivenValue } from "../../hooks/useGivensForm";
4
+ import { GivenInput } from "./GivenInput";
5
+
6
+ export interface GivensPanelProps {
7
+ givens: Given[];
8
+ values: Map<string, GivenValue>;
9
+ onChange: (name: string, value: GivenValue) => void;
10
+ onClearAll: () => void;
11
+ }
12
+
13
+ /**
14
+ * Renders the "Parameters" panel — one input per declared `given:`.
15
+ * Returns null when the model declares no givens, so callers can drop
16
+ * `<GivensPanel ... />` unconditionally without a length guard.
17
+ *
18
+ * A "Reset" button appears in the panel header when at least one value
19
+ * is set; it fires `onClearAll` which the parent should wire to
20
+ * `useGivensForm.clearAll`.
21
+ */
22
+ export function GivensPanel({
23
+ givens,
24
+ values,
25
+ onChange,
26
+ onClearAll,
27
+ }: GivensPanelProps) {
28
+ if (givens.length === 0) return null;
29
+ const hasValues = values.size > 0;
30
+
31
+ return (
32
+ <Paper
33
+ elevation={0}
34
+ sx={{
35
+ p: 3,
36
+ backgroundColor: "transparent",
37
+ border: "none",
38
+ boxShadow: "none",
39
+ }}
40
+ >
41
+ <Stack
42
+ direction="row"
43
+ alignItems="center"
44
+ justifyContent="space-between"
45
+ sx={{ mb: 2 }}
46
+ >
47
+ <Typography
48
+ variant="subtitle2"
49
+ sx={{ fontWeight: 600, color: "#333" }}
50
+ >
51
+ Parameters
52
+ </Typography>
53
+ {hasValues && (
54
+ <Button
55
+ variant="text"
56
+ size="small"
57
+ onClick={onClearAll}
58
+ sx={{ textTransform: "none" }}
59
+ >
60
+ Reset
61
+ </Button>
62
+ )}
63
+ </Stack>
64
+ <Box
65
+ sx={{
66
+ display: "grid",
67
+ gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
68
+ gap: 3,
69
+ }}
70
+ >
71
+ {givens.map((given) => (
72
+ <Box key={given.name}>
73
+ <GivenInput
74
+ given={given}
75
+ value={given.name ? values.get(given.name) : undefined}
76
+ onChange={(next) =>
77
+ given.name && onChange(given.name, next)
78
+ }
79
+ />
80
+ </Box>
81
+ ))}
82
+ </Box>
83
+ </Paper>
84
+ );
85
+ }
@@ -0,0 +1,2 @@
1
+ export { GivenInput, type GivenInputProps } from "./GivenInput";
2
+ export { GivensPanel, type GivensPanelProps } from "./GivensPanel";
@@ -31,3 +31,11 @@ export {
31
31
  type FilterType,
32
32
  type UseDimensionalFilterRangeDataParams,
33
33
  } from "./useDimensionalFilterRangeData";
34
+
35
+ // Givens hooks and types
36
+ export { useModelGivens } from "./useModelGivens";
37
+ export {
38
+ useGivensForm,
39
+ type GivenValue,
40
+ type UseGivensFormResult,
41
+ } from "./useGivensForm";
@@ -0,0 +1,110 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { Given } from "../client";
3
+
4
+ /**
5
+ * UI-side value for a given. Mirrors the JS shapes the server accepts
6
+ * for `givens` runtime values, plus `Date` (serialized to ISO before send).
7
+ */
8
+ export type GivenValue =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | Date
13
+ | string[]
14
+ | number[]
15
+ | null;
16
+
17
+ /**
18
+ * Result from the useGivensForm hook
19
+ */
20
+ export interface UseGivensFormResult {
21
+ /** Current value for each given, keyed by given name. Missing keys mean "use the model default". */
22
+ givenValues: Map<string, GivenValue>;
23
+ /** Update a given's value. Pass `null` to revert to model default. */
24
+ updateGiven: (name: string, value: GivenValue) => void;
25
+ /** Remove a single given override. */
26
+ clearGiven: (name: string) => void;
27
+ /** Remove all overrides. */
28
+ clearAll: () => void;
29
+ /** Map of just the givens that have a user-supplied (non-null) override. */
30
+ getActiveGivens: () => Map<string, Exclude<GivenValue, null>>;
31
+ }
32
+
33
+ /**
34
+ * Manages user-supplied values for model `given:` parameters.
35
+ *
36
+ * Mirrors the shape of [`useDimensionFilters`](./useDimensionFilters.ts):
37
+ * one entry per declared given, keyed by name (no source qualifier — givens
38
+ * are model-level, not per-source).
39
+ *
40
+ * When `givens` (the introspected list) changes (e.g., after the notebook
41
+ * loads), existing user overrides are preserved for matching names and new
42
+ * givens are added as empty.
43
+ */
44
+ export function useGivensForm(givens: Given[]): UseGivensFormResult {
45
+ const [givenValues, setGivenValues] = useState<Map<string, GivenValue>>(
46
+ () => new Map(),
47
+ );
48
+
49
+ // Sync the form when the declared givens list changes (e.g., model reloaded).
50
+ // Preserve existing values for givens that still exist; drop values for givens
51
+ // that no longer exist.
52
+ useEffect(() => {
53
+ setGivenValues((prev) => {
54
+ const declared = new Set(
55
+ givens.map((g) => g.name).filter((n): n is string => !!n),
56
+ );
57
+ const next = new Map<string, GivenValue>();
58
+ prev.forEach((value, name) => {
59
+ if (declared.has(name)) next.set(name, value);
60
+ });
61
+ return next;
62
+ });
63
+ }, [givens]);
64
+
65
+ const updateGiven = useCallback((name: string, value: GivenValue) => {
66
+ setGivenValues((prev) => {
67
+ const next = new Map(prev);
68
+ if (value === null) {
69
+ next.delete(name);
70
+ } else {
71
+ next.set(name, value);
72
+ }
73
+ return next;
74
+ });
75
+ }, []);
76
+
77
+ const clearGiven = useCallback((name: string) => {
78
+ setGivenValues((prev) => {
79
+ if (!prev.has(name)) return prev;
80
+ const next = new Map(prev);
81
+ next.delete(name);
82
+ return next;
83
+ });
84
+ }, []);
85
+
86
+ const clearAll = useCallback(() => {
87
+ setGivenValues((prev) => (prev.size === 0 ? prev : new Map()));
88
+ }, []);
89
+
90
+ const getActiveGivens = useCallback((): Map<
91
+ string,
92
+ Exclude<GivenValue, null>
93
+ > => {
94
+ const active = new Map<string, Exclude<GivenValue, null>>();
95
+ givenValues.forEach((value, name) => {
96
+ if (value !== null && value !== undefined) {
97
+ active.set(name, value);
98
+ }
99
+ });
100
+ return active;
101
+ }, [givenValues]);
102
+
103
+ return {
104
+ givenValues,
105
+ updateGiven,
106
+ clearGiven,
107
+ clearAll,
108
+ getActiveGivens,
109
+ };
110
+ }
@@ -0,0 +1,26 @@
1
+ import { useMemo } from "react";
2
+ import { Given, RawNotebook } from "../client";
3
+
4
+ /**
5
+ * Extracts the deduplicated list of model-level `given:` declarations from
6
+ * a notebook's sources. The server attaches the same model-level givens to
7
+ * every `Source` for SDK ergonomics (see #761); this hook collapses them
8
+ * back to one entry per name so callers render a single input per given.
9
+ *
10
+ * Returns an empty array when no givens are declared on any source.
11
+ */
12
+ export function useModelGivens(notebook: RawNotebook | undefined): Given[] {
13
+ return useMemo(() => {
14
+ if (!notebook?.sources?.length) return [];
15
+ const byName = new Map<string, Given>();
16
+ for (const source of notebook.sources) {
17
+ if (!source.givens?.length) continue;
18
+ for (const given of source.givens) {
19
+ if (given.name && !byName.has(given.name)) {
20
+ byName.set(given.name, given);
21
+ }
22
+ }
23
+ }
24
+ return Array.from(byName.values());
25
+ }, [notebook]);
26
+ }