mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
180 lines
6.9 KiB
TypeScript
180 lines
6.9 KiB
TypeScript
import * as React from 'react';
|
|
|
|
import { Box, Button, Card, CardContent, IconButton, ListItemDecorator, Typography } from '@mui/joy';
|
|
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
|
import ChatIcon from '@mui/icons-material/Chat';
|
|
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
|
import MicIcon from '@mui/icons-material/Mic';
|
|
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
|
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
|
|
|
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
|
import { animationColorRainbow } from '~/common/util/animUtils';
|
|
import { navigateBack } from '~/common/app.routes';
|
|
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
|
import { useChatStore } from '~/common/state/store-chats';
|
|
import { useUICounter } from '~/common/state/store-ui';
|
|
|
|
|
|
function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: string, button?: React.JSX.Element }) {
|
|
return (
|
|
<Card sx={{ width: '100%' }}>
|
|
<CardContent sx={{ flexDirection: 'row' }}>
|
|
<ListItemDecorator>
|
|
{props.icon}
|
|
</ListItemDecorator>
|
|
<Typography level='title-md' color={props.hasIssue ? 'warning' : undefined} sx={{ flexGrow: 1 }}>
|
|
{props.text}
|
|
{props.button}
|
|
</Typography>
|
|
<ListItemDecorator>
|
|
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckRoundedIcon color='success' />}
|
|
</ListItemDecorator>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
|
|
export function CallWizard(props: { strict?: boolean, conversationId: string | null, children: React.ReactNode }) {
|
|
|
|
// state
|
|
const [chatEmptyOverride, setChatEmptyOverride] = React.useState(false);
|
|
const [recognitionOverride, setRecognitionOverride] = React.useState(false);
|
|
|
|
// external state
|
|
const { openPreferencesTab } = useOptimaLayout();
|
|
const recognition = useCapabilityBrowserSpeechRecognition();
|
|
const synthesis = useCapabilityElevenLabs();
|
|
const chatIsEmpty = useChatStore(state => {
|
|
if (!props.conversationId)
|
|
return false;
|
|
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
|
return !(conversation?.messages?.length);
|
|
});
|
|
const { novel, touch } = useUICounter('call-wizard');
|
|
|
|
// derived state
|
|
const outOfTheBlue = !props.conversationId;
|
|
const overriddenEmptyChat = chatEmptyOverride || !chatIsEmpty;
|
|
const overriddenRecognition = recognitionOverride || recognition.mayWork;
|
|
const allGood = overriddenEmptyChat && overriddenRecognition && synthesis.mayWork;
|
|
const fatalGood = overriddenRecognition && synthesis.mayWork;
|
|
|
|
if (!novel && fatalGood)
|
|
return props.children;
|
|
|
|
const handleOverrideChatEmpty = () => setChatEmptyOverride(true);
|
|
|
|
const handleOverrideRecognition = () => setRecognitionOverride(true);
|
|
|
|
const handleConfigureElevenLabs = () => {
|
|
openPreferencesTab(PreferencesTab.Voice);
|
|
};
|
|
|
|
const handleFinishButton = () => {
|
|
if (!allGood)
|
|
return navigateBack();
|
|
touch();
|
|
};
|
|
|
|
|
|
return <>
|
|
|
|
<Box sx={{ flexGrow: 0.5 }} />
|
|
|
|
<Typography level='title-lg' sx={{ fontSize: '3rem', fontWeight: 'sm', textAlign: 'center' }}>
|
|
Welcome to<br />
|
|
<Box component='span' sx={{ animation: `${animationColorRainbow} 15s linear infinite` }}>
|
|
your first call
|
|
</Box>
|
|
</Typography>
|
|
|
|
<Box sx={{ flexGrow: 0.5 }} />
|
|
|
|
<Typography level='body-lg'>
|
|
{/*Before you receive your first call, */}
|
|
Let's get you all set up.
|
|
</Typography>
|
|
|
|
{/* Chat Empty status */}
|
|
{!outOfTheBlue && <StatusCard
|
|
icon={<ChatIcon />}
|
|
hasIssue={!overriddenEmptyChat}
|
|
text={overriddenEmptyChat ? 'Great! Your chat has messages.' : 'The chat is empty. Calls are effective when the caller has context.'}
|
|
button={overriddenEmptyChat ? undefined : (
|
|
<Button variant='outlined' onClick={handleOverrideChatEmpty} sx={{ mx: 1 }}>
|
|
Ignore
|
|
</Button>
|
|
)}
|
|
/>}
|
|
|
|
{/* Add the speech to text feature status */}
|
|
<StatusCard
|
|
icon={<MicIcon />}
|
|
text={
|
|
((overriddenRecognition && !recognition.warnings.length) ? 'Speech recognition should be good to go.' : 'There might be a speech recognition issue.')
|
|
+ (recognition.isApiAvailable ? '' : ' Your browser does not support the speech recognition API.')
|
|
+ (recognition.isDeviceNotSupported ? ' Your device does not provide this feature.' : '')
|
|
+ (recognition.warnings.length ? ' ⚠️ ' + recognition.warnings.join(' · ') : '')
|
|
}
|
|
button={overriddenRecognition ? undefined : (
|
|
<Button variant='outlined' onClick={handleOverrideRecognition} sx={{ mx: 1 }}>
|
|
Ignore
|
|
</Button>
|
|
)}
|
|
hasIssue={!overriddenRecognition}
|
|
/>
|
|
|
|
{/* Text to Speech status */}
|
|
<StatusCard
|
|
icon={<RecordVoiceOverTwoToneIcon />}
|
|
text={
|
|
(synthesis.mayWork ? 'Voice synthesis should be ready.' : 'There might be an issue with ElevenLabs voice synthesis.')
|
|
+ (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
|
|
}
|
|
button={synthesis.mayWork ? undefined : (
|
|
<Button variant='outlined' onClick={handleConfigureElevenLabs} sx={{ mx: 1 }}>
|
|
Configure
|
|
</Button>
|
|
)}
|
|
hasIssue={!synthesis.mayWork}
|
|
/>
|
|
|
|
{/*<Typography>*/}
|
|
{/* 1. To start a call, click the "Accept" button when you receive an incoming call.*/}
|
|
{/* 2. If your mic is enabled, you'll see a "Push to Talk" button. Press and hold it to speak, then release it to stop speaking.*/}
|
|
{/* 3. If your mic is disabled, you can still type your messages in the chat and the assistant will respond.*/}
|
|
{/* 4. During the call, you can control the voice synthesis settings from the menu in the top right corner.*/}
|
|
{/* 5. To end the call, click the "Hang up" button.*/}
|
|
{/*</Typography>*/}
|
|
|
|
<Box sx={{ flexGrow: 2 }} />
|
|
|
|
{/* bottom: text & button */}
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-around', alignItems: 'center', width: '100%', gap: 2, px: 0.5 }}>
|
|
|
|
<Typography level='body-lg'>
|
|
{allGood ? 'Ready, Set, Call' : 'Please resolve the issues above before proceeding with the call'}
|
|
</Typography>
|
|
|
|
<IconButton
|
|
size='lg'
|
|
variant='solid' color={allGood ? 'success' : 'danger'}
|
|
onClick={handleFinishButton}
|
|
sx={{
|
|
borderRadius: '50px',
|
|
mr: 0.5,
|
|
// animation: `${cssRainbowBackgroundKeyframes} 15s linear infinite`,
|
|
// boxShadow: allGood ? 'md' : 'none',
|
|
}}
|
|
>
|
|
{allGood ? <ArrowForwardRoundedIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
|
|
</IconButton>
|
|
</Box>
|
|
|
|
<Box sx={{ flexGrow: 2 }} />
|
|
|
|
</>;
|
|
} |