Browse Source

refac: voice input styling

Timothy J. Baek 8 months ago
parent
commit
0cf8f58efe

+ 2 - 1
backend/open_webui/apps/audio/main.py

@@ -522,7 +522,8 @@ def transcription(
             else:
                 data = transcribe(file_path)
 
-            return data
+            file_path = file_path.split("/")[-1]
+            return {**data, "filename": file_path}
         except Exception as e:
             log.exception(e)
             raise HTTPException(

+ 2 - 2
src/lib/components/chat/MessageInput.svelte

@@ -361,8 +361,8 @@
 							document.getElementById('chat-input')?.focus();
 						}}
 						on:confirm={async (e) => {
-							const response = e.detail;
-							prompt = `${prompt}${response} `;
+							const { text, filename } = e.detail;
+							prompt = `${prompt}${text} `;
 
 							recording = false;
 

+ 161 - 127
src/lib/components/chat/MessageInput/VoiceRecording.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
-	import { createEventDispatcher, tick, getContext } from 'svelte';
+	import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
 	import { config, settings } from '$lib/stores';
 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
 
@@ -52,7 +52,7 @@
 	let audioChunks = [];
 
 	const MIN_DECIBELS = -45;
-	const VISUALIZER_BUFFER_LENGTH = 300;
+	let VISUALIZER_BUFFER_LENGTH = 300;
 
 	let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
 
@@ -142,8 +142,8 @@
 		});
 
 		if (res) {
-			console.log(res.text);
-			dispatch('confirm', res.text);
+			console.log(res);
+			dispatch('confirm', res);
 		}
 	};
 
@@ -278,12 +278,40 @@
 
 		stream = null;
 	};
+
+	let resizeObserver;
+	let containerWidth;
+
+	let maxVisibleItems = 300;
+	$: maxVisibleItems = Math.floor(containerWidth / 5); // 2px width + 0.5px gap
+
+	onMount(() => {
+		// listen to width changes
+		resizeObserver = new ResizeObserver(() => {
+			VISUALIZER_BUFFER_LENGTH = Math.floor(window.innerWidth / 4);
+			if (visualizerData.length > VISUALIZER_BUFFER_LENGTH) {
+				visualizerData = visualizerData.slice(visualizerData.length - VISUALIZER_BUFFER_LENGTH);
+			} else {
+				visualizerData = Array(VISUALIZER_BUFFER_LENGTH - visualizerData.length)
+					.fill(0)
+					.concat(visualizerData);
+			}
+		});
+
+		resizeObserver.observe(document.body);
+	});
+
+	onDestroy(() => {
+		// remove resize observer
+		resizeObserver.disconnect();
+	});
 </script>
 
 <div
+	bind:clientWidth={containerWidth}
 	class="{loading
 		? ' bg-gray-100/50 dark:bg-gray-850/50'
-		: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex {className}"
+		: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex justify-between {className}"
 >
 	<div class="flex items-center mr-1">
 		<button
@@ -318,146 +346,152 @@
 		class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
 		dir="rtl"
 	>
-		<div class="flex-1 flex items-center gap-0.5 h-6">
+		<div
+			class="flex items-center gap-0.5 h-6 w-full max-w-full overflow-hidden overflow-x-hidden flex-wrap"
+		>
 			{#each visualizerData.slice().reverse() as rms}
-				<div
-					class="w-[2px]
+				<div class="flex items-center h-full">
+					<div
+						class="w-[2px] flex-shrink-0
                     
                     {loading
-						? ' bg-gray-500 dark:bg-gray-400   '
-						: 'bg-indigo-500 dark:bg-indigo-400  '} 
+							? ' bg-gray-500 dark:bg-gray-400   '
+							: 'bg-indigo-500 dark:bg-indigo-400  '} 
                     
                     inline-block h-full"
-					style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
-				/>
+						style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
+					/>
+				</div>
 			{/each}
 		</div>
 	</div>
 
-	<div class="  mx-1.5 pr-1 flex justify-center items-center">
-		<div
-			class="text-sm
+	<div class="flex">
+		<div class="  mx-1.5 pr-1 flex justify-center items-center">
+			<div
+				class="text-sm
         
         
         {loading ? ' text-gray-500  dark:text-gray-400  ' : ' text-indigo-400 '} 
        font-medium flex-1 mx-auto text-center"
-		>
-			{formatSeconds(durationSeconds)}
+			>
+				{formatSeconds(durationSeconds)}
+			</div>
 		</div>
-	</div>
 
-	<div class="flex items-center mr-1">
-		{#if loading}
-			<div class=" text-gray-500 rounded-full cursor-not-allowed">
-				<svg
-					width="24"
-					height="24"
-					viewBox="0 0 24 24"
-					xmlns="http://www.w3.org/2000/svg"
-					fill="currentColor"
-					><style>
-						.spinner_OSmW {
-							transform-origin: center;
-							animation: spinner_T6mA 0.75s step-end infinite;
-						}
-						@keyframes spinner_T6mA {
-							8.3% {
-								transform: rotate(30deg);
-							}
-							16.6% {
-								transform: rotate(60deg);
-							}
-							25% {
-								transform: rotate(90deg);
-							}
-							33.3% {
-								transform: rotate(120deg);
-							}
-							41.6% {
-								transform: rotate(150deg);
-							}
-							50% {
-								transform: rotate(180deg);
-							}
-							58.3% {
-								transform: rotate(210deg);
-							}
-							66.6% {
-								transform: rotate(240deg);
+		<div class="flex items-center">
+			{#if loading}
+				<div class=" text-gray-500 rounded-full cursor-not-allowed">
+					<svg
+						width="24"
+						height="24"
+						viewBox="0 0 24 24"
+						xmlns="http://www.w3.org/2000/svg"
+						fill="currentColor"
+						><style>
+							.spinner_OSmW {
+								transform-origin: center;
+								animation: spinner_T6mA 0.75s step-end infinite;
 							}
-							75% {
-								transform: rotate(270deg);
+							@keyframes spinner_T6mA {
+								8.3% {
+									transform: rotate(30deg);
+								}
+								16.6% {
+									transform: rotate(60deg);
+								}
+								25% {
+									transform: rotate(90deg);
+								}
+								33.3% {
+									transform: rotate(120deg);
+								}
+								41.6% {
+									transform: rotate(150deg);
+								}
+								50% {
+									transform: rotate(180deg);
+								}
+								58.3% {
+									transform: rotate(210deg);
+								}
+								66.6% {
+									transform: rotate(240deg);
+								}
+								75% {
+									transform: rotate(270deg);
+								}
+								83.3% {
+									transform: rotate(300deg);
+								}
+								91.6% {
+									transform: rotate(330deg);
+								}
+								100% {
+									transform: rotate(360deg);
+								}
 							}
-							83.3% {
-								transform: rotate(300deg);
-							}
-							91.6% {
-								transform: rotate(330deg);
-							}
-							100% {
-								transform: rotate(360deg);
-							}
-						}
-					</style><g class="spinner_OSmW"
-						><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
-							x="11"
-							y="1"
-							width="2"
-							height="5"
-							transform="rotate(30 12 12)"
-							opacity=".29"
-						/><rect
-							x="11"
-							y="1"
-							width="2"
-							height="5"
-							transform="rotate(60 12 12)"
-							opacity=".43"
-						/><rect
-							x="11"
-							y="1"
-							width="2"
-							height="5"
-							transform="rotate(90 12 12)"
-							opacity=".57"
-						/><rect
-							x="11"
-							y="1"
-							width="2"
-							height="5"
-							transform="rotate(120 12 12)"
-							opacity=".71"
-						/><rect
-							x="11"
-							y="1"
-							width="2"
-							height="5"
-							transform="rotate(150 12 12)"
-							opacity=".86"
-						/><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
-					></svg
+						</style><g class="spinner_OSmW"
+							><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
+								x="11"
+								y="1"
+								width="2"
+								height="5"
+								transform="rotate(30 12 12)"
+								opacity=".29"
+							/><rect
+								x="11"
+								y="1"
+								width="2"
+								height="5"
+								transform="rotate(60 12 12)"
+								opacity=".43"
+							/><rect
+								x="11"
+								y="1"
+								width="2"
+								height="5"
+								transform="rotate(90 12 12)"
+								opacity=".57"
+							/><rect
+								x="11"
+								y="1"
+								width="2"
+								height="5"
+								transform="rotate(120 12 12)"
+								opacity=".71"
+							/><rect
+								x="11"
+								y="1"
+								width="2"
+								height="5"
+								transform="rotate(150 12 12)"
+								opacity=".86"
+							/><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
+						></svg
+					>
+				</div>
+			{:else}
+				<button
+					type="button"
+					class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
+					on:click={async () => {
+						await confirmRecording();
+					}}
 				>
-			</div>
-		{:else}
-			<button
-				type="button"
-				class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
-				on:click={async () => {
-					await confirmRecording();
-				}}
-			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					fill="none"
-					viewBox="0 0 24 24"
-					stroke-width="2.5"
-					stroke="currentColor"
-					class="size-4"
-				>
-					<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
-				</svg>
-			</button>
-		{/if}
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="2.5"
+						stroke="currentColor"
+						class="size-4"
+					>
+						<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
+					</svg>
+				</button>
+			{/if}
+		</div>
 	</div>
 </div>
 

+ 12 - 0
src/lib/components/icons/SparklesSolid.svelte

@@ -0,0 +1,12 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
+	<path
+		fill-rule="evenodd"
+		d="M9 4.5a.75.75 0 0 1 .721.544l.813 2.846a3.75 3.75 0 0 0 2.576 2.576l2.846.813a.75.75 0 0 1 0 1.442l-2.846.813a3.75 3.75 0 0 0-2.576 2.576l-.813 2.846a.75.75 0 0 1-1.442 0l-.813-2.846a3.75 3.75 0 0 0-2.576-2.576l-2.846-.813a.75.75 0 0 1 0-1.442l2.846-.813A3.75 3.75 0 0 0 7.466 7.89l.813-2.846A.75.75 0 0 1 9 4.5ZM18 1.5a.75.75 0 0 1 .728.568l.258 1.036c.236.94.97 1.674 1.91 1.91l1.036.258a.75.75 0 0 1 0 1.456l-1.036.258c-.94.236-1.674.97-1.91 1.91l-.258 1.036a.75.75 0 0 1-1.456 0l-.258-1.036a2.625 2.625 0 0 0-1.91-1.91l-1.036-.258a.75.75 0 0 1 0-1.456l1.036-.258a2.625 2.625 0 0 0 1.91-1.91l.258-1.036A.75.75 0 0 1 18 1.5ZM16.5 15a.75.75 0 0 1 .712.513l.394 1.183c.15.447.5.799.948.948l1.183.395a.75.75 0 0 1 0 1.422l-1.183.395c-.447.15-.799.5-.948.948l-.395 1.183a.75.75 0 0 1-1.422 0l-.395-1.183a1.5 1.5 0 0 0-.948-.948l-1.183-.395a.75.75 0 0 1 0-1.422l1.183-.395c.447-.15.799-.5.948-.948l.395-1.183A.75.75 0 0 1 16.5 15Z"
+		clip-rule="evenodd"
+	/>
+</svg>

+ 121 - 0
src/lib/components/playground/Notes.svelte

@@ -0,0 +1,121 @@
+<script>
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import RichTextInput from '../common/RichTextInput.svelte';
+	import Spinner from '../common/Spinner.svelte';
+	import Sparkles from '../icons/Sparkles.svelte';
+	import SparklesSolid from '../icons/SparklesSolid.svelte';
+	import Mic from '../icons/Mic.svelte';
+	import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
+	import Tooltip from '../common/Tooltip.svelte';
+	import { toast } from 'svelte-sonner';
+
+	let name = '';
+	let content = '';
+
+	let voiceInput = false;
+	let loading = false;
+</script>
+
+<div class="relative flex-1 w-full h-full flex justify-center overflow-auto px-5 py-1">
+	{#if loading}
+		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
+			<div class="m-auto">
+				<Spinner />
+			</div>
+		</div>
+	{/if}
+
+	<div class=" w-full flex flex-col gap-2 {loading ? 'opacity-20' : ''}">
+		<div class="flex-shrink-0 w-full flex justify-between items-center">
+			<div class="w-full">
+				<input
+					class="w-full text-2xl font-medium bg-transparent outline-none"
+					type="text"
+					bind:value={name}
+					placeholder={$i18n.t('Title')}
+					required
+				/>
+			</div>
+		</div>
+
+		<div class=" flex-1 w-full h-full">
+			<RichTextInput
+				className=" input-prose-sm"
+				bind:value={content}
+				placeholder={$i18n.t('Write something...')}
+			/>
+		</div>
+	</div>
+
+	<div class="absolute bottom-0 left-0 right-0 p-5 max-w-full flex justify-end">
+		<div class="flex gap-0.5 justify-end w-full">
+			{#if voiceInput}
+				<div class="flex-1 w-full">
+					<VoiceRecording
+						bind:recording={voiceInput}
+						className="p-1 w-full max-w-full"
+						on:cancel={() => {
+							voiceInput = false;
+						}}
+						on:confirm={(e) => {
+							const { text, filename } = e.detail;
+
+							// url is hostname + /cache/audio/transcription/ + filename
+							const url = `${window.location.origin}/cache/audio/transcription/${filename}`;
+
+							// Open in new tab
+
+							if (content.trim() !== '') {
+								content = `${content}\n\n${text}\n\nRecording: ${url}\n\n`;
+							} else {
+								content = `${content}${text}\n\nRecording: ${url}\n\n`;
+							}
+
+							voiceInput = false;
+						}}
+					/>
+				</div>
+			{:else}
+				<Tooltip content={$i18n.t('Voice Input')}>
+					<button
+						class="cursor-pointer p-2.5 flex rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 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" />
+					</button>
+				</Tooltip>
+			{/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>
+</div>

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

@@ -85,8 +85,8 @@
 									voiceInput = false;
 								}}
 								on:confirm={(e) => {
-									const response = e.detail;
-									content = `${content}${response} `;
+									const { text, filename } = e.detail;
+									content = `${content}${text} `;
 
 									voiceInput = false;
 								}}

+ 9 - 0
src/routes/(app)/playground/+layout.svelte

@@ -51,6 +51,15 @@
 						href="/playground">{$i18n.t('Chat')}</a
 					>
 
+					<a
+						class="min-w-fit rounded-full p-1.5 px-3 {$page.url.pathname.includes(
+							'/playground/notes'
+						)
+							? 'bg-gray-50 dark:bg-gray-850'
+							: ''} transition"
+						href="/playground/notes">{$i18n.t('Notes')}</a
+					>
+
 					<a
 						class="min-w-fit rounded-full p-1.5 px-3 {$page.url.pathname.includes(
 							'/playground/completions'

+ 5 - 0
src/routes/(app)/playground/notes/+page.svelte

@@ -0,0 +1,5 @@
+<script>
+	import Notes from '$lib/components/playground/Notes.svelte';
+</script>
+
+<Notes />