From 36762af913cd5f4d47bff8b9f626810525cb3261 Mon Sep 17 00:00:00 2001 From: akdeb Date: Fri, 18 Apr 2025 15:32:47 +0100 Subject: [PATCH] updating frontend and removing superfluous files --- README.md | 34 ++-- frontend-nextjs/app/actions.ts | 109 +++--------- frontend-nextjs/app/api/session/route.ts | 163 ++++++------------ .../app/components/AuthTokenModal.tsx | 92 ---------- .../CreateCharacter/BuildDashboard.tsx | 4 +- .../app/components/CreditsRemaining.tsx | 44 ----- .../app/components/HomePageSubtitles.tsx | 1 - .../LandingPage/CharacterShowcase.tsx | 66 ++++--- .../Playground/CharacterSection.tsx | 86 +++++++-- .../Playground/ModifyCharacterSheet.tsx | 4 +- .../components/Playground/PickPersonality.tsx | 52 ------ .../Playground/PlaygroundComponent.tsx | 36 +--- .../components/Playground/SheetWrapper.tsx | 90 ---------- .../Playground/UserPersonalities.tsx | 4 +- .../app/components/Realtime/App.tsx | 7 +- .../Realtime/components/BottomToolbar.tsx | 26 +-- .../app/components/Settings/AppSettings.tsx | 112 +----------- .../app/components/Upsell/AddCreditsModal.tsx | 90 ---------- .../app/components/Upsell/PreorderModal.tsx | 95 ---------- frontend-nextjs/db/personalities.ts | 36 +++- frontend-nextjs/lib/utils.ts | 55 +----- 21 files changed, 263 insertions(+), 943 deletions(-) delete mode 100644 frontend-nextjs/app/components/AuthTokenModal.tsx delete mode 100644 frontend-nextjs/app/components/CreditsRemaining.tsx delete mode 100644 frontend-nextjs/app/components/Playground/PickPersonality.tsx delete mode 100644 frontend-nextjs/app/components/Playground/SheetWrapper.tsx delete mode 100644 frontend-nextjs/app/components/Upsell/AddCreditsModal.tsx delete mode 100644 frontend-nextjs/app/components/Upsell/PreorderModal.tsx diff --git a/README.md b/README.md index 0a67de2..27970ae 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ supabase start # Starts your local Supabase server with the default migrations a npm install npm run dev ``` +> **Login creds:** Email: admin@elatoai.com, Password: admin 3. Add your ESP32-S3 Device MAC Address to the Settings page in the NextJS Frontend. (Remove colons and convert to lowercase, useful for adding friendly user codes when registering multiple devices) ```bash @@ -40,27 +41,30 @@ OPENAI_API_KEY=your_openai_api_key 5. Set up your ESP32 Arduino Client. On PlatformIO, first `Build` the project, then `Upload` the project to your ESP32. - -## 🌟 Features - -- **Realtime Speech-to-Speech**: Instant speech conversion powered by OpenAI's Realtime APIs. -- **Create Custom AI Agents**: Create custom agents with different personalities and voices. -- **Secure WebSockets**: Reliable, encrypted WebSocket communication. -- **Server Turn Detection**: Intelligent conversation flow handling for smooth interactions. -- **Opus Audio Compression**: High-quality audio streaming with minimal bandwidth. -- **Global Edge Performance**: Low latency Deno Edge Functions ensuring seamless global conversations. -- **ESP32 Arduino Framework**: Optimized and easy-to-use hardware integration. - - ## 📌 Project Architecture ElatoAI consists of three main components: -1. **Frontend Client** (`Next.js` hosted on Vercel) -2. **Edge Server Functions** (`Deno` running on Deno/Supabase Edge) -3. **ESP32 IoT Client** (`PlatformIO/Arduino`) +1. **Frontend Client** (`Next.js` hosted on Vercel) - to create and talk to your AI agents and 'send' it to your ESP32 device +2. **Edge Server Functions** (`Deno` running on Deno/Supabase Edge) - to handle the websocket connections from the ESP32 device and the OpenAI API calls +3. **ESP32 IoT Client** (`PlatformIO/Arduino`) - to receive the websocket connections from the Edge Server Functions and send audio to the OpenAI API via the Deno edge server. +## 🌟 Features + +1. **Realtime Speech-to-Speech**: Instant speech conversion powered by OpenAI's Realtime APIs. +2. **Create Custom AI Agents**: Create custom agents with different personalities and voices. +3. **Customizable Voices**: Choose from a variety of voices and personalities. +4. **Secure WebSockets**: Reliable, encrypted WebSocket communication. +5. **Server Turn Detection**: Intelligent conversation flow handling for smooth interactions. +6. **Opus Audio Compression**: High-quality audio streaming with minimal bandwidth. +7. **Global Edge Performance**: Low latency Deno Edge Functions ensuring seamless global conversations. +8. **ESP32 Arduino Framework**: Optimized and easy-to-use hardware integration. +9. **Conversation History**: View your conversation history. +10. **Device Management**: Register and manage your devices. +11. **User Authentication**: Secure user authentication and authorization. +12. **Conversations with WebRTC and Websockets**: Talk to your AI with WebRTC on the NextJS webapp and with websockets on the ESP32. + ## 🛠 Tech Stack | Component | Technology Used | diff --git a/frontend-nextjs/app/actions.ts b/frontend-nextjs/app/actions.ts index ce0cca2..309d62a 100644 --- a/frontend-nextjs/app/actions.ts +++ b/frontend-nextjs/app/actions.ts @@ -4,13 +4,16 @@ import { encodedRedirect } from "@/utils/utils"; import { createClient } from "@/utils/supabase/server"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; -import { encryptSecret, getMacAddressFromDeviceCode, isValidMacAddress } from "@/lib/utils"; +import { getMacAddressFromDeviceCode, isValidMacAddress } from "@/lib/utils"; import { addUserToDevice, dbCheckUserCode } from "@/db/devices"; import { getSimpleUserById, updateUser } from "@/db/users"; export async function deleteUserApiKey(userId: string) { const supabase = createClient(); - const { error } = await supabase.from('api_keys').delete().eq('user_id', userId); + const { error } = await supabase.from("api_keys").delete().eq( + "user_id", + userId, + ); return error; } @@ -41,12 +44,13 @@ export const forgotPasswordAction = async (formData: FormData) => { return encodedRedirect( "error", "/forgot-password", - "Email is required" + "Email is required", ); } const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`, + redirectTo: + `${origin}/auth/callback?redirect_to=/protected/reset-password`, }); if (error) { @@ -54,7 +58,7 @@ export const forgotPasswordAction = async (formData: FormData) => { return encodedRedirect( "error", "/forgot-password", - "Could not reset password" + "Could not reset password", ); } @@ -65,7 +69,7 @@ export const forgotPasswordAction = async (formData: FormData) => { return encodedRedirect( "success", "/forgot-password", - "Check your email for a link to reset your password." + "Check your email for a link to reset your password.", ); }; @@ -79,7 +83,7 @@ export const resetPasswordAction = async (formData: FormData) => { encodedRedirect( "error", "/protected/reset-password", - "Password and confirm password are required" + "Password and confirm password are required", ); } @@ -87,7 +91,7 @@ export const resetPasswordAction = async (formData: FormData) => { encodedRedirect( "error", "/protected/reset-password", - "Passwords do not match" + "Passwords do not match", ); } @@ -99,7 +103,7 @@ export const resetPasswordAction = async (formData: FormData) => { encodedRedirect( "error", "/protected/reset-password", - "Password update failed" + "Password update failed", ); } @@ -118,7 +122,7 @@ export const checkDoctorAction = async (authCode: string) => { export const connectUserToDevice = async ( userId: string, - userDeviceCode: string + userDeviceCode: string, ) => { const supabase = createClient(); @@ -131,7 +135,7 @@ export const connectUserToDevice = async ( const successfullyAdded = await addUserToDevice( supabase, userDeviceCode, - userId + userId, ); return successfullyAdded; }; @@ -171,69 +175,23 @@ export const isPremiumUser = async (userId: string) => { return dbUser?.is_premium; }; -export const setDeviceReset = async (userId: string) => { - const supabase = createClient(); - await supabase - .from("users") - .update({ is_reset: true }) - .eq("user_id", userId); -}; - -export const setDeviceOta = async (userId: string) => { - const supabase = createClient(); - await supabase.from("users").update({ is_ota: true }).eq("user_id", userId); -}; - -export async function storeUserApiKey(userId: string, rawApiKey: string) { - const supabase = createClient(); - const { iv, encryptedData } = encryptSecret(rawApiKey, process.env.ENCRYPTION_KEY!); - - const { error } = await supabase - .from('api_keys') - .upsert({ - user_id: userId, - encrypted_key: encryptedData, - iv: iv, - }); - - if (error) { - console.error('Error inserting or updating user secret:', error); - throw error; - } - - console.log(`Encrypted API key for user ${userId} stored successfully.`); - } - - export async function checkIfUserHasApiKey(userId: string): Promise { - const supabase = createClient(); - const { data, error } = await supabase - .from('api_keys') - .select('*', { count: 'exact' }) - .eq('user_id', userId); - - if (error) { - console.error('Error checking if user has API key:', error); - throw error; - } - - return data.length > 0; - } - - - export async function registerDevice(userId: string, deviceCode: string) { +export async function registerDevice(userId: string, deviceCode: string) { // check if deviceCode is valid mac address if (!isValidMacAddress(deviceCode)) { - return { error: "Invalid device code" }; + return { error: "Invalid device code" }; } const supabase = createClient(); const { data, error } = await supabase - .from('devices') - .insert({ user_id: userId, user_code: deviceCode.toLowerCase(), mac_address: getMacAddressFromDeviceCode(deviceCode).toUpperCase() }).select(); - + .from("devices") + .insert({ + user_id: userId, + user_code: deviceCode, // this is the device code that the user will use to register their device (friendly code preferred) + mac_address: deviceCode, + }).select(); if (error) { - console.log(error) + console.log(error); return { error: "Error registering device" }; } @@ -242,21 +200,4 @@ export async function storeUserApiKey(userId: string, rawApiKey: string) { } return { error: null }; - } - - export const createPersonality = async (userId: string, personality: IPersonality): Promise => { - const supabase = createClient(); - const { data, error } = await supabase - .from('personalities') - .insert({ - ...personality, - creator_id: userId - }).select(); - - if (error) { - console.error('Error creating personality:', error); - throw error; - } - - return data ? data[0] : null; - } +} diff --git a/frontend-nextjs/app/api/session/route.ts b/frontend-nextjs/app/api/session/route.ts index d431b6d..9335c4e 100644 --- a/frontend-nextjs/app/api/session/route.ts +++ b/frontend-nextjs/app/api/session/route.ts @@ -10,16 +10,6 @@ interface IPayload { timestamp: string; } -const getDoctorGuidanceHistory = async ( - supabase: SupabaseClient, - userId: string, -): Promise => { - const { data, error } = await supabase.from('conversations').select('*').eq('user_id', userId).eq('role', 'doctor').order('created_at', { ascending: false }).limit(10); - return data?.map((chat: IConversation) => { - const timestamp = chat.created_at ? new Date(chat.created_at).toLocaleString() : ""; - return `${chat.role} [${timestamp}]: ${chat.content}`; - }).join("") ?? "";} - const getChatHistory = async ( supabase: SupabaseClient, userId: string, @@ -27,25 +17,27 @@ const getChatHistory = async ( ): Promise => { try { let query = supabase - .from('conversations') - .select('*') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(10); + .from("conversations") + .select("*") + .eq("user_id", userId) + .order("created_at", { ascending: false }) + .limit(10); -if (personalityKey) { - query = query.eq('personality_key', personalityKey); -} + if (personalityKey) { + query = query.eq("personality_key", personalityKey); + } const { data, error } = await query; if (error) throw error; - const messages = data.map((chat: IConversation) => `${chat.role}: ${chat.content}`) - .join('\n'); + const messages = data.map((chat: IConversation) => + `${chat.role}: ${chat.content}` + ) + .join("\n"); - return messages; + return messages; } catch (error: any) { - throw new Error(`Failed to get chat history: ${error.message}`); + throw new Error(`Failed to get chat history: ${error.message}`); } }; @@ -57,23 +49,11 @@ Your physical form is in the form of a physical object or a toy. A person interacts with you by pressing a button, sends you instructions and you must respond in a concise conversational style. `; -const DoctorPromptTemplate = (user: IUser) => { - const userMetadata = user.user_info.user_metadata as IDoctorMetadata; - const doctorName = userMetadata.doctor_name || 'Doctor'; - const hospitalName = userMetadata.hospital_name || 'An amazing hospital'; - const specialization = userMetadata.specialization || 'general medicine'; - const favoritePhrases = userMetadata.favorite_phrases || "You're doing an amazing job"; - - return ` -YOU ARE TALKING TO a patient under the care of doctor ${doctorName} from hospital or clinic ${hospitalName}. The child may be undergoing procedures such as ${specialization}. - -YOU ARE: A friendly, compassionate toy designed to offer comfort and care. You specialize in calming children and answering their questions with simple, concise and soothing explanations. - -YOUR FAVORITE PHRASES ARE: ${favoritePhrases} - `; -}; - -const getCommonPromptTemplate = (chatHistory: string, user: IUser, timestamp: string) => ` +const getCommonPromptTemplate = ( + chatHistory: string, + user: IUser, + timestamp: string, +) => ` YOUR VOICE IS: ${user.personality?.voice_prompt} YOUR CHARACTER PROMPT IS: ${user.personality?.character_prompt} @@ -83,7 +63,9 @@ ${chatHistory} USER'S CURRENT TIME IS: ${timestamp} LANGUAGE: -You may talk in any language the user would like, but the default language is ${user?.language?.name ?? 'English'}. +You may talk in any language the user would like, but the default language is ${ + user?.language?.name ?? "English" +}. `; const getStoryPromptTemplate = (user: IUser, chatHistory: string) => { @@ -122,37 +104,15 @@ Let's begin the adventure now! `; }; -const getDoctorGuidanceTemplate = async ({user, supabase, timestamp}: IPayload) => { - const chatHistory = await getDoctorGuidanceHistory(supabase, user.user_id); - const userMetadata = user.user_info.user_metadata as IDoctorMetadata; - const doctorName = userMetadata.doctor_name || 'Doctor'; - const hospitalName = userMetadata.hospital_name || 'An amazing hospital'; - const specialization = userMetadata.specialization || 'general medicine'; - return ` -- You are talking to the doctor. Your physical form is actually a medical wellness toy for children. -- The doctor will either ask you questions or give you instructions on how to help this child. -- You must respond in a concise conversational style. - -Your voice: -- Talk in a serious, sincere and professional tone. -- Do not add exclamations or excited words. - -Current time: -${new Date(timestamp).toLocaleString()}. - -Chat history with the doctor: -${chatHistory} - -Doctor background: -The doctor's name is ${doctorName} and the hospital is ${hospitalName}. The doctor is a specialist in ${specialization}. - ` -}; - const createSystemPrompt = async ( payload: IPayload, ): Promise => { const { user, supabase, timestamp } = payload; - const chatHistory = await getChatHistory(supabase, user.user_id, user.personality?.key ?? null); + const chatHistory = await getChatHistory( + supabase, + user.user_id, + user.personality?.key ?? null, + ); const commonPrompt = getCommonPromptTemplate(chatHistory, user, timestamp); const isStory = user.personality?.is_story; @@ -163,14 +123,11 @@ const createSystemPrompt = async ( let systemPrompt; switch (user.user_info.user_type) { - case 'user': - systemPrompt = UserPromptTemplate(user); - break; - case 'doctor': - systemPrompt = DoctorPromptTemplate(user); - break; - default: - throw new Error('Invalid user type'); + case "user": + systemPrompt = UserPromptTemplate(user); + break; + default: + throw new Error("Invalid user type"); } return systemPrompt + commonPrompt; }; @@ -184,46 +141,27 @@ const createSystemPrompt = async ( */ function decryptSecret(encryptedData: string, iv: string, masterKey: string) { // Decode the base64 master key - const decodedKey = Buffer.from(masterKey, 'base64'); + const decodedKey = Buffer.from(masterKey, "base64"); if (decodedKey.length !== 32) { - throw new Error('ENCRYPTION_KEY must be 32 bytes when decoded from base64.'); + throw new Error( + "ENCRYPTION_KEY must be 32 bytes when decoded from base64.", + ); } const decipher = crypto.createDecipheriv( - 'aes-256-cbc' as any, - Buffer.from(masterKey, 'base64') as any, - Buffer.from(iv, 'base64') as any + "aes-256-cbc" as any, + Buffer.from(masterKey, "base64") as any, + Buffer.from(iv, "base64") as any, ); - let decrypted = decipher.update(encryptedData, 'base64', 'utf8'); - decrypted += decipher.final('utf8'); + let decrypted = decipher.update(encryptedData, "base64", "utf8"); + decrypted += decipher.final("utf8"); return decrypted; } -const getOpenAiApiKey = async ( - supabase: SupabaseClient, - userId: string, -): Promise => { - const { data, error } = await supabase - .from('api_keys') - .select('encrypted_key, iv') - .eq('user_id', userId) - .single(); - - if (error) throw error; - - const { encrypted_key, iv } = data; - const masterKey = process.env.ENCRYPTION_KEY!; - - const decryptedKey = decryptSecret(encrypted_key, iv, masterKey); - - return decryptedKey; -}; - - export async function GET(request: NextRequest) { const supabase = createClient(); - + const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -234,9 +172,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } - const isDoctor = dbUser.user_info.user_type === "doctor"; - const openAiApiKey = await getOpenAiApiKey(supabase, user.id); - const systemPrompt = isDoctor ? await getDoctorGuidanceTemplate({ user: dbUser, supabase, timestamp: new Date().toISOString() }) : await createSystemPrompt({ user: dbUser, supabase, timestamp: new Date().toISOString() }); + const openAiApiKey = process.env.OPENAI_API_KEY; + const systemPrompt = await createSystemPrompt({ + user: dbUser, + supabase, + timestamp: new Date().toISOString(), + }); try { const response = await fetch( @@ -248,11 +189,11 @@ export async function GET(request: NextRequest) { "Content-Type": "application/json", }, body: JSON.stringify({ - model: 'gpt-4o-mini-realtime-preview-2024-12-17', + model: "gpt-4o-mini-realtime-preview-2024-12-17", instructions: systemPrompt, - voice: isDoctor ? 'ballad' : dbUser.personality?.oai_voice ?? 'ballad', + voice: dbUser.personality?.oai_voice ?? "ballad", }), - } + }, ); const data = await response.json(); return NextResponse.json(data); @@ -260,7 +201,7 @@ export async function GET(request: NextRequest) { console.error("Error in /session:", error); return NextResponse.json( { error: "Internal Server Error" }, - { status: 500 } + { status: 500 }, ); } } diff --git a/frontend-nextjs/app/components/AuthTokenModal.tsx b/frontend-nextjs/app/components/AuthTokenModal.tsx deleted file mode 100644 index f4288a4..0000000 --- a/frontend-nextjs/app/components/AuthTokenModal.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Check, Key, Trash } from "lucide-react"; -import { checkIfUserHasApiKey, storeUserApiKey, deleteUserApiKey } from "../actions"; -import { useState } from "react"; -import { useToast } from "@/components/ui/use-toast"; - -interface AuthTokenModalProps { - user: IUser; - userHasApiKey: () => void; - hasApiKey: boolean; - setHasApiKey: (hasApiKey: boolean) => void; -} - -const AuthTokenModal: React.FC = ({ user, userHasApiKey, hasApiKey, setHasApiKey }) => { - const { toast } = useToast(); - const [apiKey, setApiKey] = useState(""); - - - return ( - - - - - - - Set your OpenAI API Key - - This key is kept encrypted and never stored on our servers as plain text. - - -
- { - setApiKey(e.target.value); - }} - /> - -
-
-
- ); -}; - -export default AuthTokenModal; diff --git a/frontend-nextjs/app/components/CreateCharacter/BuildDashboard.tsx b/frontend-nextjs/app/components/CreateCharacter/BuildDashboard.tsx index 681b675..976edd8 100644 --- a/frontend-nextjs/app/components/CreateCharacter/BuildDashboard.tsx +++ b/frontend-nextjs/app/components/CreateCharacter/BuildDashboard.tsx @@ -10,7 +10,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { ArrowLeft, ArrowRight, Check, Mic, Volume2 } from "lucide-react"; import Twemoji from "react-twemoji"; -import { createPersonality } from "../../actions"; +import { createPersonality } from "@/db/personalities"; import { v4 as uuidv4 } from 'uuid'; import { toast } from "@/components/ui/use-toast"; import { useRouter } from "next/navigation"; @@ -163,7 +163,7 @@ const SettingsDashboard: React.FC = ({ } try { - const personality = await createPersonality(selectedUser.user_id, { + const personality = await createPersonality(supabase, selectedUser.user_id, { title: formData.title, subtitle: "", character_prompt: formData.prompt, diff --git a/frontend-nextjs/app/components/CreditsRemaining.tsx b/frontend-nextjs/app/components/CreditsRemaining.tsx deleted file mode 100644 index 3a19d42..0000000 --- a/frontend-nextjs/app/components/CreditsRemaining.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { getCreditsRemaining } from "@/lib/utils"; -import { Sparkles } from "lucide-react"; -import AddCreditsModal from "./Upsell/AddCreditsModal"; - -const CreditsRemaining: React.FC<{ - user: IUser; - languageCode: string; -}> = ({ user, languageCode }) => { - const creditsRemaining = getCreditsRemaining(user); - const hasNoCredits = creditsRemaining <= 0; - - if (user.is_premium) { - return null; - } - - return ( -
-

- {creditsRemaining} {"credits remaining"} -

- {creditsRemaining <= 50 && ( - - - - )} -
- ); -}; - -export default CreditsRemaining; diff --git a/frontend-nextjs/app/components/HomePageSubtitles.tsx b/frontend-nextjs/app/components/HomePageSubtitles.tsx index b460065..c160a05 100644 --- a/frontend-nextjs/app/components/HomePageSubtitles.tsx +++ b/frontend-nextjs/app/components/HomePageSubtitles.tsx @@ -1,4 +1,3 @@ -import CreditsRemaining from "./CreditsRemaining"; interface HomePageSubtitlesProps { user: IUser; page: "home" | "settings" | "create"; diff --git a/frontend-nextjs/app/components/LandingPage/CharacterShowcase.tsx b/frontend-nextjs/app/components/LandingPage/CharacterShowcase.tsx index 5f353ca..7c180f0 100644 --- a/frontend-nextjs/app/components/LandingPage/CharacterShowcase.tsx +++ b/frontend-nextjs/app/components/LandingPage/CharacterShowcase.tsx @@ -1,59 +1,71 @@ 'use client'; import { useState } from 'react'; -import Image from 'next/image'; -import { motion } from 'framer-motion'; -import SheetWrapper from '../Playground/SheetWrapper'; +import Image from "next/image"; +import { cn, getPersonalityImageSrc } from "@/lib/utils"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { EmojiComponent } from "../Playground/EmojiImage"; -// Example character data - replace with your actual data -const characters = [ - { id: 1, name: 'Assistant', description: 'Helpful AI assistant', image: '/characters/assistant.png' }, - { id: 2, name: 'Storyteller', description: 'Creative storytelling companion', image: '/characters/storyteller.png' }, - { id: 3, name: 'Tutor', description: 'Patient educational guide', image: '/characters/tutor.png' }, - { id: 4, name: 'Comedian', description: 'Witty joke-telling friend', image: '/characters/comedian.png' }, - { id: 5, name: 'Chef', description: 'Culinary expert and recipe advisor', image: '/characters/chef.png' }, - { id: 6, name: 'Fitness Coach', description: 'Motivational exercise companion', image: '/characters/fitness.png' }, - // Add more characters as needed -]; interface CharacterShowcaseProps { allPersonalities: IPersonality[]; } export const CharacterShowcase = ({ allPersonalities }: CharacterShowcaseProps) => { - const [hoveredCharacter, setHoveredCharacter] = useState(null); - return (
- {/* Character List - On left for desktop, bottom for mobile */}
- {/* Top scroll indicator/halo */}
{allPersonalities.map((personality, index) => ( - {}} - disableButtons={true} - /> + + + {personality.creator_id === null ? ( + {personality.key} + ) : ( +
+ +
+ )} +
+ + + {personality.title} + + + {personality.subtitle} + + +
))}
- {/* Bottom scroll indicator/halo */}
- {/* Text Content - On right for desktop, top for mobile */}

Meet Our AI Characters diff --git a/frontend-nextjs/app/components/Playground/CharacterSection.tsx b/frontend-nextjs/app/components/Playground/CharacterSection.tsx index 6b9315a..d1b3546 100644 --- a/frontend-nextjs/app/components/Playground/CharacterSection.tsx +++ b/frontend-nextjs/app/components/Playground/CharacterSection.tsx @@ -1,5 +1,16 @@ -import { EnglishCopy } from "@/utils/i18n"; -import SheetWrapper from "./SheetWrapper"; +import ModifyCharacterSheet from "./ModifyCharacterSheet"; +import Image from "next/image"; +import { cn, getPersonalityImageSrc } from "@/lib/utils"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Check, CheckCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { EmojiComponent } from "./EmojiImage"; interface CharacterSectionProps { allPersonalities: IPersonality[]; @@ -20,6 +31,7 @@ const CharacterSection = ({ disableButtons, selectedFilters, }: CharacterSectionProps) => { + const filteredPersonalities = allPersonalities.filter((personality) => { return selectedFilters.every((filter) => { return personality[filter] === true; @@ -37,16 +49,66 @@ const CharacterSection = ({

- {filteredPersonalities.map((personality, index) => ( - - ))} + {filteredPersonalities.map((personality, index) => { + const isCurrentPersonality = personalityIdState === personality.personality_id; + return ( + + onPersonalityPicked(personality)} + > + + {personality.creator_id === null ? ( + {personality.key} + ) : ( +
+ +
+ )} + +
+ + + {personality.title} + + + {personality.subtitle} + + +
+
+ ); + })}

diff --git a/frontend-nextjs/app/components/Playground/ModifyCharacterSheet.tsx b/frontend-nextjs/app/components/Playground/ModifyCharacterSheet.tsx index a06f85d..c22bbc7 100644 --- a/frontend-nextjs/app/components/Playground/ModifyCharacterSheet.tsx +++ b/frontend-nextjs/app/components/Playground/ModifyCharacterSheet.tsx @@ -41,8 +41,6 @@ const ModifyCharacterSheet: React.FC = ({ return (
); diff --git a/frontend-nextjs/app/components/Playground/PickPersonality.tsx b/frontend-nextjs/app/components/Playground/PickPersonality.tsx deleted file mode 100644 index 9cd3ccf..0000000 --- a/frontend-nextjs/app/components/Playground/PickPersonality.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import DoctorPersonalities from "./DoctorPersonalities"; -import UserPersonalities from "./UserPersonalities"; - -interface PickPersonalityProps { - onPersonalityPicked: (personalityIdPicked: string) => void; - allPersonalities: IPersonality[]; - personalityIdState: string; - currentUser?: IUser; - languageState: string; - disableButtons: boolean; - selectedFilters: PersonalityFilter[]; - myPersonalities: IPersonality[]; -} - -const PickPersonality: React.FC = ({ - onPersonalityPicked, - allPersonalities, - personalityIdState, - currentUser, - languageState, - disableButtons, - selectedFilters, - myPersonalities, -}) => { - if (currentUser?.user_info?.user_type === "doctor") { - return ( - - ); - } - - return ( - - ); -}; - -export default PickPersonality; diff --git a/frontend-nextjs/app/components/Playground/PlaygroundComponent.tsx b/frontend-nextjs/app/components/Playground/PlaygroundComponent.tsx index d34c1d1..251e080 100644 --- a/frontend-nextjs/app/components/Playground/PlaygroundComponent.tsx +++ b/frontend-nextjs/app/components/Playground/PlaygroundComponent.tsx @@ -1,9 +1,7 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useState } from "react"; import { createClient } from "@/utils/supabase/client"; -import { getCreditsRemaining } from "@/lib/utils"; -import PickPersonality from "./PickPersonality"; import { updateUser } from "@/db/users"; import _ from "lodash"; import HomePageSubtitles from "../HomePageSubtitles"; @@ -11,8 +9,8 @@ import PersonalityFilters from "./PersonalityFilters"; import { TranscriptProvider } from "../Realtime/contexts/TranscriptContext"; import { EventProvider } from "../Realtime/contexts/EventContext"; import App from "../Realtime/App"; -import { checkIfUserHasApiKey } from "@/app/actions"; import { defaultPersonalityId } from "@/lib/data"; +import UserPersonalities from "./UserPersonalities"; interface PlaygroundProps { currentUser: IUser; @@ -25,20 +23,10 @@ const Playground: React.FC = ({ allPersonalities, myPersonalities, }) => { - const [hasApiKey, setHasApiKey] = useState(false); const isDoctor = currentUser.user_info.user_type === "doctor"; - useEffect(() => { - const checkApiKey = async () => { - const hasApiKey = await checkIfUserHasApiKey(currentUser.user_id); - setHasApiKey(hasApiKey); - }; - checkApiKey(); - }, [currentUser.user_id]); - const supabase = createClient(); - // Remove userState entirely and just use personalityState const [personalityIdState, setPersonalityIdState] = useState( currentUser.personality!.personality_id ?? defaultPersonalityId // Initial value from props ); @@ -47,12 +35,7 @@ const Playground: React.FC = ({ [] ); - const creditsRemaining = getCreditsRemaining(currentUser); - const outOfCredits = creditsRemaining <= 0 && !currentUser.is_premium; - // const ref: any = useRef | null>(null); - const onPersonalityPicked = async (personalityIdPicked: string) => { - // Instantaneously update the state variable setPersonalityIdState(personalityIdPicked); await updateUser( supabase, @@ -74,7 +57,7 @@ const Playground: React.FC = ({
- +
@@ -93,11 +76,10 @@ const Playground: React.FC = ({ languageState={'en-US'} currentUser={currentUser} /> - = ({ />
- {/* */} ); }; diff --git a/frontend-nextjs/app/components/Playground/SheetWrapper.tsx b/frontend-nextjs/app/components/Playground/SheetWrapper.tsx deleted file mode 100644 index 30023f8..0000000 --- a/frontend-nextjs/app/components/Playground/SheetWrapper.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import ModifyCharacterSheet from "./ModifyCharacterSheet"; -import Image from "next/image"; -import { cn, getPersonalityImageSrc } from "@/lib/utils"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Check, CheckCircle, CircleCheck } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { EmojiComponent } from "./EmojiImage"; - -interface SheetWrapperProps { - personality: IPersonality; - personalityIdState: string; - onPersonalityPicked: (personalityId: string) => void; - languageState: string; - disableButtons: boolean; -} - -const SheetWrapper: React.FC = ({ - personality, - personalityIdState, - onPersonalityPicked, - languageState, - disableButtons, -}) => { - const isCurrentPersonality = personalityIdState === personality.personality_id; - return ( - - onPersonalityPicked(personality)} - > - - {personality.creator_id === null ? ( - {personality.key} - ) : ( -
- -
- )} - -
- - - {personality.title} - - - {personality.subtitle} - - -
-
- ); -}; - -export default SheetWrapper; diff --git a/frontend-nextjs/app/components/Playground/UserPersonalities.tsx b/frontend-nextjs/app/components/Playground/UserPersonalities.tsx index 8efbf78..447b250 100644 --- a/frontend-nextjs/app/components/Playground/UserPersonalities.tsx +++ b/frontend-nextjs/app/components/Playground/UserPersonalities.tsx @@ -32,7 +32,7 @@ const UserPersonalities: React.FC = ({ disableButtons={disableButtons} /> )} - = ({ selectedFilters={selectedFilters} /> - + ); }; diff --git a/frontend-nextjs/app/components/Realtime/App.tsx b/frontend-nextjs/app/components/Realtime/App.tsx index fdec928..7d91a9b 100644 --- a/frontend-nextjs/app/components/Realtime/App.tsx +++ b/frontend-nextjs/app/components/Realtime/App.tsx @@ -25,17 +25,14 @@ import { getPersonalityById } from "@/db/personalities"; import { createClient } from "@/utils/supabase/client"; interface AppProps { - hasApiKey: boolean; personalityIdState: string; isDoctor: boolean; userId: string; } -function App({ hasApiKey, personalityIdState, isDoctor, userId }: AppProps) { +function App({ personalityIdState, isDoctor, userId }: AppProps) { const supabase = createClient(); - const searchParams = useSearchParams(); - const { transcriptItems, addTranscriptMessage, addTranscriptBreadcrumb } = useTranscript(); const { logClientEvent, logServerEvent } = useEvent(); @@ -376,8 +373,6 @@ function App({ hasApiKey, personalityIdState, isDoctor, userId }: AppProps) { diff --git a/frontend-nextjs/app/components/Realtime/components/BottomToolbar.tsx b/frontend-nextjs/app/components/Realtime/components/BottomToolbar.tsx index f362b1f..c69232f 100644 --- a/frontend-nextjs/app/components/Realtime/components/BottomToolbar.tsx +++ b/frontend-nextjs/app/components/Realtime/components/BottomToolbar.tsx @@ -3,24 +3,16 @@ import { SessionStatus } from "@/app/components/Realtime/types"; import { Paperclip, PhoneCall, Play, Stethoscope } from "lucide-react"; import { Loader2, X } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { getPersonalityById } from "@/db/personalities"; -import { createClient } from "@/utils/supabase/client"; -import Image from "next/image"; -import { EmojiComponent } from "../../Playground/EmojiImage"; interface BottomToolbarProps { sessionStatus: SessionStatus; onToggleConnection: () => void; - hasApiKey: boolean; - personality: IPersonality; isDoctor: boolean; } function BottomToolbar({ sessionStatus, onToggleConnection, - hasApiKey, - personality, isDoctor, }: BottomToolbarProps) { const isConnected = sessionStatus === "CONNECTED"; @@ -44,7 +36,7 @@ function BottomToolbar({ return "Doctor chat"; } - const isDisabled = isConnecting || !hasApiKey; + const isDisabled = isConnecting; function getConnectionButtonClasses() { const baseClasses = "text-white text-base p-2 w-fit rounded-full shadow-lg flex flex-row items-center justify-center gap-2 px-4"; @@ -69,27 +61,13 @@ function BottomToolbar({ diff --git a/frontend-nextjs/app/components/Settings/AppSettings.tsx b/frontend-nextjs/app/components/Settings/AppSettings.tsx index 560723e..e96506e 100644 --- a/frontend-nextjs/app/components/Settings/AppSettings.tsx +++ b/frontend-nextjs/app/components/Settings/AppSettings.tsx @@ -1,4 +1,4 @@ -import { checkIfUserHasApiKey, registerDevice, signOutAction } from "@/app/actions"; +import { registerDevice, signOutAction } from "@/app/actions"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -35,15 +35,7 @@ const AppSettings: React.FC = ({ const userFormRef = React.useRef<{ submitForm: () => void } | null>(null); const [deviceCode, setDeviceCode] = React.useState(""); const [error, setError] = React.useState(""); - const [hasApiKey, setHasApiKey] = React.useState(false); - - const userHasApiKey = useCallback(async () => { - const hasApiKey = await checkIfUserHasApiKey(selectedUser.user_id); - setHasApiKey(hasApiKey); - }, [selectedUser.user_id]); - // ... existing code ... - const handleSave = () => { if (selectedUser.user_info.user_type === "doctor") { doctorFormRef.current?.submitForm(); @@ -58,16 +50,9 @@ const AppSettings: React.FC = ({ ); }, [selectedUser.user_id, supabase]); - React.useEffect(() => { - checkIfUserHasDevice(); - userHasApiKey(); - }, [checkIfUserHasDevice, userHasApiKey]); - const [volume, setVolume] = React.useState([ selectedUser.device?.volume ?? 50, ]); - const [isReset, setIsReset] = React.useState(selectedUser.device?.is_reset ?? false); - const [isOta, setIsOta] = React.useState(selectedUser.device?.is_ota ?? false); const debouncedUpdateVolume = _.debounce(async () => { if (selectedUser.device?.device_id) { @@ -137,24 +122,6 @@ const AppSettings: React.FC = ({ Device settings
- {/*
-
- -
-
-
- -
-

- Your keys are E2E encrypted and never stored on our servers as plain text. -

-
*/}
- {/*
- -
- - {isOta ? ( -

- - Your device will be updated on next - start -

- ) : ( -

- This will update your device software to - the latest version. -

- )} -
- -
- - {isReset ? ( -

- - Your device will be factory reset on next start -

- ) : ( -

- Caution: This will reset your wifi and authentication details on your device. -

- )} -
-
*/} -