feat: add video page, implement token generation API, and enhance chat components with improved styles and functionality
This commit is contained in:
parent
c1769e0d1a
commit
a3c67a3bcc
27 changed files with 10948 additions and 182 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -24,4 +24,5 @@ dist-ssr
|
|||
*.sw?
|
||||
output.sh
|
||||
.next
|
||||
ui
|
||||
ui
|
||||
.env
|
||||
|
|
20
app/api/bot/create-conversation/route.ts
Normal file
20
app/api/bot/create-conversation/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
14
app/api/get-token/route.ts
Normal file
14
app/api/get-token/route.ts
Normal 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)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}</>;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -27,8 +27,8 @@
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.markdown-container h1,
|
||||
.markdown-container h2,
|
||||
.markdown-container h1,
|
||||
.markdown-container h2,
|
||||
.markdown-container h3 {
|
||||
color: #00f3ff;
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
255
app/meet/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
|
||||
images: { unoptimized: true }
|
||||
}
|
||||
|
||||
|
|
|
@ -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
10551
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue