@gradio/core 1.0.0 → 1.0.2

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,21 +76,25 @@ 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[],
78
83
  layout: LayoutNode,
79
84
  dependencies: Dependency[],
80
- config: AppConfig,
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;
86
92
  });
87
93
  this.reactive_formatter = reactive_formatter;
88
-
89
- this.#config = config;
94
+ this.#config = {
95
+ ...config,
96
+ api_url: new URL(config.api_prefix, config.root).toString()
97
+ };
90
98
  this.#component_payload = components;
91
99
  this.#layout_payload = layout;
92
100
  this.#dependency_payload = dependencies;
@@ -123,17 +131,22 @@ export class AppTree {
123
131
  this.initial_tabs = {};
124
132
  gather_initial_tabs(this.root!, this.initial_tabs);
125
133
  this.postprocess(this.root!);
134
+ this.#event_dispatcher = event_dispatcher;
135
+ this.root_untracked = this.root;
126
136
  }
127
137
 
128
138
  reload(
129
139
  components: ComponentMeta[],
130
140
  layout: LayoutNode,
131
141
  dependencies: Dependency[],
132
- config: AppConfig
142
+ config: Omit<AppConfig, "api_url">
133
143
  ) {
134
144
  this.#layout_payload = layout;
135
145
  this.#component_payload = components;
136
- this.#config = config;
146
+ this.#config = {
147
+ ...config,
148
+ api_url: new URL(config.api_prefix, config.root).toString()
149
+ };
137
150
  this.#dependency_payload = dependencies;
138
151
 
139
152
  this.root = this.create_node(
@@ -203,22 +216,22 @@ export class AppTree {
203
216
 
204
217
  postprocess(tree: ProcessedComponentMeta) {
205
218
  this.root = this.traverse(tree, [
206
- (node) => handle_visibility(node, this.#config.root),
219
+ (node) => handle_visibility(node, this.#config.api_url),
207
220
  (node) =>
208
221
  untrack_children_of_invisible_parents(
209
222
  node,
210
- this.#config.root,
211
223
  this.components_to_register
212
224
  ),
225
+ (node) => handle_empty_forms(node, this.components_to_register),
226
+ (node) => translate_props(node),
227
+ (node) => apply_initial_tabs(node, this.initial_tabs),
228
+ (node) => this.find_attached_events(node, this.#dependency_payload),
213
229
  (node) =>
214
- handle_empty_forms(
230
+ untrack_children_of_closed_accordions_or_inactive_tabs(
215
231
  node,
216
- this.#config.root,
217
- this.components_to_register
218
- ),
219
- (node) => translate_props(node, this.#config.root),
220
- (node) => apply_initial_tabs(node, this.#config.root, this.initial_tabs),
221
- (node) => this.find_attached_events(node, this.#dependency_payload)
232
+ this.components_to_register,
233
+ this.#hidden_on_startup
234
+ )
222
235
  ]);
223
236
  }
224
237
 
@@ -317,6 +330,7 @@ export class AppTree {
317
330
  component.props,
318
331
  [this.#input_ids, this.#output_ids],
319
332
  this.client,
333
+ this.#config.api_url,
320
334
  { ...this.#config }
321
335
  );
322
336
 
@@ -335,14 +349,14 @@ export class AppTree {
335
349
  ? get_component(
336
350
  component.type,
337
351
  component.component_class_id,
338
- this.#config.root || ""
352
+ this.#config.api_url || ""
339
353
  )
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),
394
- (n) => handle_visibility(n, this.#config.root)
421
+ //@ts-ignore
422
+ (n) => update_parent_visibility(n, id, new_state.visible),
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.root)
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
@@ -464,28 +559,15 @@ function gather_props(
464
559
  props: ComponentMeta["props"],
465
560
  dependencies: [Set<number>, Set<number>],
466
561
  client: client_return,
562
+ api_url: string,
467
563
  additional: Record<string, unknown> = {}
468
564
  ): {
469
565
  shared_props: SharedProps;
470
566
  props: Record<string, unknown>;
471
567
  } {
472
- const _shared_props: Partial<SharedProps> = {};
473
- const _props: Record<string, unknown> = {};
474
- for (const key in props) {
475
- // For Tabs (or any component that already has an id prop)
476
- // Set the id to the props so that it doesn't get overwritten
477
- if (key === "id" || key === "autoscroll") {
478
- _props[key] = props[key];
479
- } else if (allowed_shared_props.includes(key as keyof SharedProps)) {
480
- const _key = key as keyof SharedProps;
481
- _shared_props[_key] = props[key];
482
- if (_key === "server_fns") {
483
- _shared_props.server = process_server_fn(id, props.server_fns, client);
484
- }
485
- } else {
486
- _props[key] = props[key];
487
- }
488
- }
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);
489
571
 
490
572
  for (const key in additional) {
491
573
  if (allowed_shared_props.includes(key as keyof SharedProps)) {
@@ -508,13 +590,7 @@ function gather_props(
508
590
  _shared_props.load_component = (
509
591
  name: string,
510
592
  variant: "base" | "component" | "example"
511
- ) =>
512
- get_component(
513
- name,
514
- "",
515
- _shared_props.root || "",
516
- variant
517
- ) as LoadingComponent;
593
+ ) => get_component(name, "", api_url, variant) as LoadingComponent;
518
594
 
519
595
  _shared_props.visible =
520
596
  _shared_props.visible === undefined ? true : _shared_props.visible;
@@ -525,19 +601,19 @@ function gather_props(
525
601
 
526
602
  function handle_visibility(
527
603
  node: ProcessedComponentMeta,
528
- root: string
604
+ api_url: string
529
605
  ): ProcessedComponentMeta {
530
606
  // Check if the node is visible
531
607
  if (node.props.shared_props.visible && !node.component) {
532
608
  const result: ProcessedComponentMeta = {
533
609
  ...node,
534
- component: get_component(node.type, node.component_class_id, root),
610
+ component: get_component(node.type, node.component_class_id, api_url),
535
611
  children: []
536
612
  };
537
613
 
538
614
  if (node.children) {
539
615
  result.children = node.children.map((child) =>
540
- handle_visibility(child, root)
616
+ handle_visibility(child, api_url)
541
617
  );
542
618
  }
543
619
  return result;
@@ -570,7 +646,6 @@ function _untrack(
570
646
 
571
647
  function untrack_children_of_invisible_parents(
572
648
  node: ProcessedComponentMeta,
573
- root: string,
574
649
  components_to_register: Set<number>
575
650
  ): ProcessedComponentMeta {
576
651
  // Check if the node is visible
@@ -580,9 +655,51 @@ function untrack_children_of_invisible_parents(
580
655
  return node;
581
656
  }
582
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
+
583
701
  function handle_empty_forms(
584
702
  node: ProcessedComponentMeta,
585
- root: string,
586
703
  components_to_register: Set<number>
587
704
  ): ProcessedComponentMeta {
588
705
  // Check if the node is visible
@@ -601,10 +718,34 @@ function handle_empty_forms(
601
718
  return node;
602
719
  }
603
720
 
604
- function translate_props(
721
+ function update_parent_visibility(
605
722
  node: ProcessedComponentMeta,
606
- root: string
723
+ child_made_visible: number,
724
+ visibility_state: boolean | "hidden"
607
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
+
748
+ function translate_props(node: ProcessedComponentMeta): ProcessedComponentMeta {
608
749
  const supported_props = [
609
750
  "description",
610
751
  "info",
@@ -631,12 +772,13 @@ function translate_props(
631
772
 
632
773
  function apply_initial_tabs(
633
774
  node: ProcessedComponentMeta,
634
- root: string,
635
775
  initial_tabs: Record<number, Tab[]>
636
776
  ): ProcessedComponentMeta {
637
777
  if (node.type === "tabs" && node.id in initial_tabs) {
638
778
  const tabs = initial_tabs[node.id].sort((a, b) => a.order! - b.order!);
639
779
  node.props.props.initial_tabs = tabs;
780
+ } else if (node.type === "tabitem") {
781
+ node.props.props.component_id = node.id;
640
782
  }
641
783
  return node;
642
784
  }
@@ -660,7 +802,8 @@ function _gather_initial_tabs(
660
802
  elem_id: node.props.shared_props.elem_id,
661
803
  visible: node.props.shared_props.visible as boolean,
662
804
  interactive: node.props.shared_props.interactive,
663
- scale: node.props.shared_props.scale || null
805
+ scale: node.props.shared_props.scale || null,
806
+ component_id: node.id
664
807
  });
665
808
  node.props.props.order = order;
666
809
  }
@@ -715,3 +858,21 @@ function find_node_by_id(
715
858
 
716
859
  return null;
717
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 */
@@ -126,4 +127,5 @@ export interface AppConfig {
126
127
  max_file_size?: number;
127
128
  autoscroll: boolean;
128
129
  api_prefix: string;
130
+ api_url: string;
129
131
  }