refac: call overlay styling

This commit is contained in:
Timothy J. Baek 2024-08-23 16:42:36 +02:00
parent 4519ddd0e9
commit 48503573c1
5 changed files with 336 additions and 319 deletions

View File

@ -1634,6 +1634,25 @@
<audio id="audioElement" src="" style="display: none;" /> <audio id="audioElement" src="" style="display: none;" />
<ChatControls
models={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model) {
return [...a, model];
}
return a;
}, [])}
bind:show={showControls}
bind:chatFiles
bind:params
bind:files
{submitPrompt}
{stopResponse}
modelId={selectedModelIds?.at(0) ?? null}
chatId={$chatId}
{eventTarget}
/>
<EventConfirmDialog <EventConfirmDialog
bind:show={showEventConfirmation} bind:show={showEventConfirmation}
title={eventConfirmationTitle} title={eventConfirmationTitle}
@ -1653,17 +1672,6 @@
}} }}
/> />
{#if $showCallOverlay}
<CallOverlay
{submitPrompt}
{stopResponse}
bind:files
modelId={selectedModelIds?.at(0) ?? null}
chatId={$chatId}
{eventTarget}
/>
{/if}
{#if !chatIdProp || (loaded && chatIdProp)} {#if !chatIdProp || (loaded && chatIdProp)}
<div <div
class="h-screen max-h-[100dvh] {$showSidebar class="h-screen max-h-[100dvh] {$showSidebar
@ -1774,21 +1782,11 @@
{messages} {messages}
{submitPrompt} {submitPrompt}
{stopResponse} {stopResponse}
on:call={() => {
showControls = true;
}}
/> />
</div> </div>
</div> </div>
<ChatControls
models={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model) {
return [...a, model];
}
return a;
}, [])}
bind:show={showControls}
bind:chatFiles
bind:params
/>
</div> </div>
{/if} {/if}

View File

@ -3,6 +3,8 @@
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte'; import Controls from './Controls/Controls.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { mobile, showCallOverlay } from '$lib/stores';
import CallOverlay from './MessageInput/CallOverlay.svelte';
export let show = false; export let show = false;
@ -12,6 +14,12 @@
export let chatFiles = []; export let chatFiles = [];
export let params = {}; export let params = {};
export let eventTarget: EventTarget;
export let submitPrompt: Function;
export let stopResponse: Function;
export let files;
export let modelId;
let largeScreen = false; let largeScreen = false;
onMount(() => { onMount(() => {
// listen to resize 1024px // listen to resize 1024px
@ -42,18 +50,37 @@
<div <div
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden" class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
> >
<Controls {#if $showCallOverlay}
on:close={() => { <CallOverlay
show = false; bind:files
}} {submitPrompt}
{models} {stopResponse}
bind:chatFiles {modelId}
bind:params {chatId}
/> {eventTarget}
/>
{:else}
<Controls
on:close={() => {
show = false;
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}
{:else if $showCallOverlay}
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
<div
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<CallOverlay bind:files {submitPrompt} {stopResponse} {modelId} {chatId} {eventTarget} />
</div>
</div>
{:else} {:else}
<Modal bind:show> <Modal bind:show>
<div class=" px-6 py-4 h-full"> <div class=" px-6 py-4 h-full">

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'; import { createEventDispatcher, getContext } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -10,9 +10,7 @@
import Collapsible from '$lib/components/common/Collapsible.svelte'; import Collapsible from '$lib/components/common/Collapsible.svelte';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
export let models = []; export let models = [];
export let chatFiles = []; export let chatFiles = [];
export let params = {}; export let params = {};
</script> </script>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { import {
type Model, type Model,
@ -762,10 +763,14 @@
} }
// check if user has access to getUserMedia // check if user has access to getUserMedia
try { try {
await navigator.mediaDevices.getUserMedia({ audio: true }); let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// If the user grants the permission, proceed to show the call overlay // If the user grants the permission, proceed to show the call overlay
stream.getTracks().forEach((track) => track.stop());
stream = null;
showCallOverlay.set(true); showCallOverlay.set(true);
dispatch('call');
} catch (err) { } catch (err) {
// If the user denies the permission or an error occurs, show an error message // If the user denies the permission or an error occurs, show an error message
toast.error($i18n.t('Permission denied when accessing media devices')); toast.error($i18n.t('Permission denied when accessing media devices'));

View File

@ -19,12 +19,9 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let eventTarget: EventTarget; export let eventTarget: EventTarget;
export let submitPrompt: Function; export let submitPrompt: Function;
export let stopResponse: Function; export let stopResponse: Function;
export let files; export let files;
export let chatId; export let chatId;
export let modelId; export let modelId;
@ -631,299 +628,216 @@
</script> </script>
{#if $showCallOverlay} {#if $showCallOverlay}
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden"> <div class="max-w-lg w-full h-full max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
<div {#if camera}
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center" <button
> type="button"
<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6"> class="flex justify-center items-center w-full h-20 min-h-20"
{#if camera} on:click={() => {
<button if (assistantSpeaking) {
type="button" stopAllAudio();
class="flex justify-center items-center w-full h-20 min-h-20" }
on:click={() => { }}
if (assistantSpeaking) { >
stopAllAudio(); {#if emoji}
} <div
}} class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '4.5'
: rmsLevel * 100 > 2
? '4.25'
: rmsLevel * 100 > 1
? '3.75'
: '3.5'}rem;width: 100%; text-align:center;"
> >
{#if emoji} {emoji}
<div </div>
class=" transition-all rounded-full" {:else if loading || assistantSpeaking}
style="font-size:{rmsLevel * 100 > 4 <svg
? '4.5' class="size-12 text-gray-900 dark:text-gray-400"
: rmsLevel * 100 > 2 viewBox="0 0 24 24"
? '4.25' fill="currentColor"
: rmsLevel * 100 > 1 xmlns="http://www.w3.org/2000/svg"
? '3.75' ><style>
: '3.5'}rem;width: 100%; text-align:center;" .spinner_qM83 {
> animation: spinner_8HQG 1.05s infinite;
{emoji} }
</div> .spinner_oXPr {
{:else if loading || assistantSpeaking} animation-delay: 0.1s;
<svg }
class="size-12 text-gray-900 dark:text-gray-400" .spinner_ZTLf {
viewBox="0 0 24 24" animation-delay: 0.2s;
fill="currentColor" }
xmlns="http://www.w3.org/2000/svg" @keyframes spinner_8HQG {
><style> 0%,
.spinner_qM83 { 57.14% {
animation: spinner_8HQG 1.05s infinite; animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
} transform: translate(0);
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-[4.5rem]'
: rmsLevel * 100 > 2
? ' size-16'
: rmsLevel * 100 > 1
? 'size-14'
: 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
<!-- navbar -->
</button>
{/if}
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
{#if !camera}
<button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
} }
}} 28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-[4.5rem]'
: rmsLevel * 100 > 2
? ' size-16'
: rmsLevel * 100 > 1
? 'size-14'
: 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
<!-- navbar -->
</button>
{/if}
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
{#if !camera}
<button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
>
{emoji}
</div>
{:else if loading || assistantSpeaking}
<svg
class="size-44 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
> >
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
>
{emoji}
</div>
{:else if loading || assistantSpeaking}
<svg
class="size-44 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-52'
: rmsLevel * 100 > 2
? 'size-48'
: rmsLevel * 100 > 1
? 'size-[11.5rem]'
: 'size-44'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} "
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
</button>
{:else} {:else}
<div <div
class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full" class=" {rmsLevel * 100 > 4
> ? ' size-52'
<video : rmsLevel * 100 > 2
id="camera-feed" ? 'size-48'
autoplay : rmsLevel * 100 > 1
class="rounded-2xl h-full min-w-full object-cover object-center" ? 'size-[11.5rem]'
playsinline : 'size-44'} transition-all rounded-full {(model?.info?.meta
/> ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
<canvas id="camera-canvas" style="display:none;" /> : 'bg-black dark:bg-white'} "
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
<div class=" absolute top-4 md:top-8 left-4"> '/static/favicon.png'
<button ? `background-image: url('${model?.info?.meta?.profile_image_url}');`
type="button" : ''}
class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full" />
on:click={() => {
stopCamera();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-6"
>
<path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
/>
</svg>
</button>
</div>
</div>
{/if} {/if}
</div> </button>
{:else}
<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
<video
id="camera-feed"
autoplay
class="rounded-2xl h-full min-w-full object-cover object-center"
playsinline
/>
<div class="flex justify-between items-center pb-2 w-full"> <canvas id="camera-canvas" style="display:none;" />
<div>
{#if camera}
<VideoInputMenu
devices={videoInputDevices}
on:change={async (e) => {
console.log(e.detail);
selectedVideoInputDeviceId = e.detail;
await stopVideoStream();
await startVideoStream();
}}
>
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
clip-rule="evenodd"
/>
</svg>
</button>
</VideoInputMenu>
{:else}
<Tooltip content={$i18n.t('Camera')}>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
type="button"
on:click={async () => {
await navigator.mediaDevices.getUserMedia({ video: true });
startCamera();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
<div> <div class=" absolute top-4 md:top-8 left-4">
<button <button
type="button" type="button"
class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full"
on:click={() => { on:click={() => {
if (assistantSpeaking) { stopCamera();
stopAllAudio();
}
}} }}
> >
<div class=" line-clamp-1 text-sm font-medium"> <svg
{#if loading} xmlns="http://www.w3.org/2000/svg"
{$i18n.t('Thinking...')} viewBox="0 0 16 16"
{:else if assistantSpeaking} fill="currentColor"
{$i18n.t('Tap to interrupt')} class="size-6"
{:else} >
{$i18n.t('Listening...')} <path
{/if} d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
</div> />
</svg>
</button> </button>
</div> </div>
</div>
{/if}
</div>
<div> <div class="flex justify-between items-center pb-2 w-full">
<button <div>
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" {#if camera}
on:click={async () => { <VideoInputMenu
showCallOverlay.set(false); devices={videoInputDevices}
}} on:change={async (e) => {
type="button" console.log(e.detail);
> selectedVideoInputDeviceId = e.detail;
await stopVideoStream();
await startVideoStream();
}}
>
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@ -931,12 +845,87 @@
class="size-5" class="size-5"
> >
<path <path
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
clip-rule="evenodd"
/> />
</svg> </svg>
</button> </button>
</VideoInputMenu>
{:else}
<Tooltip content={$i18n.t('Camera')}>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
type="button"
on:click={async () => {
await navigator.mediaDevices.getUserMedia({ video: true });
startCamera();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
<div>
<button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
<div class=" line-clamp-1 text-sm font-medium">
{#if loading}
{$i18n.t('Thinking...')}
{:else if assistantSpeaking}
{$i18n.t('Tap to interrupt')}
{:else}
{$i18n.t('Listening...')}
{/if}
</div> </div>
</div> </button>
</div>
<div>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
on:click={async () => {
showCallOverlay.set(false);
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>