@gradio/multimodaltextbox 0.11.0-dev.2 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,27 @@
1
1
  # @gradio/multimodaltextbox
2
+ ## 0.11.0
3
+
4
+ ### Features
5
+
6
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - remove_placeholder_type_here
7
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Clear Error statuses
8
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Improve gr.MultimodalTextbox in mobile
9
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add minimal recording UI to gr.MultimodalTextbox
10
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add back default values for labels
11
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Unit test fixes
12
+ - [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix
13
+
14
+ ### Dependencies
15
+
16
+ - @gradio/atoms@0.19.0
17
+ - @gradio/icons@0.15.0
18
+ - @gradio/statustracker@0.12.0
19
+ - @gradio/utils@0.10.3
20
+ - @gradio/upload@0.17.2
21
+ - @gradio/image@0.24.0
22
+ - @gradio/video@0.17.0
23
+ - @gradio/client@2.0.0
24
+ - @gradio/audio@0.20.0
2
25
 
3
26
  ## 0.11.0-dev.2
4
27
 
package/Index.svelte CHANGED
@@ -39,8 +39,8 @@
39
39
 
40
40
  const value = $derived(gradio.props.value || { text: "", files: [] });
41
41
 
42
- let dragging: boolean;
43
- let active_source: "microphone" | null = null;
42
+ let dragging = $state<boolean>(false);
43
+ let active_source = $state<"microphone" | null>(null);
44
44
 
45
45
  let color_accent = "darkorange";
46
46
 
@@ -127,7 +127,7 @@
127
127
  autoscroll={gradio.shared.autoscroll}
128
128
  i18n={gradio.i18n}
129
129
  {...gradio.shared.loading_status}
130
- on:clear_status={() =>
130
+ on_clear_status={() =>
131
131
  gradio.dispatch("clear_status", gradio.shared.loading_status)}
132
132
  />
133
133
  {/if}
@@ -28,7 +28,8 @@ describe("MultimodalTextbox", () => {
28
28
  value: { text: "hello world", files: [] },
29
29
  label: "Textbox",
30
30
  interactive: false,
31
- root: ""
31
+ root: "",
32
+ sources: []
32
33
  });
33
34
 
34
35
  const item: HTMLInputElement = getByDisplayValue(
@@ -38,19 +39,17 @@ describe("MultimodalTextbox", () => {
38
39
  });
39
40
 
40
41
  test("changing the text should update the value", async () => {
41
- const { component, getByDisplayValue, listen } = await render(
42
- MultimodalTextbox,
43
- {
44
- show_label: true,
45
- max_lines: 10,
46
- loading_status,
47
- lines: 1,
48
- value: { text: "hi ", files: [] },
49
- label: "MultimodalTextbox",
50
- interactive: true,
51
- root: ""
52
- }
53
- );
42
+ const { getByDisplayValue, listen } = await render(MultimodalTextbox, {
43
+ show_label: true,
44
+ max_lines: 10,
45
+ loading_status,
46
+ lines: 1,
47
+ value: { text: "hi ", files: [] },
48
+ label: "MultimodalTextbox",
49
+ interactive: true,
50
+ root: "",
51
+ sources: []
52
+ });
54
53
 
55
54
  const item: HTMLInputElement = getByDisplayValue("hi") as HTMLInputElement;
56
55
 
@@ -60,14 +59,13 @@ describe("MultimodalTextbox", () => {
60
59
  await event.keyboard("some text");
61
60
 
62
61
  assert.equal(item.value, "hi some text");
63
- assert.equal(component.value.text, "hi some text");
64
62
  assert.equal(mock.callCount, 9);
65
63
  assert.equal(mock.calls[8][0].detail.data.text, "hi some text");
66
64
  assert.equal(mock.calls[8][0].detail.data.files.length, 0);
67
65
  });
68
66
 
69
67
  test("submitting should clear mic_audio", async () => {
70
- const { component } = await render(MultimodalTextbox, {
68
+ const { getByTestId, listen } = await render(MultimodalTextbox, {
71
69
  show_label: true,
72
70
  max_lines: 10,
73
71
  loading_status,
@@ -76,12 +74,13 @@ describe("MultimodalTextbox", () => {
76
74
  label: "MultimodalTextbox",
77
75
  interactive: true,
78
76
  root: "",
79
- sources: ["microphone"]
77
+ sources: ["microphone"],
78
+ submit_btn: true
80
79
  });
81
80
 
82
- component.$set({ mic_audio: { url: "test.mp3", mime_type: "audio/mp3" } });
83
- component.$set({ active_source: "microphone" });
84
- await component.$$.ctx[component.$$.props["handle_submit"]];
85
- assert.equal(component.mic_audio, null);
81
+ const mock = listen("submit");
82
+ const submitButton = getByTestId("submit-button");
83
+ await event.click(submitButton);
84
+ assert.equal(mock.callCount, 1);
86
85
  });
87
86
  });
package/README.md CHANGED
@@ -11,7 +11,7 @@ BaseMultimodalTextbox
11
11
  export let value = "";
12
12
  export let value_is_output = false;
13
13
  export let lines = 1;
14
- export let placeholder = "Type here...";
14
+ export let placeholder = "";
15
15
  export let label: string;
16
16
  export let info: string | undefined = undefined;
17
17
  export let disabled = false;
package/dist/Index.svelte CHANGED
@@ -39,8 +39,8 @@
39
39
 
40
40
  const value = $derived(gradio.props.value || { text: "", files: [] });
41
41
 
42
- let dragging: boolean;
43
- let active_source: "microphone" | null = null;
42
+ let dragging = $state<boolean>(false);
43
+ let active_source = $state<"microphone" | null>(null);
44
44
 
45
45
  let color_accent = "darkorange";
46
46
 
@@ -127,7 +127,7 @@
127
127
  autoscroll={gradio.shared.autoscroll}
128
128
  i18n={gradio.i18n}
129
129
  {...gradio.shared.loading_status}
130
- on:clear_status={() =>
130
+ on_clear_status={() =>
131
131
  gradio.dispatch("clear_status", gradio.shared.loading_status)}
132
132
  />
133
133
  {/if}
@@ -21,10 +21,15 @@
21
21
  Video,
22
22
  ArrowUp,
23
23
  Square,
24
- Microphone
24
+ Microphone,
25
+ Check
25
26
  } from "@gradio/icons";
26
27
  import type { SelectData } from "@gradio/utils";
27
28
  import { BaseInteractiveAudio as InteractiveAudio } from "@gradio/audio";
29
+ import {
30
+ MinimalAudioPlayer,
31
+ MinimalAudioRecorder
32
+ } from "@gradio/audio/shared";
28
33
  import type { InputHTMLAttributes } from "./types";
29
34
 
30
35
  export let value: { text: string; files: FileData[] } = {
@@ -35,7 +40,7 @@
35
40
  export let value_is_output = false;
36
41
  export let lines = 1;
37
42
  export let i18n: I18nFormatter;
38
- export let placeholder = "Type here...";
43
+ export let placeholder = "";
39
44
  export let disabled = false;
40
45
  export let label: string;
41
46
  export let info: string | undefined = undefined;
@@ -181,10 +186,10 @@
181
186
  max_lines >= 1
182
187
  ) {
183
188
  e.preventDefault();
189
+ add_mic_audio_to_files();
190
+ active_source = null;
184
191
  await tick();
185
192
  dispatch("submit");
186
- active_source = null;
187
- add_mic_audio_to_files();
188
193
  }
189
194
  }
190
195
 
@@ -241,13 +246,14 @@
241
246
  value.files.push(mic_audio);
242
247
  value = value;
243
248
  mic_audio = null;
249
+ dispatch("change", value);
244
250
  }
245
251
  }
246
252
 
247
253
  function handle_submit(): void {
248
- dispatch("submit");
249
- active_source = null;
250
254
  add_mic_audio_to_files();
255
+ active_source = null;
256
+ dispatch("submit");
251
257
  }
252
258
 
253
259
  async function handle_paste(event: ClipboardEvent): Promise<void> {
@@ -343,36 +349,71 @@
343
349
  <BlockTitle {show_label} {info} {rtl}>{label}</BlockTitle>
344
350
  <div class="input-container">
345
351
  {#if sources && sources.includes("microphone") && active_source === "microphone"}
346
- <InteractiveAudio
347
- on:change={({ detail }) => {
348
- if (detail !== null) {
349
- mic_audio = detail;
350
- }
351
- }}
352
- on:clear={() => {
353
- active_source = null;
354
- }}
355
- on:start_recording={() => dispatch("start_recording")}
356
- on:pause_recording={() => dispatch("pause_recording")}
357
- on:stop_recording={() => dispatch("stop_recording")}
358
- sources={["microphone"]}
359
- class_name="compact-audio"
360
- {recording}
361
- {waveform_settings}
362
- {waveform_options}
363
- {i18n}
364
- {active_source}
365
- {upload}
366
- {stream_handler}
367
- stream_every={1}
368
- editable={true}
369
- {label}
370
- {root}
371
- loop={false}
372
- show_label={false}
373
- buttons={[]}
374
- dragging={false}
375
- />
352
+ <div class="recording-overlay" class:has-audio={mic_audio !== null}>
353
+ {#if !mic_audio}
354
+ <div class="recording-content">
355
+ <MinimalAudioRecorder
356
+ label={label || "Audio"}
357
+ {waveform_settings}
358
+ {recording}
359
+ {upload}
360
+ {root}
361
+ {max_file_size}
362
+ bind:upload_promise
363
+ on:change={({ detail }) => {
364
+ mic_audio = detail;
365
+ }}
366
+ on:stop_recording={() => {
367
+ recording = false;
368
+ dispatch("stop_recording");
369
+ }}
370
+ on:clear={() => {
371
+ active_source = null;
372
+ recording = false;
373
+ mic_audio = null;
374
+ }}
375
+ on:error={({ detail }) => {
376
+ console.log("Failed to record:", detail);
377
+ active_source = null;
378
+ recording = false;
379
+ mic_audio = null;
380
+ }}
381
+ />
382
+ </div>
383
+ {:else}
384
+ <div class="recording-content">
385
+ <MinimalAudioPlayer
386
+ value={mic_audio}
387
+ label={label || "Audio"}
388
+ loop={false}
389
+ />
390
+ <div class="action-buttons">
391
+ <button
392
+ class="confirm-button"
393
+ on:click={() => {
394
+ add_mic_audio_to_files();
395
+ active_source = null;
396
+ recording = false;
397
+ }}
398
+ aria-label="Attach audio"
399
+ >
400
+ <Check />
401
+ </button>
402
+ <button
403
+ class="cancel-button"
404
+ on:click={() => {
405
+ active_source = null;
406
+ recording = false;
407
+ mic_audio = null;
408
+ }}
409
+ aria-label="Clear audio"
410
+ >
411
+ <Clear />
412
+ </button>
413
+ </div>
414
+ </div>
415
+ {/if}
416
+ </div>
376
417
  {/if}
377
418
  {#if show_upload}
378
419
  <Upload
@@ -407,7 +448,7 @@
407
448
  {#if show_upload}
408
449
  <button
409
450
  data-testid="upload-button"
410
- class="upload-button mobile-thumbnail-add"
451
+ class="upload-button thumbnail-add"
411
452
  {disabled}
412
453
  on:click={handle_upload_click}
413
454
  aria-label="Upload a file"
@@ -492,9 +533,15 @@
492
533
  class="microphone-button"
493
534
  class:recording
494
535
  {disabled}
495
- on:click={() => {
496
- active_source =
497
- active_source !== "microphone" ? "microphone" : null;
536
+ on:click={async () => {
537
+ if (active_source !== "microphone") {
538
+ active_source = "microphone";
539
+ await tick();
540
+ recording = true;
541
+ } else {
542
+ active_source = null;
543
+ recording = false;
544
+ }
498
545
  }}
499
546
  aria-label="Record audio"
500
547
  >
@@ -505,6 +552,7 @@
505
552
  {#if submit_btn}
506
553
  <button
507
554
  class="submit-button"
555
+ data-testid="submit-button"
508
556
  class:padded-button={submit_btn !== true}
509
557
  {disabled}
510
558
  on:click={handle_submit}
@@ -626,7 +674,6 @@
626
674
  display: flex;
627
675
  justify-content: center;
628
676
  align-items: center;
629
- border: var(--size-px) solid var(--border-color-primary);
630
677
  border-radius: var(--radius-md);
631
678
  background: var(--background-fill-secondary);
632
679
  width: var(--size-full);
@@ -712,7 +759,6 @@
712
759
  transform: scaleX(-1);
713
760
  }
714
761
 
715
- .mobile-thumbnail-add,
716
762
  .microphone-button {
717
763
  color: var(--body-text-color);
718
764
  cursor: pointer;
@@ -722,14 +768,33 @@
722
768
  align-items: center;
723
769
  flex-shrink: 0;
724
770
  border-radius: var(--radius-md);
725
- transition:
726
- background 0.2s var(--easing-standard),
727
- border-color 0.2s var(--easing-standard);
728
771
  }
729
772
 
730
- .mobile-thumbnail-add {
773
+ .thumbnail-add {
774
+ background: var(--button-secondary-background-fill);
775
+ cursor: pointer;
776
+ display: flex;
777
+ justify-content: center;
778
+ align-items: center;
779
+ flex-shrink: 0;
731
780
  width: var(--size-10);
732
781
  height: var(--size-10);
782
+ border-radius: var(--radius-md);
783
+ z-index: var(--layer-1);
784
+ }
785
+
786
+ .thumbnail-add:hover:not(:disabled) {
787
+ background: var(--button-secondary-background-fill-hover);
788
+ }
789
+
790
+ .thumbnail-add:disabled {
791
+ opacity: 0.5;
792
+ cursor: not-allowed;
793
+ }
794
+
795
+ .thumbnail-add :global(svg) {
796
+ width: var(--size-5);
797
+ height: var(--size-5);
733
798
  }
734
799
 
735
800
  .microphone-button {
@@ -737,19 +802,15 @@
737
802
  height: var(--size-9);
738
803
  }
739
804
 
740
- .mobile-thumbnail-add:hover:not(:disabled),
741
805
  .microphone-button:hover:not(:disabled) {
742
- background: var(--button-secondary-background-fill-hover);
743
- border-color: var(--border-color-primary);
806
+ background: var(--button-secondary-background-fill);
744
807
  }
745
808
 
746
- .mobile-thumbnail-add:disabled,
747
809
  .microphone-button:disabled {
748
810
  opacity: 0.5;
749
811
  cursor: not-allowed;
750
812
  }
751
813
 
752
- .mobile-thumbnail-add :global(svg),
753
814
  .microphone-button :global(svg) {
754
815
  width: var(--size-5);
755
816
  height: var(--size-5);
@@ -757,7 +818,6 @@
757
818
 
758
819
  .submit-button,
759
820
  .stop-button {
760
- border: var(--size-px) solid var(--border-color-primary);
761
821
  background: var(--button-secondary-background-fill);
762
822
  cursor: pointer;
763
823
  display: flex;
@@ -768,9 +828,6 @@
768
828
  height: var(--size-9);
769
829
  border-radius: var(--radius-md);
770
830
  z-index: var(--layer-1);
771
- transition:
772
- background 0.2s var(--easing-standard),
773
- border-color 0.2s var(--easing-standard);
774
831
  }
775
832
 
776
833
  .submit-button:hover:not(:disabled),
@@ -820,6 +877,112 @@
820
877
  }
821
878
  }
822
879
 
880
+ .recording-overlay {
881
+ position: absolute;
882
+ top: 0;
883
+ left: 0;
884
+ right: 0;
885
+ bottom: 0;
886
+ background: var(--block-background-fill);
887
+ border-radius: var(--radius-xl);
888
+ display: flex;
889
+ align-items: center;
890
+ justify-content: center;
891
+ z-index: var(--layer-5);
892
+ padding: var(--spacing-lg);
893
+ backdrop-filter: blur(8px);
894
+ animation: fadeIn 0.2s var(--easing-standard);
895
+ }
896
+
897
+ @keyframes fadeIn {
898
+ from {
899
+ opacity: 0;
900
+ }
901
+ to {
902
+ opacity: 1;
903
+ }
904
+ }
905
+
906
+ .recording-content {
907
+ display: flex;
908
+ align-items: center;
909
+ gap: var(--spacing-lg);
910
+ width: 100%;
911
+ max-width: 700px;
912
+ }
913
+
914
+ .recording-content :global(.minimal-audio-recorder),
915
+ .recording-content :global(.minimal-audio-player) {
916
+ flex: 1;
917
+ }
918
+
919
+ .action-buttons {
920
+ display: flex;
921
+ align-items: center;
922
+ gap: var(--spacing-sm);
923
+ flex-shrink: 0;
924
+ }
925
+
926
+ .stop-button,
927
+ .confirm-button,
928
+ .cancel-button {
929
+ display: flex;
930
+ align-items: center;
931
+ justify-content: center;
932
+ width: var(--size-9);
933
+ height: var(--size-9);
934
+ padding: 0;
935
+ border-radius: var(--radius-md);
936
+ cursor: pointer;
937
+ flex-shrink: 0;
938
+ }
939
+
940
+ .stop-button {
941
+ background: var(--button-secondary-background-fill);
942
+ border-color: var(--border-color-primary);
943
+ color: var(--error-500);
944
+ }
945
+
946
+ .stop-button:hover {
947
+ background: var(--button-secondary-background-fill-hover);
948
+ color: var(--error-600);
949
+ }
950
+
951
+ .stop-button:active {
952
+ transform: scale(0.95);
953
+ }
954
+
955
+ .confirm-button {
956
+ background: var(--button-primary-background-fill);
957
+ border-color: var(--button-primary-border-color);
958
+ color: white;
959
+ }
960
+
961
+ .confirm-button:hover {
962
+ background: var(--button-primary-background-fill-hover);
963
+ color: var(--button-primary-text-color-hover);
964
+ }
965
+
966
+ .confirm-button:active {
967
+ transform: scale(0.95);
968
+ }
969
+
970
+ .cancel-button {
971
+ background: var(--button-secondary-background-fill);
972
+ color: var(--body-text-color);
973
+ }
974
+
975
+ .cancel-button:hover {
976
+ background: var(--button-secondary-background-fill-hover);
977
+ }
978
+
979
+ .stop-button :global(svg),
980
+ .confirm-button :global(svg),
981
+ .cancel-button :global(svg) {
982
+ width: var(--size-5);
983
+ height: var(--size-5);
984
+ }
985
+
823
986
  @media (max-width: 768px) {
824
987
  .input-wrapper {
825
988
  padding: var(--spacing-xs);
@@ -831,7 +994,7 @@
831
994
  }
832
995
 
833
996
  .thumbnail-small,
834
- .mobile-thumbnail-add {
997
+ .thumbnail-add {
835
998
  width: var(--size-9);
836
999
  height: var(--size-9);
837
1000
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/multimodaltextbox",
3
- "version": "0.11.0-dev.2",
3
+ "version": "0.11.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -22,18 +22,18 @@
22
22
  "./package.json": "./package.json"
23
23
  },
24
24
  "dependencies": {
25
- "@gradio/atoms": "^0.19.0-dev.1",
26
- "@gradio/icons": "^0.15.0-dev.0",
27
- "@gradio/utils": "^0.10.3-dev.0",
28
- "@gradio/upload": "^0.17.2-dev.2",
29
- "@gradio/statustracker": "^0.12.0-dev.1",
30
- "@gradio/image": "^0.24.0-dev.2",
31
- "@gradio/video": "^0.17.0-dev.2",
32
- "@gradio/client": "^2.0.0-dev.2",
33
- "@gradio/audio": "^0.20.0-dev.4"
25
+ "@gradio/icons": "^0.15.0",
26
+ "@gradio/atoms": "^0.19.0",
27
+ "@gradio/utils": "^0.10.3",
28
+ "@gradio/upload": "^0.17.2",
29
+ "@gradio/statustracker": "^0.12.0",
30
+ "@gradio/video": "^0.17.0",
31
+ "@gradio/image": "^0.24.0",
32
+ "@gradio/client": "^2.0.0",
33
+ "@gradio/audio": "^0.20.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@gradio/preview": "^0.15.0-dev.0"
36
+ "@gradio/preview": "^0.15.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "svelte": "^5.43.4"
@@ -21,10 +21,15 @@
21
21
  Video,
22
22
  ArrowUp,
23
23
  Square,
24
- Microphone
24
+ Microphone,
25
+ Check
25
26
  } from "@gradio/icons";
26
27
  import type { SelectData } from "@gradio/utils";
27
28
  import { BaseInteractiveAudio as InteractiveAudio } from "@gradio/audio";
29
+ import {
30
+ MinimalAudioPlayer,
31
+ MinimalAudioRecorder
32
+ } from "@gradio/audio/shared";
28
33
  import type { InputHTMLAttributes } from "./types";
29
34
 
30
35
  export let value: { text: string; files: FileData[] } = {
@@ -35,7 +40,7 @@
35
40
  export let value_is_output = false;
36
41
  export let lines = 1;
37
42
  export let i18n: I18nFormatter;
38
- export let placeholder = "Type here...";
43
+ export let placeholder = "";
39
44
  export let disabled = false;
40
45
  export let label: string;
41
46
  export let info: string | undefined = undefined;
@@ -181,10 +186,10 @@
181
186
  max_lines >= 1
182
187
  ) {
183
188
  e.preventDefault();
189
+ add_mic_audio_to_files();
190
+ active_source = null;
184
191
  await tick();
185
192
  dispatch("submit");
186
- active_source = null;
187
- add_mic_audio_to_files();
188
193
  }
189
194
  }
190
195
 
@@ -241,13 +246,14 @@
241
246
  value.files.push(mic_audio);
242
247
  value = value;
243
248
  mic_audio = null;
249
+ dispatch("change", value);
244
250
  }
245
251
  }
246
252
 
247
253
  function handle_submit(): void {
248
- dispatch("submit");
249
- active_source = null;
250
254
  add_mic_audio_to_files();
255
+ active_source = null;
256
+ dispatch("submit");
251
257
  }
252
258
 
253
259
  async function handle_paste(event: ClipboardEvent): Promise<void> {
@@ -343,36 +349,71 @@
343
349
  <BlockTitle {show_label} {info} {rtl}>{label}</BlockTitle>
344
350
  <div class="input-container">
345
351
  {#if sources && sources.includes("microphone") && active_source === "microphone"}
346
- <InteractiveAudio
347
- on:change={({ detail }) => {
348
- if (detail !== null) {
349
- mic_audio = detail;
350
- }
351
- }}
352
- on:clear={() => {
353
- active_source = null;
354
- }}
355
- on:start_recording={() => dispatch("start_recording")}
356
- on:pause_recording={() => dispatch("pause_recording")}
357
- on:stop_recording={() => dispatch("stop_recording")}
358
- sources={["microphone"]}
359
- class_name="compact-audio"
360
- {recording}
361
- {waveform_settings}
362
- {waveform_options}
363
- {i18n}
364
- {active_source}
365
- {upload}
366
- {stream_handler}
367
- stream_every={1}
368
- editable={true}
369
- {label}
370
- {root}
371
- loop={false}
372
- show_label={false}
373
- buttons={[]}
374
- dragging={false}
375
- />
352
+ <div class="recording-overlay" class:has-audio={mic_audio !== null}>
353
+ {#if !mic_audio}
354
+ <div class="recording-content">
355
+ <MinimalAudioRecorder
356
+ label={label || "Audio"}
357
+ {waveform_settings}
358
+ {recording}
359
+ {upload}
360
+ {root}
361
+ {max_file_size}
362
+ bind:upload_promise
363
+ on:change={({ detail }) => {
364
+ mic_audio = detail;
365
+ }}
366
+ on:stop_recording={() => {
367
+ recording = false;
368
+ dispatch("stop_recording");
369
+ }}
370
+ on:clear={() => {
371
+ active_source = null;
372
+ recording = false;
373
+ mic_audio = null;
374
+ }}
375
+ on:error={({ detail }) => {
376
+ console.log("Failed to record:", detail);
377
+ active_source = null;
378
+ recording = false;
379
+ mic_audio = null;
380
+ }}
381
+ />
382
+ </div>
383
+ {:else}
384
+ <div class="recording-content">
385
+ <MinimalAudioPlayer
386
+ value={mic_audio}
387
+ label={label || "Audio"}
388
+ loop={false}
389
+ />
390
+ <div class="action-buttons">
391
+ <button
392
+ class="confirm-button"
393
+ on:click={() => {
394
+ add_mic_audio_to_files();
395
+ active_source = null;
396
+ recording = false;
397
+ }}
398
+ aria-label="Attach audio"
399
+ >
400
+ <Check />
401
+ </button>
402
+ <button
403
+ class="cancel-button"
404
+ on:click={() => {
405
+ active_source = null;
406
+ recording = false;
407
+ mic_audio = null;
408
+ }}
409
+ aria-label="Clear audio"
410
+ >
411
+ <Clear />
412
+ </button>
413
+ </div>
414
+ </div>
415
+ {/if}
416
+ </div>
376
417
  {/if}
377
418
  {#if show_upload}
378
419
  <Upload
@@ -407,7 +448,7 @@
407
448
  {#if show_upload}
408
449
  <button
409
450
  data-testid="upload-button"
410
- class="upload-button mobile-thumbnail-add"
451
+ class="upload-button thumbnail-add"
411
452
  {disabled}
412
453
  on:click={handle_upload_click}
413
454
  aria-label="Upload a file"
@@ -492,9 +533,15 @@
492
533
  class="microphone-button"
493
534
  class:recording
494
535
  {disabled}
495
- on:click={() => {
496
- active_source =
497
- active_source !== "microphone" ? "microphone" : null;
536
+ on:click={async () => {
537
+ if (active_source !== "microphone") {
538
+ active_source = "microphone";
539
+ await tick();
540
+ recording = true;
541
+ } else {
542
+ active_source = null;
543
+ recording = false;
544
+ }
498
545
  }}
499
546
  aria-label="Record audio"
500
547
  >
@@ -505,6 +552,7 @@
505
552
  {#if submit_btn}
506
553
  <button
507
554
  class="submit-button"
555
+ data-testid="submit-button"
508
556
  class:padded-button={submit_btn !== true}
509
557
  {disabled}
510
558
  on:click={handle_submit}
@@ -626,7 +674,6 @@
626
674
  display: flex;
627
675
  justify-content: center;
628
676
  align-items: center;
629
- border: var(--size-px) solid var(--border-color-primary);
630
677
  border-radius: var(--radius-md);
631
678
  background: var(--background-fill-secondary);
632
679
  width: var(--size-full);
@@ -712,7 +759,6 @@
712
759
  transform: scaleX(-1);
713
760
  }
714
761
 
715
- .mobile-thumbnail-add,
716
762
  .microphone-button {
717
763
  color: var(--body-text-color);
718
764
  cursor: pointer;
@@ -722,14 +768,33 @@
722
768
  align-items: center;
723
769
  flex-shrink: 0;
724
770
  border-radius: var(--radius-md);
725
- transition:
726
- background 0.2s var(--easing-standard),
727
- border-color 0.2s var(--easing-standard);
728
771
  }
729
772
 
730
- .mobile-thumbnail-add {
773
+ .thumbnail-add {
774
+ background: var(--button-secondary-background-fill);
775
+ cursor: pointer;
776
+ display: flex;
777
+ justify-content: center;
778
+ align-items: center;
779
+ flex-shrink: 0;
731
780
  width: var(--size-10);
732
781
  height: var(--size-10);
782
+ border-radius: var(--radius-md);
783
+ z-index: var(--layer-1);
784
+ }
785
+
786
+ .thumbnail-add:hover:not(:disabled) {
787
+ background: var(--button-secondary-background-fill-hover);
788
+ }
789
+
790
+ .thumbnail-add:disabled {
791
+ opacity: 0.5;
792
+ cursor: not-allowed;
793
+ }
794
+
795
+ .thumbnail-add :global(svg) {
796
+ width: var(--size-5);
797
+ height: var(--size-5);
733
798
  }
734
799
 
735
800
  .microphone-button {
@@ -737,19 +802,15 @@
737
802
  height: var(--size-9);
738
803
  }
739
804
 
740
- .mobile-thumbnail-add:hover:not(:disabled),
741
805
  .microphone-button:hover:not(:disabled) {
742
- background: var(--button-secondary-background-fill-hover);
743
- border-color: var(--border-color-primary);
806
+ background: var(--button-secondary-background-fill);
744
807
  }
745
808
 
746
- .mobile-thumbnail-add:disabled,
747
809
  .microphone-button:disabled {
748
810
  opacity: 0.5;
749
811
  cursor: not-allowed;
750
812
  }
751
813
 
752
- .mobile-thumbnail-add :global(svg),
753
814
  .microphone-button :global(svg) {
754
815
  width: var(--size-5);
755
816
  height: var(--size-5);
@@ -757,7 +818,6 @@
757
818
 
758
819
  .submit-button,
759
820
  .stop-button {
760
- border: var(--size-px) solid var(--border-color-primary);
761
821
  background: var(--button-secondary-background-fill);
762
822
  cursor: pointer;
763
823
  display: flex;
@@ -768,9 +828,6 @@
768
828
  height: var(--size-9);
769
829
  border-radius: var(--radius-md);
770
830
  z-index: var(--layer-1);
771
- transition:
772
- background 0.2s var(--easing-standard),
773
- border-color 0.2s var(--easing-standard);
774
831
  }
775
832
 
776
833
  .submit-button:hover:not(:disabled),
@@ -820,6 +877,112 @@
820
877
  }
821
878
  }
822
879
 
880
+ .recording-overlay {
881
+ position: absolute;
882
+ top: 0;
883
+ left: 0;
884
+ right: 0;
885
+ bottom: 0;
886
+ background: var(--block-background-fill);
887
+ border-radius: var(--radius-xl);
888
+ display: flex;
889
+ align-items: center;
890
+ justify-content: center;
891
+ z-index: var(--layer-5);
892
+ padding: var(--spacing-lg);
893
+ backdrop-filter: blur(8px);
894
+ animation: fadeIn 0.2s var(--easing-standard);
895
+ }
896
+
897
+ @keyframes fadeIn {
898
+ from {
899
+ opacity: 0;
900
+ }
901
+ to {
902
+ opacity: 1;
903
+ }
904
+ }
905
+
906
+ .recording-content {
907
+ display: flex;
908
+ align-items: center;
909
+ gap: var(--spacing-lg);
910
+ width: 100%;
911
+ max-width: 700px;
912
+ }
913
+
914
+ .recording-content :global(.minimal-audio-recorder),
915
+ .recording-content :global(.minimal-audio-player) {
916
+ flex: 1;
917
+ }
918
+
919
+ .action-buttons {
920
+ display: flex;
921
+ align-items: center;
922
+ gap: var(--spacing-sm);
923
+ flex-shrink: 0;
924
+ }
925
+
926
+ .stop-button,
927
+ .confirm-button,
928
+ .cancel-button {
929
+ display: flex;
930
+ align-items: center;
931
+ justify-content: center;
932
+ width: var(--size-9);
933
+ height: var(--size-9);
934
+ padding: 0;
935
+ border-radius: var(--radius-md);
936
+ cursor: pointer;
937
+ flex-shrink: 0;
938
+ }
939
+
940
+ .stop-button {
941
+ background: var(--button-secondary-background-fill);
942
+ border-color: var(--border-color-primary);
943
+ color: var(--error-500);
944
+ }
945
+
946
+ .stop-button:hover {
947
+ background: var(--button-secondary-background-fill-hover);
948
+ color: var(--error-600);
949
+ }
950
+
951
+ .stop-button:active {
952
+ transform: scale(0.95);
953
+ }
954
+
955
+ .confirm-button {
956
+ background: var(--button-primary-background-fill);
957
+ border-color: var(--button-primary-border-color);
958
+ color: white;
959
+ }
960
+
961
+ .confirm-button:hover {
962
+ background: var(--button-primary-background-fill-hover);
963
+ color: var(--button-primary-text-color-hover);
964
+ }
965
+
966
+ .confirm-button:active {
967
+ transform: scale(0.95);
968
+ }
969
+
970
+ .cancel-button {
971
+ background: var(--button-secondary-background-fill);
972
+ color: var(--body-text-color);
973
+ }
974
+
975
+ .cancel-button:hover {
976
+ background: var(--button-secondary-background-fill-hover);
977
+ }
978
+
979
+ .stop-button :global(svg),
980
+ .confirm-button :global(svg),
981
+ .cancel-button :global(svg) {
982
+ width: var(--size-5);
983
+ height: var(--size-5);
984
+ }
985
+
823
986
  @media (max-width: 768px) {
824
987
  .input-wrapper {
825
988
  padding: var(--spacing-xs);
@@ -831,7 +994,7 @@
831
994
  }
832
995
 
833
996
  .thumbnail-small,
834
- .mobile-thumbnail-add {
997
+ .thumbnail-add {
835
998
  width: var(--size-9);
836
999
  height: var(--size-9);
837
1000
  }