@gradio/multimodaltextbox 0.11.0-dev.2 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @gradio/multimodaltextbox
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Dependency updates
6
+
7
+ - @gradio/audio@0.21.0
8
+ - @gradio/video@0.18.0
9
+
10
+ ## 0.11.0
11
+
12
+ ### Dependency updates
13
+
14
+ - @gradio/utils@0.10.4
15
+
16
+ ## 0.11.0
17
+
18
+ ### Features
19
+
20
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - remove_placeholder_type_here
21
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Clear Error statuses
22
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Improve gr.MultimodalTextbox in mobile
23
+ - [#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
24
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add back default values for labels
25
+ - [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Unit test fixes
26
+ - [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix
27
+
28
+ ### Dependencies
29
+
30
+ - @gradio/atoms@0.19.0
31
+ - @gradio/icons@0.15.0
32
+ - @gradio/statustracker@0.12.0
33
+ - @gradio/utils@0.10.3
34
+ - @gradio/upload@0.17.2
35
+ - @gradio/image@0.24.0
36
+ - @gradio/video@0.17.0
37
+ - @gradio/client@2.0.0
38
+ - @gradio/audio@0.20.0
39
+
3
40
  ## 0.11.0-dev.2
4
41
 
5
42
  ### Dependency updates
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,70 @@
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
+ active_source = null;
377
+ recording = false;
378
+ mic_audio = null;
379
+ }}
380
+ />
381
+ </div>
382
+ {:else}
383
+ <div class="recording-content">
384
+ <MinimalAudioPlayer
385
+ value={mic_audio}
386
+ label={label || "Audio"}
387
+ loop={false}
388
+ />
389
+ <div class="action-buttons">
390
+ <button
391
+ class="confirm-button"
392
+ on:click={() => {
393
+ add_mic_audio_to_files();
394
+ active_source = null;
395
+ recording = false;
396
+ }}
397
+ aria-label="Attach audio"
398
+ >
399
+ <Check />
400
+ </button>
401
+ <button
402
+ class="cancel-button"
403
+ on:click={() => {
404
+ active_source = null;
405
+ recording = false;
406
+ mic_audio = null;
407
+ }}
408
+ aria-label="Clear audio"
409
+ >
410
+ <Clear />
411
+ </button>
412
+ </div>
413
+ </div>
414
+ {/if}
415
+ </div>
376
416
  {/if}
377
417
  {#if show_upload}
378
418
  <Upload
@@ -407,7 +447,7 @@
407
447
  {#if show_upload}
408
448
  <button
409
449
  data-testid="upload-button"
410
- class="upload-button mobile-thumbnail-add"
450
+ class="upload-button thumbnail-add"
411
451
  {disabled}
412
452
  on:click={handle_upload_click}
413
453
  aria-label="Upload a file"
@@ -492,9 +532,15 @@
492
532
  class="microphone-button"
493
533
  class:recording
494
534
  {disabled}
495
- on:click={() => {
496
- active_source =
497
- active_source !== "microphone" ? "microphone" : null;
535
+ on:click={async () => {
536
+ if (active_source !== "microphone") {
537
+ active_source = "microphone";
538
+ await tick();
539
+ recording = true;
540
+ } else {
541
+ active_source = null;
542
+ recording = false;
543
+ }
498
544
  }}
499
545
  aria-label="Record audio"
500
546
  >
@@ -505,6 +551,7 @@
505
551
  {#if submit_btn}
506
552
  <button
507
553
  class="submit-button"
554
+ data-testid="submit-button"
508
555
  class:padded-button={submit_btn !== true}
509
556
  {disabled}
510
557
  on:click={handle_submit}
@@ -626,7 +673,6 @@
626
673
  display: flex;
627
674
  justify-content: center;
628
675
  align-items: center;
629
- border: var(--size-px) solid var(--border-color-primary);
630
676
  border-radius: var(--radius-md);
631
677
  background: var(--background-fill-secondary);
632
678
  width: var(--size-full);
@@ -712,7 +758,6 @@
712
758
  transform: scaleX(-1);
713
759
  }
714
760
 
715
- .mobile-thumbnail-add,
716
761
  .microphone-button {
717
762
  color: var(--body-text-color);
718
763
  cursor: pointer;
@@ -722,14 +767,33 @@
722
767
  align-items: center;
723
768
  flex-shrink: 0;
724
769
  border-radius: var(--radius-md);
725
- transition:
726
- background 0.2s var(--easing-standard),
727
- border-color 0.2s var(--easing-standard);
728
770
  }
729
771
 
730
- .mobile-thumbnail-add {
772
+ .thumbnail-add {
773
+ background: var(--button-secondary-background-fill);
774
+ cursor: pointer;
775
+ display: flex;
776
+ justify-content: center;
777
+ align-items: center;
778
+ flex-shrink: 0;
731
779
  width: var(--size-10);
732
780
  height: var(--size-10);
781
+ border-radius: var(--radius-md);
782
+ z-index: var(--layer-1);
783
+ }
784
+
785
+ .thumbnail-add:hover:not(:disabled) {
786
+ background: var(--button-secondary-background-fill-hover);
787
+ }
788
+
789
+ .thumbnail-add:disabled {
790
+ opacity: 0.5;
791
+ cursor: not-allowed;
792
+ }
793
+
794
+ .thumbnail-add :global(svg) {
795
+ width: var(--size-5);
796
+ height: var(--size-5);
733
797
  }
734
798
 
735
799
  .microphone-button {
@@ -737,19 +801,15 @@
737
801
  height: var(--size-9);
738
802
  }
739
803
 
740
- .mobile-thumbnail-add:hover:not(:disabled),
741
804
  .microphone-button:hover:not(:disabled) {
742
- background: var(--button-secondary-background-fill-hover);
743
- border-color: var(--border-color-primary);
805
+ background: var(--button-secondary-background-fill);
744
806
  }
745
807
 
746
- .mobile-thumbnail-add:disabled,
747
808
  .microphone-button:disabled {
748
809
  opacity: 0.5;
749
810
  cursor: not-allowed;
750
811
  }
751
812
 
752
- .mobile-thumbnail-add :global(svg),
753
813
  .microphone-button :global(svg) {
754
814
  width: var(--size-5);
755
815
  height: var(--size-5);
@@ -757,7 +817,6 @@
757
817
 
758
818
  .submit-button,
759
819
  .stop-button {
760
- border: var(--size-px) solid var(--border-color-primary);
761
820
  background: var(--button-secondary-background-fill);
762
821
  cursor: pointer;
763
822
  display: flex;
@@ -768,9 +827,6 @@
768
827
  height: var(--size-9);
769
828
  border-radius: var(--radius-md);
770
829
  z-index: var(--layer-1);
771
- transition:
772
- background 0.2s var(--easing-standard),
773
- border-color 0.2s var(--easing-standard);
774
830
  }
775
831
 
776
832
  .submit-button:hover:not(:disabled),
@@ -820,6 +876,112 @@
820
876
  }
821
877
  }
822
878
 
879
+ .recording-overlay {
880
+ position: absolute;
881
+ top: 0;
882
+ left: 0;
883
+ right: 0;
884
+ bottom: 0;
885
+ background: var(--block-background-fill);
886
+ border-radius: var(--radius-xl);
887
+ display: flex;
888
+ align-items: center;
889
+ justify-content: center;
890
+ z-index: var(--layer-5);
891
+ padding: var(--spacing-lg);
892
+ backdrop-filter: blur(8px);
893
+ animation: fadeIn 0.2s var(--easing-standard);
894
+ }
895
+
896
+ @keyframes fadeIn {
897
+ from {
898
+ opacity: 0;
899
+ }
900
+ to {
901
+ opacity: 1;
902
+ }
903
+ }
904
+
905
+ .recording-content {
906
+ display: flex;
907
+ align-items: center;
908
+ gap: var(--spacing-lg);
909
+ width: 100%;
910
+ max-width: 700px;
911
+ }
912
+
913
+ .recording-content :global(.minimal-audio-recorder),
914
+ .recording-content :global(.minimal-audio-player) {
915
+ flex: 1;
916
+ }
917
+
918
+ .action-buttons {
919
+ display: flex;
920
+ align-items: center;
921
+ gap: var(--spacing-sm);
922
+ flex-shrink: 0;
923
+ }
924
+
925
+ .stop-button,
926
+ .confirm-button,
927
+ .cancel-button {
928
+ display: flex;
929
+ align-items: center;
930
+ justify-content: center;
931
+ width: var(--size-9);
932
+ height: var(--size-9);
933
+ padding: 0;
934
+ border-radius: var(--radius-md);
935
+ cursor: pointer;
936
+ flex-shrink: 0;
937
+ }
938
+
939
+ .stop-button {
940
+ background: var(--button-secondary-background-fill);
941
+ border-color: var(--border-color-primary);
942
+ color: var(--error-500);
943
+ }
944
+
945
+ .stop-button:hover {
946
+ background: var(--button-secondary-background-fill-hover);
947
+ color: var(--error-600);
948
+ }
949
+
950
+ .stop-button:active {
951
+ transform: scale(0.95);
952
+ }
953
+
954
+ .confirm-button {
955
+ background: var(--button-primary-background-fill);
956
+ border-color: var(--button-primary-border-color);
957
+ color: white;
958
+ }
959
+
960
+ .confirm-button:hover {
961
+ background: var(--button-primary-background-fill-hover);
962
+ color: var(--button-primary-text-color-hover);
963
+ }
964
+
965
+ .confirm-button:active {
966
+ transform: scale(0.95);
967
+ }
968
+
969
+ .cancel-button {
970
+ background: var(--button-secondary-background-fill);
971
+ color: var(--body-text-color);
972
+ }
973
+
974
+ .cancel-button:hover {
975
+ background: var(--button-secondary-background-fill-hover);
976
+ }
977
+
978
+ .stop-button :global(svg),
979
+ .confirm-button :global(svg),
980
+ .cancel-button :global(svg) {
981
+ width: var(--size-5);
982
+ height: var(--size-5);
983
+ }
984
+
823
985
  @media (max-width: 768px) {
824
986
  .input-wrapper {
825
987
  padding: var(--spacing-xs);
@@ -831,7 +993,7 @@
831
993
  }
832
994
 
833
995
  .thumbnail-small,
834
- .mobile-thumbnail-add {
996
+ .thumbnail-add {
835
997
  width: var(--size-9);
836
998
  height: var(--size-9);
837
999
  }
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.1",
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/atoms": "^0.19.0",
26
+ "@gradio/icons": "^0.15.0",
27
+ "@gradio/statustracker": "^0.12.0",
28
+ "@gradio/utils": "^0.10.4",
29
+ "@gradio/upload": "^0.17.2",
30
+ "@gradio/video": "^0.18.0",
31
+ "@gradio/image": "^0.24.0",
32
+ "@gradio/client": "^2.0.0",
33
+ "@gradio/audio": "^0.21.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@gradio/preview": "^0.15.0-dev.0"
36
+ "@gradio/preview": "^0.15.1"
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,70 @@
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
+ active_source = null;
377
+ recording = false;
378
+ mic_audio = null;
379
+ }}
380
+ />
381
+ </div>
382
+ {:else}
383
+ <div class="recording-content">
384
+ <MinimalAudioPlayer
385
+ value={mic_audio}
386
+ label={label || "Audio"}
387
+ loop={false}
388
+ />
389
+ <div class="action-buttons">
390
+ <button
391
+ class="confirm-button"
392
+ on:click={() => {
393
+ add_mic_audio_to_files();
394
+ active_source = null;
395
+ recording = false;
396
+ }}
397
+ aria-label="Attach audio"
398
+ >
399
+ <Check />
400
+ </button>
401
+ <button
402
+ class="cancel-button"
403
+ on:click={() => {
404
+ active_source = null;
405
+ recording = false;
406
+ mic_audio = null;
407
+ }}
408
+ aria-label="Clear audio"
409
+ >
410
+ <Clear />
411
+ </button>
412
+ </div>
413
+ </div>
414
+ {/if}
415
+ </div>
376
416
  {/if}
377
417
  {#if show_upload}
378
418
  <Upload
@@ -407,7 +447,7 @@
407
447
  {#if show_upload}
408
448
  <button
409
449
  data-testid="upload-button"
410
- class="upload-button mobile-thumbnail-add"
450
+ class="upload-button thumbnail-add"
411
451
  {disabled}
412
452
  on:click={handle_upload_click}
413
453
  aria-label="Upload a file"
@@ -492,9 +532,15 @@
492
532
  class="microphone-button"
493
533
  class:recording
494
534
  {disabled}
495
- on:click={() => {
496
- active_source =
497
- active_source !== "microphone" ? "microphone" : null;
535
+ on:click={async () => {
536
+ if (active_source !== "microphone") {
537
+ active_source = "microphone";
538
+ await tick();
539
+ recording = true;
540
+ } else {
541
+ active_source = null;
542
+ recording = false;
543
+ }
498
544
  }}
499
545
  aria-label="Record audio"
500
546
  >
@@ -505,6 +551,7 @@
505
551
  {#if submit_btn}
506
552
  <button
507
553
  class="submit-button"
554
+ data-testid="submit-button"
508
555
  class:padded-button={submit_btn !== true}
509
556
  {disabled}
510
557
  on:click={handle_submit}
@@ -626,7 +673,6 @@
626
673
  display: flex;
627
674
  justify-content: center;
628
675
  align-items: center;
629
- border: var(--size-px) solid var(--border-color-primary);
630
676
  border-radius: var(--radius-md);
631
677
  background: var(--background-fill-secondary);
632
678
  width: var(--size-full);
@@ -712,7 +758,6 @@
712
758
  transform: scaleX(-1);
713
759
  }
714
760
 
715
- .mobile-thumbnail-add,
716
761
  .microphone-button {
717
762
  color: var(--body-text-color);
718
763
  cursor: pointer;
@@ -722,14 +767,33 @@
722
767
  align-items: center;
723
768
  flex-shrink: 0;
724
769
  border-radius: var(--radius-md);
725
- transition:
726
- background 0.2s var(--easing-standard),
727
- border-color 0.2s var(--easing-standard);
728
770
  }
729
771
 
730
- .mobile-thumbnail-add {
772
+ .thumbnail-add {
773
+ background: var(--button-secondary-background-fill);
774
+ cursor: pointer;
775
+ display: flex;
776
+ justify-content: center;
777
+ align-items: center;
778
+ flex-shrink: 0;
731
779
  width: var(--size-10);
732
780
  height: var(--size-10);
781
+ border-radius: var(--radius-md);
782
+ z-index: var(--layer-1);
783
+ }
784
+
785
+ .thumbnail-add:hover:not(:disabled) {
786
+ background: var(--button-secondary-background-fill-hover);
787
+ }
788
+
789
+ .thumbnail-add:disabled {
790
+ opacity: 0.5;
791
+ cursor: not-allowed;
792
+ }
793
+
794
+ .thumbnail-add :global(svg) {
795
+ width: var(--size-5);
796
+ height: var(--size-5);
733
797
  }
734
798
 
735
799
  .microphone-button {
@@ -737,19 +801,15 @@
737
801
  height: var(--size-9);
738
802
  }
739
803
 
740
- .mobile-thumbnail-add:hover:not(:disabled),
741
804
  .microphone-button:hover:not(:disabled) {
742
- background: var(--button-secondary-background-fill-hover);
743
- border-color: var(--border-color-primary);
805
+ background: var(--button-secondary-background-fill);
744
806
  }
745
807
 
746
- .mobile-thumbnail-add:disabled,
747
808
  .microphone-button:disabled {
748
809
  opacity: 0.5;
749
810
  cursor: not-allowed;
750
811
  }
751
812
 
752
- .mobile-thumbnail-add :global(svg),
753
813
  .microphone-button :global(svg) {
754
814
  width: var(--size-5);
755
815
  height: var(--size-5);
@@ -757,7 +817,6 @@
757
817
 
758
818
  .submit-button,
759
819
  .stop-button {
760
- border: var(--size-px) solid var(--border-color-primary);
761
820
  background: var(--button-secondary-background-fill);
762
821
  cursor: pointer;
763
822
  display: flex;
@@ -768,9 +827,6 @@
768
827
  height: var(--size-9);
769
828
  border-radius: var(--radius-md);
770
829
  z-index: var(--layer-1);
771
- transition:
772
- background 0.2s var(--easing-standard),
773
- border-color 0.2s var(--easing-standard);
774
830
  }
775
831
 
776
832
  .submit-button:hover:not(:disabled),
@@ -820,6 +876,112 @@
820
876
  }
821
877
  }
822
878
 
879
+ .recording-overlay {
880
+ position: absolute;
881
+ top: 0;
882
+ left: 0;
883
+ right: 0;
884
+ bottom: 0;
885
+ background: var(--block-background-fill);
886
+ border-radius: var(--radius-xl);
887
+ display: flex;
888
+ align-items: center;
889
+ justify-content: center;
890
+ z-index: var(--layer-5);
891
+ padding: var(--spacing-lg);
892
+ backdrop-filter: blur(8px);
893
+ animation: fadeIn 0.2s var(--easing-standard);
894
+ }
895
+
896
+ @keyframes fadeIn {
897
+ from {
898
+ opacity: 0;
899
+ }
900
+ to {
901
+ opacity: 1;
902
+ }
903
+ }
904
+
905
+ .recording-content {
906
+ display: flex;
907
+ align-items: center;
908
+ gap: var(--spacing-lg);
909
+ width: 100%;
910
+ max-width: 700px;
911
+ }
912
+
913
+ .recording-content :global(.minimal-audio-recorder),
914
+ .recording-content :global(.minimal-audio-player) {
915
+ flex: 1;
916
+ }
917
+
918
+ .action-buttons {
919
+ display: flex;
920
+ align-items: center;
921
+ gap: var(--spacing-sm);
922
+ flex-shrink: 0;
923
+ }
924
+
925
+ .stop-button,
926
+ .confirm-button,
927
+ .cancel-button {
928
+ display: flex;
929
+ align-items: center;
930
+ justify-content: center;
931
+ width: var(--size-9);
932
+ height: var(--size-9);
933
+ padding: 0;
934
+ border-radius: var(--radius-md);
935
+ cursor: pointer;
936
+ flex-shrink: 0;
937
+ }
938
+
939
+ .stop-button {
940
+ background: var(--button-secondary-background-fill);
941
+ border-color: var(--border-color-primary);
942
+ color: var(--error-500);
943
+ }
944
+
945
+ .stop-button:hover {
946
+ background: var(--button-secondary-background-fill-hover);
947
+ color: var(--error-600);
948
+ }
949
+
950
+ .stop-button:active {
951
+ transform: scale(0.95);
952
+ }
953
+
954
+ .confirm-button {
955
+ background: var(--button-primary-background-fill);
956
+ border-color: var(--button-primary-border-color);
957
+ color: white;
958
+ }
959
+
960
+ .confirm-button:hover {
961
+ background: var(--button-primary-background-fill-hover);
962
+ color: var(--button-primary-text-color-hover);
963
+ }
964
+
965
+ .confirm-button:active {
966
+ transform: scale(0.95);
967
+ }
968
+
969
+ .cancel-button {
970
+ background: var(--button-secondary-background-fill);
971
+ color: var(--body-text-color);
972
+ }
973
+
974
+ .cancel-button:hover {
975
+ background: var(--button-secondary-background-fill-hover);
976
+ }
977
+
978
+ .stop-button :global(svg),
979
+ .confirm-button :global(svg),
980
+ .cancel-button :global(svg) {
981
+ width: var(--size-5);
982
+ height: var(--size-5);
983
+ }
984
+
823
985
  @media (max-width: 768px) {
824
986
  .input-wrapper {
825
987
  padding: var(--spacing-xs);
@@ -831,7 +993,7 @@
831
993
  }
832
994
 
833
995
  .thumbnail-small,
834
- .mobile-thumbnail-add {
996
+ .thumbnail-add {
835
997
  width: var(--size-9);
836
998
  height: var(--size-9);
837
999
  }