Răsfoiți Sursa

feat: capture display media audio

Timothy Jaeryang Baek 5 luni în urmă
părinte
comite
9c4a931d22

+ 29 - 6
src/lib/components/chat/MessageInput/VoiceRecording.svelte

@@ -10,6 +10,8 @@
 
 	export let recording = false;
 	export let transcribe = true;
+	export let displayMedia = false;
+
 	export let className = ' p-2.5 w-full max-w-full';
 
 	export let onCancel = () => {};
@@ -175,13 +177,34 @@
 	const startRecording = async () => {
 		loading = true;
 
-		stream = await navigator.mediaDevices.getUserMedia({
-			audio: {
-				echoCancellation: true,
-				noiseSuppression: true,
-				autoGainControl: true
+		try {
+			if (displayMedia) {
+				stream = await navigator.mediaDevices.getDisplayMedia({
+					video: {
+						mediaSource: 'screen'
+					},
+					audio: {
+						echoCancellation: true,
+						noiseSuppression: true,
+						autoGainControl: true
+					}
+				});
+			} else {
+				stream = await navigator.mediaDevices.getUserMedia({
+					audio: {
+						echoCancellation: true,
+						noiseSuppression: true,
+						autoGainControl: true
+					}
+				});
 			}
-		});
+		} catch (err) {
+			console.error('Error accessing media devices.', err);
+			toast.error($i18n.t('Error accessing media devices.'));
+			loading = false;
+			recording = false;
+			return;
+		}
 
 		mediaRecorder = new MediaRecorder(stream);
 		mediaRecorder.onstart = () => {

+ 19 - 0
src/lib/components/icons/CursorArrowRays.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59"
+	/>
+</svg>

+ 12 - 3
src/lib/components/icons/Mic.svelte

@@ -1,10 +1,19 @@
 <script lang="ts">
 	export let className = 'size-4';
+	export let strokeWidth = '1.5';
 </script>
 
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
-	<path d="M7 4a3 3 0 0 1 6 0v6a3 3 0 1 1-6 0V4Z" />
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
 	<path
-		d="M5.5 9.643a.75.75 0 0 0-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-1.5v-1.546A6.001 6.001 0 0 0 16 10v-.357a.75.75 0 0 0-1.5 0V10a4.5 4.5 0 0 1-9 0v-.357Z"
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z"
 	/>
 </svg>

+ 10 - 0
src/lib/components/icons/MicSolid.svelte

@@ -0,0 +1,10 @@
+<script lang="ts">
+	export let className = 'size-4';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
+	<path d="M7 4a3 3 0 0 1 6 0v6a3 3 0 1 1-6 0V4Z" />
+	<path
+		d="M5.5 9.643a.75.75 0 0 0-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-1.5v-1.546A6.001 6.001 0 0 0 16 10v-.357a.75.75 0 0 0-1.5 0V10a4.5 4.5 0 0 1-9 0v-.357Z"
+	/>
+</svg>

+ 47 - 38
src/lib/components/notes/NoteEditor.svelte

@@ -38,7 +38,7 @@
 
 	import RichTextInput from '../common/RichTextInput.svelte';
 	import Spinner from '../common/Spinner.svelte';
-	import Mic from '../icons/Mic.svelte';
+	import MicSolid from '../icons/MicSolid.svelte';
 	import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
 	import Tooltip from '../common/Tooltip.svelte';
 
@@ -49,6 +49,7 @@
 	import Image from '../common/Image.svelte';
 	import FileItem from '../common/FileItem.svelte';
 	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
+	import RecordMenu from './RecordMenu.svelte';
 
 	export let id: null | string = null;
 
@@ -67,7 +68,8 @@
 	};
 
 	let files = [];
-	let voiceInput = false;
+	let recording = false;
+	let displayMediaRecord = false;
 
 	let dragged = false;
 	let loading = false;
@@ -380,64 +382,71 @@
 
 <div class="absolute bottom-0 right-0 p-5 max-w-full flex justify-end">
 	<div
-		class="flex gap-0.5 justify-end w-full {$showSidebar && voiceInput
+		class="flex gap-0.5 justify-end w-full {$showSidebar && recording
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''} max-w-full"
 	>
-		{#if voiceInput}
+		{#if recording}
 			<div class="flex-1 w-full">
 				<VoiceRecording
-					bind:recording={voiceInput}
+					bind:recording
 					className="p-1 w-full max-w-full"
 					transcribe={false}
+					displayMedia={displayMediaRecord}
 					onCancel={() => {
-						voiceInput = false;
+						recording = false;
+						displayMediaRecord = false;
 					}}
 					onConfirm={(data) => {
 						if (data?.file) {
 							uploadFileHandler(data?.file);
 						}
+
+						recording = false;
+						displayMediaRecord = false;
 					}}
 				/>
 			</div>
 		{:else}
-			<Tooltip content={$i18n.t('Record')}>
+			<RecordMenu
+				onRecord={async () => {
+					displayMediaRecord = false;
+
+					try {
+						let stream = await navigator.mediaDevices
+							.getUserMedia({ audio: true })
+							.catch(function (err) {
+								toast.error(
+									$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
+										error: err
+									})
+								);
+								return null;
+							});
+
+						if (stream) {
+							recording = true;
+							const tracks = stream.getTracks();
+							tracks.forEach((track) => track.stop());
+						}
+						stream = null;
+					} catch {
+						toast.error($i18n.t('Permission denied when accessing microphone'));
+					}
+				}}
+				onCaptureAudio={async () => {
+					displayMediaRecord = true;
+
+					recording = true;
+				}}
+			>
 				<button
 					class="cursor-pointer p-2.5 flex rounded-full border border-gray-50 dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition shadow-xl"
 					type="button"
-					on:click={async () => {
-						try {
-							let stream = await navigator.mediaDevices
-								.getUserMedia({ audio: true })
-								.catch(function (err) {
-									toast.error(
-										$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
-											error: err
-										})
-									);
-									return null;
-								});
-
-							if (stream) {
-								voiceInput = true;
-								const tracks = stream.getTracks();
-								tracks.forEach((track) => track.stop());
-							}
-							stream = null;
-						} catch {
-							toast.error($i18n.t('Permission denied when accessing microphone'));
-						}
-					}}
 				>
-					<Mic className="size-4.5" />
+					<MicSolid className="size-4.5" />
 				</button>
-			</Tooltip>
+			</RecordMenu>
 		{/if}
-
-		<!-- <button
-            class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition shadow-xl"
-        >
-            <SparklesSolid className="size-4" />
-        </button> -->
 	</div>
 </div>

+ 70 - 0
src/lib/components/notes/RecordMenu.svelte

@@ -0,0 +1,70 @@
+<script lang="ts">
+	import { DropdownMenu } from 'bits-ui';
+	import { createEventDispatcher, getContext, onMount } from 'svelte';
+
+	import { showSettings, activeUserIds, USAGE_POOL, mobile, showSidebar, user } from '$lib/stores';
+	import { fade, slide } from 'svelte/transition';
+	import { userSignOut } from '$lib/apis/auths';
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
+	import Mic from '../icons/Mic.svelte';
+	import CursorArrowRays from '../icons/CursorArrowRays.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let show = false;
+	export let className = 'max-w-[160px]';
+
+	export let onRecord = () => {};
+	export let onCaptureAudio = () => {};
+
+	const dispatch = createEventDispatcher();
+</script>
+
+<DropdownMenu.Root
+	bind:open={show}
+	onOpenChange={(state) => {
+		dispatch('change', state);
+	}}
+>
+	<DropdownMenu.Trigger>
+		<slot />
+	</DropdownMenu.Trigger>
+
+	<slot name="content">
+		<DropdownMenu.Content
+			class="w-full {className} text-sm rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
+			sideOffset={8}
+			side="bottom"
+			align="start"
+			transition={(e) => fade(e, { duration: 100 })}
+		>
+			<button
+				class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+				on:click={async () => {
+					onRecord();
+					show = false;
+				}}
+			>
+				<div class=" self-center mr-2">
+					<Mic className="size-5" strokeWidth="1.5" />
+				</div>
+				<div class=" self-center truncate">{$i18n.t('Record')}</div>
+			</button>
+
+			<button
+				class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+				on:click={() => {
+					onCaptureAudio();
+					show = false;
+				}}
+			>
+				<div class=" self-center mr-2">
+					<CursorArrowRays className="size-5" strokeWidth="1.5" />
+				</div>
+				<div class=" self-center truncate">{$i18n.t('Capture Audio')}</div>
+			</button>
+		</DropdownMenu.Content>
+	</slot>
+</DropdownMenu.Root>

+ 2 - 2
src/lib/components/workspace/Knowledge/KnowledgeBase/AddTextContentModal.svelte

@@ -9,7 +9,7 @@
 	import Modal from '$lib/components/common/Modal.svelte';
 	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
 	import XMark from '$lib/components/icons/XMark.svelte';
-	import Mic from '$lib/components/icons/Mic.svelte';
+	import MicSolid from '$lib/components/icons/MicSolid.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import VoiceRecording from '$lib/components/chat/MessageInput/VoiceRecording.svelte';
 	export let show = false;
@@ -125,7 +125,7 @@
 									}
 								}}
 							>
-								<Mic className="size-5" />
+								<MicSolid className="size-5" />
 							</button>
 						</Tooltip>
 					{/if}