@gradio/core 0.27.1 → 0.28.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/src/Blocks.svelte +80 -2
  3. package/dist/src/Embed.svelte +45 -4
  4. package/dist/src/Embed.svelte.d.ts +1 -0
  5. package/dist/src/api_docs/ApiDocs.svelte +18 -3
  6. package/dist/src/api_docs/CodeSnippet.svelte +38 -36
  7. package/dist/src/api_docs/CodeSnippet.svelte.d.ts +1 -0
  8. package/dist/src/api_docs/EndpointDetail.svelte +23 -1
  9. package/dist/src/api_docs/EndpointDetail.svelte.d.ts +1 -0
  10. package/dist/src/api_docs/MCPSnippet.svelte +39 -0
  11. package/dist/src/api_docs/MCPSnippet.svelte.d.ts +3 -0
  12. package/dist/src/api_docs/utils.d.ts +2 -0
  13. package/dist/src/api_docs/utils.js +14 -0
  14. package/dist/src/i18n.d.ts +2 -1
  15. package/dist/src/i18n.js +3 -3
  16. package/dist/src/init.js +19 -15
  17. package/dist/src/lang/id.json +154 -0
  18. package/dist/src/navbar_store.d.ts +6 -0
  19. package/dist/src/navbar_store.js +2 -0
  20. package/dist/src/stores.d.ts +2 -0
  21. package/dist/src/stores.js +10 -0
  22. package/dist/src/stories/I18nMultiLanguageTest.stories.d.ts +0 -1
  23. package/dist/src/stories/I18nMultiLanguageTest.stories.js +1 -1
  24. package/package.json +57 -52
  25. package/src/Blocks.svelte +102 -2
  26. package/src/Embed.svelte +69 -4
  27. package/src/api_docs/ApiDocs.svelte +24 -3
  28. package/src/api_docs/CodeSnippet.svelte +38 -36
  29. package/src/api_docs/EndpointDetail.svelte +24 -1
  30. package/src/api_docs/MCPSnippet.svelte +40 -0
  31. package/src/api_docs/utils.ts +14 -0
  32. package/src/i18n.ts +5 -3
  33. package/src/init.ts +23 -15
  34. package/src/lang/id.json +154 -0
  35. package/src/navbar_store.ts +9 -0
  36. package/src/stores.ts +19 -0
  37. package/src/stories/I18nMultiLanguageTest.stories.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @gradio/core
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Features
6
+
7
+ - [#11814](https://github.com/gradio-app/gradio/pull/11814) [`013784a`](https://github.com/gradio-app/gradio/commit/013784a7086047651e8e661a38bde7d5c7f10db7) - add validation support. Thanks @pngwn!
8
+ - [#11833](https://github.com/gradio-app/gradio/pull/11833) [`a446fcb`](https://github.com/gradio-app/gradio/commit/a446fcba6f3fe59c32194beb7f27fb6f80b61347) - Add gr.Navbar component for multipage apps. Thanks @abidlabs!
9
+ - [#11783](https://github.com/gradio-app/gradio/pull/11783) [`f407daf`](https://github.com/gradio-app/gradio/commit/f407daf8046f37e042ab8b86730ff0ab8d174bcf) - Add Walkthrough and Step compoents to facilitate multi-step workflows. Thanks @pngwn!
10
+
11
+ ### Fixes
12
+
13
+ - [#11815](https://github.com/gradio-app/gradio/pull/11815) [`1a477c5`](https://github.com/gradio-app/gradio/commit/1a477c5202c13097d1089fb70a32a08db22d7660) - Fix i18n string visible during load and i18n not respecting HTML. Thanks @freddyaboulton!
14
+ - [#11749](https://github.com/gradio-app/gradio/pull/11749) [`70f4532`](https://github.com/gradio-app/gradio/commit/70f4532a4dc7576dbdbe1d0a43a05644a0dfcf43) - fix various iFrame related UI issues when deploying to spaces. Thanks @pngwn!
15
+
16
+ ### Dependency updates
17
+
18
+ - @gradio/client@1.18.0
19
+ - @gradio/icons@0.14.0
20
+ - @gradio/atoms@0.17.0
21
+ - @gradio/statustracker@0.11.0
22
+ - @gradio/gallery@0.15.32
23
+ - @gradio/plot@0.9.23
24
+ - @gradio/upload@0.16.17
25
+ - @gradio/file@0.12.29
26
+ - @gradio/image@0.22.18
27
+ - @gradio/video@0.15.1
28
+ - @gradio/tabitem@0.6.0
29
+ - @gradio/tabs@0.5.0
30
+ - @gradio/code@0.14.16
31
+ - @gradio/paramviewer@0.7.16
32
+ - @gradio/column@0.2.1
33
+ - @gradio/textbox@0.11.0
34
+ - @gradio/dropdown@0.10.3
35
+ - @gradio/button@0.5.12
36
+ - @gradio/checkbox@0.4.29
37
+
38
+ ## 0.27.2
39
+
40
+ ### Features
41
+
42
+ - [#11764](https://github.com/gradio-app/gradio/pull/11764) [`e6ce731`](https://github.com/gradio-app/gradio/commit/e6ce731bbcf2889c8147e57bc2ca97e2c731ddf5) - Display performance metrics for API/MCP requests in View API page. Thanks @freddyaboulton!
43
+
44
+ ### Dependency updates
45
+
46
+ - @gradio/statustracker@0.10.18
47
+
3
48
  ## 0.27.1
4
49
 
5
50
  ### Features
@@ -193,6 +193,17 @@ async function handle_update(data, fn_index) {
193
193
  }
194
194
  });
195
195
  update_value(updates);
196
+ updates.forEach((update) => {
197
+ const component = components.find((comp) => comp.id === update.id);
198
+ if (component && component.type === "navbar") {
199
+ import("./navbar_store").then(({ navbar_config }) => {
200
+ navbar_config.update((current) => ({
201
+ ...current,
202
+ [update.prop]: update.value
203
+ }));
204
+ });
205
+ }
206
+ });
196
207
  await tick();
197
208
  }
198
209
  let submit_map = /* @__PURE__ */ new Map();
@@ -485,6 +496,40 @@ async function trigger_api_call(dep_index, trigger_id = null, event_data = null)
485
496
  }
486
497
  }
487
498
  function handle_status_update(message) {
499
+ if (message.code === "validation_error") {
500
+ const dep2 = dependencies.find((dep3) => dep3.id === message.fn_index);
501
+ if (dep2 === void 0 || message.message === void 0 || typeof message.message === "string") {
502
+ return;
503
+ }
504
+ const validation_error_data = [];
505
+ message.message.forEach((message2, i) => {
506
+ if (message2.is_valid) {
507
+ return;
508
+ }
509
+ validation_error_data.push({
510
+ id: dep2.inputs[i],
511
+ prop: "validation_error",
512
+ value: message2.message
513
+ });
514
+ validation_error_data.push({
515
+ id: dep2.inputs[i],
516
+ prop: "loading_status",
517
+ value: { validation_error: message2.message }
518
+ });
519
+ });
520
+ if (validation_error_data.length > 0) {
521
+ update_value(validation_error_data);
522
+ loading_status.update({
523
+ status: "complete",
524
+ fn_index: message.fn_index,
525
+ eta: 0,
526
+ queue: false,
527
+ queue_position: null
528
+ });
529
+ set_status($loading_status);
530
+ return;
531
+ }
532
+ }
488
533
  if (message.broken && !broken_connection) {
489
534
  messages = [
490
535
  new_message(
@@ -567,7 +612,7 @@ async function trigger_api_call(dep_index, trigger_id = null, event_data = null)
567
612
  submit_map.delete(dep_index);
568
613
  }
569
614
  if (status.stage === "error" && !broken_connection && !message.session_not_found) {
570
- if (status.message) {
615
+ if (status.message && typeof status.message === "string") {
571
616
  const _message = status.message.replace(
572
617
  MESSAGE_QUOTE_RE,
573
618
  (_2, b) => b
@@ -636,6 +681,11 @@ async function handle_mount() {
636
681
  target.addEventListener("prop_change", (e) => {
637
682
  if (!isCustomEvent(e)) throw new Error("not a custom event");
638
683
  const { id, prop, value } = e.detail;
684
+ if (prop === "value") {
685
+ update_value([
686
+ { id, prop: "loading_status", value: { validation_error: void 0 } }
687
+ ]);
688
+ }
639
689
  update_value([{ id, prop, value }]);
640
690
  if (prop === "input_ready" && value === false) {
641
691
  inputs_waiting.push(id);
@@ -780,6 +830,33 @@ function screen_recording() {
780
830
  screen_recorder.startRecording();
781
831
  }
782
832
  }
833
+ let footer_height = 0;
834
+ let root_container;
835
+ $: root_node = $_layout && get_root_node(root_container);
836
+ function get_root_node(container) {
837
+ if (!container) return null;
838
+ return container.children[container.children.length - 1];
839
+ }
840
+ onMount(() => {
841
+ if ("parentIFrame" in window) {
842
+ window.parentIFrame?.autoResize(false);
843
+ }
844
+ const mut = new MutationObserver((mutations) => {
845
+ if ("parentIFrame" in window) {
846
+ const box = root_node?.getBoundingClientRect();
847
+ if (!box) return;
848
+ window.parentIFrame?.size(box.bottom + footer_height + 32);
849
+ }
850
+ });
851
+ mut.observe(root_container, {
852
+ childList: true,
853
+ subtree: true,
854
+ attributes: true
855
+ });
856
+ return () => {
857
+ mut.disconnect();
858
+ };
859
+ });
783
860
  </script>
784
861
 
785
862
  <svelte:head>
@@ -795,6 +872,7 @@ function screen_recording() {
795
872
  <div
796
873
  class="contain"
797
874
  style:flex-grow={app_mode ? "1" : "auto"}
875
+ bind:this={root_container}
798
876
  style:margin-right={vibe_mode ? `${vibe_editor_width}px` : "0"}
799
877
  >
800
878
  {#if $_layout && app.config}
@@ -813,7 +891,7 @@ function screen_recording() {
813
891
  </div>
814
892
 
815
893
  {#if show_footer}
816
- <footer>
894
+ <footer bind:clientHeight={footer_height}>
817
895
  {#if show_api}
818
896
  <button
819
897
  on:click={() => {
@@ -1,6 +1,7 @@
1
- <script>import { getContext } from "svelte";
1
+ <script>import { getContext, onMount } from "svelte";
2
2
  import space_logo from "./images/spaces.svg";
3
3
  import { _ } from "svelte-i18n";
4
+ import { navbar_config } from "./navbar_store";
4
5
  export let wrapper;
5
6
  export let version;
6
7
  export let initial_height;
@@ -14,7 +15,38 @@ export let loaded;
14
15
  export let pages = [];
15
16
  export let current_page = "";
16
17
  export let root;
18
+ export let components = [];
17
19
  const set_page = getContext("set_lite_page");
20
+ let navbar_component = components.find((c) => c.type === "navbar");
21
+ let navbar = navbar_component ? {
22
+ visible: navbar_component.props.visible,
23
+ main_page_name: navbar_component.props.main_page_name,
24
+ value: navbar_component.props.value
25
+ } : null;
26
+ if (navbar) {
27
+ navbar_config.set(navbar);
28
+ }
29
+ $: if ($navbar_config) {
30
+ navbar = {
31
+ visible: $navbar_config.visible ?? true,
32
+ main_page_name: $navbar_config.main_page_name ?? "Home",
33
+ value: $navbar_config.value ?? null
34
+ };
35
+ }
36
+ $: show_navbar = pages.length > 1 && (navbar === null || navbar.visible !== false);
37
+ $: effective_pages = (() => {
38
+ let base_pages = navbar && navbar.main_page_name !== false && navbar.main_page_name !== "Home" ? pages.map(
39
+ ([route, label], index) => index === 0 && route === "" && label === "Home" ? [route, navbar.main_page_name] : [route, label]
40
+ ) : pages;
41
+ if (navbar?.value && navbar.value.length > 0) {
42
+ const existing_routes = new Set(base_pages.map(([route]) => route));
43
+ const additional_pages = navbar.value.map(
44
+ ([page_name, page_path]) => [page_path, page_name]
45
+ ).filter(([route]) => !existing_routes.has(route));
46
+ return [...base_pages, ...additional_pages];
47
+ }
48
+ return base_pages;
49
+ })();
18
50
  </script>
19
51
 
20
52
  <div
@@ -27,10 +59,10 @@ const set_page = getContext("set_lite_page");
27
59
  style:flex-grow={!display ? "1" : "auto"}
28
60
  data-iframe-height
29
61
  >
30
- {#if pages.length > 1}
62
+ {#if show_navbar}
31
63
  <div class="nav-holder">
32
64
  <nav class="fillable" class:fill_width>
33
- {#each pages as [route, label], i}
65
+ {#each effective_pages as [route, label], i}
34
66
  {#if is_lite}
35
67
  <button
36
68
  class:active={route === current_page}
@@ -42,9 +74,18 @@ const set_page = getContext("set_lite_page");
42
74
  </button>
43
75
  {:else}
44
76
  <a
45
- href={`${root}/${route}`}
77
+ href={route.startsWith("http://") || route.startsWith("https://")
78
+ ? route
79
+ : `${root}/${route}`}
46
80
  class:active={route === current_page}
47
81
  data-sveltekit-reload
82
+ target={route.startsWith("http://") ||
83
+ route.startsWith("https://")
84
+ ? "_blank"
85
+ : "_self"}
86
+ rel={route.startsWith("http://") || route.startsWith("https://")
87
+ ? "noopener noreferrer"
88
+ : ""}
48
89
  >{label}
49
90
  </a>
50
91
  {/if}
@@ -14,6 +14,7 @@ declare const __propDef: {
14
14
  pages?: [string, string][];
15
15
  current_page?: string;
16
16
  root: string;
17
+ components?: any[];
17
18
  };
18
19
  events: {
19
20
  [evt: string]: CustomEvent<any>;
@@ -69,12 +69,21 @@ async function get_js_info() {
69
69
  }
70
70
  let info;
71
71
  let js_info;
72
+ let analytics;
72
73
  get_info().then((data) => {
73
74
  info = data;
74
75
  });
75
76
  get_js_info().then((js_api_info) => {
76
77
  js_info = js_api_info;
77
78
  });
79
+ async function get_summary() {
80
+ let response = await fetch(root.replace(/\/$/, "") + "/monitoring/summary");
81
+ let data = await response.json();
82
+ return data;
83
+ }
84
+ get_summary().then((summary) => {
85
+ analytics = summary.functions;
86
+ });
78
87
  const dispatch = createEventDispatcher();
79
88
  $: selected_tools_array = Array.from(selected_tools);
80
89
  $: selected_tools_without_prefix = selected_tools_array.map(remove_tool_prefix);
@@ -122,7 +131,8 @@ async function fetch_mcp_tools() {
122
131
  description: tool.description || "",
123
132
  parameters: tool.inputSchema?.properties || {},
124
133
  meta: tool.meta,
125
- expanded: false
134
+ expanded: false,
135
+ endpoint_name: tool.endpoint_name
126
136
  }));
127
137
  selected_tools = new Set(tools.map((tool) => tool.name));
128
138
  headers = schema.map((tool) => tool.meta?.headers || []).flat();
@@ -185,6 +195,8 @@ async function fetch_mcp_tools() {
185
195
  }
186
196
  }
187
197
  onMount(() => {
198
+ const controller = new AbortController();
199
+ const signal = controller.signal;
188
200
  document.body.style.overflow = "hidden";
189
201
  if ("parentIFrame" in window) {
190
202
  window.parentIFrame?.scrollTo(0, 0);
@@ -193,7 +205,7 @@ onMount(() => {
193
205
  if (is_valid_language(lang_param)) {
194
206
  current_language = lang_param;
195
207
  }
196
- fetch(mcp_server_url).then((response) => {
208
+ fetch(mcp_server_url, { signal }).then((response) => {
197
209
  mcp_server_active = response.ok;
198
210
  if (mcp_server_active) {
199
211
  fetch_mcp_tools();
@@ -205,6 +217,7 @@ onMount(() => {
205
217
  current_language = "python";
206
218
  }
207
219
  }
220
+ controller.abort();
208
221
  }).catch(() => {
209
222
  mcp_server_active = false;
210
223
  });
@@ -214,7 +227,7 @@ onMount(() => {
214
227
  });
215
228
  </script>
216
229
 
217
- {#if info}
230
+ {#if info && analytics}
218
231
  {#if api_count}
219
232
  <div class="banner-wrap">
220
233
  <ApiBanner
@@ -304,6 +317,7 @@ onMount(() => {
304
317
  {mcp_json_stdio}
305
318
  {file_data_present}
306
319
  {mcp_docs}
320
+ {analytics}
307
321
  />
308
322
  {:else}
309
323
  1. Confirm that you have cURL installed on your system.
@@ -380,6 +394,7 @@ onMount(() => {
380
394
  api_description={info.named_endpoints[
381
395
  "/" + dependency.api_name
382
396
  ].description}
397
+ {analytics}
383
398
  />
384
399
 
385
400
  <ParametersSnippet
@@ -10,6 +10,7 @@ export let endpoint_parameters;
10
10
  export let username;
11
11
  export let current_language;
12
12
  export let api_description = null;
13
+ export let analytics;
13
14
  let python_code;
14
15
  let js_code;
15
16
  let bash_post_code;
@@ -29,6 +30,7 @@ $: normalised_root = root.replace(/\/$/, "");
29
30
  <EndpointDetail
30
31
  api_name={dependency.api_name}
31
32
  description={api_description}
33
+ {analytics}
32
34
  />
33
35
  {#if current_language === "python"}
34
36
  <Block>
@@ -41,13 +43,13 @@ $: normalised_root = root.replace(/\/$/, "");
41
43
  class="highlight">import</span
42
44
  > Client{#if has_file_path}, handle_file{/if}
43
45
 
44
- client = Client(<span class="token string">"{space_id || root}"</span
46
+ client = Client(<span class="token string">"{space_id || root}"</span
45
47
  >{#if username !== null}, auth=("{username}", **password**){/if})
46
- result = client.<span class="highlight">predict</span
48
+ result = client.<span class="highlight">predict</span
47
49
  >(<!--
48
- -->{#each endpoint_parameters as { python_type, example_input, parameter_name, parameter_has_default, parameter_default }, i}<!--
49
- -->
50
- {parameter_name
50
+ -->{#each endpoint_parameters as { python_type, example_input, parameter_name, parameter_has_default, parameter_default }, i}<!--
51
+ -->
52
+ {parameter_name
51
53
  ? parameter_name + "="
52
54
  : ""}<span
53
55
  >{represent_value(
@@ -57,11 +59,11 @@ result = client.<span class="highlight">predict</span
57
59
  )}</span
58
60
  >,{/each}<!--
59
61
 
60
- -->
61
- api_name=<span class="api-name">"/{dependency.api_name}"</span><!--
62
- -->
63
- )
64
- <span class="highlight">print</span>(result)</pre>
62
+ -->
63
+ api_name=<span class="api-name">"/{dependency.api_name}"</span><!--
64
+ -->
65
+ )
66
+ <span class="highlight">print</span>(result)</pre>
65
67
  </div>
66
68
  </code>
67
69
  </Block>
@@ -73,44 +75,44 @@ result = client.<span class="highlight">predict</span
73
75
  </div>
74
76
  <div bind:this={js_code}>
75
77
  <pre>import &lbrace; Client &rbrace; from "@gradio/client";
76
- {#each blob_examples as { component, example_input }, i}<!--
77
- -->
78
- const response_{i} = await fetch("{example_input.url}");
79
- const example{component} = await response_{i}.blob();
78
+ {#each blob_examples as { component, example_input }, i}<!--
79
+ -->
80
+ const response_{i} = await fetch("{example_input.url}");
81
+ const example{component} = await response_{i}.blob();
80
82
  {/each}<!--
81
- -->
82
- const client = await Client.connect(<span class="token string"
83
+ -->
84
+ const client = await Client.connect(<span class="token string"
83
85
  >"{space_id || root}"</span
84
86
  >{#if username !== null}, &lbrace;auth: ["{username}", **password**]&rbrace;{/if});
85
- const result = await client.predict(<span class="api-name"
87
+ const result = await client.predict(<span class="api-name"
86
88
  >"/{dependency.api_name}"</span
87
89
  >, &lbrace; <!--
88
- -->{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}<!--
89
- -->{#if blob_components.includes(component)}<!--
90
- -->
91
- <span
90
+ -->{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}<!--
91
+ -->{#if blob_components.includes(component)}<!--
92
+ -->
93
+ <span
92
94
  class="example-inputs"
93
95
  >{parameter_name}: example{component}</span
94
96
  >, <!--
95
- --><span class="desc"><!--
96
- --></span
97
+ --><span class="desc"><!--
98
+ --></span
97
99
  ><!--
98
- -->{:else}<!--
99
- -->
100
- <span class="example-inputs"
100
+ -->{:else}<!--
101
+ -->
102
+ <span class="example-inputs"
101
103
  >{parameter_name}: {represent_value(
102
104
  example_input,
103
105
  python_type.type,
104
106
  "js"
105
107
  )}</span
106
108
  >, <!--
107
- --><!--
108
- -->{/if}
109
+ --><!--
110
+ -->{/if}
109
111
  {/each}
110
- &rbrace;);
112
+ &rbrace;);
111
113
 
112
- console.log(result.data);
113
- </pre>
114
+ console.log(result.data);
115
+ </pre>
114
116
  </div>
115
117
  </code>
116
118
  </Block>
@@ -123,18 +125,18 @@ console.log(result.data);
123
125
 
124
126
  <div bind:this={bash_post_code}>
125
127
  <pre>curl -X POST {normalised_root}{normalised_api_prefix}/call/{dependency.api_name} -s -H "Content-Type: application/json" -d '{"{"}
126
- "data": [{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}
128
+ "data": [{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}
127
129
  <!--
128
- -->{represent_value(
130
+ -->{represent_value(
129
131
  example_input,
130
132
  python_type.type,
131
133
  "bash"
132
134
  )}{#if i < endpoint_parameters.length - 1},
133
135
  {/if}
134
136
  {/each}
135
- ]{"}"}' \
136
- | awk -F'"' '{"{"} print $4{"}"}' \
137
- | read EVENT_ID; curl -N {normalised_root}{normalised_api_prefix}/call/{dependency.api_name}/$EVENT_ID</pre>
137
+ ]{"}"}' \
138
+ | awk -F'"' '{"{"} print $4{"}"}' \
139
+ | read EVENT_ID; curl -N {normalised_root}{normalised_api_prefix}/call/{dependency.api_name}/$EVENT_ID</pre>
138
140
  </div>
139
141
  </code>
140
142
  </Block>
@@ -10,6 +10,7 @@ declare const __propDef: {
10
10
  username: string | null;
11
11
  current_language: "python" | "javascript" | "bash";
12
12
  api_description?: string | null;
13
+ analytics: Record<string, any>;
13
14
  };
14
15
  events: {
15
16
  [evt: string]: CustomEvent<any>;
@@ -1,11 +1,28 @@
1
1
  <script>export let api_name = null;
2
2
  export let description = null;
3
+ export let analytics;
4
+ import { format_latency, get_color_from_success_rate } from "./utils";
5
+ const success_rate = api_name ? analytics[api_name]?.success_rate : 0;
6
+ const color = get_color_from_success_rate(success_rate);
3
7
  </script>
4
8
 
5
9
  <h3>
6
10
  API name:
7
11
  <span class="post">{"/" + api_name}</span>
8
12
  <span class="desc">{description}</span>
13
+ {#if analytics && api_name && analytics[api_name]}
14
+ <span class="analytics">
15
+ Total requests: {analytics[api_name].total_requests} (<span style={color}
16
+ >{Math.round(success_rate * 100)}%</span
17
+ >
18
+ successful) &nbsp;|&nbsp; p50/p90/p99:
19
+ {format_latency(analytics[api_name].process_time_percentiles["50th"])}
20
+ /
21
+ {format_latency(analytics[api_name].process_time_percentiles["90th"])}
22
+ /
23
+ {format_latency(analytics[api_name].process_time_percentiles["99th"])}
24
+ </span>
25
+ {/if}
9
26
  </h3>
10
27
 
11
28
  <style>
@@ -27,8 +44,13 @@ export let description = null;
27
44
  font-weight: var(--weight-semibold);
28
45
  }
29
46
 
30
- .desc {
47
+ .analytics {
31
48
  color: var(--body-text-color-subdued);
49
+ margin-top: var(--size-1);
50
+ }
51
+
52
+ .desc {
53
+ color: var(--body-text-color);
32
54
  font-size: var(--text-lg);
33
55
  margin-top: var(--size-1);
34
56
  }
@@ -3,6 +3,7 @@ declare const __propDef: {
3
3
  props: {
4
4
  api_name?: string | null;
5
5
  description?: string | null;
6
+ analytics: Record<string, any>;
6
7
  };
7
8
  events: {
8
9
  [evt: string]: CustomEvent<any>;
@@ -1,6 +1,7 @@
1
1
  <script>import { Block } from "@gradio/atoms";
2
2
  import CopyButton from "./CopyButton.svelte";
3
3
  import { Tool, Prompt, Resource } from "@gradio/icons";
4
+ import { format_latency, get_color_from_success_rate } from "./utils";
4
5
  export let mcp_server_active;
5
6
  export let mcp_server_url;
6
7
  export let mcp_server_url_streamable;
@@ -11,6 +12,7 @@ export let mcp_json_sse;
11
12
  export let mcp_json_stdio;
12
13
  export let file_data_present;
13
14
  export let mcp_docs;
15
+ export let analytics;
14
16
  let current_transport = "streamable_http";
15
17
  let include_file_upload = true;
16
18
  const transports = [
@@ -137,6 +139,9 @@ $: mcp_json_stdio_updated = update_config_with_file_upload(
137
139
  </div>
138
140
  <div class="mcp-tools">
139
141
  {#each all_tools.length > 0 ? all_tools : tools as tool}
142
+ {@const success_rate =
143
+ analytics[tool.meta.endpoint_name]?.success_rate || 0}
144
+ {@const color = get_color_from_success_rate(success_rate)}
140
145
  <div class="tool-item">
141
146
  <div class="tool-header-wrapper">
142
147
  {#if all_tools.length > 0}
@@ -179,6 +184,36 @@ $: mcp_json_stdio_updated = update_config_with_file_upload(
179
184
  ? tool.description
180
185
  : "⚠︎ No description provided in function docstring"}
181
186
  </span>
187
+ {#if analytics[tool.meta.endpoint_name]}
188
+ <span
189
+ class="tool-analytics"
190
+ style="color: var(--body-text-color-subdued); margin-left: 1em;"
191
+ >
192
+ Total requests: {analytics[tool.meta.endpoint_name]
193
+ .total_requests}
194
+ <span style={color}
195
+ >({Math.round(success_rate * 100)}% successful)</span
196
+ >
197
+ &nbsp;|&nbsp; p50/p90/p99:
198
+ {format_latency(
199
+ analytics[tool.meta.endpoint_name].process_time_percentiles[
200
+ "50th"
201
+ ]
202
+ )}
203
+ /
204
+ {format_latency(
205
+ analytics[tool.meta.endpoint_name].process_time_percentiles[
206
+ "90th"
207
+ ]
208
+ )}
209
+ /
210
+ {format_latency(
211
+ analytics[tool.meta.endpoint_name].process_time_percentiles[
212
+ "99th"
213
+ ]
214
+ )}
215
+ </span>
216
+ {/if}
182
217
  </span>
183
218
  <span class="tool-arrow">{tool.expanded ? "▼" : "▶"}</span>
184
219
  </button>
@@ -303,6 +338,10 @@ $: mcp_json_stdio_updated = update_config_with_file_upload(
303
338
  {/if}
304
339
 
305
340
  <style>
341
+ .tool-analytics {
342
+ font-size: 0.95em;
343
+ color: var(--body-text-color-subdued);
344
+ }
306
345
  .transport-selection {
307
346
  margin-bottom: var(--size-4);
308
347
  }
@@ -18,6 +18,7 @@ declare const __propDef: {
18
18
  meta: {
19
19
  mcp_type: "tool" | "resource" | "prompt";
20
20
  file_data_present: boolean;
21
+ endpoint_name: string;
21
22
  };
22
23
  }[];
23
24
  all_tools?: {
@@ -34,6 +35,7 @@ declare const __propDef: {
34
35
  meta: {
35
36
  mcp_type: "tool" | "resource" | "prompt";
36
37
  file_data_present: boolean;
38
+ endpoint_name: string;
37
39
  };
38
40
  }[];
39
41
  selected_tools?: Set<string>;
@@ -41,6 +43,7 @@ declare const __propDef: {
41
43
  mcp_json_stdio: any;
42
44
  file_data_present: boolean;
43
45
  mcp_docs: string;
46
+ analytics: Record<string, any>;
44
47
  };
45
48
  events: {
46
49
  [evt: string]: CustomEvent<any>;
@@ -1,2 +1,4 @@
1
1
  export declare function represent_value(value: string, type: string | undefined, lang?: "js" | "py" | "bash" | null): string | null | number | boolean | Record<string, unknown>;
2
2
  export declare function is_potentially_nested_file_data(obj: any): boolean;
3
+ export declare function format_latency(val: number): string;
4
+ export declare function get_color_from_success_rate(success_rate: number): string;
@@ -133,3 +133,17 @@ function stringify_except_file_function(obj) {
133
133
  const regexNone = /"UNQUOTEDNone"/g;
134
134
  return jsonString.replace(regexNone, "None");
135
135
  }
136
+ export function format_latency(val) {
137
+ if (val < 1)
138
+ return `${Math.round(val * 1000)} ms`;
139
+ return `${val.toFixed(2)} s`;
140
+ }
141
+ export function get_color_from_success_rate(success_rate) {
142
+ if (success_rate > 0.9) {
143
+ return "color: green;";
144
+ }
145
+ else if (success_rate > 0.1) {
146
+ return "color: orange;";
147
+ }
148
+ return "color: red;";
149
+ }