Files
sam-kd/ai_sam/ref/App.tsx
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

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;