Skip to content

Commit

Permalink
feat: options stored in memory, responsiveness, input (#31)
Browse files Browse the repository at this point in the history
* fix: LCP of app

Relates to #29
* fix: accessibility

* fix: LCP, performance increase from 50 to 70%

* fix: Fix mobile responsive, store voice

* fix: layout to avoid duplication of MessageAudio

* fix: build failed because of types

* fix: border to 2rem
  • Loading branch information
RaiVaibhav authored May 5, 2024
1 parent 89e5f51 commit 7e26d79
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 92 deletions.
51 changes: 33 additions & 18 deletions app/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { SendIcon } from "./icons/SendIcon";
import { Settings } from "./Settings";
import { useMicrophone } from "../context/Microphone";
import { useNowPlaying } from "react-nowplaying";
import { useSubmit } from "../lib/hooks/useSubmit";

// Better to use library, a lot of complexity is involved
// in building the resizable input
import TextareaAutosize from 'react-textarea-autosize';


export const Controls = ({
input,
Expand All @@ -21,6 +27,7 @@ export const Controls = ({
messages: Message[];
}) => {
const { startMicrophone, stopMicrophone, microphoneOpen } = useMicrophone();
const { formRef, onKeyDown } = useSubmit()

useEffect(() => {
startMicrophone();
Expand All @@ -46,21 +53,23 @@ export const Controls = ({
(e: any) => {
handleSubmit(e);
stopAudio();
e.target.value = '';
handleInputChange(e)
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stopAudio, handleSubmit]
);

return (
<form onSubmit={submitter}>
<form onSubmit={submitter} ref={formRef}>
<div className="relative">
<div className="absolute w-full -top-[4.5rem] py-4 flex justify-between">
<Settings />
<Download messages={messages} />
</div>
<div className="flex bg-[#101014] rounded-full">
<span
className={`rounded-s-full ps-0.5 py-0.5 ${
className={`rounded-tl-[2rem] rounded-bl-[2rem] ps-0.5 py-0.5 ${
microphoneOpen
? "bg-gradient-to-r bg-gradient to-[#13EF93]/50 from-red-500"
: "bg-gradient-to-r bg-gradient to-[#13EF93]/50 from-[#149AFB]/80"
Expand All @@ -70,18 +79,18 @@ export const Controls = ({
<a
href="#"
onClick={(e: any) => microphoneToggle(e)}
className={`w-20 sm:w-24 py-4 px-2 sm:px-8 rounded-s-full font-bold bg-[#101014] text-light-900 text-sm sm:text-base flex items-center justify-center group`}
className={`rounded-tl-[2rem] rounded-bl-[2rem] w-16 md:w-20 sm:w-24 py-2 md:py-4 px-2 h-full sm:px-8 font-bold bg-[#101014] text-light-900 text-sm sm:text-base flex items-center justify-center group`}
>
{microphoneOpen && (
<div className="w-auto items-center justify-center hidden sm:flex absolute shrink-0">
<MicrophoneIcon
micOpen={microphoneOpen}
className="h-6 animate-ping-short"
className="h-5 md:h-6 animate-ping-short"
/>
</div>
)}
<div className="w-auto flex items-center justify-center shrink-0">
<MicrophoneIcon micOpen={microphoneOpen} className="h-6" />
<MicrophoneIcon micOpen={microphoneOpen} className="h-5 md:h-6" />
</div>
{/* <span>
{microphoneOpen ? (
Expand All @@ -94,24 +103,30 @@ export const Controls = ({
</Tooltip>
</span>

<span className="flex-grow bg-[#13EF93]/50 py-0.5">
<input
type="text"
className="py-4 sm:px-4 w-full h-full bg-[#101014] text-light-900 border-0 text-sm sm:text-base outline-none focus:ring-0"
placeholder="Type a message to send..."
value={input}
onChange={handleInputChange}
/>
</span>
<div className="flex-grow bg-[#13EF93]/50 py-0.5 inline">
<div className=" bg-[#101014] h-full">
<TextareaAutosize
onKeyDown={onKeyDown}
rows={1}
spellCheck={false}
autoCorrect="off"
className="py-2 md:py-4 -mb-[0.4rem] min-h-10 overflow-hidden sm:px-4 w-full resize-none bg-[#101014] text-light-900 border-0 text-sm sm:text-base outline-none focus:ring-0"
placeholder="Send a message"
value={input}
onChange={handleInputChange}
/>
</div>

<span className="rounded-e-full bg-gradient-to-l to-[#13EF93]/50 from-[#149AFB]/80 pe-0.5 py-0.5">
</div>

<div className="inline h-auto rounded-tr-[2rem] rounded-br-[2rem] bg-gradient-to-l to-[#13EF93]/50 from-[#149AFB]/80 pe-0.5 py-0.5">
<Tooltip showArrow content="Send a message.">
<button className="w-20 sm:w-24 py-4 px-2 sm:px-8 rounded-e-full font-bold bg-[#101014] text-light-900 text-sm sm:text-base flex items-center justify-center">
<button type="submit" className="w-16 md:w-24 h-full py-2 md:py-4 px-2 rounded-tr-[2rem] rounded-br-[2rem] font-bold bg-[#101014] text-light-900 text-sm sm:text-base flex items-center justify-center">
{/* <span>Send text</span> */}
<SendIcon className="h-6 w-6" />
<SendIcon className="w-5 md:w-6" />
</button>
</Tooltip>
</span>
</div>
</div>
</div>
</form>
Expand Down
5 changes: 1 addition & 4 deletions app/components/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,7 @@ export default function Conversation(): JSX.Element {
>
<div className="grid grid-cols-12 overflow-x-auto gap-y-2">
{initialLoad ? (
<InitialLoad
fn={startConversation}
connecting={!connection}
/>
<InitialLoad fn={startConversation} connecting={!connection} />
) : (
<>
{chatMessages.length > 0 &&
Expand Down
2 changes: 1 addition & 1 deletion app/components/Download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const DownloadButton = ({ content }: { content: string }) => {
return (
<span className="bg-white/10 rounded-full flex">
<a
className={`relative m-px bg-black w-[10.5rem] md:w-10 h-10 rounded-full text-sm p-2.5 group hover:w-[10.5rem] transition-all ease-in-out duration-1000 overflow-hidden whitespace-nowrap`}
className={`relative m-px bg-black md:w-[10.5rem] w-10 h-10 rounded-full text-sm p-2.5 group md:hover:w-[10.5rem] transition-all ease-in-out duration-1000 overflow-hidden whitespace-nowrap`}
download="transcript.txt"
target="_blank"
rel="noreferrer"
Expand Down
8 changes: 3 additions & 5 deletions app/components/InitialLoad.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ExclamationIcon } from "./icons/ExclamationIcon";
import { Headphones } from "./Headphones";
import { isBrowser, isIOS } from "react-device-detect";
import Image from "next/image";
import { isBrowser } from "react-device-detect";
import { Spinner } from "@nextui-org/react";

export const InitialLoad = ({ fn, connecting = true }: { fn: () => void, connecting: boolean }) => {
Expand All @@ -27,9 +25,9 @@ export const InitialLoad = ({ fn, connecting = true }: { fn: () => void, connect
</ul>
</div>
<span className="mt-4 block font-semibold">
<div className="bg-white text-black rounded px-10 py-3 font-semibold sm:w-fit sm:mx-auto opacity-90">
<div className="bg-white text-black rounded px-6 md:px-8 py-3 font-semibold sm:w-fit sm:mx-auto opacity-90">
{connecting ? (
<div className="w-auto h-full items-center flex justify-center opacity-40 cursor-not-allowed">
<div className="w-full h-full items-center flex justify-center opacity-40 cursor-not-allowed">
<Spinner size={"sm"} className="-mt-1 mr-2" />
Connecting...
</div>
Expand Down
33 changes: 19 additions & 14 deletions app/components/LeftBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,31 @@ import { TextContent } from "./TextContext";
export const LeftBubble = ({ message }: { message: Message }) => {
return (
<>
<div className="col-start-1 col-end-13 sm:col-end-11 md:col-end-9 lg:col-end-8 xl:col-end-7 px-3 pt-3">
<div className="flex items-start gap-2">
<div className="h-5 w-12 text-white shrink-0">
<AgentAvatar message={message} />
</div>
<div className="glass flex p-4 rounded-e-xl rounded-es-xl">
<div className="flex flex-col">
<MessageHeader message={message} />
<div className="text-sm font-normal pt-2 text-white/80 markdown">
<TextContent text={message.content} />
<div className="col-start-1 col-end-13 sm:col-end-11 md:col-end-9 lg:col-end-8 xl:col-end-7 md:px-3 pt-3">
<div className="flex items-start gap-2 flex-col md:flex-row">
<div className="flex items-start gap-2 flex-col md:flex-row max-w-full md:max-w-none">
<div className="min-w-12 text-white shrink-0">
<AgentAvatar message={message} />
</div>
<div className="glass flex p-4 rounded-e-xl rounded-es-xl max-w-full md:max-w-none">
<div className="flex flex-col overflow-hidden pre-overflow-y-auto">
<MessageHeader message={message} />
<div className="text-sm font-normal pt-2 text-white/80 markdown">
<TextContent text={message.content} />
</div>
</div>
</div>
</div>
<div className="h-6 w-6 shrink-0 self-center">
<MessageAudio message={message} />
<div className="md:px-1 pb-3 flex gap-2 self-start md:self-center">
<div className="h-6 w-6 shrink-0">
<MessageAudio message={message} />
</div>
<MessageMeta className="md:hidden" message={message} />
</div>
</div>
</div>
<div className="col-start-1 col-end-13 px-3 pb-3">
<MessageMeta className="ml-14" message={message} />
<div className="hidden col-start-1 col-end-13 md:px-3 pb-3 md:flex gap-2">
<MessageMeta className="md:ml-14" message={message} />
</div>
</>
);
Expand Down
6 changes: 3 additions & 3 deletions app/components/MessageMeta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ const MessageMeta = ({
const ttsTotal = foundAudio.networkLatency;

return (
<>
<div className="flex flex-col">
<div
className={`flex gap-x-2.5 text-xs text-[#BBBBBF] ${className} flex-wrap`}
className={`flex gap-x-2.5 pt-1 text-xs text-[#BBBBBF] ${className} flex-wrap`}
>
<span>
<BoltIcon className="w-[1em] h-[1em]" />
Expand Down Expand Up @@ -81,7 +81,7 @@ const MessageMeta = ({
TTS total: {(ttsTotal / 1000).toFixed(1)}s
</span>
</div>
</>
</div>
);
}
};
Expand Down
19 changes: 11 additions & 8 deletions app/components/RightBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ export const RightBubble = ({
}) => {
return (
<>
<div className="col-start-6 col-end-13 p-3">
<div className="flex justify-start flex-row-reverse gap-2">
<div className="h-6 w-6 text-white shrink-0 pt-1 mt-1 rounded-full bg-black border border-zinc-300 overflow-hidden">
<UserAvatar />
</div>
<div className="glass relative text-sm py-2 px-4 shadow rounded-s-xl rounded-ee-xl">
<div className="text-sm font-normal text-white/80 markdown min-w-[10em]">
<TextContent text={message?.content ?? text ?? ""} />
<div className="col-start-1 col-end-13 md:p-3">
<div className="flex flex-row justify-end">
<div className="flex justify-end md:justify-start gap-2 flex-col md:flex-row-reverse">
<div className="self-end md:self-start h-6 w-6 text-white shrink-0 pt-1 mt-1 rounded-full bg-black border border-zinc-300 overflow-hidden">
<UserAvatar />
</div>
<div className="glass relative text-sm py-2 px-4 shadow rounded-s-xl rounded-ee-xl">
<div className="text-sm font-normal text-white/80 markdown word-break">
<TextContent text={message?.content ?? text ?? ""} />
</div>
</div>
</div>
</div>

</div>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion app/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const Settings = () => {
<div className="flex items-center gap-2.5 text-sm">
<span className="bg-gradient-to-r to-[#13EF93]/50 from-[#149AFB]/80 rounded-full flex">
<a
className={`relative m-px bg-black w-[9.25rem] md:w-10 h-10 rounded-full text-sm p-2.5 group hover:w-[9.25rem] transition-all ease-in-out duration-1000 overflow-hidden whitespace-nowrap`}
className={`relative m-px bg-black md:w-[9.25rem] w-10 h-10 rounded-full text-sm p-2.5 group md:hover:w-[9.25rem] transition-all ease-in-out duration-1000 overflow-hidden whitespace-nowrap`}
href="#"
onClick={onOpen}
>
Expand Down
56 changes: 21 additions & 35 deletions app/context/Deepgram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import {
useState,
} from "react";
import { useToast } from "./Toast";
import { useLocalStorage } from "../lib/hooks/useLocalStorage";

type DeepgramContext = {
ttsOptions: SpeakSchema | undefined;
setTtsOptions: Dispatch<SetStateAction<SpeakSchema | undefined>>;
sttOptions: LiveSchema | undefined;
setSttOptions: Dispatch<SetStateAction<LiveSchema | undefined>>;
ttsOptions: SpeakSchema;
setTtsOptions: (value: SpeakSchema) => void;
sttOptions: LiveSchema;
setSttOptions: (value: LiveSchema) => void;
connection: LiveClient | undefined;
connectionReady: boolean;
};
Expand All @@ -33,6 +34,9 @@ interface DeepgramContextInterface {

const DeepgramContext = createContext({} as DeepgramContext);

const DEFAULT_TTS_MODEL = 'aura-asteria-en';
const DEFAULT_STT_MODEL = 'nova-2';
;
/**
* TTS Voice Options
*/
Expand All @@ -44,7 +48,7 @@ const voices: {
accent: string;
};
} = {
"aura-asteria-en": {
[DEFAULT_TTS_MODEL]: {
name: "Asteria",
avatar: "/aura-asteria-en.svg",
language: "English",
Expand Down Expand Up @@ -132,8 +136,17 @@ const getApiKey = async (): Promise<string> => {

const DeepgramContextProvider = ({ children }: DeepgramContextInterface) => {
const { toast } = useToast();
const [ttsOptions, setTtsOptions] = useState<SpeakSchema>();
const [sttOptions, setSttOptions] = useState<LiveSchema>();
const [ttsOptions, setTtsOptions] = useLocalStorage<SpeakSchema>('ttsModel', {
model: DEFAULT_TTS_MODEL
});
const [sttOptions, setSttOptions] = useLocalStorage<LiveSchema>('sttModel', {
model: DEFAULT_STT_MODEL,
interim_results: true,
smart_format: true,
endpointing: 350,
utterance_end_ms: 1000,
filler_words: true,
});
const [connection, setConnection] = useState<LiveClient>();
const [connecting, setConnecting] = useState<boolean>(false);
const [connectionReady, setConnectionReady] = useState<boolean>(false);
Expand All @@ -145,14 +158,7 @@ const DeepgramContextProvider = ({ children }: DeepgramContextInterface) => {
const connection = new LiveClient(
await getApiKey(),
{},
{
model: "nova-2",
interim_results: true,
smart_format: true,
endpointing: 550,
utterance_end_ms: 1500,
filler_words: true,
}
sttOptions
);

setConnection(connection);
Expand All @@ -164,26 +170,6 @@ const DeepgramContextProvider = ({ children }: DeepgramContextInterface) => {
useEffect(() => {
// it must be the first open of the page, let's set up the defaults

/**
* Default TTS Voice when the app loads.
*/
if (ttsOptions === undefined) {
setTtsOptions({
model: "aura-asteria-en",
});
}

if (!sttOptions === undefined) {
setSttOptions({
model: "nova-2",
interim_results: true,
smart_format: true,
endpointing: 350,
utterance_end_ms: 1000,
filler_words: true,
});
}

if (connection === undefined) {
connect();
}
Expand Down
7 changes: 7 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ body {
@apply leading-normal break-words;
}

.pre-overflow-y-auto pre {
@apply overflow-y-auto;
}

.word-break {
word-break: break-word;
}
.markdown > * + * {
@apply my-2;
}
Expand Down
24 changes: 24 additions & 0 deletions app/lib/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'

export const useLocalStorage = <T>(
key: string,
initialValue: T
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState(initialValue)

useEffect(() => {
// Retrieve from localStorage
const item = window.localStorage.getItem(key)
if (item) {
setStoredValue(JSON.parse(item))
}
}, [key])

const setValue = (value: T) => {
// Save state
setStoredValue(value)
// Save to localStorage
window.localStorage.setItem(key, JSON.stringify(value))
}
return [storedValue, setValue]
}
Loading

0 comments on commit 7e26d79

Please sign in to comment.