feat: add video page, implement token generation API, and enhance chat components with improved styles and functionality

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-04-26 21:44:35 -03:00
parent c1769e0d1a
commit a3c67a3bcc
27 changed files with 10948 additions and 182 deletions

3
.gitignore vendored
View file

@ -24,4 +24,5 @@ dist-ssr
*.sw?
output.sh
.next
ui
ui
.env

View file

@ -0,0 +1,20 @@
import { NextResponse } from 'next/server'
export async function POST() {
try {
const response = await fetch('https://generalbots.online/directline/PROD-GeneralBots006/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create conversation' },
{ status: 500 }
)
}
}

View file

@ -0,0 +1,14 @@
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const room = searchParams.get('room')
const identity = searchParams.get('identity')
const response = await fetch(
`https://generalbots.online/PROD-GeneralBots006/meeting-token?room=${room}&identity=${identity}`
)
const data = await response.json()
return NextResponse.json(data)
}

View file

@ -4,19 +4,15 @@ import '../../styles/chat.css';
export function ChatHeader() {
const { instance } = useChat();
return (
<div className="chat-header">
<div className="header-content">
<h2 className="header-title">
{instance?.name || 'Chat'}
</h2>
<h2 className="header-title">{instance?.name || 'Qwen Chat'}</h2>
<span className="header-subtitle">Online</span>
</div>
<button className="header-button">
<svg className="icon" viewBox="0 0 24 24">
<path d="M12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm14 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
<path d="M12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm14 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z" />
</svg>
</button>
</div>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { EmojiPicker } from '../ui/emoji-picker';
import { useChat } from '../../providers/chat-provider';
import { useSound } from '../../providers/sound-provider';
import '../../styles/chat.css';
@ -20,9 +20,9 @@ export function ChatInput() {
setMessage('');
};
const handleEmojiSelect = (emoji: string) => {
const handleEmojiSelect = (emoji) => {
playSound('click');
setMessage(prev => prev + emoji);
setMessage((prev) => prev + emoji);
};
return (
@ -30,46 +30,43 @@ export function ChatInput() {
<div className="input-container">
<button className="icon-button" onClick={() => playSound('click')}>
<svg className="icon" viewBox="0 0 24 24">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
</svg>
</button>
<button className="icon-button" onClick={() => setShowEmoji(true)}>
<svg className="icon" viewBox="0 0 24 24">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/>
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01" />
</svg>
</button>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
className="chat-input"
placeholder="Type a message..."
/>
{message.trim().length > 0 ? (
<button className="send-button" onClick={handleSend}>
<svg className="icon" viewBox="0 0 24 24">
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
</svg>
</button>
) : (
<button className="icon-button" onClick={() => playSound('click')}>
<svg className="icon" viewBox="0 0 24 24">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4"/>
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4" />
</svg>
</button>
)}
</div>
<EmojiPicker
{/* <EmojiPicker
visible={showEmoji}
onClose={() => {
playSound('click');
setShowEmoji(false);
}}
onEmojiSelect={handleEmojiSelect}
/>
/> */}
</>
);
}

View file

@ -13,13 +13,11 @@ export function ChatWindow() {
React.useEffect(() => {
if (!line) return;
const subscription = line.activity$.subscribe((activity: any) => {
const subscription = line.activity$.subscribe((activity) => {
if (activity.type === 'message') {
setMessages(prev => [...prev, activity as Message]);
setMessages((prev) => [...prev, activity]);
}
});
return () => subscription.unsubscribe();
}, [line]);

View file

@ -9,7 +9,7 @@ interface MessageListProps {
}
export function MessageList({ messages }: MessageListProps) {
const scrollRef = React.useRef<HTMLDivElement>(null);
const scrollRef = React.useRef(null);
const { user } = useChat();
const { playSound } = useSound();
const prevMessagesLength = React.useRef(messages.length);
@ -30,14 +30,10 @@ export function MessageList({ messages }: MessageListProps) {
{messages.map((message, index) => (
<div
key={`${message.id}-${index}`}
className={`message-container ${
message.from.id === user.id ? 'user-message' : 'bot-message'
}`}
className={`message-container ${message.from.id === user.id ? 'user-message' : 'bot-message'}`}
>
<p className="message-text">{message.text}</p>
<span className="message-time">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
<span className="message-time">{new Date(message.timestamp).toLocaleTimeString()}</span>
</div>
))}
</div>

View file

@ -1,5 +1,4 @@
"use client";
import React from 'react';
import { VideoPlayer } from './video-player';
import { ImageViewer } from './image-viewer';
@ -9,24 +8,20 @@ import '../../styles/projector.css';
export function ProjectorView() {
const { line } = useChat();
const [content, setContent] = React.useState<any>(null);
const [content, setContent] = React.useState(null);
React.useEffect(() => {
if (!line) return;
const subscription = line.activity$
.subscribe((activity: any) => {
if (activity.type === 'event' && activity.name === 'project') {
setContent(activity.value);
}
});
const subscription = line.activity$.subscribe((activity) => {
if (activity.type === 'event' && activity.name === 'project') {
setContent(activity.value);
}
});
return () => subscription.unsubscribe();
}, [line]);
const renderContent = () => {
if (!content) return null;
switch (content.type) {
case 'video':
return <VideoPlayer url={content.url} />;
@ -39,9 +34,5 @@ export function ProjectorView() {
}
};
return (
<div className="projector-container">
{renderContent()}
</div>
);
return <div className="projector-container">{renderContent()}</div>;
}

View file

@ -10,14 +10,11 @@ export function PersonSelector() {
return (
<div className="selector-container">
<div className="selector-header">
{instance?.logo && (
<img src={instance.logo} className="selector-logo" alt="Logo" />
)}
{instance?.logo && <img src={instance.logo} className="selector-logo" alt="Logo" />}
</div>
<div className="search-container">
<svg className="search-icon" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
value={search}
@ -26,7 +23,6 @@ export function PersonSelector() {
placeholder="Search conversations..."
/>
</div>
<div className="selector-list">
{['FAQ', 'Support', 'Sales'].map((item) => (
<div key={item} className="selector-item">

View file

@ -1,39 +1,28 @@
"use client";
import React, { useEffect, useState } from 'react';
import { soundAssets } from '../../../../public/sounds/manifest';
//import { cacheAssets } from '../lib/asset-loader';
export function SoundInitializer({ children }: { children: React.ReactNode }) {
export function SoundInitializer({ children }) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState(null);
useEffect(() => {
const initializeSounds = async () => {
try {
// await cacheAssets(Object.values(soundAssets));
// Simulate sound initialization
setIsReady(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to initialize sounds');
setError(err.message || 'Failed to initialize sounds');
}
};
initializeSounds();
}, []);
if (error) {
return (
<div className="error-container">
<p className="error-text">Error: {error}</p>
</div>
);
return <div className="error-container"><p>Error: {error}</p></div>;
}
if (!isReady) {
return (
<div className="loading-container">
<p>Loading sounds...</p>
</div>
);
return <div className="loading-container"><p>Loading sounds...</p></div>;
}
return <>{children}</>;

View file

@ -1,14 +1,14 @@
export function formatTimestamp(date: Date): string {
export function formatTimestamp(date) {
return new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
export function cn(...classes: string[]): string {
export function cn(...classes) {
return classes.filter(Boolean).join(' ');
}
export function generateId(): string {
export function generateId() {
return Math.random().toString(36).slice(2);
}

View file

@ -4,7 +4,7 @@ import { ChatLayout } from './components/chat-layout';
import { SoundInitializer } from './components/sound-initializer';
import { SoundProvider } from './providers/sound-provider';
export default function Chat() {
export default function Chat() {
return (
<SoundInitializer>
<SoundProvider>

View file

@ -2,102 +2,66 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { User, ChatInstance } from '../types';
interface ChatContextType {
line: any;
user: User;
instance: ChatInstance | null;
sendActivity: (activity: any) => Promise<void>;
selectedVoice: any;
setVoice: (voice: any) => void;
const ChatContext = createContext(undefined);
async function apiFetch(endpoint, options = {}) {
const baseUrl = 'http://localhost:4242/';
const response = await fetch(`${baseUrl}${endpoint}`, options);
if (!response.ok) throw new Error('API request failed');
return response.json();
}
const ChatContext = createContext<ChatContextType | undefined>(undefined);
// Unified API caller that works in both environments
async function apiFetch(endpoint: string, options?: RequestInit) {
const baseUrl = 'http://localhost:4242/'; //typeof window !== 'undefined' ? window.location.origin : '';
if (typeof window !== 'undefined' && '__TAURI__' in window) {
// Tauri environment - use HTTP module for better security
const { http } = await import('@tauri-apps/api');
return http.fetch(`${baseUrl}${endpoint}`, options);
}
// Web environment - standard fetch
return fetch(`${baseUrl}${endpoint}`, options);
}
export function ChatProvider({ children }: { children: React.ReactNode }) {
const [line, setLine] = useState<any>(null);
const [instance, setInstance] = useState<ChatInstance | null>(null);
const [selectedVoice, setSelectedVoice] = useState(null);
export function ChatProvider({ children }) {
const [line, setLine] = useState(null);
const [instance, setInstance] = useState(null);
const [user] = useState<User>({
id: `user_${Math.random().toString(36).slice(2)}`,
name: 'You'
name: 'You',
});
useEffect(() => {
const initializeChat = async () => {
try {
const botId = 'doula'; // window.location.pathname.split('/')[1] || 'default';
// Get instance from REST API
const response = await apiFetch(`/instances/${botId}`);
if (!response.ok) throw new Error('Failed to get chat instance');
const instanceData = await response.json();
const botId = 'doula'; // Default bot ID
const instanceData = await apiFetch(`/instances/${botId}`);
setInstance(instanceData);
// Initialize chat service
const chatService = {
activity$: { subscribe: () => {} },
postActivity: (activity: any) => ({
subscribe: (observer: any) => {
// Handle real-time updates if needed
postActivity: (activity) => ({
subscribe: (observer) => {
return { unsubscribe: () => {} };
}
})
},
}),
};
setLine(chatService);
} catch (error) {
console.error('Failed to initialize chat:', error);
}
};
initializeChat();
}, []);
const sendActivity = async (activity: any) => {
const sendActivity = async (activity) => {
try {
const fullActivity = {
...activity,
from: user,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
};
// Send activity via REST API
const response = await apiFetch('/activities', {
await apiFetch('/activities', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(fullActivity)
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fullActivity),
});
if (!response.ok) throw new Error('Failed to send activity');
// Notify local chat service
line?.postActivity(fullActivity).subscribe();
} catch (error) {
console.error('Failed to send activity:', error);
}
};
const setVoice = (voice: any) => {
setSelectedVoice(voice);
};
return (
<ChatContext.Provider value={{ line, user, instance, sendActivity, selectedVoice, setVoice }}>
<ChatContext.Provider value={{ line, user, instance, sendActivity }}>
{children}
</ChatContext.Provider>
);
@ -109,4 +73,4 @@ export function useChat() {
throw new Error('useChat must be used within ChatProvider');
}
return context;
}
}

View file

@ -1,24 +1,15 @@
"use client";
import React, { createContext, useContext, useCallback } from 'react';
import { core } from '@tauri-apps/api';
interface SoundContextType {
playSound: (sound: string) => void;
setEnabled: (enabled: boolean) => void;
}
const SoundContext = createContext(undefined);
const SoundContext = createContext<SoundContextType | undefined>(undefined);
export function SoundProvider({ children }: { children: React.ReactNode }) {
export function SoundProvider({ children }) {
const [enabled, setEnabled] = React.useState(true);
const playSound = useCallback(async (sound: string) => {
const playSound = useCallback((sound) => {
if (!enabled) return;
try {
await core.invoke('play_sound', { sound });
} catch (error) {
console.error('Failed to play sound:', error);
}
const audio = new Audio();
audio.play().catch((err) => console.error('Failed to play sound:', err));
}, [enabled]);
return (

View file

@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
height: 100%;
background-color: #111;
background-color: #f5f5f5;
}
.chat-header {
@ -10,8 +10,8 @@
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: #111;
border-bottom: 1px solid #333;
background-color: #fff;
border-bottom: 1px solid #ccc;
}
.header-content {
@ -20,15 +20,14 @@
}
.header-title {
color: white;
font-size: 1.25rem;
font-weight: bold;
margin: 0;
}
.header-subtitle {
color: #00f3ff;
font-size: 0.875rem;
color: #007dff;
margin-top: 0.25rem;
}
@ -47,24 +46,23 @@
.user-message {
align-self: flex-end;
background-color: rgba(0, 243, 255, 0.1);
border: 1px solid #00f3ff;
background-color: rgba(0, 180, 255, 0.15);
border: 1px solid #00cfff;
}
.bot-message {
align-self: flex-start;
background-color: rgba(191, 0, 255, 0.1);
border: 1px solid #bf00ff;
background-color: rgba(180, 0, 255, 0.1);
border: 1px solid #c400ff;
}
.message-text {
color: white;
margin: 0;
}
.message-time {
color: #666;
font-size: 0.75rem;
color: #666;
margin-top: 0.25rem;
}
@ -72,8 +70,8 @@
display: flex;
align-items: center;
padding: 1rem;
border-top: 1px solid #333;
background-color: #111;
border-top: 1px solid #ccc;
background-color: #fff;
}
.chat-input {
@ -82,9 +80,9 @@
max-height: 6rem;
padding: 0.5rem 1rem;
margin: 0 0.5rem;
background-color: #1a1a1a;
color: white;
border: 1px solid #333;
background-color: #f0f0f0;
color: #000;
border: 1px solid #ccc;
border-radius: 1.25rem;
resize: none;
}
@ -92,14 +90,14 @@
.icon-button {
background: none;
border: none;
color: #00f3ff;
color: #007dff;
cursor: pointer;
padding: 0.5rem;
}
.send-button {
background-color: rgba(0, 243, 255, 0.1);
border: 1px solid #00f3ff;
background-color: rgba(0, 180, 255, 0.15);
border: 1px solid #00cfff;
border-radius: 50%;
padding: 0.5rem;
cursor: pointer;

View file

@ -12,11 +12,12 @@
.main-content {
flex: 1;
display: flex;
flex-direction: column;
}
.projector {
width: 40%;
border-right: 1px solid #333;
height: 30%;
border-bottom: 1px solid #333;
}
.chat-area {

View file

@ -27,8 +27,8 @@
overflow-y: auto;
}
.markdown-container h1,
.markdown-container h2,
.markdown-container h1,
.markdown-container h2,
.markdown-container h3 {
color: #00f3ff;
}

View file

@ -4,7 +4,7 @@
right: 2rem;
width: 20rem;
max-height: 25rem;
background-color: rgba(0, 0, 0, 0.95);
background-color: whitesmoke;
border: 1px solid #00f3ff;
border-radius: 0.75rem;
box-shadow: 0 0 1rem rgba(0, 243, 255, 0.5);
@ -111,7 +111,7 @@
right: 0;
bottom: 0;
background-color: #333;
transition: .4s;
transition: 0.4s;
border-radius: 1.5rem;
}
@ -123,7 +123,7 @@
left: 0.2rem;
bottom: 0.2rem;
background-color: #666;
transition: .4s;
transition: 0.4s;
border-radius: 50%;
}

View file

@ -4,17 +4,17 @@ import { usePathname, useRouter } from 'next/navigation';
import { Button } from '../src/components/ui/button';
const examples = [
{ name: "Home", href: "/auth" },
{ name: "Dashboard", href: "/dashboard" },
{ name: "Chat", href: "/chat" },
{ name: "Meet", href: "/meet" },
{ name: "Dashboard", href: "/dashboard" },
{ name: "Mail", href: "/mail" },
{ name: "Tree", href: "/tree" },
{ name: "Editor", href: "/editor" },
{ name: "Tables", href: "/table" },
{ name: "Video", href: "/video" },
{ name: "Videos", href: "/videos" },
{ name: "Music", href: "/music" },
{ name: "Templates", href: "/templates" },
{ name: "Settings", href: "/sync" },
{ name: "Settings", href: "/settings" },
];
export function Nav() {

View file

@ -1,3 +1,5 @@
import { ThemeProvider } from "@/components/ui/theme-provider";
import { Metadata } from 'next';
import { Nav } from './client-nav';
import './globals.css';
@ -8,9 +10,11 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Nav />
{children}
<Nav />
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{/* <ModeToggle /> */}
{children}
</ThemeProvider>
</body>
</html>
)

255
app/meet/page.tsx Normal file
View file

@ -0,0 +1,255 @@
// app/meeting/[room]/page.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
import { LiveKitRoom, VideoConference } from '@livekit/components-react'
import { RoomEvent, Track } from 'livekit-client'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Mic, MicOff, Video, VideoOff, Send, Bot } from 'lucide-react'
export default function MeetingPage({ params }: { params: { room: string } }) {
const router = useRouter()
const [name, setName] = useState('')
const [roomName, setRoomName] = useState(params.room || '')
const [token, setToken] = useState('')
const [isConnected, setIsConnected] = useState(false)
const [messages, setMessages] = useState<Array<{sender: string, text: string, isBot: boolean}>>([])
const [inputMessage, setInputMessage] = useState('')
const [micEnabled, setMicEnabled] = useState(true)
const [cameraEnabled, setCameraEnabled] = useState(true)
const [botTyping, setBotTyping] = useState(false)
const botConnectionRef = useRef<any>(null)
const audioContextRef = useRef<AudioContext>()
const processorRef = useRef<ScriptProcessorNode>()
const participantsRef = useRef<Map<string, MediaStreamAudioSourceNode>>(new Map())
// Connect to LiveKit room
const connectToRoom = async () => {
try {
const resp = await fetch(`/api/get-token?room=${roomName}&identity=${name || 'user'}`)
const data = await resp.json()
setToken(data.token)
setIsConnected(true)
// Connect bot when first user joins
await connectBot()
} catch (e) {
console.error(e)
}
}
// Connect to Bot Framework via Next.js API proxy
const connectBot = async () => {
try {
// First create a conversation through our API proxy
const convResponse = await fetch('/api/bot/create-conversation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
const convData = await convResponse.json();
if (!convData.conversationId) {
throw new Error('Failed to create conversation');
}
// Then connect using WebSocket (which doesn't have CORS restrictions)
const { DirectLine } = await import('botframework-directlinejs')
const userId = `user_${Math.random().toString(36).slice(2)}`
botConnectionRef.current = new DirectLine({
domain: 'https://generalbots.online/directline/PROD-GeneralBots006',
conversationId: convData.conversationId,
userId: userId,
webSocket: false // Force WebSocket connection
} as any);
botConnectionRef.current.setUserId(userId);
// Listen for bot responses
botConnectionRef.current.activity$.subscribe((activity: any) => {
console.log('Bot activity:', activity)
if (activity.text) {
setBotTyping(false)
setMessages(prev => [...prev, {
sender: 'AI Assistant',
text: activity.text,
isBot: true
}])
speak(activity.text)
}
});
// Send welcome message
setMessages(prev => [...prev, {
sender: 'System',
text: 'AI assistant joined the meeting',
isBot: true
}]);
} catch (error) {
console.error('Error connecting to bot:', error);
setMessages(prev => [...prev, {
sender: 'System',
text: 'Failed to connect AI assistant',
isBot: true
}]);
}
}
// Text-to-Speech for bot responses
const speak = (text: string) => {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text)
utterance.rate = 1.0
utterance.pitch = 1.0
speechSynthesis.speak(utterance)
}
}
// Send message to bot
const sendToBot = (text: string, sender: string) => {
if (!text.trim() || !botConnectionRef.current) return
setMessages(prev => [...prev, { sender, text, isBot: false }])
setBotTyping(true)
botConnectionRef.current.postActivity({
from: { id: botConnectionRef.current.userIdOnStartConversation, name: sender },
type: 'message',
text
}).subscribe(
() => {},
(err: any) => console.error('Error sending to bot:', err)
)
}
// Manual message send
const sendMessage = () => {
sendToBot(inputMessage, name)
setInputMessage('')
}
useEffect(() => {
if (isConnected) {
}
}, [isConnected])
if (!isConnected) {
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md p-6">
<h1 className="text-2xl font-bold mb-4">Join Meeting</h1>
<div className="space-y-4">
<Input
placeholder="Your Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Input
placeholder="Room Name"
value={roomName}
onChange={(e) => setRoomName(e.target.value)}
/>
<Button className="w-full" onClick={connectToRoom}>
Join
</Button>
</div>
</Card>
</div>
)
}
return (
<div className="flex flex-col h-screen">
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
connect={true}
audio={micEnabled}
video={cameraEnabled}
>
<div className="flex flex-1">
{/* Video Area */}
<div className="flex-1 bg-gray-800 relative">
<VideoConference />
<div className="absolute bottom-4 left-4 flex gap-2">
<Button
variant={micEnabled ? 'default' : 'outline'}
size="icon"
onClick={() => setMicEnabled(!micEnabled)}
>
{micEnabled ? <Mic className="h-4 w-4" /> : <MicOff className="h-4 w-4" />}
</Button>
<Button
variant={cameraEnabled ? 'default' : 'outline'}
size="icon"
onClick={() => setCameraEnabled(!cameraEnabled)}
>
{cameraEnabled ? <Video className="h-4 w-4" /> : <VideoOff className="h-4 w-4" />}
</Button>
</div>
</div>
{/* Chat/Transcript Area */}
<div className="w-80 border-l flex flex-col bg-background">
<ScrollArea className="flex-1 p-4">
{messages.map((msg, i) => (
<div key={i} className={`mb-3 ${msg.isBot ? 'bg-muted/50' : ''} p-2 rounded`}>
<div className="flex items-center gap-2 mb-1">
{msg.isBot ? (
<Avatar className="h-6 w-6 bg-primary">
<AvatarFallback className="bg-primary">
<Bot className="h-3 w-3" />
</AvatarFallback>
</Avatar>
) : (
<Avatar className="h-6 w-6">
<AvatarFallback>{msg.sender[0]}</AvatarFallback>
</Avatar>
)}
<span className="font-medium">{msg.sender}</span>
</div>
<p className="text-sm">{msg.text}</p>
</div>
))}
{botTyping && (
<div className="mb-3 bg-muted/50 p-2 rounded">
<div className="flex items-center gap-2 mb-1">
<Avatar className="h-6 w-6 bg-primary">
<AvatarFallback className="bg-primary">
<Bot className="h-3 w-3" />
</AvatarFallback>
</Avatar>
<span className="font-medium">AI Assistant</span>
</div>
<p className="text-sm text-muted-foreground">Typing...</p>
</div>
)}
</ScrollArea>
<div className="p-4 border-t">
<div className="flex gap-2 mb-2">
<Input
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
placeholder="Type a message"
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
/>
<Button size="icon" onClick={sendMessage}>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</LiveKitRoom>
</div>
)
}

View file

@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: { unoptimized: true }
}

View file

@ -10,6 +10,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@livekit/components-react": "^2.9.3",
"@next/font": "^14.2.15",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
@ -49,9 +50,11 @@
"cmdk": "^1.1.1",
"date-fns": "^2.30.0",
"jotai": "^2.12.2",
"livekit-client": "^2.11.3",
"lucide-react": "0.454.0",
"nativewind": "2.0.10",
"next": "^15.2.4",
"next-themes": "^0.4.6",
"postcss": "8.4.35",
"react": "18.3.1",
"react-day-picker": "^8.10.1",

10551
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

1
tsconfig.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long