@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/src/Blocks.svelte CHANGED
@@ -255,6 +255,19 @@
255
255
  });
256
256
  update_value(updates);
257
257
 
258
+ // Handle navbar updates separately since they need to be updated in the store.
259
+ updates.forEach((update) => {
260
+ const component = components.find((comp) => comp.id === update.id);
261
+ if (component && component.type === "navbar") {
262
+ import("./navbar_store").then(({ navbar_config }) => {
263
+ navbar_config.update((current) => ({
264
+ ...current,
265
+ [update.prop]: update.value
266
+ }));
267
+ });
268
+ }
269
+ });
270
+
258
271
  await tick();
259
272
  }
260
273
 
@@ -623,6 +636,53 @@
623
636
 
624
637
  /* eslint-disable complexity */
625
638
  function handle_status_update(message: StatusMessage): void {
639
+ if (message.code === "validation_error") {
640
+ const dep = dependencies.find((dep) => dep.id === message.fn_index);
641
+ if (
642
+ dep === undefined ||
643
+ message.message === undefined ||
644
+ typeof message.message === "string"
645
+ ) {
646
+ return;
647
+ }
648
+
649
+ const validation_error_data: {
650
+ id: number;
651
+ prop: string;
652
+ value: unknown;
653
+ }[] = [];
654
+
655
+ message.message.forEach((message, i) => {
656
+ if (message.is_valid) {
657
+ return;
658
+ }
659
+ validation_error_data.push({
660
+ id: dep.inputs[i],
661
+ prop: "validation_error",
662
+ value: message.message
663
+ });
664
+
665
+ validation_error_data.push({
666
+ id: dep.inputs[i],
667
+ prop: "loading_status",
668
+ value: { validation_error: message.message }
669
+ });
670
+ });
671
+
672
+ if (validation_error_data.length > 0) {
673
+ update_value(validation_error_data);
674
+ loading_status.update({
675
+ status: "complete",
676
+ fn_index: message.fn_index,
677
+ eta: 0,
678
+ queue: false,
679
+ queue_position: null
680
+ });
681
+ set_status($loading_status);
682
+
683
+ return;
684
+ }
685
+ }
626
686
  if (message.broken && !broken_connection) {
627
687
  messages = [
628
688
  new_message(
@@ -729,7 +789,7 @@
729
789
  !broken_connection &&
730
790
  !message.session_not_found
731
791
  ) {
732
- if (status.message) {
792
+ if (status.message && typeof status.message === "string") {
733
793
  const _message = status.message.replace(
734
794
  MESSAGE_QUOTE_RE,
735
795
  (_, b) => b
@@ -814,6 +874,11 @@
814
874
  target.addEventListener("prop_change", (e: Event) => {
815
875
  if (!isCustomEvent(e)) throw new Error("not a custom event");
816
876
  const { id, prop, value } = e.detail;
877
+ if (prop === "value") {
878
+ update_value([
879
+ { id, prop: "loading_status", value: { validation_error: undefined } }
880
+ ]);
881
+ }
817
882
  update_value([{ id, prop, value }]);
818
883
  if (prop === "input_ready" && value === false) {
819
884
  inputs_waiting.push(id);
@@ -990,6 +1055,40 @@
990
1055
  screen_recorder.startRecording();
991
1056
  }
992
1057
  }
1058
+
1059
+ let footer_height = 0;
1060
+
1061
+ let root_container: HTMLElement;
1062
+ $: root_node = $_layout && get_root_node(root_container);
1063
+
1064
+ function get_root_node(container: HTMLElement | null): HTMLElement | null {
1065
+ if (!container) return null;
1066
+ return container.children[container.children.length - 1] as HTMLElement;
1067
+ }
1068
+
1069
+ onMount(() => {
1070
+ if ("parentIFrame" in window) {
1071
+ window.parentIFrame?.autoResize(false);
1072
+ }
1073
+
1074
+ const mut = new MutationObserver((mutations) => {
1075
+ if ("parentIFrame" in window) {
1076
+ const box = root_node?.getBoundingClientRect();
1077
+ if (!box) return;
1078
+ window.parentIFrame?.size(box.bottom + footer_height + 32);
1079
+ }
1080
+ });
1081
+
1082
+ mut.observe(root_container, {
1083
+ childList: true,
1084
+ subtree: true,
1085
+ attributes: true
1086
+ });
1087
+
1088
+ return () => {
1089
+ mut.disconnect();
1090
+ };
1091
+ });
993
1092
  </script>
994
1093
 
995
1094
  <svelte:head>
@@ -1005,6 +1104,7 @@
1005
1104
  <div
1006
1105
  class="contain"
1007
1106
  style:flex-grow={app_mode ? "1" : "auto"}
1107
+ bind:this={root_container}
1008
1108
  style:margin-right={vibe_mode ? `${vibe_editor_width}px` : "0"}
1009
1109
  >
1010
1110
  {#if $_layout && app.config}
@@ -1023,7 +1123,7 @@
1023
1123
  </div>
1024
1124
 
1025
1125
  {#if show_footer}
1026
- <footer>
1126
+ <footer bind:clientHeight={footer_height}>
1027
1127
  {#if show_api}
1028
1128
  <button
1029
1129
  on:click={() => {
package/src/Embed.svelte CHANGED
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
- import { getContext } from "svelte";
2
+ import { getContext, onMount } from "svelte";
3
3
  import space_logo from "./images/spaces.svg";
4
4
  import { _ } from "svelte-i18n";
5
+ import { navbar_config } from "./navbar_store";
6
+
5
7
  export let wrapper: HTMLDivElement;
6
8
  export let version: string;
7
9
  export let initial_height: string;
@@ -16,9 +18,63 @@
16
18
  export let pages: [string, string][] = [];
17
19
  export let current_page = "";
18
20
  export let root: string;
21
+ export let components: any[] = [];
19
22
 
20
23
  const set_page: ((page: string) => void) | undefined =
21
24
  getContext("set_lite_page");
25
+
26
+ let navbar_component = components.find((c) => c.type === "navbar");
27
+ let navbar: {
28
+ visible: boolean;
29
+ main_page_name: string | false;
30
+ value: [string, string][] | null;
31
+ } | null = navbar_component
32
+ ? {
33
+ visible: navbar_component.props.visible,
34
+ main_page_name: navbar_component.props.main_page_name,
35
+ value: navbar_component.props.value
36
+ }
37
+ : null;
38
+
39
+ if (navbar) {
40
+ navbar_config.set(navbar);
41
+ }
42
+
43
+ $: if ($navbar_config) {
44
+ navbar = {
45
+ visible: $navbar_config.visible ?? true,
46
+ main_page_name: $navbar_config.main_page_name ?? "Home",
47
+ value: $navbar_config.value ?? null
48
+ };
49
+ }
50
+
51
+ $: show_navbar =
52
+ pages.length > 1 && (navbar === null || navbar.visible !== false);
53
+
54
+ $: effective_pages = (() => {
55
+ let base_pages =
56
+ navbar &&
57
+ navbar.main_page_name !== false &&
58
+ navbar.main_page_name !== "Home"
59
+ ? pages.map(([route, label], index) =>
60
+ index === 0 && route === "" && label === "Home"
61
+ ? ([route, navbar!.main_page_name] as [string, string])
62
+ : ([route, label] as [string, string])
63
+ )
64
+ : pages;
65
+
66
+ if (navbar?.value && navbar.value.length > 0) {
67
+ const existing_routes = new Set(base_pages.map(([route]) => route));
68
+ const additional_pages = navbar.value
69
+ .map(
70
+ ([page_name, page_path]) => [page_path, page_name] as [string, string]
71
+ )
72
+ .filter(([route]) => !existing_routes.has(route));
73
+ return [...base_pages, ...additional_pages];
74
+ }
75
+
76
+ return base_pages;
77
+ })();
22
78
  </script>
23
79
 
24
80
  <div
@@ -31,10 +87,10 @@
31
87
  style:flex-grow={!display ? "1" : "auto"}
32
88
  data-iframe-height
33
89
  >
34
- {#if pages.length > 1}
90
+ {#if show_navbar}
35
91
  <div class="nav-holder">
36
92
  <nav class="fillable" class:fill_width>
37
- {#each pages as [route, label], i}
93
+ {#each effective_pages as [route, label], i}
38
94
  {#if is_lite}
39
95
  <button
40
96
  class:active={route === current_page}
@@ -46,9 +102,18 @@
46
102
  </button>
47
103
  {:else}
48
104
  <a
49
- href={`${root}/${route}`}
105
+ href={route.startsWith("http://") || route.startsWith("https://")
106
+ ? route
107
+ : `${root}/${route}`}
50
108
  class:active={route === current_page}
51
109
  data-sveltekit-reload
110
+ target={route.startsWith("http://") ||
111
+ route.startsWith("https://")
112
+ ? "_blank"
113
+ : "_self"}
114
+ rel={route.startsWith("http://") || route.startsWith("https://")
115
+ ? "noopener noreferrer"
116
+ : ""}
52
117
  >{label}
53
118
  </a>
54
119
  {/if}
@@ -99,6 +99,7 @@
99
99
  };
100
100
 
101
101
  let js_info: Record<string, any>;
102
+ let analytics: Record<string, any>;
102
103
 
103
104
  get_info().then((data) => {
104
105
  info = data;
@@ -108,6 +109,18 @@
108
109
  js_info = js_api_info;
109
110
  });
110
111
 
112
+ async function get_summary(): Promise<{
113
+ functions: any;
114
+ }> {
115
+ let response = await fetch(root.replace(/\/$/, "") + "/monitoring/summary");
116
+ let data = await response.json();
117
+ return data;
118
+ }
119
+
120
+ get_summary().then((summary) => {
121
+ analytics = summary.functions;
122
+ });
123
+
111
124
  const dispatch = createEventDispatcher();
112
125
 
113
126
  $: selected_tools_array = Array.from(selected_tools);
@@ -148,6 +161,7 @@
148
161
  meta: {
149
162
  mcp_type: "tool" | "resource" | "prompt";
150
163
  file_data_present: boolean;
164
+ endpoint_name: string;
151
165
  };
152
166
  }
153
167
 
@@ -192,7 +206,8 @@
192
206
  description: tool.description || "",
193
207
  parameters: tool.inputSchema?.properties || {},
194
208
  meta: tool.meta,
195
- expanded: false
209
+ expanded: false,
210
+ endpoint_name: tool.endpoint_name
196
211
  }));
197
212
  selected_tools = new Set(tools.map((tool) => tool.name));
198
213
  headers = schema.map((tool: any) => tool.meta?.headers || []).flat();
@@ -260,6 +275,9 @@
260
275
  }
261
276
 
262
277
  onMount(() => {
278
+ const controller = new AbortController();
279
+ const signal = controller.signal;
280
+
263
281
  document.body.style.overflow = "hidden";
264
282
  if ("parentIFrame" in window) {
265
283
  window.parentIFrame?.scrollTo(0, 0);
@@ -271,7 +289,7 @@
271
289
  }
272
290
 
273
291
  // Check MCP server status and fetch tools if active
274
- fetch(mcp_server_url)
292
+ fetch(mcp_server_url, { signal: signal })
275
293
  .then((response) => {
276
294
  mcp_server_active = response.ok;
277
295
  if (mcp_server_active) {
@@ -284,6 +302,7 @@
284
302
  current_language = "python";
285
303
  }
286
304
  }
305
+ controller.abort();
287
306
  })
288
307
  .catch(() => {
289
308
  mcp_server_active = false;
@@ -295,7 +314,7 @@
295
314
  });
296
315
  </script>
297
316
 
298
- {#if info}
317
+ {#if info && analytics}
299
318
  {#if api_count}
300
319
  <div class="banner-wrap">
301
320
  <ApiBanner
@@ -385,6 +404,7 @@
385
404
  {mcp_json_stdio}
386
405
  {file_data_present}
387
406
  {mcp_docs}
407
+ {analytics}
388
408
  />
389
409
  {:else}
390
410
  1. Confirm that you have cURL installed on your system.
@@ -461,6 +481,7 @@
461
481
  api_description={info.named_endpoints[
462
482
  "/" + dependency.api_name
463
483
  ].description}
484
+ {analytics}
464
485
  />
465
486
 
466
487
  <ParametersSnippet
@@ -22,6 +22,7 @@
22
22
  export let username: string | null;
23
23
  export let current_language: "python" | "javascript" | "bash";
24
24
  export let api_description: string | null = null;
25
+ export let analytics: Record<string, any>;
25
26
 
26
27
  let python_code: HTMLElement;
27
28
  let js_code: HTMLElement;
@@ -44,6 +45,7 @@
44
45
  <EndpointDetail
45
46
  api_name={dependency.api_name}
46
47
  description={api_description}
48
+ {analytics}
47
49
  />
48
50
  {#if current_language === "python"}
49
51
  <Block>
@@ -56,13 +58,13 @@
56
58
  class="highlight">import</span
57
59
  > Client{#if has_file_path}, handle_file{/if}
58
60
 
59
- client = Client(<span class="token string">"{space_id || root}"</span
61
+ client = Client(<span class="token string">"{space_id || root}"</span
60
62
  >{#if username !== null}, auth=("{username}", **password**){/if})
61
- result = client.<span class="highlight">predict</span
63
+ result = client.<span class="highlight">predict</span
62
64
  >(<!--
63
- -->{#each endpoint_parameters as { python_type, example_input, parameter_name, parameter_has_default, parameter_default }, i}<!--
64
- -->
65
- {parameter_name
65
+ -->{#each endpoint_parameters as { python_type, example_input, parameter_name, parameter_has_default, parameter_default }, i}<!--
66
+ -->
67
+ {parameter_name
66
68
  ? parameter_name + "="
67
69
  : ""}<span
68
70
  >{represent_value(
@@ -72,11 +74,11 @@ result = client.<span class="highlight">predict</span
72
74
  )}</span
73
75
  >,{/each}<!--
74
76
 
75
- -->
76
- api_name=<span class="api-name">"/{dependency.api_name}"</span><!--
77
- -->
78
- )
79
- <span class="highlight">print</span>(result)</pre>
77
+ -->
78
+ api_name=<span class="api-name">"/{dependency.api_name}"</span><!--
79
+ -->
80
+ )
81
+ <span class="highlight">print</span>(result)</pre>
80
82
  </div>
81
83
  </code>
82
84
  </Block>
@@ -88,44 +90,44 @@ result = client.<span class="highlight">predict</span
88
90
  </div>
89
91
  <div bind:this={js_code}>
90
92
  <pre>import &lbrace; Client &rbrace; from "@gradio/client";
91
- {#each blob_examples as { component, example_input }, i}<!--
92
- -->
93
- const response_{i} = await fetch("{example_input.url}");
94
- const example{component} = await response_{i}.blob();
93
+ {#each blob_examples as { component, example_input }, i}<!--
94
+ -->
95
+ const response_{i} = await fetch("{example_input.url}");
96
+ const example{component} = await response_{i}.blob();
95
97
  {/each}<!--
96
- -->
97
- const client = await Client.connect(<span class="token string"
98
+ -->
99
+ const client = await Client.connect(<span class="token string"
98
100
  >"{space_id || root}"</span
99
101
  >{#if username !== null}, &lbrace;auth: ["{username}", **password**]&rbrace;{/if});
100
- const result = await client.predict(<span class="api-name"
102
+ const result = await client.predict(<span class="api-name"
101
103
  >"/{dependency.api_name}"</span
102
104
  >, &lbrace; <!--
103
- -->{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}<!--
104
- -->{#if blob_components.includes(component)}<!--
105
- -->
106
- <span
105
+ -->{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}<!--
106
+ -->{#if blob_components.includes(component)}<!--
107
+ -->
108
+ <span
107
109
  class="example-inputs"
108
110
  >{parameter_name}: example{component}</span
109
111
  >, <!--
110
- --><span class="desc"><!--
111
- --></span
112
+ --><span class="desc"><!--
113
+ --></span
112
114
  ><!--
113
- -->{:else}<!--
114
- -->
115
- <span class="example-inputs"
115
+ -->{:else}<!--
116
+ -->
117
+ <span class="example-inputs"
116
118
  >{parameter_name}: {represent_value(
117
119
  example_input,
118
120
  python_type.type,
119
121
  "js"
120
122
  )}</span
121
123
  >, <!--
122
- --><!--
123
- -->{/if}
124
+ --><!--
125
+ -->{/if}
124
126
  {/each}
125
- &rbrace;);
127
+ &rbrace;);
126
128
 
127
- console.log(result.data);
128
- </pre>
129
+ console.log(result.data);
130
+ </pre>
129
131
  </div>
130
132
  </code>
131
133
  </Block>
@@ -138,18 +140,18 @@ console.log(result.data);
138
140
 
139
141
  <div bind:this={bash_post_code}>
140
142
  <pre>curl -X POST {normalised_root}{normalised_api_prefix}/call/{dependency.api_name} -s -H "Content-Type: application/json" -d '{"{"}
141
- "data": [{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}
143
+ "data": [{#each endpoint_parameters as { label, parameter_name, type, python_type, component, example_input, serializer }, i}
142
144
  <!--
143
- -->{represent_value(
145
+ -->{represent_value(
144
146
  example_input,
145
147
  python_type.type,
146
148
  "bash"
147
149
  )}{#if i < endpoint_parameters.length - 1},
148
150
  {/if}
149
151
  {/each}
150
- ]{"}"}' \
151
- | awk -F'"' '{"{"} print $4{"}"}' \
152
- | read EVENT_ID; curl -N {normalised_root}{normalised_api_prefix}/call/{dependency.api_name}/$EVENT_ID</pre>
152
+ ]{"}"}' \
153
+ | awk -F'"' '{"{"} print $4{"}"}' \
154
+ | read EVENT_ID; curl -N {normalised_root}{normalised_api_prefix}/call/{dependency.api_name}/$EVENT_ID</pre>
153
155
  </div>
154
156
  </code>
155
157
  </Block>
@@ -1,12 +1,30 @@
1
1
  <script lang="ts">
2
2
  export let api_name: string | null = null;
3
3
  export let description: string | null = null;
4
+ export let analytics: Record<string, any>;
5
+ import { format_latency, get_color_from_success_rate } from "./utils";
6
+
7
+ const success_rate = api_name ? analytics[api_name]?.success_rate : 0;
8
+ const color = get_color_from_success_rate(success_rate);
4
9
  </script>
5
10
 
6
11
  <h3>
7
12
  API name:
8
13
  <span class="post">{"/" + api_name}</span>
9
14
  <span class="desc">{description}</span>
15
+ {#if analytics && api_name && analytics[api_name]}
16
+ <span class="analytics">
17
+ Total requests: {analytics[api_name].total_requests} (<span style={color}
18
+ >{Math.round(success_rate * 100)}%</span
19
+ >
20
+ successful) &nbsp;|&nbsp; p50/p90/p99:
21
+ {format_latency(analytics[api_name].process_time_percentiles["50th"])}
22
+ /
23
+ {format_latency(analytics[api_name].process_time_percentiles["90th"])}
24
+ /
25
+ {format_latency(analytics[api_name].process_time_percentiles["99th"])}
26
+ </span>
27
+ {/if}
10
28
  </h3>
11
29
 
12
30
  <style>
@@ -28,8 +46,13 @@
28
46
  font-weight: var(--weight-semibold);
29
47
  }
30
48
 
31
- .desc {
49
+ .analytics {
32
50
  color: var(--body-text-color-subdued);
51
+ margin-top: var(--size-1);
52
+ }
53
+
54
+ .desc {
55
+ color: var(--body-text-color);
33
56
  font-size: var(--text-lg);
34
57
  margin-top: var(--size-1);
35
58
  }
@@ -2,6 +2,7 @@
2
2
  import { Block } from "@gradio/atoms";
3
3
  import CopyButton from "./CopyButton.svelte";
4
4
  import { Tool, Prompt, Resource } from "@gradio/icons";
5
+ import { format_latency, get_color_from_success_rate } from "./utils";
5
6
 
6
7
  export let mcp_server_active: boolean;
7
8
  export let mcp_server_url: string;
@@ -13,6 +14,7 @@
13
14
  export let mcp_json_stdio: any;
14
15
  export let file_data_present: boolean;
15
16
  export let mcp_docs: string;
17
+ export let analytics: Record<string, any>;
16
18
 
17
19
  interface ToolParameter {
18
20
  title?: string;
@@ -30,6 +32,7 @@
30
32
  meta: {
31
33
  mcp_type: "tool" | "resource" | "prompt";
32
34
  file_data_present: boolean;
35
+ endpoint_name: string;
33
36
  };
34
37
  }
35
38
 
@@ -178,6 +181,9 @@
178
181
  </div>
179
182
  <div class="mcp-tools">
180
183
  {#each all_tools.length > 0 ? all_tools : tools as tool}
184
+ {@const success_rate =
185
+ analytics[tool.meta.endpoint_name]?.success_rate || 0}
186
+ {@const color = get_color_from_success_rate(success_rate)}
181
187
  <div class="tool-item">
182
188
  <div class="tool-header-wrapper">
183
189
  {#if all_tools.length > 0}
@@ -220,6 +226,36 @@
220
226
  ? tool.description
221
227
  : "⚠︎ No description provided in function docstring"}
222
228
  </span>
229
+ {#if analytics[tool.meta.endpoint_name]}
230
+ <span
231
+ class="tool-analytics"
232
+ style="color: var(--body-text-color-subdued); margin-left: 1em;"
233
+ >
234
+ Total requests: {analytics[tool.meta.endpoint_name]
235
+ .total_requests}
236
+ <span style={color}
237
+ >({Math.round(success_rate * 100)}% successful)</span
238
+ >
239
+ &nbsp;|&nbsp; p50/p90/p99:
240
+ {format_latency(
241
+ analytics[tool.meta.endpoint_name].process_time_percentiles[
242
+ "50th"
243
+ ]
244
+ )}
245
+ /
246
+ {format_latency(
247
+ analytics[tool.meta.endpoint_name].process_time_percentiles[
248
+ "90th"
249
+ ]
250
+ )}
251
+ /
252
+ {format_latency(
253
+ analytics[tool.meta.endpoint_name].process_time_percentiles[
254
+ "99th"
255
+ ]
256
+ )}
257
+ </span>
258
+ {/if}
223
259
  </span>
224
260
  <span class="tool-arrow">{tool.expanded ? "▼" : "▶"}</span>
225
261
  </button>
@@ -344,6 +380,10 @@
344
380
  {/if}
345
381
 
346
382
  <style>
383
+ .tool-analytics {
384
+ font-size: 0.95em;
385
+ color: var(--body-text-color-subdued);
386
+ }
347
387
  .transport-selection {
348
388
  margin-bottom: var(--size-4);
349
389
  }
@@ -141,3 +141,17 @@ function stringify_except_file_function(obj: any): string {
141
141
  const regexNone = /"UNQUOTEDNone"/g;
142
142
  return jsonString.replace(regexNone, "None");
143
143
  }
144
+
145
+ export function format_latency(val: number): string {
146
+ if (val < 1) return `${Math.round(val * 1000)} ms`;
147
+ return `${val.toFixed(2)} s`;
148
+ }
149
+
150
+ export function get_color_from_success_rate(success_rate: number): string {
151
+ if (success_rate > 0.9) {
152
+ return "color: green;";
153
+ } else if (success_rate > 0.1) {
154
+ return "color: orange;";
155
+ }
156
+ return "color: red;";
157
+ }
package/src/i18n.ts CHANGED
@@ -71,13 +71,14 @@ export function is_translation_metadata(obj: any): obj is I18nData {
71
71
  return result;
72
72
  }
73
73
 
74
+ export const i18n_marker = "__i18n__";
75
+
74
76
  // handles strings with embedded JSON metadata of shape "__i18n__{"key": "some.key"}"
75
77
  export function translate_if_needed(value: any): string {
76
78
  if (typeof value !== "string") {
77
79
  return value;
78
80
  }
79
81
 
80
- const i18n_marker = "__i18n__";
81
82
  const marker_index = value.indexOf(i18n_marker);
82
83
 
83
84
  if (marker_index === -1) {
@@ -155,7 +156,8 @@ let i18n_initialized = false;
155
156
  let previous_translations: Record<string, Record<string, string>> | undefined;
156
157
 
157
158
  export async function setupi18n(
158
- custom_translations?: Record<string, Record<string, string>>
159
+ custom_translations?: Record<string, Record<string, string>>,
160
+ preferred_locale?: string
159
161
  ): Promise<void> {
160
162
  const should_reinitialize =
161
163
  i18n_initialized && custom_translations !== previous_translations;
@@ -171,7 +173,7 @@ export async function setupi18n(
171
173
  custom_translations: custom_translations ?? {}
172
174
  });
173
175
 
174
- const browser_locale = getLocaleFromNavigator();
176
+ const browser_locale = preferred_locale ?? getLocaleFromNavigator();
175
177
 
176
178
  let initial_locale =
177
179
  browser_locale && available_locales.includes(browser_locale)