refac: voice input styling

This commit is contained in:
Timothy J. Baek 2024-10-26 00:21:46 -07:00
parent 1cd036e768
commit 0cf8f58efe
8 changed files with 317 additions and 135 deletions

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; 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 { config, settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
@ -52,7 +52,7 @@
let audioChunks = []; let audioChunks = [];
const MIN_DECIBELS = -45; const MIN_DECIBELS = -45;
const VISUALIZER_BUFFER_LENGTH = 300; let VISUALIZER_BUFFER_LENGTH = 300;
let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0); let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
@ -142,8 +142,8 @@
}); });
if (res) { if (res) {
console.log(res.text); console.log(res);
dispatch('confirm', res.text); dispatch('confirm', res);
} }
}; };
@ -278,12 +278,40 @@
stream = null; 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> </script>
<div <div
bind:clientWidth={containerWidth}
class="{loading class="{loading
? ' bg-gray-100/50 dark:bg-gray-850/50' ? ' 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"> <div class="flex items-center mr-1">
<button <button
@ -318,10 +346,13 @@
class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6" class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
dir="rtl" dir="rtl"
> >
<div class="flex-1 flex items-center gap-0.5 h-6">
{#each visualizerData.slice().reverse() as rms}
<div <div
class="w-[2px] 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="flex items-center h-full">
<div
class="w-[2px] flex-shrink-0
{loading {loading
? ' bg-gray-500 dark:bg-gray-400 ' ? ' bg-gray-500 dark:bg-gray-400 '
@ -330,10 +361,12 @@
inline-block h-full" 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} {/each}
</div> </div>
</div> </div>
<div class="flex">
<div class=" mx-1.5 pr-1 flex justify-center items-center"> <div class=" mx-1.5 pr-1 flex justify-center items-center">
<div <div
class="text-sm class="text-sm
@ -346,7 +379,7 @@
</div> </div>
</div> </div>
<div class="flex items-center mr-1"> <div class="flex items-center">
{#if loading} {#if loading}
<div class=" text-gray-500 rounded-full cursor-not-allowed"> <div class=" text-gray-500 rounded-full cursor-not-allowed">
<svg <svg
@ -459,6 +492,7 @@
</button> </button>
{/if} {/if}
</div> </div>
</div>
</div> </div>
<style> <style>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -51,6 +51,15 @@
href="/playground">{$i18n.t('Chat')}</a 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 <a
class="min-w-fit rounded-full p-1.5 px-3 {$page.url.pathname.includes( class="min-w-fit rounded-full p-1.5 px-3 {$page.url.pathname.includes(
'/playground/completions' '/playground/completions'

View File

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