- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
223 lines
12 KiB
TypeScript
223 lines
12 KiB
TypeScript
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
import Sidebar from './components/Sidebar';
|
|
import FileList from './components/FileList';
|
|
import Visualizer from './components/Visualizer';
|
|
import { MOCK_FILES, MOCK_MENU } from './constants';
|
|
import { LiveManager } from './services/liveManager';
|
|
import { ConnectionStatus } from './types';
|
|
|
|
const App: React.FC = () => {
|
|
const [activeMenuId, setActiveMenuId] = useState('dashboard');
|
|
const [fileFilter, setFileFilter] = useState('');
|
|
const [status, setStatus] = useState<ConnectionStatus>(ConnectionStatus.DISCONNECTED);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
|
|
// Audio Visualizer State
|
|
const [analyser, setAnalyser] = useState<AnalyserNode | null>(null);
|
|
const [isTalking, setIsTalking] = useState(false);
|
|
|
|
const liveManagerRef = useRef<LiveManager | null>(null);
|
|
|
|
const handleToolCall = useCallback(async (name: string, args: any) => {
|
|
console.log("App executing tool:", name, args);
|
|
if (name === 'navigateToPage') {
|
|
const pageId = args.pageId.toLowerCase();
|
|
// Simple mapping validation
|
|
const exists = MOCK_MENU.some(m => m.id === pageId);
|
|
if (exists) {
|
|
setActiveMenuId(pageId);
|
|
return { success: true, message: `Navigated to ${pageId}` };
|
|
} else {
|
|
// Fuzzy match or default
|
|
return { success: false, message: `Page ${pageId} not found` };
|
|
}
|
|
} else if (name === 'searchDocuments') {
|
|
setFileFilter(args.query);
|
|
setActiveMenuId('files'); // Switch to files view automatically
|
|
return { success: true, count: 5 }; // Mock return
|
|
}
|
|
return { error: 'Unknown tool' };
|
|
}, []);
|
|
|
|
const connectToSam = useCallback(() => {
|
|
if (!process.env.API_KEY) {
|
|
setErrorMsg("API Key not found in environment.");
|
|
return;
|
|
}
|
|
|
|
if (liveManagerRef.current) {
|
|
liveManagerRef.current.disconnect();
|
|
}
|
|
|
|
const manager = new LiveManager(
|
|
process.env.API_KEY,
|
|
handleToolCall,
|
|
(s) => {
|
|
if (s === 'connected') {
|
|
setStatus(ConnectionStatus.CONNECTED);
|
|
setAnalyser(manager.outputAnalyser);
|
|
} else if (s === 'disconnected') {
|
|
setStatus(ConnectionStatus.DISCONNECTED);
|
|
setAnalyser(null);
|
|
} else if (s === 'error') {
|
|
setStatus(ConnectionStatus.ERROR);
|
|
setAnalyser(null);
|
|
} else {
|
|
setStatus(ConnectionStatus.CONNECTING);
|
|
}
|
|
},
|
|
(level) => {
|
|
// Simple voice activity detection for visualizer fallback or other UI effects
|
|
// Not strictly needed since visualizer connects to output, but good for mic input feedback
|
|
}
|
|
);
|
|
|
|
liveManagerRef.current = manager;
|
|
manager.connect();
|
|
}, [handleToolCall]);
|
|
|
|
const disconnect = () => {
|
|
liveManagerRef.current?.disconnect();
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-screen w-full bg-slate-50">
|
|
<Sidebar menus={MOCK_MENU} activeId={activeMenuId} />
|
|
|
|
<main className="flex-1 flex flex-col h-screen overflow-hidden relative">
|
|
{/* Header / Top Bar */}
|
|
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-8 z-10">
|
|
<h1 className="text-xl font-bold text-slate-800">
|
|
{MOCK_MENU.find(m => m.id === activeMenuId)?.label || 'Dashboard'}
|
|
</h1>
|
|
|
|
<div className="flex items-center gap-4">
|
|
{status === ConnectionStatus.ERROR && (
|
|
<span className="text-red-500 text-sm font-medium">Connection Error</span>
|
|
)}
|
|
|
|
{status === ConnectionStatus.CONNECTED ? (
|
|
<button
|
|
onClick={disconnect}
|
|
className="flex items-center gap-2 px-4 py-2 bg-red-50 text-red-600 rounded-full hover:bg-red-100 transition-colors border border-red-200"
|
|
>
|
|
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
|
|
<span className="font-semibold text-sm">Disconnect Sam</span>
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={connectToSam}
|
|
disabled={status === ConnectionStatus.CONNECTING}
|
|
className="flex items-center gap-2 px-6 py-2 bg-slate-900 text-white rounded-full hover:bg-slate-800 transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50"
|
|
>
|
|
{status === ConnectionStatus.CONNECTING ? (
|
|
<span className="flex items-center gap-2">
|
|
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
|
Connecting...
|
|
</span>
|
|
) : (
|
|
<>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
|
<span>Call Sam</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Content Area */}
|
|
<div className="flex-1 p-8 overflow-hidden flex gap-6">
|
|
|
|
{/* Main Content based on selection */}
|
|
<div className="flex-1 flex flex-col gap-6 h-full overflow-hidden">
|
|
{activeMenuId === 'dashboard' && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
|
|
<div className="text-slate-500 text-sm mb-1">Total Revenue</div>
|
|
<div className="text-3xl font-bold text-slate-900">$124,500</div>
|
|
<div className="text-green-500 text-sm font-medium mt-2">+12.5% from last month</div>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
|
|
<div className="text-slate-500 text-sm mb-1">Active Projects</div>
|
|
<div className="text-3xl font-bold text-slate-900">8</div>
|
|
<div className="text-slate-400 text-sm font-medium mt-2">2 pending approval</div>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
|
|
<div className="text-slate-500 text-sm mb-1">Team Efficiency</div>
|
|
<div className="text-3xl font-bold text-slate-900">94%</div>
|
|
<div className="text-indigo-500 text-sm font-medium mt-2">Top 5% in sector</div>
|
|
</div>
|
|
|
|
{/* Placeholder for chart */}
|
|
<div className="col-span-full bg-white p-6 rounded-2xl shadow-sm border border-slate-200 h-64 flex items-center justify-center text-slate-400 bg-slate-50/50">
|
|
Analytics Chart Placeholder
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(activeMenuId === 'files' || activeMenuId !== 'dashboard') && (
|
|
<FileList files={MOCK_FILES} filter={fileFilter} />
|
|
)}
|
|
</div>
|
|
|
|
{/* AI Assistant Overlay / Side Panel */}
|
|
<div className="w-80 flex flex-col gap-4">
|
|
<div className={`rounded-3xl p-6 shadow-xl transition-all duration-500 border relative overflow-hidden flex flex-col items-center justify-center
|
|
${status === ConnectionStatus.CONNECTED ? 'bg-gradient-to-br from-indigo-900 to-slate-900 border-indigo-500/50 h-64' : 'bg-white border-slate-200 h-48'}
|
|
`}>
|
|
{/* Visualizer Background */}
|
|
{status === ConnectionStatus.CONNECTED && (
|
|
<div className="absolute inset-0 opacity-40">
|
|
<Visualizer analyser={analyser} active={true} />
|
|
</div>
|
|
)}
|
|
|
|
<div className="relative z-10 flex flex-col items-center text-center">
|
|
<div className={`w-16 h-16 rounded-full flex items-center justify-center mb-4 transition-all duration-300 ${status === ConnectionStatus.CONNECTED ? 'bg-white text-indigo-600 shadow-[0_0_30px_rgba(255,255,255,0.3)]' : 'bg-slate-100 text-slate-400'}`}>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
|
</div>
|
|
|
|
<h3 className={`text-lg font-bold mb-1 ${status === ConnectionStatus.CONNECTED ? 'text-white' : 'text-slate-800'}`}>
|
|
{status === ConnectionStatus.CONNECTED ? 'Sam is Listening' : 'Sam is Idle'}
|
|
</h3>
|
|
<p className={`text-sm ${status === ConnectionStatus.CONNECTED ? 'text-indigo-200' : 'text-slate-500'}`}>
|
|
{status === ConnectionStatus.CONNECTED ? 'Say "Find the quarterly report"' : 'Click "Call Sam" to start'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Suggestions */}
|
|
<div className="bg-white rounded-2xl p-6 shadow-sm border border-slate-200 flex-1">
|
|
<h4 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Try Asking</h4>
|
|
<div className="space-y-3">
|
|
<button className="w-full text-left p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors text-sm text-slate-700 flex items-center gap-3 group">
|
|
<span className="w-6 h-6 rounded-full bg-white border border-slate-200 flex items-center justify-center text-indigo-500 group-hover:scale-110 transition-transform">?</span>
|
|
"Show me the finance dashboard"
|
|
</button>
|
|
<button className="w-full text-left p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors text-sm text-slate-700 flex items-center gap-3 group">
|
|
<span className="w-6 h-6 rounded-full bg-white border border-slate-200 flex items-center justify-center text-indigo-500 group-hover:scale-110 transition-transform">?</span>
|
|
"Find the Q3 report from October"
|
|
</button>
|
|
<button className="w-full text-left p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors text-sm text-slate-700 flex items-center gap-3 group">
|
|
<span className="w-6 h-6 rounded-full bg-white border border-slate-200 flex items-center justify-center text-indigo-500 group-hover:scale-110 transition-transform">?</span>
|
|
"Settings 화면으로 이동해줘"
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{errorMsg && (
|
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 bg-red-600 text-white px-6 py-3 rounded-full shadow-lg z-50 animate-bounce">
|
|
{errorMsg}
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|