瀏覽代碼

refac: call overlay styling

Timothy J. Baek 8 月之前
父節點
當前提交
48503573c1

+ 22 - 24
src/lib/components/chat/Chat.svelte

@@ -1634,6 +1634,25 @@
 
 <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
 	bind:show={showEventConfirmation}
 	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)}
 	<div
 		class="h-screen max-h-[100dvh] {$showSidebar
@@ -1774,21 +1782,11 @@
 					{messages}
 					{submitPrompt}
 					{stopResponse}
+					on:call={() => {
+						showControls = true;
+					}}
 				/>
 			</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>
 {/if}

+ 35 - 8
src/lib/components/chat/ChatControls.svelte

@@ -3,6 +3,8 @@
 	import Modal from '../common/Modal.svelte';
 	import Controls from './Controls/Controls.svelte';
 	import { onMount } from 'svelte';
+	import { mobile, showCallOverlay } from '$lib/stores';
+	import CallOverlay from './MessageInput/CallOverlay.svelte';
 
 	export let show = false;
 
@@ -12,6 +14,12 @@
 	export let chatFiles = [];
 	export let params = {};
 
+	export let eventTarget: EventTarget;
+	export let submitPrompt: Function;
+	export let stopResponse: Function;
+	export let files;
+	export let modelId;
+
 	let largeScreen = false;
 	onMount(() => {
 		// listen to resize 1024px
@@ -42,18 +50,37 @@
 				<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"
 				>
-					<Controls
-						on:close={() => {
-							show = false;
-						}}
-						{models}
-						bind:chatFiles
-						bind:params
-					/>
+					{#if $showCallOverlay}
+						<CallOverlay
+							bind:files
+							{submitPrompt}
+							{stopResponse}
+							{modelId}
+							{chatId}
+							{eventTarget}
+						/>
+					{:else}
+						<Controls
+							on:close={() => {
+								show = false;
+							}}
+							{models}
+							bind:chatFiles
+							bind:params
+						/>
+					{/if}
 				</div>
 			</div>
 		</div>
 	{/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}
 	<Modal bind:show>
 		<div class="  px-6 py-4 h-full">

+ 1 - 3
src/lib/components/chat/Controls/Controls.svelte

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

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

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

+ 266 - 277
src/lib/components/chat/MessageInput/CallOverlay.svelte

@@ -19,12 +19,9 @@
 	const i18n = getContext('i18n');
 
 	export let eventTarget: EventTarget;
-
 	export let submitPrompt: Function;
 	export let stopResponse: Function;
-
 	export let files;
-
 	export let chatId;
 	export let modelId;
 
@@ -631,312 +628,304 @@
 </script>
 
 {#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"
-		>
-			<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
-				{#if camera}
-					<button
-						type="button"
-						class="flex justify-center items-center w-full h-20 min-h-20"
-						on:click={() => {
-							if (assistantSpeaking) {
-								stopAllAudio();
+	<div class="max-w-lg w-full h-full max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
+		{#if camera}
+			<button
+				type="button"
+				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;"
+					>
+						{emoji}
+					</div>
+				{:else if loading || assistantSpeaking}
+					<svg
+						class="size-12 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
-									? '4.5'
-									: rmsLevel * 100 > 2
-										? '4.25'
-										: rmsLevel * 100 > 1
-											? '3.75'
-											: '3.5'}rem;width: 100%; text-align:center;"
-							>
-								{emoji}
-							</div>
-						{:else if loading || assistantSpeaking}
-							<svg
-								class="size-12 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;
+				{: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);
 									}
-									.spinner_ZTLf {
-										animation-delay: 0.2s;
+									28.57% {
+										animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
+										transform: translateY(-6px);
 									}
-									@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);
-										}
+									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();
 								}
-							}}
+							</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}
 						<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
-							/>
-
-							<canvas id="camera-canvas" style="display:none;" />
-
-							<div class=" absolute top-4 md:top-8 left-4">
-								<button
-									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>
+							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}
-				</div>
-
-				<div class="flex justify-between items-center pb-2 w-full">
-					<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>
+				</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
+					/>
+
+					<canvas id="camera-canvas" style="display:none;" />
+
+					<div class=" absolute top-4 md:top-8 left-4">
 						<button
 							type="button"
+							class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full"
 							on:click={() => {
-								if (assistantSpeaking) {
-									stopAllAudio();
-								}
+								stopCamera();
 							}}
 						>
-							<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>
+							<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}
+		</div>
 
-					<div>
+		<div class="flex justify-between items-center pb-2 w-full">
+			<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 () => {
-								showCallOverlay.set(false);
+								await navigator.mediaDevices.getUserMedia({ video: true });
+								startCamera();
 							}}
-							type="button"
 						>
 							<svg
 								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 20 20"
-								fill="currentColor"
+								fill="none"
+								viewBox="0 0 24 24"
+								stroke-width="1.5"
+								stroke="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"
+									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>
+				</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>