@gradio/core 0.17.0 → 0.18.1

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.
@@ -0,0 +1,255 @@
1
+ let isRecording = false;
2
+ let mediaRecorder = null;
3
+ let recordedChunks = [];
4
+ let recordingStartTime = 0;
5
+ let animationFrameId = null;
6
+ let removeSegment = {};
7
+ let root;
8
+ let add_message_callback;
9
+ let onRecordingStateChange = null;
10
+ let zoomEffects = [];
11
+ export function initialize(rootPath, add_new_message, recordingStateCallback) {
12
+ root = rootPath;
13
+ add_message_callback = add_new_message;
14
+ if (recordingStateCallback) {
15
+ onRecordingStateChange = recordingStateCallback;
16
+ }
17
+ }
18
+ export async function startRecording() {
19
+ if (isRecording) {
20
+ return;
21
+ }
22
+ try {
23
+ const originalTitle = document.title;
24
+ document.title = "[Sharing] Gradio Tab";
25
+ const stream = await navigator.mediaDevices.getDisplayMedia({
26
+ video: {
27
+ width: { ideal: 1920 },
28
+ height: { ideal: 1080 },
29
+ frameRate: { ideal: 30 }
30
+ },
31
+ audio: true,
32
+ selfBrowserSurface: "include"
33
+ });
34
+ document.title = originalTitle;
35
+ const options = {
36
+ videoBitsPerSecond: 5000000
37
+ };
38
+ mediaRecorder = new MediaRecorder(stream, options);
39
+ recordedChunks = [];
40
+ removeSegment = {};
41
+ mediaRecorder.ondataavailable = handleDataAvailable;
42
+ mediaRecorder.onstop = handleStop;
43
+ mediaRecorder.start(1000);
44
+ isRecording = true;
45
+ if (onRecordingStateChange) {
46
+ onRecordingStateChange(true);
47
+ }
48
+ recordingStartTime = Date.now();
49
+ }
50
+ catch (error) {
51
+ add_message_callback("Recording Error", "Failed to start recording: " + error.message, "error");
52
+ }
53
+ }
54
+ export function stopRecording() {
55
+ if (!isRecording || !mediaRecorder) {
56
+ return;
57
+ }
58
+ mediaRecorder.stop();
59
+ isRecording = false;
60
+ if (onRecordingStateChange) {
61
+ onRecordingStateChange(false);
62
+ }
63
+ }
64
+ export function isCurrentlyRecording() {
65
+ return isRecording;
66
+ }
67
+ export function markRemoveSegmentStart() {
68
+ if (!isRecording) {
69
+ return;
70
+ }
71
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
72
+ removeSegment.start = currentTime;
73
+ }
74
+ export function markRemoveSegmentEnd() {
75
+ if (!isRecording || removeSegment.start === undefined) {
76
+ return;
77
+ }
78
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
79
+ removeSegment.end = currentTime;
80
+ }
81
+ export function clearRemoveSegment() {
82
+ removeSegment = {};
83
+ }
84
+ export function addZoomEffect(is_input, params) {
85
+ if (!isRecording) {
86
+ return;
87
+ }
88
+ const FPS = 30;
89
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
90
+ const currentFrame = is_input
91
+ ? Math.floor((currentTime - 2) * FPS)
92
+ : Math.floor(currentTime * FPS);
93
+ if (params.boundingBox &&
94
+ params.boundingBox.topLeft &&
95
+ params.boundingBox.bottomRight &&
96
+ params.boundingBox.topLeft.length === 2 &&
97
+ params.boundingBox.bottomRight.length === 2) {
98
+ const newEffectDuration = params.duration || 2.0;
99
+ const newEffectEndFrame = currentFrame + Math.floor(newEffectDuration * FPS);
100
+ const hasOverlap = zoomEffects.some((existingEffect) => {
101
+ const existingEffectEndFrame = existingEffect.start_frame +
102
+ Math.floor((existingEffect.duration || 2.0) * FPS);
103
+ return ((currentFrame >= existingEffect.start_frame &&
104
+ currentFrame <= existingEffectEndFrame) ||
105
+ (newEffectEndFrame >= existingEffect.start_frame &&
106
+ newEffectEndFrame <= existingEffectEndFrame) ||
107
+ (currentFrame <= existingEffect.start_frame &&
108
+ newEffectEndFrame >= existingEffectEndFrame));
109
+ });
110
+ if (!hasOverlap) {
111
+ zoomEffects.push({
112
+ boundingBox: params.boundingBox,
113
+ start_frame: currentFrame,
114
+ duration: newEffectDuration
115
+ });
116
+ }
117
+ }
118
+ }
119
+ export function zoom(is_input, elements, duration = 2.0) {
120
+ if (!isRecording) {
121
+ return;
122
+ }
123
+ try {
124
+ setTimeout(() => {
125
+ if (!elements || elements.length === 0) {
126
+ return;
127
+ }
128
+ let minLeft = Infinity;
129
+ let minTop = Infinity;
130
+ let maxRight = 0;
131
+ let maxBottom = 0;
132
+ let foundElements = false;
133
+ for (const elementId of elements) {
134
+ const selector = `#component-${elementId}`;
135
+ const element = document.querySelector(selector);
136
+ if (element) {
137
+ foundElements = true;
138
+ const rect = element.getBoundingClientRect();
139
+ minLeft = Math.min(minLeft, rect.left);
140
+ minTop = Math.min(minTop, rect.top);
141
+ maxRight = Math.max(maxRight, rect.right);
142
+ maxBottom = Math.max(maxBottom, rect.bottom);
143
+ }
144
+ }
145
+ if (!foundElements) {
146
+ return;
147
+ }
148
+ const viewportWidth = window.innerWidth;
149
+ const viewportHeight = window.innerHeight;
150
+ const boxWidth = Math.min(maxRight, viewportWidth) - Math.max(0, minLeft);
151
+ const boxHeight = Math.min(maxBottom, viewportHeight) - Math.max(0, minTop);
152
+ const widthPercentage = boxWidth / viewportWidth;
153
+ const heightPercentage = boxHeight / viewportHeight;
154
+ if (widthPercentage >= 0.8 || heightPercentage >= 0.8) {
155
+ return;
156
+ }
157
+ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
158
+ let topLeft = [
159
+ Math.max(0, minLeft) / viewportWidth,
160
+ Math.max(0, minTop) / viewportHeight
161
+ ];
162
+ let bottomRight = [
163
+ Math.min(maxRight, viewportWidth) / viewportWidth,
164
+ Math.min(maxBottom, viewportHeight) / viewportHeight
165
+ ];
166
+ if (isSafari) {
167
+ topLeft[0] = Math.max(0, topLeft[0] * 0.9);
168
+ bottomRight[0] = Math.min(1, bottomRight[0] * 0.9);
169
+ const width = bottomRight[0] - topLeft[0];
170
+ const center = (topLeft[0] + bottomRight[0]) / 2;
171
+ const newCenter = center * 0.9;
172
+ topLeft[0] = Math.max(0, newCenter - width / 2);
173
+ bottomRight[0] = Math.min(1, newCenter + width / 2);
174
+ }
175
+ topLeft[0] = Math.max(0, topLeft[0]);
176
+ topLeft[1] = Math.max(0, topLeft[1]);
177
+ bottomRight[0] = Math.min(1, bottomRight[0]);
178
+ bottomRight[1] = Math.min(1, bottomRight[1]);
179
+ addZoomEffect(is_input, {
180
+ boundingBox: {
181
+ topLeft,
182
+ bottomRight
183
+ },
184
+ duration: duration
185
+ });
186
+ }, 300);
187
+ }
188
+ catch (error) {
189
+ // pass
190
+ }
191
+ }
192
+ function handleDataAvailable(event) {
193
+ if (event.data.size > 0) {
194
+ recordedChunks.push(event.data);
195
+ }
196
+ }
197
+ function handleStop() {
198
+ isRecording = false;
199
+ if (onRecordingStateChange) {
200
+ onRecordingStateChange(false);
201
+ }
202
+ const blob = new Blob(recordedChunks, {
203
+ type: "video/mp4"
204
+ });
205
+ handleRecordingComplete(blob);
206
+ const screenStream = mediaRecorder?.stream?.getTracks() || [];
207
+ screenStream.forEach((track) => track.stop());
208
+ if (animationFrameId !== null) {
209
+ cancelAnimationFrame(animationFrameId);
210
+ animationFrameId = null;
211
+ }
212
+ }
213
+ async function handleRecordingComplete(recordedBlob) {
214
+ try {
215
+ add_message_callback("Processing video", "This may take a few seconds...", "info");
216
+ const formData = new FormData();
217
+ formData.append("video", recordedBlob, "recording.mp4");
218
+ if (removeSegment.start !== undefined && removeSegment.end !== undefined) {
219
+ formData.append("remove_segment_start", removeSegment.start.toString());
220
+ formData.append("remove_segment_end", removeSegment.end.toString());
221
+ }
222
+ if (zoomEffects.length > 0) {
223
+ formData.append("zoom_effects", JSON.stringify(zoomEffects));
224
+ }
225
+ const response = await fetch(root + "/gradio_api/process_recording", {
226
+ method: "POST",
227
+ body: formData
228
+ });
229
+ if (!response.ok) {
230
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
231
+ }
232
+ const processedBlob = await response.blob();
233
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
234
+ saveWithDownloadAttribute(processedBlob, defaultFilename);
235
+ zoomEffects = [];
236
+ }
237
+ catch (error) {
238
+ add_message_callback("Processing Error", "Failed to process recording. Saving original version.", "warning");
239
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
240
+ saveWithDownloadAttribute(recordedBlob, defaultFilename);
241
+ }
242
+ }
243
+ function saveWithDownloadAttribute(blob, suggestedName) {
244
+ const url = URL.createObjectURL(blob);
245
+ const a = document.createElement("a");
246
+ a.style.display = "none";
247
+ a.href = url;
248
+ a.download = suggestedName;
249
+ document.body.appendChild(a);
250
+ a.click();
251
+ setTimeout(() => {
252
+ document.body.removeChild(a);
253
+ URL.revokeObjectURL(url);
254
+ }, 100);
255
+ }
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
1
  {
2
2
  "name": "@gradio/core",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
- "@gradio/accordion": "^0.5.15",
7
- "@gradio/annotatedimage": "^0.9.19",
6
+ "@gradio/accordion": "^0.5.16",
7
+ "@gradio/annotatedimage": "^0.9.20",
8
8
  "@gradio/atoms": "^0.16.1",
9
- "@gradio/audio": "^0.17.14",
10
9
  "@gradio/box": "^0.2.19",
11
- "@gradio/chatbot": "^0.26.7",
12
- "@gradio/button": "^0.5.0",
13
- "@gradio/checkboxgroup": "^0.6.21",
14
- "@gradio/client": "^1.15.0",
15
- "@gradio/checkbox": "^0.4.21",
16
- "@gradio/code": "^0.14.4",
17
- "@gradio/colorpicker": "^0.4.21",
10
+ "@gradio/audio": "^0.17.15",
11
+ "@gradio/button": "^0.5.1",
12
+ "@gradio/chatbot": "^0.26.9",
13
+ "@gradio/checkbox": "^0.4.22",
14
+ "@gradio/checkboxgroup": "^0.6.22",
15
+ "@gradio/code": "^0.14.5",
16
+ "@gradio/client": "^1.15.1",
17
+ "@gradio/colorpicker": "^0.4.22",
18
18
  "@gradio/column": "^0.2.0",
19
- "@gradio/dataframe": "^0.17.12",
20
- "@gradio/dataset": "^0.4.19",
21
- "@gradio/datetime": "^0.3.13",
22
- "@gradio/downloadbutton": "^0.4.0",
23
- "@gradio/dropdown": "^0.9.21",
24
- "@gradio/file": "^0.12.18",
19
+ "@gradio/datetime": "^0.3.14",
20
+ "@gradio/dataframe": "^0.17.13",
21
+ "@gradio/dataset": "^0.4.20",
22
+ "@gradio/downloadbutton": "^0.4.1",
23
+ "@gradio/fallback": "^0.4.22",
24
+ "@gradio/file": "^0.12.19",
25
+ "@gradio/dropdown": "^0.9.22",
26
+ "@gradio/fileexplorer": "^0.5.30",
25
27
  "@gradio/form": "^0.2.19",
26
- "@gradio/fallback": "^0.4.21",
27
- "@gradio/fileexplorer": "^0.5.29",
28
- "@gradio/highlightedtext": "^0.9.4",
29
- "@gradio/gallery": "^0.15.19",
30
- "@gradio/html": "^0.6.13",
31
28
  "@gradio/group": "^0.2.0",
32
29
  "@gradio/icons": "^0.12.0",
33
- "@gradio/image": "^0.22.6",
34
- "@gradio/imageeditor": "^0.15.0",
35
- "@gradio/imageslider": "^0.2.2",
36
- "@gradio/label": "^0.5.13",
37
- "@gradio/json": "^0.5.22",
30
+ "@gradio/gallery": "^0.15.20",
31
+ "@gradio/html": "^0.6.14",
32
+ "@gradio/image": "^0.22.7",
33
+ "@gradio/imageslider": "^0.2.3",
34
+ "@gradio/imageeditor": "^0.15.1",
35
+ "@gradio/highlightedtext": "^0.9.5",
36
+ "@gradio/json": "^0.5.24",
37
+ "@gradio/label": "^0.5.14",
38
38
  "@gradio/browserstate": "^0.3.2",
39
- "@gradio/markdown": "^0.13.12",
40
- "@gradio/multimodaltextbox": "^0.10.6",
41
- "@gradio/nativeplot": "^0.5.16",
42
- "@gradio/paramviewer": "^0.7.9",
43
- "@gradio/number": "^0.5.21",
44
- "@gradio/model3d": "^0.14.13",
45
- "@gradio/plot": "^0.9.16",
46
- "@gradio/radio": "^0.7.4",
39
+ "@gradio/markdown": "^0.13.14",
40
+ "@gradio/multimodaltextbox": "^0.10.7",
41
+ "@gradio/model3d": "^0.14.15",
42
+ "@gradio/nativeplot": "^0.5.17",
43
+ "@gradio/paramviewer": "^0.7.10",
44
+ "@gradio/plot": "^0.9.17",
45
+ "@gradio/number": "^0.5.22",
46
+ "@gradio/radio": "^0.7.5",
47
+ "@gradio/sidebar": "^0.1.14",
47
48
  "@gradio/row": "^0.2.1",
48
- "@gradio/sidebar": "^0.1.13",
49
- "@gradio/simpledropdown": "^0.3.21",
50
- "@gradio/sketchbox": "^0.6.8",
51
- "@gradio/simpletextbox": "^0.3.21",
52
- "@gradio/simpleimage": "^0.8.29",
53
- "@gradio/slider": "^0.6.9",
54
- "@gradio/statustracker": "^0.10.11",
49
+ "@gradio/simpleimage": "^0.8.30",
50
+ "@gradio/simpledropdown": "^0.3.22",
51
+ "@gradio/simpletextbox": "^0.3.22",
52
+ "@gradio/sketchbox": "^0.6.9",
53
+ "@gradio/slider": "^0.6.11",
54
+ "@gradio/statustracker": "^0.10.12",
55
55
  "@gradio/state": "^0.1.2",
56
56
  "@gradio/tabitem": "^0.4.4",
57
57
  "@gradio/tabs": "^0.4.4",
58
- "@gradio/textbox": "^0.10.11",
59
58
  "@gradio/theme": "^0.4.0",
59
+ "@gradio/upload": "^0.16.6",
60
+ "@gradio/textbox": "^0.10.12",
60
61
  "@gradio/timer": "^0.4.5",
61
- "@gradio/upload": "^0.16.5",
62
- "@gradio/uploadbutton": "^0.9.0",
63
62
  "@gradio/utils": "^0.10.2",
63
+ "@gradio/video": "^0.14.15",
64
64
  "@gradio/wasm": "^0.18.1",
65
- "@gradio/video": "^0.14.14"
65
+ "@gradio/uploadbutton": "^0.9.1"
66
66
  },
67
67
  "msw": {
68
68
  "workerDirectory": "public"
package/src/Blocks.svelte CHANGED
@@ -2,6 +2,7 @@
2
2
  import { tick, onMount } from "svelte";
3
3
  import { _ } from "svelte-i18n";
4
4
  import { Client } from "@gradio/client";
5
+ import { writable } from "svelte/store";
5
6
 
6
7
  import type { LoadingStatus, LoadingStatusCollection } from "./stores";
7
8
 
@@ -19,12 +20,14 @@
19
20
  import logo from "./images/logo.svg";
20
21
  import api_logo from "./api_docs/img/api-logo.svg";
21
22
  import settings_logo from "./api_docs/img/settings-logo.svg";
23
+ import record_stop from "./api_docs/img/record-stop.svg";
22
24
  import { create_components, AsyncFunction } from "./init";
23
25
  import type {
24
26
  LogMessage,
25
27
  RenderMessage,
26
28
  StatusMessage
27
29
  } from "@gradio/client";
30
+ import * as screen_recorder from "./screen_recorder";
28
31
 
29
32
  export let root: string;
30
33
  export let components: ComponentMeta[];
@@ -50,8 +53,6 @@
50
53
  export let initial_layout: ComponentMeta | undefined = undefined;
51
54
  export let css: string | null | undefined = null;
52
55
 
53
- setupi18n(app.config?.i18n_translations ?? undefined);
54
-
55
56
  let {
56
57
  layout: _layout,
57
58
  targets,
@@ -97,6 +98,8 @@
97
98
  let settings_visible = search_params.get("view") === "settings";
98
99
  let api_recorder_visible =
99
100
  search_params.get("view") === "api-recorder" && show_api;
101
+ let allow_zoom = true;
102
+ let allow_video_trim = true;
100
103
 
101
104
  function set_api_docs_visible(visible: boolean): void {
102
105
  api_recorder_visible = false;
@@ -126,11 +129,28 @@
126
129
  export let render_complete = false;
127
130
  async function handle_update(data: any, fn_index: number): Promise<void> {
128
131
  const dep = dependencies.find((dep) => dep.id === fn_index);
132
+ const input_type = components.find(
133
+ (comp) => comp.id === dep?.inputs[0]
134
+ )?.type;
135
+ if (allow_zoom && dep && input_type !== "dataset") {
136
+ if (dep && dep.inputs && dep.inputs.length > 0 && $is_screen_recording) {
137
+ screen_recorder.zoom(true, dep.inputs, 1.0);
138
+ }
139
+
140
+ if (
141
+ dep &&
142
+ dep.outputs &&
143
+ dep.outputs.length > 0 &&
144
+ $is_screen_recording
145
+ ) {
146
+ screen_recorder.zoom(false, dep.outputs, 2.0);
147
+ }
148
+ }
149
+
129
150
  if (!dep) {
130
151
  return;
131
152
  }
132
153
  const outputs = dep.outputs;
133
-
134
154
  const meta_updates = data?.map((value: any, i: number) => {
135
155
  return {
136
156
  id: outputs[i],
@@ -372,6 +392,9 @@
372
392
  payload: Payload,
373
393
  streaming = false
374
394
  ): Promise<void> {
395
+ if (allow_video_trim) {
396
+ screen_recorder.markRemoveSegmentStart();
397
+ }
375
398
  if (api_recorder_visible) {
376
399
  api_calls = [...api_calls, JSON.parse(JSON.stringify(payload))];
377
400
  }
@@ -601,6 +624,9 @@
601
624
  });
602
625
  }
603
626
  }
627
+ if (allow_video_trim) {
628
+ screen_recorder.markRemoveSegmentEnd();
629
+ }
604
630
  }
605
631
  }
606
632
  /* eslint-enable complexity */
@@ -773,6 +799,8 @@
773
799
  return "detail" in event;
774
800
  }
775
801
 
802
+ let is_screen_recording = writable(false);
803
+
776
804
  onMount(() => {
777
805
  document.addEventListener("visibilitychange", function () {
778
806
  if (document.visibilityState === "hidden") {
@@ -784,6 +812,29 @@
784
812
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
785
813
  navigator.userAgent
786
814
  );
815
+
816
+ screen_recorder.initialize(
817
+ root,
818
+ (title, message, type) => {
819
+ add_new_message(title, message, type);
820
+ },
821
+ (isRecording) => {
822
+ $is_screen_recording = isRecording;
823
+ }
824
+ );
825
+ });
826
+
827
+ function screen_recording(): void {
828
+ if ($is_screen_recording) {
829
+ screen_recorder.stopRecording();
830
+ } else {
831
+ screen_recorder.startRecording();
832
+ }
833
+ }
834
+
835
+ let i18n_ready = false;
836
+ setupi18n(app.config?.i18n_translations ?? undefined).then(() => {
837
+ i18n_ready = true;
787
838
  });
788
839
  </script>
789
840
 
@@ -798,7 +849,7 @@
798
849
 
799
850
  <div class="wrap" style:min-height={app_mode ? "100%" : "auto"}>
800
851
  <div class="contain" style:flex-grow={app_mode ? "1" : "auto"}>
801
- {#if $_layout && app.config}
852
+ {#if $_layout && app.config && i18n_ready}
802
853
  <MountComponents
803
854
  rootNode={$_layout}
804
855
  {root}
@@ -840,6 +891,17 @@
840
891
  {$_("common.built_with_gradio")}
841
892
  <img src={logo} alt={$_("common.logo")} />
842
893
  </a>
894
+ <div class="divider" class:hidden={!$is_screen_recording}>·</div>
895
+ <button
896
+ class:hidden={!$is_screen_recording}
897
+ on:click={() => {
898
+ screen_recording();
899
+ }}
900
+ class="record"
901
+ >
902
+ {$_("common.stop_recording")}
903
+ <img src={record_stop} alt={$_("common.stop_recording")} />
904
+ </button>
843
905
  <div class="divider">·</div>
844
906
  <button
845
907
  on:click={() => {
@@ -912,9 +974,14 @@
912
974
  />
913
975
  <div class="api-docs-wrap">
914
976
  <Settings
977
+ bind:allow_zoom
978
+ bind:allow_video_trim
915
979
  on:close={(event) => {
916
980
  set_settings_visible(false);
917
981
  }}
982
+ on:start_recording={(event) => {
983
+ screen_recording();
984
+ }}
918
985
  pwa_enabled={app.config.pwa}
919
986
  {root}
920
987
  {space_id}
@@ -954,7 +1021,8 @@
954
1021
  }
955
1022
 
956
1023
  .show-api,
957
- .settings {
1024
+ .settings,
1025
+ .record {
958
1026
  display: flex;
959
1027
  align-items: center;
960
1028
  }
@@ -974,13 +1042,20 @@
974
1042
  width: var(--size-4);
975
1043
  }
976
1044
 
1045
+ .record img {
1046
+ margin-right: var(--size-1);
1047
+ margin-left: var(--size-1);
1048
+ width: var(--size-3);
1049
+ }
1050
+
977
1051
  .built-with {
978
1052
  display: flex;
979
1053
  align-items: center;
980
1054
  }
981
1055
 
982
1056
  .built-with:hover,
983
- .settings:hover {
1057
+ .settings:hover,
1058
+ .record:hover {
984
1059
  color: var(--body-text-color);
985
1060
  }
986
1061
 
@@ -1051,4 +1126,8 @@
1051
1126
  .show-api:hover {
1052
1127
  color: var(--body-text-color);
1053
1128
  }
1129
+
1130
+ .hidden {
1131
+ display: none;
1132
+ }
1054
1133
  </style>
@@ -101,6 +101,7 @@
101
101
  type: string;
102
102
  description: string;
103
103
  format?: string;
104
+ default?: any;
104
105
  }
105
106
 
106
107
  interface Tool {
@@ -267,24 +268,24 @@
267
268
  <div class="tool-content">
268
269
  {#if Object.keys(tool.parameters).length > 0}
269
270
  <div class="tool-parameters">
270
- {#if Object.keys(tool.parameters).length > 0}
271
- {#each Object.entries(tool.parameters) as [name, param]}
272
- <div class="parameter">
273
- <code>{name}</code>
274
- <span class="parameter-type"
275
- >({param.type})</span
276
- >
277
- <p class="parameter-description">
278
- {param.description
279
- ? param.description
280
- : "⚠︎ No description for this parameter in function docstring"}
281
- </p>
282
- </div>
283
- {/each}
284
- {:else}
285
- <p>No parameters</p>
286
- {/if}
271
+ {#each Object.entries(tool.parameters) as [name, param]}
272
+ <div class="parameter">
273
+ <code>{name}</code>
274
+ <span class="parameter-type">
275
+ ({param.type}{param.default !== undefined
276
+ ? `, default: ${JSON.stringify(param.default)}`
277
+ : ""})
278
+ </span>
279
+ <p class="parameter-description">
280
+ {param.description
281
+ ? param.description
282
+ : "⚠︎ No description for this parameter in function docstring"}
283
+ </p>
284
+ </div>
285
+ {/each}
287
286
  </div>
287
+ {:else}
288
+ <p>Takes no input parameters</p>
288
289
  {/if}
289
290
  </div>
290
291
  {/if}