diff --git a/clients/web/app/(auth)/sign-in/SignInForm.tsx b/clients/web/app/(auth)/sign-in/SignInForm.tsx index 73ba47e..045d856 100644 --- a/clients/web/app/(auth)/sign-in/SignInForm.tsx +++ b/clients/web/app/(auth)/sign-in/SignInForm.tsx @@ -28,7 +28,7 @@ export default function SignInForm() { }; return ( - +

Sign in

diff --git a/clients/web/app/(auth)/sign-in/actions.ts b/clients/web/app/(auth)/sign-in/actions.ts index 5e60426..214399b 100644 --- a/clients/web/app/(auth)/sign-in/actions.ts +++ b/clients/web/app/(auth)/sign-in/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { LOGIN_COOKIE_KEY } from "@/lib/constants"; +import { EMAIL_COOKIE_KEY, LOGIN_COOKIE_KEY } from "@/lib/constants"; import { getApiClient } from "@/lib/sesameApiClient"; import { cookies } from "next/headers"; @@ -15,8 +15,12 @@ export async function login(email: string, password: string) { }); if (ok && data.token) { const cookiez = await cookies(); + const expires = Date.now() + 259200000; cookiez.set(LOGIN_COOKIE_KEY, data.token, { - expires: Date.now() + 259200000, + expires, + }); + cookiez.set(EMAIL_COOKIE_KEY, email, { + expires, }); return true; } diff --git a/clients/web/app/(auth)/sign-in/page.tsx b/clients/web/app/(auth)/sign-in/page.tsx index b6a5002..e658ffa 100644 --- a/clients/web/app/(auth)/sign-in/page.tsx +++ b/clients/web/app/(auth)/sign-in/page.tsx @@ -1,5 +1,25 @@ import SignInForm from "@/app/(auth)/sign-in/SignInForm"; +import Logo from "@/components/svg/Logo"; + +const drops = [0, 1, 2, 3]; export default function SignInPage() { - return ; + return ( +
+
+
+ {drops.map((k) => ( + + ))} + + {drops.map((k) => ( + + ))} +
+
+
+ +
+
+ ); } diff --git a/clients/web/app/(authenticated)/[workspaceId]/ConversationList.tsx b/clients/web/app/(authenticated)/[workspaceId]/ConversationList.tsx index 3c2bb33..bdfa706 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/ConversationList.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/ConversationList.tsx @@ -102,7 +102,7 @@ const ConversationList = ({ acc[group].push(conversation); return acc; }, - {} + {}, ); const hasConversations = Object.keys(groupedConversations).length > 0; diff --git a/clients/web/app/(authenticated)/[workspaceId]/ConversationListItem.tsx b/clients/web/app/(authenticated)/[workspaceId]/ConversationListItem.tsx index 1bf9b46..b449303 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/ConversationListItem.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/ConversationListItem.tsx @@ -107,7 +107,7 @@ export default function ConversationListItem({ { "bg-secondary-foreground/[.05] font-medium": isActive, "hover:bg-input": !isEditing, - } + }, )} > {isEditing ? ( @@ -150,7 +150,7 @@ export default function ConversationListItem({ <> {title} @@ -162,7 +162,7 @@ export default function ConversationListItem({ "shrink-0 text-foreground/50 group-hover:visible group-focus-within:visible aria-expanded:visible hover:bg-transparent hover:text-foreground", { invisible: !isActive, - } + }, )} size="icon" variant="ghost" diff --git a/clients/web/app/(authenticated)/[workspaceId]/DeleteConversationModal.tsx b/clients/web/app/(authenticated)/[workspaceId]/DeleteConversationModal.tsx index 18186d6..c53aa90 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/DeleteConversationModal.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/DeleteConversationModal.tsx @@ -63,7 +63,7 @@ export default function DeleteConversationModal({ }; const conversation = conversations.find( - (c) => c.conversation_id === conversationId + (c) => c.conversation_id === conversationId, ); return ( diff --git a/clients/web/app/(authenticated)/[workspaceId]/Navbar.tsx b/clients/web/app/(authenticated)/[workspaceId]/Navbar.tsx index 42285e8..543c18b 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/Navbar.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/Navbar.tsx @@ -1,6 +1,13 @@ "use client"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import emitter from "@/lib/eventEmitter"; import { LLMModel } from "@/lib/llm"; import { Menu, Settings2 } from "lucide-react"; @@ -22,7 +29,7 @@ const Navbar: React.FC = ({ currentModelValue, models }) => { const [selectedModel, setSelectedModel] = useState(currentModelValue); - const currentModel = models.find(m => m.model === selectedModel); + const currentModel = models.find((m) => m.model === selectedModel); return (
@@ -34,27 +41,34 @@ const Navbar: React.FC = ({ currentModelValue, models }) => { - { + setSelectedModel(v); + emitter.emit("changeLlmModel", v); + }} + > + {currentModel?.label} {models.map((m) => ( - {m.label} + + {m.label} + ))} {/* Settings Icon */} - + + Settings +
); }; diff --git a/clients/web/app/(authenticated)/[workspaceId]/SearchResults.tsx b/clients/web/app/(authenticated)/[workspaceId]/SearchResults.tsx index 6416b7d..89b75c0 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/SearchResults.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/SearchResults.tsx @@ -20,7 +20,7 @@ const SearchResults = memo( workspaceId={workspaceId} /> ); - } + }, ); SearchResults.displayName = "SearchResults"; diff --git a/clients/web/app/(authenticated)/[workspaceId]/Sidebar.tsx b/clients/web/app/(authenticated)/[workspaceId]/Sidebar.tsx index e0c0b30..a2fd1e2 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/Sidebar.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/Sidebar.tsx @@ -1,10 +1,10 @@ "use client"; +import UserMenu from "@/app/(authenticated)/[workspaceId]/UserMenu"; import PageTransitionLink from "@/components/PageTransitionLink"; import QueryClientProvider, { queryClient, } from "@/components/QueryClientProvider"; -import SignOutButton from "@/components/SignOutButton"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { @@ -16,7 +16,7 @@ import { import emitter from "@/lib/eventEmitter"; import { ConversationModel, WorkspaceModel } from "@/lib/sesameApi"; import { cn } from "@/lib/utils"; -import { Edit, LoaderCircleIcon } from "lucide-react"; +import { EditIcon, LoaderCircleIcon } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { Suspense, @@ -27,10 +27,11 @@ import { } from "react"; import ConversationList from "./ConversationList"; import SearchResults from "./SearchResults"; -import SidebarTitle from "./SidebarTitle"; +import WorkspaceMenu from "./WorkspaceMenu"; interface SidebarProps { conversations: ConversationModel[]; + email?: string; signOut?: boolean; workspace?: WorkspaceModel | null; workspaces?: WorkspaceModel[]; @@ -38,6 +39,7 @@ interface SidebarProps { export default function Sidebar({ conversations, + email, signOut = false, workspace, workspaces = [], @@ -89,22 +91,18 @@ export default function Sidebar({ setIsOpen(false)} > - - New chat + + New conversation )} - - - {signOut && ( - Sign out - )} +
+ + + {signOut && ( + <> + + + + )} +
); @@ -146,32 +155,18 @@ export default function Sidebar({
- - { - setIsOpen(false); - }} - workspace={workspace} - workspaces={workspaces} - /> - - -
{content}
+ Navigation + +
+ {content} +
{/* Desktop Sidebar */}
-
- - {content} -
+
{content}
); diff --git a/clients/web/app/(authenticated)/[workspaceId]/UserMenu.tsx b/clients/web/app/(authenticated)/[workspaceId]/UserMenu.tsx new file mode 100644 index 0000000..4013166 --- /dev/null +++ b/clients/web/app/(authenticated)/[workspaceId]/UserMenu.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { EMAIL_COOKIE_KEY, LOGIN_COOKIE_KEY } from "@/lib/constants"; +import { LogOutIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import Gravatar from "react-gravatar"; + +interface UserMenuProps { + className?: string; + email?: string; +} + +const UserMenu = ({ className, email = "My account" }: UserMenuProps) => { + const { push } = useRouter(); + + const handleSignout = () => { + document.cookie = `${LOGIN_COOKIE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + document.cookie = `${EMAIL_COOKIE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + push("/sign-in"); + }; + + return ( +
+ + + + + {email} + + + + + Sign out + + + + +
+ ); +}; + +export default UserMenu; diff --git a/clients/web/app/(authenticated)/[workspaceId]/SidebarTitle.tsx b/clients/web/app/(authenticated)/[workspaceId]/WorkspaceMenu.tsx similarity index 68% rename from clients/web/app/(authenticated)/[workspaceId]/SidebarTitle.tsx rename to clients/web/app/(authenticated)/[workspaceId]/WorkspaceMenu.tsx index 633afd8..47c9326 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/SidebarTitle.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/WorkspaceMenu.tsx @@ -1,5 +1,6 @@ "use client"; +import PageTransitionLink from "@/components/PageTransitionLink"; import { DropdownMenu, DropdownMenuContent, @@ -11,41 +12,31 @@ import { } from "@/components/ui/dropdown-menu"; import emitter from "@/lib/eventEmitter"; import { WorkspaceModel } from "@/lib/sesameApi"; -import { cn } from "@/lib/utils"; -import { CheckIcon, ChevronsUpDownIcon, LayoutGridIcon } from "lucide-react"; -import Link from "next/link"; +import { BoxIcon, CheckIcon, LayoutGridIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -interface SidebarTitleProps extends React.HTMLAttributes { +interface WorkspaceMenuProps { className: string; onSwitchWorkspace?: () => void; workspace?: WorkspaceModel | null; workspaces?: WorkspaceModel[]; } -const SidebarTitle = ({ +const WorkspaceMenu = ({ className, onSwitchWorkspace, workspace, workspaces = [], - ...h1Props -}: SidebarTitleProps) => { +}: WorkspaceMenuProps) => { const { push } = useRouter(); return ( -
-

- {workspace?.title ?? "No workspace"} -

+
- - + + + {workspace?.title ?? "No workspace"} - + Switch to {workspaces.map((ws) => ( @@ -57,12 +48,14 @@ const SidebarTitle = ({ // Timeout to close sidebar and reset scroll lock styles setTimeout(() => { emitter.emit("showPageTransitionLoader"); - push(`/${ws.workspace_id}`) + push(`/${ws.workspace_id}`); }, 200); }} >
-
{ws.title}
+
+ {ws.title} +
{ws.workspace_id === workspace?.workspace_id ? ( ) : ( @@ -74,13 +67,13 @@ const SidebarTitle = ({ - Manage workspaces… - + @@ -88,4 +81,4 @@ const SidebarTitle = ({ ); }; -export default SidebarTitle; +export default WorkspaceMenu; diff --git a/clients/web/app/(authenticated)/[workspaceId]/layout.tsx b/clients/web/app/(authenticated)/[workspaceId]/layout.tsx index 3198cfe..100ed71 100644 --- a/clients/web/app/(authenticated)/[workspaceId]/layout.tsx +++ b/clients/web/app/(authenticated)/[workspaceId]/layout.tsx @@ -2,6 +2,7 @@ import ErrorPage from "@/components/ErrorPage"; import QueryClientProvider from "@/components/QueryClientProvider"; +import { getEmail } from "@/lib/auth"; import { getLLMModelsByService } from "@/lib/llm"; import { WorkspaceWithConversations } from "@/lib/sesameApi"; import { getWorkspaces, getWorkspaceStructuredData } from "@/lib/workspaces"; @@ -45,18 +46,21 @@ export default async function WorkspaceLayout({ const structuredWorkspaceData = getWorkspaceStructuredData(workspace.config); const models = getLLMModelsByService(structuredWorkspaceData.llm.service); + const email = await getEmail(); + return ( -
+
{/* Sidebar */} {/* Main content area */} -
+
{/* Navbar */} ; } -export default async function WorkspacePage({ - params, -}: WorkspacePageProps) { +export default async function WorkspacePage({ params }: WorkspacePageProps) { const { workspaceId } = await params; const workspace = await getWorkspace(workspaceId); diff --git a/clients/web/app/(authenticated)/layout.tsx b/clients/web/app/(authenticated)/layout.tsx index 0a3e68d..70dd56c 100644 --- a/clients/web/app/(authenticated)/layout.tsx +++ b/clients/web/app/(authenticated)/layout.tsx @@ -3,7 +3,5 @@ export default function AuthenticatedLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return ( - <>{children} - ); + return <>{children}; } diff --git a/clients/web/app/(authenticated)/workspaces/DeleteWorkspaceModal.tsx b/clients/web/app/(authenticated)/workspaces/DeleteWorkspaceModal.tsx index 5d18731..a8f977b 100644 --- a/clients/web/app/(authenticated)/workspaces/DeleteWorkspaceModal.tsx +++ b/clients/web/app/(authenticated)/workspaces/DeleteWorkspaceModal.tsx @@ -8,7 +8,7 @@ import { DialogDescription, DialogFooter, DialogHeader, - DialogTitle + DialogTitle, } from "@/components/ui/dialog"; import { useToast } from "@/hooks/use-toast"; import emitter from "@/lib/eventEmitter"; @@ -59,7 +59,7 @@ export default function DeleteWorkspaceModal() { const handleClose = () => { setWorkspace(null); setIsDeleting(false); - } + }; if (!workspace) return null; diff --git a/clients/web/app/(authenticated)/workspaces/Navbar.tsx b/clients/web/app/(authenticated)/workspaces/Navbar.tsx index 93bfcaf..a4df76b 100644 --- a/clients/web/app/(authenticated)/workspaces/Navbar.tsx +++ b/clients/web/app/(authenticated)/workspaces/Navbar.tsx @@ -12,7 +12,7 @@ const Navbar: React.FC = () => { }; return ( -
+
{/* Sidebar Toggle Button */} - - {/* Workspace Title */} -

Workspaces

- -
); }; diff --git a/clients/web/app/(authenticated)/workspaces/Sidebar.tsx b/clients/web/app/(authenticated)/workspaces/Sidebar.tsx index 2f002b5..1a00dcb 100644 --- a/clients/web/app/(authenticated)/workspaces/Sidebar.tsx +++ b/clients/web/app/(authenticated)/workspaces/Sidebar.tsx @@ -1,7 +1,7 @@ "use client"; +import UserMenu from "@/app/(authenticated)/[workspaceId]/UserMenu"; import PageTransitionLink from "@/components/PageTransitionLink"; -import SignOutButton from "@/components/SignOutButton"; import { DropdownMenu, DropdownMenuContent, @@ -24,11 +24,16 @@ import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; interface SidebarProps { + email?: string; signOut?: boolean; workspaces: WorkspaceModel[]; } -export default function Sidebar({ signOut = false, workspaces }: SidebarProps) { +export default function Sidebar({ + email, + signOut = false, + workspaces, +}: SidebarProps) { const [isOpen, setIsOpen] = useState(false); const pathname = usePathname(); @@ -48,27 +53,21 @@ export default function Sidebar({ signOut = false, workspaces }: SidebarProps) { const hasWorkspaces = workspaces.length > 0; - const getContent = (hasTitle: boolean) => ( -
- {hasTitle && ( -

- Workspaces -

- )} - -
+ const getContent = () => ( +
+
setIsOpen(false)} > - Create new workspaces + Create new workspace setIsOpen(false)} > @@ -99,10 +98,10 @@ export default function Sidebar({ signOut = false, workspaces }: SidebarProps) {
  • @@ -151,12 +150,13 @@ export default function Sidebar({ signOut = false, workspaces }: SidebarProps) { No workspaces
  • )} - {signOut && ( - - Sign out - - )} + +
    +
    + {signOut && } +
    +
    ); @@ -165,19 +165,17 @@ export default function Sidebar({ signOut = false, workspaces }: SidebarProps) { {/* Mobile Sidebar using Sheet component */}
    - - - Workspaces - + + Workspaces - {getContent(false)} + {getContent()}
    {/* Desktop Sidebar */} -
    - {getContent(true)} +
    + {getContent()}
    ); diff --git a/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationForm.tsx b/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationForm.tsx index 8fda572..3598151 100644 --- a/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationForm.tsx +++ b/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationForm.tsx @@ -109,7 +109,7 @@ export default function ConfigurationForm({ name: workspace.title, }, }), - [structuredData, workspace] + [structuredData, workspace], ); const [formState, setFormState] = useState(defaultState); @@ -142,7 +142,7 @@ export default function ConfigurationForm({ setIsSaving(true); const voice = voiceOptions.find( - (v) => v.voiceId === formState.voiceSettings.defaultVoice.selectedVoice + (v) => v.voiceId === formState.voiceSettings.defaultVoice.selectedVoice, ); const updatedWorkspace: Pick< @@ -236,7 +236,7 @@ export default function ConfigurationForm({ { method: isNewWorkspace ? "POST" : "PUT", body: JSON.stringify(updatedWorkspace), - } + }, ); if (response.ok) { const json = await response.json(); @@ -249,13 +249,13 @@ export default function ConfigurationForm({ push( isNewWorkspace ? `/${json.workspace_id}` - : `/workspaces/${workspace.workspace_id}` + : `/workspaces/${workspace.workspace_id}`, ); } else { throw new Error( `${response.status}: ${ response.statusText - } - ${await response.text()}` + } - ${await response.text()}`, ); } } catch (e: unknown) { @@ -272,7 +272,7 @@ export default function ConfigurationForm({ return (
    {workspace.workspace_id !== "new" && ( diff --git a/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationItem.tsx b/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationItem.tsx index 67f3e7e..a2ec16b 100644 --- a/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationItem.tsx +++ b/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationItem.tsx @@ -17,7 +17,7 @@ export default function ConfigurationItem({ "grid grid-cols-1 sm:grid-cols-[1fr_2fr] gap-0 sm:gap-2", `items-${align}`, border ? "border border-transparent border-t-border" : "", - "[&>*]:p-2" + "[&>*]:p-2", )} > {children} diff --git a/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationSection.tsx b/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationSection.tsx index 68b654b..625950d 100644 --- a/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationSection.tsx +++ b/clients/web/app/(authenticated)/workspaces/[workspaceId]/ConfigurationSection.tsx @@ -88,7 +88,7 @@ export default function ConfigurationSection({ onClose={() => setAddService(false)} onSaved={() => { refresh(); - setAddService(false) + setAddService(false); }} services={availableServices} workspaces={[workspace]} @@ -96,13 +96,13 @@ export default function ConfigurationSection({ )} {/* LLM Provider */}
    - + LLM Provider @@ -112,11 +112,16 @@ export default function ConfigurationSection({ id={s.service_id} value={s.service_provider!} aria-label={s.title} + className="text-nowrap" > {s.title} ))} - setAddService(true)} value=""> + setAddService(true)} + value="" + > Add @@ -216,7 +221,7 @@ export default function ConfigurationSection({ type="button" onClick={() => { const updatedPrompt = prompt.filter( - (_, i) => i !== index + (_, i) => i !== index, ); setFormState((state) => ({ ...state, diff --git a/clients/web/app/(authenticated)/workspaces/[workspaceId]/VoiceSettingsSection.tsx b/clients/web/app/(authenticated)/workspaces/[workspaceId]/VoiceSettingsSection.tsx index 96ac84c..d9c5f0a 100644 --- a/clients/web/app/(authenticated)/workspaces/[workspaceId]/VoiceSettingsSection.tsx +++ b/clients/web/app/(authenticated)/workspaces/[workspaceId]/VoiceSettingsSection.tsx @@ -8,9 +8,19 @@ import { SelectValue, } from "@/components/ui/select"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { ServiceInfo, ServiceModel, WorkspaceModel } from "@/lib/sesameApi"; -import { getVoicesByProvider, InteractionMode, languageOptions, TTSService } from "@/lib/voice"; +import { + getVoicesByProvider, + InteractionMode, + languageOptions, + TTSService, +} from "@/lib/voice"; import { HelpCircleIcon, PlusIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -31,15 +41,16 @@ export default function VoiceSettingsSection({ services, formState, setFormState, - workspace + workspace, }: Props) { - const { mainLanguage, defaultVoice, interactionMode } = formState.voiceSettings; + const { mainLanguage, defaultVoice, interactionMode } = + formState.voiceSettings; const { refresh } = useRouter(); const [addService, setAddService] = useState(false); const validVoices = getVoicesByProvider( defaultVoice.ttsProvider, - mainLanguage + mainLanguage, ); const handleProviderChange = (provider: TTSService) => { @@ -67,7 +78,7 @@ export default function VoiceSettingsSection({ interactionMode, }, })); - } + }; return ( @@ -78,7 +89,7 @@ export default function VoiceSettingsSection({ onClose={() => setAddService(false)} onSaved={() => { refresh(); - setAddService(false) + setAddService(false); }} services={availableServices} workspaces={[workspace]} @@ -87,12 +98,12 @@ export default function VoiceSettingsSection({ {/* TTS Provider */}
    - + TTS Provider @@ -102,6 +113,7 @@ export default function VoiceSettingsSection({ id={s.service_id} value={s.service_provider!} aria-label={s.title} + className="text-nowrap" > {s.title} @@ -125,7 +137,7 @@ export default function VoiceSettingsSection({ onValueChange={(lang) => { const availableVoices = getVoicesByProvider( defaultVoice.ttsProvider, - lang + lang, ).map((v) => v.voiceId); setFormState((state) => ({ @@ -136,7 +148,7 @@ export default function VoiceSettingsSection({ defaultVoice: { ...state.voiceSettings.defaultVoice, selectedVoice: availableVoices.includes( - state.voiceSettings.defaultVoice.selectedVoice + state.voiceSettings.defaultVoice.selectedVoice, ) ? state.voiceSettings.defaultVoice.selectedVoice : availableVoices[0], @@ -155,8 +167,8 @@ export default function VoiceSettingsSection({ (lang) => getVoicesByProvider( defaultVoice.ttsProvider, - lang[defaultVoice.ttsProvider] - ).length > 0 + lang[defaultVoice.ttsProvider], + ).length > 0, ) .map((lang) => ( {voice.label} - ) + ), )} @@ -219,7 +231,8 @@ export default function VoiceSettingsSection({ - A conversational workspace displays words as the bot speaks. Informational displays the full LLM output at once. + A conversational workspace displays words as the bot speaks. + Informational displays the full LLM output at once. diff --git a/clients/web/app/(authenticated)/workspaces/[workspaceId]/WorkspaceOptionsSection.tsx b/clients/web/app/(authenticated)/workspaces/[workspaceId]/WorkspaceOptionsSection.tsx index cf3e734..ad594bc 100644 --- a/clients/web/app/(authenticated)/workspaces/[workspaceId]/WorkspaceOptionsSection.tsx +++ b/clients/web/app/(authenticated)/workspaces/[workspaceId]/WorkspaceOptionsSection.tsx @@ -23,10 +23,10 @@ export default function WorkspaceOptionsSection({ ...fs, workspaceOptions: { ...fs.workspaceOptions, - botProfile: profile - } - })) - } + botProfile: profile, + }, + })); + }; return ( @@ -62,22 +62,21 @@ export default function WorkspaceOptionsSection({ Bot Profile - - - - Voice - - - - Vision - - + type="single" + variant="outline" + className="justify-start" + value={formState.workspaceOptions.botProfile} + onValueChange={handleBotProfileChange} + > + + + Voice + + + + Vision + + ); diff --git a/clients/web/app/(authenticated)/workspaces/layout.tsx b/clients/web/app/(authenticated)/workspaces/layout.tsx index c4164f9..b1c5d9f 100644 --- a/clients/web/app/(authenticated)/workspaces/layout.tsx +++ b/clients/web/app/(authenticated)/workspaces/layout.tsx @@ -2,6 +2,7 @@ import DeleteWorkspaceModal from "@/app/(authenticated)/workspaces/DeleteWorkspaceModal"; import ErrorPage from "@/components/ErrorPage"; +import { getEmail } from "@/lib/auth"; import { WorkspaceWithConversations } from "@/lib/sesameApi"; import { getWorkspaces } from "@/lib/workspaces"; import React from "react"; @@ -28,17 +29,23 @@ export default async function WorkspacesLayout({ ); } + const email = await getEmail(); + return ( -
    +
    {/* Sidebar */} - + {/* Main content area */} -
    +
    {/* Page content */} -
    +
    {children}
    diff --git a/clients/web/app/(authenticated)/workspaces/services/EditServiceModal.tsx b/clients/web/app/(authenticated)/workspaces/services/EditServiceModal.tsx index 967a434..89ba7b0 100644 --- a/clients/web/app/(authenticated)/workspaces/services/EditServiceModal.tsx +++ b/clients/web/app/(authenticated)/workspaces/services/EditServiceModal.tsx @@ -33,7 +33,7 @@ export default function EditServiceModal({ const [error, setError] = useState(""); const workspace = workspaces.find( - (ws) => ws.workspace_id === service.workspace_id + (ws) => ws.workspace_id === service.workspace_id, ); const handleSubmit = async (ev: React.FormEvent) => { diff --git a/clients/web/app/(authenticated)/workspaces/services/ServiceConfig.tsx b/clients/web/app/(authenticated)/workspaces/services/ServiceConfig.tsx index e9a8e97..662f331 100644 --- a/clients/web/app/(authenticated)/workspaces/services/ServiceConfig.tsx +++ b/clients/web/app/(authenticated)/workspaces/services/ServiceConfig.tsx @@ -31,7 +31,11 @@ const serviceTypeLabels: Record = { transport: "Transport", }; -export default function ServiceConfig({ availableServices, services, workspaces }: Props) { +export default function ServiceConfig({ + availableServices, + services, + workspaces, +}: Props) { const { refresh } = useRouter(); const [addService, setAddService] = useState(false); const [editServiceId, setEditServiceId] = useState(""); @@ -77,9 +81,9 @@ export default function ServiceConfig({ availableServices, services, workspaces {s.workspace_id - ? workspaces.find( - (ws) => ws.workspace_id === s.workspace_id - )?.title ?? "Unknown" + ? (workspaces.find( + (ws) => ws.workspace_id === s.workspace_id, + )?.title ?? "Unknown") : "All"} @@ -114,7 +118,7 @@ export default function ServiceConfig({ availableServices, services, workspaces refresh(); setAddService(false); toast({ - title: "Service added" + title: "Service added", }); }} services={availableServices} @@ -128,7 +132,7 @@ export default function ServiceConfig({ availableServices, services, workspaces refresh(); setEditServiceId(""); toast({ - title: "Service saved" + title: "Service saved", }); }} service={editService} @@ -142,7 +146,7 @@ export default function ServiceConfig({ availableServices, services, workspaces refresh(); setDeleteServiceId(""); toast({ - title: "Service deleted" + title: "Service deleted", }); }} service={deleteService} diff --git a/clients/web/app/NavigationState.tsx b/clients/web/app/NavigationState.tsx index 54ae9a2..6c21540 100644 --- a/clients/web/app/NavigationState.tsx +++ b/clients/web/app/NavigationState.tsx @@ -23,7 +23,7 @@ export default function NavigationState() { return (
    diff --git a/clients/web/app/layout.tsx b/clients/web/app/layout.tsx index 3d502eb..835bc79 100644 --- a/clients/web/app/layout.tsx +++ b/clients/web/app/layout.tsx @@ -28,7 +28,7 @@ export default async function RootLayout({ return ( {children} diff --git a/clients/web/components/BotReadyAudio.tsx b/clients/web/components/BotReadyAudio.tsx index 74a51e5..1b58c8b 100644 --- a/clients/web/components/BotReadyAudio.tsx +++ b/clients/web/components/BotReadyAudio.tsx @@ -14,7 +14,7 @@ export default function BotReadyAudio({ active }: Props) { useCallback(() => { if (!active) return; audioRef.current?.play(); - }, [active]) + }, [active]), ); return ( diff --git a/clients/web/components/ChatControls.tsx b/clients/web/components/ChatControls.tsx index f4c0a43..4cbe619 100644 --- a/clients/web/components/ChatControls.tsx +++ b/clients/web/components/ChatControls.tsx @@ -6,8 +6,9 @@ import emitter from "@/lib/eventEmitter"; import { ImageContent, Message } from "@/lib/messages"; import { cn } from "@/lib/utils"; import { + ArrowLeftToLineIcon, + ArrowRightToLineIcon, ArrowUpIcon, - Keyboard, LoaderCircle, LoaderCircleIcon, Maximize2Icon, @@ -17,9 +18,9 @@ import { PaperclipIcon, Speech, TriangleAlertIcon, - VideoIcon, - VideoOffIcon, + WebcamIcon, X, + XIcon, } from "lucide-react"; import Image from "next/image"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -49,7 +50,7 @@ import { CarouselNext, CarouselPrevious, } from "./ui/carousel"; -import { Dialog, DialogClose, DialogContent } from "./ui/dialog"; +import { Dialog, DialogClose, DialogContent, DialogTitle } from "./ui/dialog"; import { Textarea } from "./ui/textarea"; import { Tooltip, @@ -78,6 +79,7 @@ interface Props { } type VideoSize = "small" | "large"; +type VideoPlacement = "left" | "right"; const ChatControls: React.FC = ({ conversationId, @@ -95,6 +97,7 @@ const ChatControls: React.FC = ({ const [isCamMuted, setIsCamMuted] = useState(!vision); const [isMicMuted, setIsMicMuted] = useState(false); const [videoSize, setVideoSize] = useState("small"); + const [videoPlacement, setVideoPlacement] = useState("right"); const [, setSelectedImages] = useState([]); // Track selected image files const [previewUrls, setPreviewUrls] = useState([]); // Track preview URLs const [imageZoom, setImageZoom] = useState(false); @@ -238,7 +241,7 @@ const ChatControls: React.FC = ({ if (vision) rtviClient?.enableCam(true); onChangeMode?.(true); setEndDate(new Date(Number(rtviClient?.transportExpiry) * 1000)); - }, [onChangeMode, rtviClient]); + }, [onChangeMode, rtviClient, vision]); const handleDisconnect = useCallback(() => { setIsVoiceMode(false); @@ -367,24 +370,27 @@ const ChatControls: React.FC = ({ setStartIndex(0); }, [previewUrls.length]); - const isReadyToSpeak = transportState === "ready"; - const feedbackClassName = - "bg-gradient-to-t from-background absolute w-full bottom-full translate-y-2 pt-4 flex gap-2 items-center justify-center z-10"; + "bg-gradient-to-t from-background absolute w-full bottom-full pt-4 pb-2 flex gap-2 items-center justify-center z-10"; - const ToggledCamIcon = isCamMuted ? VideoOffIcon : VideoIcon; const ToggledMicIcon = isMicMuted ? MicOffIcon : MicIcon; const camTrack = useRTVIClientMediaTrack("video", "local"); + const isConnecting = + transportState === "authenticating" || + transportState === "connecting" || + transportState === "connected"; + return ( -
    +
    + Image preview @@ -426,9 +432,7 @@ const ChatControls: React.FC = ({ {error}
    - ) : transportState === "authenticating" || - transportState === "connecting" || - transportState === "connected" ? ( + ) : isConnecting ? (
    Connecting… @@ -442,6 +446,13 @@ const ChatControls: React.FC = ({ ? "Thinking…" : "Listening"} + {endDate && ( +
    + + + +
    + )}
    ) : processingAction ? (
    @@ -449,79 +460,84 @@ const ChatControls: React.FC = ({
    ) : null} - {/* Image Preview (if an image is selected) */} - {previewUrls.length > 0 && ( -
    - {previewUrls.map((url, idx) => ( -
    - Selected Preview { - setStartIndex(idx); - setImageZoom(true); - }} - height={80} - width={80} - /> - {/* Remove button */} - - - - - - - Remove image - - - -
    - ))} -
    - )} - - {/* Chat Controls */} -
    - {/* Image Button (File picker with camera support on mobile) */} - - - - - - - Attach images - - - + {/* Remove button */} + + + + + + + Remove image + + + +
    + ))} +
    + )} + +