@gradio/core 1.0.1 → 1.1.0

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.
@@ -5,6 +5,7 @@ import {
5
5
  } from "./init_utils";
6
6
  import { translate_if_needed } from "./i18n";
7
7
  import { tick } from "svelte";
8
+ import { dequal } from "dequal";
8
9
 
9
10
  import type {
10
11
  ComponentMeta,
@@ -15,7 +16,7 @@ import type {
15
16
  AppConfig,
16
17
  ServerFunctions
17
18
  } from "./types";
18
- import type { SharedProps } from "@gradio/utils";
19
+ import { type SharedProps } from "@gradio/utils";
19
20
  import { allowed_shared_props } from "@gradio/utils";
20
21
  import { Client } from "@gradio/client";
21
22
 
@@ -33,6 +34,7 @@ type Tab = {
33
34
  elem_id: string | undefined;
34
35
  scale: number | null;
35
36
  order?: number;
37
+ component_id: number;
36
38
  };
37
39
 
38
40
  const type_map = {
@@ -54,6 +56,7 @@ export class AppTree {
54
56
 
55
57
  /** the root node of the processed layout tree */
56
58
  root = $state<ProcessedComponentMeta>();
59
+ root_untracked: ProcessedComponentMeta;
57
60
 
58
61
  /** a set of all component IDs that are inputs to dependencies */
59
62
  #input_ids: Set<number> = new Set();
@@ -65,6 +68,7 @@ export class AppTree {
65
68
 
66
69
  #get_callbacks = new Map<number, get_data_type>();
67
70
  #set_callbacks = new Map<number, set_data_type>();
71
+ #event_dispatcher: (id: number, event: string, data: unknown) => void;
68
72
  component_ids: number[];
69
73
  initial_tabs: Record<number, Tab[]> = {};
70
74
 
@@ -72,6 +76,7 @@ export class AppTree {
72
76
  ready: Promise<void>;
73
77
  ready_resolve!: () => void;
74
78
  resolved: boolean = false;
79
+ #hidden_on_startup: Set<number> = new Set();
75
80
 
76
81
  constructor(
77
82
  components: ComponentMeta[],
@@ -79,7 +84,8 @@ export class AppTree {
79
84
  dependencies: Dependency[],
80
85
  config: Omit<AppConfig, "api_url">,
81
86
  app: client_return,
82
- reactive_formatter: (str: string) => string
87
+ reactive_formatter: (str: string) => string,
88
+ event_dispatcher: (id: number, event: string, data: unknown) => void
83
89
  ) {
84
90
  this.ready = new Promise<void>((resolve) => {
85
91
  this.ready_resolve = resolve;
@@ -125,6 +131,8 @@ export class AppTree {
125
131
  this.initial_tabs = {};
126
132
  gather_initial_tabs(this.root!, this.initial_tabs);
127
133
  this.postprocess(this.root!);
134
+ this.#event_dispatcher = event_dispatcher;
135
+ this.root_untracked = this.root;
128
136
  }
129
137
 
130
138
  reload(
@@ -217,7 +225,13 @@ export class AppTree {
217
225
  (node) => handle_empty_forms(node, this.components_to_register),
218
226
  (node) => translate_props(node),
219
227
  (node) => apply_initial_tabs(node, this.initial_tabs),
220
- (node) => this.find_attached_events(node, this.#dependency_payload)
228
+ (node) => this.find_attached_events(node, this.#dependency_payload),
229
+ (node) =>
230
+ untrack_children_of_closed_accordions_or_inactive_tabs(
231
+ node,
232
+ this.components_to_register,
233
+ this.#hidden_on_startup
234
+ )
221
235
  ]);
222
236
  }
223
237
 
@@ -340,9 +354,9 @@ export class AppTree {
340
354
  : null,
341
355
  key: component.key,
342
356
  rendered_in: component.rendered_in,
343
- documentation: component.documentation
357
+ documentation: component.documentation,
358
+ original_visibility: processed_props.shared_props.visible
344
359
  };
345
-
346
360
  return node;
347
361
  }
348
362
 
@@ -369,6 +383,19 @@ export class AppTree {
369
383
  n.children = subtree.children;
370
384
  }
371
385
 
386
+ async update_visibility(
387
+ node: ProcessedComponentMeta,
388
+ new_state: any
389
+ ): Promise<void> {
390
+ node.children.forEach((child) => {
391
+ const _set_data = this.#set_callbacks.get(child.id);
392
+ if (_set_data) {
393
+ _set_data(new_state);
394
+ }
395
+ this.update_visibility(child, new_state);
396
+ });
397
+ }
398
+
372
399
  /*
373
400
  * Updates the state of a component by its ID
374
401
  * @param id the ID of the component to update
@@ -391,20 +418,42 @@ export class AppTree {
391
418
  this.root = this.traverse(this.root!, [
392
419
  //@ts-ignore
393
420
  (n) => set_visibility_for_updated_node(n, id, new_state.visible),
421
+ //@ts-ignore
422
+ (n) => update_parent_visibility(n, id, new_state.visible),
394
423
  (n) => handle_visibility(n, this.#config.api_url)
395
424
  ]);
425
+ await tick();
396
426
  already_updated_visibility = true;
397
427
  }
398
428
  const _set_data = this.#set_callbacks.get(id);
399
- if (!_set_data) return;
400
- _set_data(new_state);
429
+ if (!_set_data) {
430
+ const old_value = node?.props.props.value;
431
+ // @ts-ignore
432
+ const new_props = create_props_shared_props(new_state);
433
+ node!.props.shared_props = {
434
+ ...node?.props.shared_props,
435
+ ...new_props.shared_props
436
+ };
437
+ node!.props.props = { ...node?.props.props, ...new_props.props };
438
+ if ("value" in new_state && !dequal(old_value, new_state.value)) {
439
+ this.#event_dispatcher(id, "change", null);
440
+ }
441
+ } else if (_set_data) {
442
+ _set_data(new_state);
443
+ }
401
444
  if (!check_visibility || already_updated_visibility) return;
402
445
  // need to let the UI settle before traversing again
403
446
  // otherwise there could be
404
447
  await tick();
405
- this.root = this.traverse(this.root!, (n) =>
406
- handle_visibility(n, this.#config.api_url)
407
- );
448
+ // Update the visibility in a way that does not
449
+ // re-render the root/tree. Doing that would nuke
450
+ // any values currently in the UI.
451
+ // @ts-ignore
452
+ await this.update_visibility(node, new_state);
453
+ const parent_node = find_parent(this.root!, id);
454
+ if (parent_node)
455
+ // @ts-ignore
456
+ update_parent_visibility(parent_node, id, new_state.visible);
408
457
  }
409
458
 
410
459
  /**
@@ -414,14 +463,39 @@ export class AppTree {
414
463
  */
415
464
  async get_state(id: number): Promise<Record<string, unknown> | null> {
416
465
  const _get_data = this.#get_callbacks.get(id);
417
- const component = this.#component_payload.find((c) => c.id === id);
466
+ const component = find_node_by_id(this.root!, id);
418
467
  if (!_get_data && !component) return null;
419
468
  if (_get_data) return await _get_data();
420
469
 
421
- if (component) return Promise.resolve({ value: component.props.value });
470
+ if (component)
471
+ return Promise.resolve({ value: component.props.props.value });
422
472
 
423
473
  return null;
424
474
  }
475
+
476
+ async render_previously_invisible_children(id: number) {
477
+ this.root = this.traverse(this.root!, [
478
+ (node) => {
479
+ if (node.id === id) {
480
+ make_visible_if_not_rendered(node, this.#hidden_on_startup);
481
+ }
482
+ return node;
483
+ },
484
+ (node) => handle_visibility(node, this.#config.api_url)
485
+ ]);
486
+ }
487
+ }
488
+
489
+ function make_visible_if_not_rendered(
490
+ node: ProcessedComponentMeta,
491
+ hidden_on_startup: Set<number>
492
+ ): void {
493
+ node.props.shared_props.visible = hidden_on_startup.has(node.id)
494
+ ? true
495
+ : node.props.shared_props.visible;
496
+ node.children.forEach((child) => {
497
+ make_visible_if_not_rendered(child, hidden_on_startup);
498
+ });
425
499
  }
426
500
 
427
501
  /**
@@ -451,6 +525,27 @@ export function process_server_fn(
451
525
  }, {} as ServerFunctions);
452
526
  }
453
527
 
528
+ function create_props_shared_props(props: ComponentMeta["props"]): {
529
+ shared_props: SharedProps;
530
+ props: Record<string, unknown>;
531
+ } {
532
+ const _shared_props: Partial<SharedProps> = {};
533
+ const _props: Record<string, unknown> = {};
534
+ for (const key in props) {
535
+ // For Tabs (or any component that already has an id prop)
536
+ // Set the id to the props so that it doesn't get overwritten
537
+ if (key === "id" || key === "autoscroll") {
538
+ _props[key] = props[key];
539
+ } else if (allowed_shared_props.includes(key as keyof SharedProps)) {
540
+ const _key = key as keyof SharedProps;
541
+ _shared_props[_key] = props[key];
542
+ } else {
543
+ _props[key] = props[key];
544
+ }
545
+ }
546
+ return { shared_props: _shared_props as SharedProps, props: _props };
547
+ }
548
+
454
549
  /**
455
550
  * Gathers the props for a component
456
551
  * @param id the ID of the component
@@ -470,23 +565,9 @@ function gather_props(
470
565
  shared_props: SharedProps;
471
566
  props: Record<string, unknown>;
472
567
  } {
473
- const _shared_props: Partial<SharedProps> = {};
474
- const _props: Record<string, unknown> = {};
475
- for (const key in props) {
476
- // For Tabs (or any component that already has an id prop)
477
- // Set the id to the props so that it doesn't get overwritten
478
- if (key === "id" || key === "autoscroll") {
479
- _props[key] = props[key];
480
- } else if (allowed_shared_props.includes(key as keyof SharedProps)) {
481
- const _key = key as keyof SharedProps;
482
- _shared_props[_key] = props[key];
483
- if (_key === "server_fns") {
484
- _shared_props.server = process_server_fn(id, props.server_fns, client);
485
- }
486
- } else {
487
- _props[key] = props[key];
488
- }
489
- }
568
+ const { shared_props: _shared_props, props: _props } =
569
+ create_props_shared_props(props);
570
+ _shared_props.server = process_server_fn(id, props.server_fns, client);
490
571
 
491
572
  for (const key in additional) {
492
573
  if (allowed_shared_props.includes(key as keyof SharedProps)) {
@@ -574,6 +655,49 @@ function untrack_children_of_invisible_parents(
574
655
  return node;
575
656
  }
576
657
 
658
+ function mark_component_invisible_if_visible(
659
+ node: ProcessedComponentMeta,
660
+ hidden_on_startup: Set<number>
661
+ ): ProcessedComponentMeta {
662
+ if (node.props.shared_props.visible === true) {
663
+ hidden_on_startup.add(node.id);
664
+ node.props.shared_props.visible = false;
665
+ }
666
+ node.children.forEach((child) => {
667
+ mark_component_invisible_if_visible(child, hidden_on_startup);
668
+ });
669
+ return node;
670
+ }
671
+
672
+ function untrack_children_of_closed_accordions_or_inactive_tabs(
673
+ node: ProcessedComponentMeta,
674
+ components_to_register: Set<number>,
675
+ hidden_on_startup: Set<number>
676
+ ): ProcessedComponentMeta {
677
+ // Check if the node is an accordion or tabs
678
+ if (node.type === "accordion" && node.props.props.open === false) {
679
+ _untrack(node, components_to_register);
680
+ if (node.children) {
681
+ node.children.forEach((child) => {
682
+ mark_component_invisible_if_visible(child, hidden_on_startup);
683
+ });
684
+ }
685
+ }
686
+ if (node.type === "tabs") {
687
+ node.children.forEach((child) => {
688
+ if (
689
+ child.type === "tabitem" &&
690
+ child.props.props.id !==
691
+ (node.props.props.selected || node.props.props.initial_tabs[0].id)
692
+ ) {
693
+ _untrack(child, components_to_register);
694
+ mark_component_invisible_if_visible(child, hidden_on_startup);
695
+ }
696
+ });
697
+ }
698
+ return node;
699
+ }
700
+
577
701
  function handle_empty_forms(
578
702
  node: ProcessedComponentMeta,
579
703
  components_to_register: Set<number>
@@ -594,6 +718,33 @@ function handle_empty_forms(
594
718
  return node;
595
719
  }
596
720
 
721
+ function update_parent_visibility(
722
+ node: ProcessedComponentMeta,
723
+ child_made_visible: number,
724
+ visibility_state: boolean | "hidden"
725
+ ): ProcessedComponentMeta {
726
+ // This function was added to address a tricky situation:
727
+ // Form components are wrapped in a Form component automatically.
728
+ // If all the children of the Form are invisible, the Form itself is marked invisible.
729
+ // in AppTree.postprocess -> handle_empty_forms
730
+ // This is to avoid rendering empty forms in the UI. They look ugly.
731
+ // So what happens when a child inside the Form is made visible again?
732
+ // The Form needs to become visible again too.
733
+ // If the child is made invisible, the form should be too if all other children are invisible.
734
+ // However, we are not doing this now since what we want to do is fetch the latest visibility of all
735
+ // the children from the UI. However, get_data only returns the props, not the shared props.
736
+ if (
737
+ node.type === "form" &&
738
+ node.children.length &&
739
+ node.children.some((child) => child.id === child_made_visible)
740
+ ) {
741
+ if (visibility_state === true) node.props.shared_props.visible = true;
742
+ else if (!visibility_state && node.children.length === 1)
743
+ node.props.shared_props.visible = "hidden";
744
+ }
745
+ return node;
746
+ }
747
+
597
748
  function translate_props(node: ProcessedComponentMeta): ProcessedComponentMeta {
598
749
  const supported_props = [
599
750
  "description",
@@ -626,6 +777,8 @@ function apply_initial_tabs(
626
777
  if (node.type === "tabs" && node.id in initial_tabs) {
627
778
  const tabs = initial_tabs[node.id].sort((a, b) => a.order! - b.order!);
628
779
  node.props.props.initial_tabs = tabs;
780
+ } else if (node.type === "tabitem") {
781
+ node.props.props.component_id = node.id;
629
782
  }
630
783
  return node;
631
784
  }
@@ -649,7 +802,8 @@ function _gather_initial_tabs(
649
802
  elem_id: node.props.shared_props.elem_id,
650
803
  visible: node.props.shared_props.visible as boolean,
651
804
  interactive: node.props.shared_props.interactive,
652
- scale: node.props.shared_props.scale || null
805
+ scale: node.props.shared_props.scale || null,
806
+ component_id: node.id
653
807
  });
654
808
  node.props.props.order = order;
655
809
  }
@@ -704,3 +858,21 @@ function find_node_by_id(
704
858
 
705
859
  return null;
706
860
  }
861
+
862
+ function find_parent(
863
+ tree: ProcessedComponentMeta,
864
+ id: number
865
+ ): ProcessedComponentMeta | null {
866
+ if (tree.children) {
867
+ for (const child of tree.children) {
868
+ if (child.id === id) {
869
+ return tree;
870
+ }
871
+ const result = find_parent(child, id);
872
+ if (result) {
873
+ return result;
874
+ }
875
+ }
876
+ }
877
+ return null;
878
+ }
package/src/init.test.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  determine_interactivity,
11
11
  process_server_fn,
12
12
  get_component
13
- } from "./init";
13
+ } from "./_init";
14
14
 
15
15
  describe("process_frontend_fn", () => {
16
16
  test("empty source code returns null", () => {
package/src/types.ts CHANGED
@@ -32,6 +32,7 @@ export interface ProcessedComponentMeta {
32
32
  component_class_id: string; // ?;
33
33
  key: string | number | null; // ?;
34
34
  rendered_in?: number; // ?;
35
+ original_visibility: boolean | "hidden";
35
36
  }
36
37
 
37
38
  /** Dictates whether a dependency is continous and/or a generator */