171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import { useSessionContext, useSessionMessages } from '@livekit/components-react';
|
|
import type { AppConfig } from '@/app-config';
|
|
import {
|
|
AgentControlBar,
|
|
type AgentControlBarControls,
|
|
} from '@/components/agents-ui/agent-control-bar';
|
|
import { ChatTranscript } from '@/components/app/chat-transcript';
|
|
import { GeneratedImagePanel } from '@/components/app/generated-image-panel';
|
|
import { ImageGallery } from '@/components/app/image-gallery';
|
|
import { TileLayout } from '@/components/app/tile-layout';
|
|
import { GeneratedImagesProvider } from '@/hooks/useGeneratedImages';
|
|
import { cn } from '@/lib/shadcn/utils';
|
|
import { Shimmer } from '../ai-elements/shimmer';
|
|
|
|
const MotionBottom = motion.create('div');
|
|
|
|
const MotionMessage = motion.create(Shimmer);
|
|
|
|
const BOTTOM_VIEW_MOTION_PROPS = {
|
|
variants: {
|
|
visible: {
|
|
opacity: 1,
|
|
translateY: '0%',
|
|
},
|
|
hidden: {
|
|
opacity: 0,
|
|
translateY: '100%',
|
|
},
|
|
},
|
|
initial: 'hidden',
|
|
animate: 'visible',
|
|
exit: 'hidden',
|
|
transition: {
|
|
duration: 0.3,
|
|
delay: 0.5,
|
|
ease: 'easeOut',
|
|
},
|
|
};
|
|
|
|
const SHIMMER_MOTION_PROPS = {
|
|
variants: {
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
ease: 'easeIn',
|
|
duration: 0.5,
|
|
delay: 0.8,
|
|
},
|
|
},
|
|
hidden: {
|
|
opacity: 0,
|
|
transition: {
|
|
ease: 'easeIn',
|
|
duration: 0.5,
|
|
delay: 0,
|
|
},
|
|
},
|
|
},
|
|
initial: 'hidden',
|
|
animate: 'visible',
|
|
exit: 'hidden',
|
|
};
|
|
|
|
interface FadeProps {
|
|
top?: boolean;
|
|
bottom?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function Fade({ top = false, bottom = false, className }: FadeProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'from-background pointer-events-none h-4 bg-linear-to-b to-transparent',
|
|
top && 'bg-linear-to-b',
|
|
bottom && 'bg-linear-to-t',
|
|
className
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface SessionViewProps {
|
|
appConfig: AppConfig;
|
|
}
|
|
|
|
export const SessionView = ({
|
|
appConfig,
|
|
...props
|
|
}: React.ComponentProps<'section'> & SessionViewProps) => {
|
|
const session = useSessionContext();
|
|
const { messages } = useSessionMessages(session);
|
|
const [chatOpen, setChatOpen] = useState(false);
|
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
|
|
const controls: AgentControlBarControls = {
|
|
leave: true,
|
|
microphone: true,
|
|
chat: appConfig.supportsChatInput,
|
|
camera: appConfig.supportsVideoInput,
|
|
screenShare: appConfig.supportsScreenShare,
|
|
};
|
|
|
|
useEffect(() => {
|
|
const lastMessage = messages.at(-1);
|
|
const lastMessageIsLocal = lastMessage?.from?.isLocal === true;
|
|
|
|
if (scrollAreaRef.current && lastMessageIsLocal) {
|
|
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight;
|
|
}
|
|
}, [messages]);
|
|
|
|
return (
|
|
<section className="bg-background relative z-10 h-svh w-svw overflow-hidden" {...props}>
|
|
<Fade top className="absolute inset-x-4 top-0 z-10 h-40" />
|
|
{/* transcript */}
|
|
<ChatTranscript
|
|
hidden={!chatOpen}
|
|
messages={messages}
|
|
className="space-y-3 transition-opacity duration-300 ease-out"
|
|
/>
|
|
{/* Tile layout */}
|
|
<TileLayout chatOpen={chatOpen} />
|
|
{/* Single provider registers the byte stream handler once for both image components */}
|
|
<GeneratedImagesProvider>
|
|
{/* Generated image panel — appears when the agent calls generate_image */}
|
|
<GeneratedImagePanel chatOpen={chatOpen} />
|
|
{/* Gallery — persistent access to all generated images */}
|
|
<ImageGallery />
|
|
</GeneratedImagesProvider>
|
|
{/* Bottom */}
|
|
<MotionBottom
|
|
{...BOTTOM_VIEW_MOTION_PROPS}
|
|
className="fixed inset-x-3 bottom-0 z-50 md:inset-x-12"
|
|
>
|
|
{/* Pre-connect message */}
|
|
{appConfig.isPreConnectBufferEnabled && (
|
|
<AnimatePresence>
|
|
{messages.length === 0 && (
|
|
<MotionMessage
|
|
key="pre-connect-message"
|
|
duration={2}
|
|
aria-hidden={messages.length > 0}
|
|
{...SHIMMER_MOTION_PROPS}
|
|
className="pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold"
|
|
>
|
|
Agent is listening, ask it a question
|
|
</MotionMessage>
|
|
)}
|
|
</AnimatePresence>
|
|
)}
|
|
<div className="bg-background relative mx-auto max-w-2xl pb-3 md:pb-12">
|
|
<Fade bottom className="absolute inset-x-0 top-0 h-4 -translate-y-full" />
|
|
<AgentControlBar
|
|
variant="livekit"
|
|
controls={controls}
|
|
isChatOpen={chatOpen}
|
|
isConnected={session.isConnected}
|
|
onDisconnect={session.end}
|
|
onIsChatOpenChange={setChatOpen}
|
|
/>
|
|
</div>
|
|
</MotionBottom>
|
|
</section>
|
|
);
|
|
};
|