Files
sam-manage/resources/views/lab/ai/sam-ai-menu.blade.php
kent 5d0f2d1346 feat(lab): SAM AI 음성 어시스턴트 레거시 마이그레이션
레거시 5130.sam.kr/ai_sam의 Google Gemini Live API 음성 어시스턴트를
MNG 프로젝트로 이전 (React → Pure JS + Blade)

변경 내용:
- GeminiController: API 키 제공 엔드포인트 추가
- sam-ai-live.js: LiveManager, AudioVisualizer ES 모듈
- sam-ai-menu.blade.php: 전면 재작성 (Tailwind UI)
- 환경변수: GEMINI_API_KEY, GEMINI_PROJECT_ID 추가

기능:
- 실시간 음성 입출력 (WebAudio API)
- UI 도구: navigateToPage, searchDocuments
- 오디오 시각화 (Canvas API)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 02:13:20 +09:00

579 lines
31 KiB
PHP

@extends('layouts.presentation')
@section('title', 'SAM AI - 음성 어시스턴트')
@push('styles')
<style>
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
.sam-container { display: flex; height: 100vh; width: 100%; background-color: #f8fafc; }
/* Sidebar */
.sam-sidebar { width: 16rem; background-color: #0f172a; color: white; height: 100vh; display: flex; flex-direction: column; border-right: 1px solid #1e293b; }
.sam-sidebar-header { padding: 1.5rem; display: flex; align-items: center; gap: 0.75rem; }
.sam-sidebar-logo { width: 2rem; height: 2rem; border-radius: 0.5rem; background: linear-gradient(135deg, #6366f1, #9333ea); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem; }
.sam-sidebar-title { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; }
.sam-sidebar-nav { flex: 1; padding: 1.5rem 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
.sam-menu-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; border-radius: 0.75rem; transition: all 0.2s; cursor: pointer; color: #94a3b8; }
.sam-menu-item:hover { background-color: #1e293b; color: white; }
.sam-menu-item.active { background-color: #4f46e5; color: white; box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3); }
.sam-menu-item.active .sam-menu-indicator { display: block; }
.sam-menu-icon { width: 1.25rem; height: 1.25rem; }
.sam-menu-label { font-weight: 500; }
.sam-menu-indicator { display: none; margin-left: auto; width: 0.375rem; height: 0.375rem; border-radius: 9999px; background-color: white; box-shadow: 0 0 8px rgba(255,255,255,0.8); }
.sam-sidebar-footer { padding: 1rem; border-top: 1px solid #1e293b; }
.sam-user-info { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border-radius: 0.5rem; background-color: rgba(30, 41, 59, 0.5); }
.sam-user-avatar { width: 2rem; height: 2rem; border-radius: 9999px; background-color: #334155; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; }
.sam-user-name { font-size: 0.875rem; font-weight: 500; }
.sam-user-role { font-size: 0.75rem; color: #94a3b8; }
/* Main Content */
.sam-main { flex: 1; display: flex; flex-direction: column; height: 100vh; overflow: hidden; position: relative; }
.sam-header { height: 4rem; background-color: white; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: space-between; padding: 0 2rem; z-index: 10; }
.sam-header-title { font-size: 1.25rem; font-weight: 700; color: #1e293b; }
.sam-header-actions { display: flex; align-items: center; gap: 1rem; }
.sam-error-msg { color: #ef4444; font-size: 0.875rem; font-weight: 500; }
.sam-btn { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.5rem; border-radius: 9999px; font-weight: 600; font-size: 0.875rem; transition: all 0.2s; cursor: pointer; border: none; }
.sam-btn-primary { background-color: #0f172a; color: white; box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.2); }
.sam-btn-primary:hover { background-color: #1e293b; }
.sam-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.sam-btn-danger { background-color: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
.sam-btn-danger:hover { background-color: #fee2e2; }
.sam-btn-icon { width: 1.125rem; height: 1.125rem; }
.sam-status-dot { width: 0.5rem; height: 0.5rem; border-radius: 9999px; }
.sam-status-dot.connecting { background-color: #eab308; animation: pulse 1s infinite; }
.sam-status-dot.connected { background-color: #dc2626; animation: pulse 1s infinite; }
/* Content Area */
.sam-content { flex: 1; padding: 2rem; overflow: hidden; display: flex; gap: 1.5rem; }
.sam-content-main { flex: 1; display: flex; flex-direction: column; gap: 1.5rem; height: 100%; overflow: hidden; }
.sam-content-sidebar { width: 20rem; display: flex; flex-direction: column; gap: 1rem; }
/* Dashboard Cards */
.sam-dashboard { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.sam-card { background-color: white; padding: 1.5rem; border-radius: 1rem; box-shadow: 0 1px 2px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; }
.sam-card-label { color: #64748b; font-size: 0.875rem; margin-bottom: 0.25rem; }
.sam-card-value { font-size: 1.875rem; font-weight: 700; color: #0f172a; }
.sam-card-change { font-size: 0.875rem; font-weight: 500; margin-top: 0.5rem; }
.sam-card-change.positive { color: #22c55e; }
.sam-card-change.neutral { color: #94a3b8; }
.sam-card-change.highlight { color: #6366f1; }
.sam-chart-placeholder { grid-column: span 3; height: 16rem; display: flex; align-items: center; justify-content: center; color: #94a3b8; background-color: rgba(248, 250, 252, 0.5); }
/* File List */
.sam-file-list { background-color: white; border-radius: 1rem; box-shadow: 0 1px 2px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; overflow: hidden; display: flex; flex-direction: column; height: 100%; }
.sam-file-header { padding: 1rem 1.5rem; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; }
.sam-file-title { font-size: 1.125rem; font-weight: 600; color: #1e293b; }
.sam-filter-badge { padding: 0.25rem 0.5rem; background-color: #eef2ff; color: #4338ca; font-size: 0.75rem; border-radius: 9999px; font-weight: 500; }
.sam-file-body { flex: 1; overflow-y: auto; padding: 0.5rem; }
.sam-file-table { width: 100%; text-align: left; font-size: 0.875rem; color: #475569; }
.sam-file-table thead { font-size: 0.75rem; text-transform: uppercase; background-color: #f8fafc; color: #64748b; font-weight: 600; }
.sam-file-table th { padding: 0.75rem 1rem; }
.sam-file-table th:first-child { border-radius: 0.5rem 0 0 0.5rem; }
.sam-file-table th:last-child { border-radius: 0 0.5rem 0.5rem 0; }
.sam-file-table tbody tr { border-bottom: 1px solid #f8fafc; transition: background-color 0.2s; }
.sam-file-table tbody tr:hover { background-color: #f8fafc; }
.sam-file-table tbody tr:last-child { border-bottom: none; }
.sam-file-table td { padding: 0.75rem 1rem; }
.sam-file-name { font-weight: 500; color: #0f172a; display: flex; align-items: center; gap: 0.5rem; }
.sam-file-icon { width: 2rem; height: 2rem; border-radius: 0.25rem; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; }
.sam-file-icon.pdf { background-color: #fee2e2; color: #dc2626; }
.sam-file-icon.xlsx { background-color: #dcfce7; color: #16a34a; }
.sam-file-icon.docx { background-color: #dbeafe; color: #2563eb; }
.sam-file-icon.pptx { background-color: #ffedd5; color: #ea580c; }
.sam-file-icon.folder { background-color: #f1f5f9; color: #475569; }
.sam-file-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; }
.sam-file-tag { padding: 0.125rem 0.5rem; background-color: #f1f5f9; border-radius: 9999px; font-size: 0.75rem; color: #64748b; }
.sam-file-empty { padding: 2rem 1rem; text-align: center; color: #94a3b8; }
/* Sam Panel */
.sam-panel { border-radius: 1.5rem; padding: 1.5rem; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); transition: all 0.5s; border: 1px solid; position: relative; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.sam-panel.idle { background-color: white; border-color: #e2e8f0; height: 12rem; }
.sam-panel.active { background: linear-gradient(135deg, #312e81, #0f172a); border-color: rgba(99, 102, 241, 0.5); height: 16rem; }
.sam-visualizer { position: absolute; inset: 0; opacity: 0.4; }
.sam-panel-content { position: relative; z-index: 10; display: flex; flex-direction: column; align-items: center; text-align: center; }
.sam-panel-icon { width: 4rem; height: 4rem; border-radius: 9999px; display: flex; align-items: center; justify-content: center; margin-bottom: 1rem; transition: all 0.3s; }
.sam-panel.idle .sam-panel-icon { background-color: #f1f5f9; color: #94a3b8; }
.sam-panel.active .sam-panel-icon { background-color: white; color: #4f46e5; box-shadow: 0 0 30px rgba(255,255,255,0.3); }
.sam-panel-title { font-size: 1.125rem; font-weight: 700; margin-bottom: 0.25rem; }
.sam-panel.idle .sam-panel-title { color: #1e293b; }
.sam-panel.active .sam-panel-title { color: white; }
.sam-panel-subtitle { font-size: 0.875rem; }
.sam-panel.idle .sam-panel-subtitle { color: #64748b; }
.sam-panel.active .sam-panel-subtitle { color: #a5b4fc; }
/* Try Asking */
.sam-suggestions { background-color: white; border-radius: 1rem; padding: 1.5rem; box-shadow: 0 1px 2px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; flex: 1; }
.sam-suggestions-title { font-size: 0.75rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1rem; }
.sam-suggestions-list { display: flex; flex-direction: column; gap: 0.75rem; }
.sam-suggestion { width: 100%; text-align: left; padding: 0.75rem; border-radius: 0.75rem; background-color: #f8fafc; transition: background-color 0.2s; font-size: 0.875rem; color: #334155; display: flex; align-items: center; gap: 0.75rem; cursor: pointer; border: none; }
.sam-suggestion:hover { background-color: #f1f5f9; }
.sam-suggestion-icon { width: 1.5rem; height: 1.5rem; border-radius: 9999px; background-color: white; border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; color: #6366f1; transition: transform 0.2s; font-size: 0.75rem; }
.sam-suggestion:hover .sam-suggestion-icon { transform: scale(1.1); }
/* Error Toast */
.sam-toast { position: absolute; bottom: 2rem; left: 50%; transform: translateX(-50%); background-color: #dc2626; color: white; padding: 0.75rem 1.5rem; border-radius: 9999px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); z-index: 50; animation: bounce 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes bounce { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(-10px); } }
</style>
@endpush
@section('content')
<div class="sam-container">
<!-- Sidebar -->
<div class="sam-sidebar">
<div class="sam-sidebar-header">
<div class="sam-sidebar-logo">S</div>
<span class="sam-sidebar-title">Sam AI</span>
</div>
<nav class="sam-sidebar-nav" id="sam-nav">
<div class="sam-menu-item active" data-page="dashboard">
<svg class="sam-menu-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>
<span class="sam-menu-label">Dashboard</span>
<div class="sam-menu-indicator"></div>
</div>
<div class="sam-menu-item" data-page="files">
<svg class="sam-menu-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-3.25 7a2 2 0 0 1-1.8 1.18H7a2 2 0 0 1-2-2Z"/><path d="M3 7v10a2 2 0 0 0 2 2h2"/><path d="M3 7l2.6-2.6A2 2 0 0 1 7 3h7a2 2 0 0 1 2 2v2"/></svg>
<span class="sam-menu-label">My Files</span>
<div class="sam-menu-indicator"></div>
</div>
<div class="sam-menu-item" data-page="analytics">
<svg class="sam-menu-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>
<span class="sam-menu-label">Analytics</span>
<div class="sam-menu-indicator"></div>
</div>
<div class="sam-menu-item" data-page="settings">
<svg class="sam-menu-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.72v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
<span class="sam-menu-label">Settings</span>
<div class="sam-menu-indicator"></div>
</div>
<div class="sam-menu-item" data-page="team">
<svg class="sam-menu-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span class="sam-menu-label">Team Directory</span>
<div class="sam-menu-indicator"></div>
</div>
<div class="sam-menu-item" data-page="calendar">
<svg class="sam-menu-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>
<span class="sam-menu-label">Calendar</span>
<div class="sam-menu-indicator"></div>
</div>
</nav>
<div class="sam-sidebar-footer">
<div class="sam-user-info">
<div class="sam-user-avatar">{{ mb_substr(auth()->user()->name ?? 'U', 0, 1) }}</div>
<div>
<div class="sam-user-name">{{ auth()->user()->name ?? 'User' }}</div>
<div class="sam-user-role">Pro Member</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<main class="sam-main">
<header class="sam-header">
<h1 class="sam-header-title" id="page-title">Dashboard</h1>
<div class="sam-header-actions">
<span class="sam-error-msg" id="error-msg" style="display: none;"></span>
<button class="sam-btn sam-btn-danger" id="btn-disconnect" style="display: none;">
<span class="sam-status-dot connected"></span>
<span>Disconnect Sam</span>
</button>
<button class="sam-btn sam-btn-primary" id="btn-connect">
<svg class="sam-btn-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><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 id="btn-connect-text">Call Sam</span>
</button>
</div>
</header>
<div class="sam-content">
<div class="sam-content-main">
<!-- Dashboard View -->
<div id="view-dashboard" class="sam-dashboard">
<div class="sam-card">
<div class="sam-card-label">Total Revenue</div>
<div class="sam-card-value">$124,500</div>
<div class="sam-card-change positive">+12.5% from last month</div>
</div>
<div class="sam-card">
<div class="sam-card-label">Active Projects</div>
<div class="sam-card-value">8</div>
<div class="sam-card-change neutral">2 pending approval</div>
</div>
<div class="sam-card">
<div class="sam-card-label">Team Efficiency</div>
<div class="sam-card-value">94%</div>
<div class="sam-card-change highlight">Top 5% in sector</div>
</div>
<div class="sam-card sam-chart-placeholder">
Analytics Chart Placeholder
</div>
</div>
<!-- Files View -->
<div id="view-files" class="sam-file-list" style="display: none;">
<div class="sam-file-header">
<h2 class="sam-file-title">Recent Files</h2>
<span class="sam-filter-badge" id="filter-badge" style="display: none;"></span>
</div>
<div class="sam-file-body">
<table class="sam-file-table">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Size</th>
<th>Tags</th>
</tr>
</thead>
<tbody id="file-list-body"></tbody>
</table>
</div>
</div>
</div>
<div class="sam-content-sidebar">
<!-- Sam Panel -->
<div class="sam-panel idle" id="sam-panel">
<canvas class="sam-visualizer" id="visualizer" width="300" height="100"></canvas>
<div class="sam-panel-content">
<div class="sam-panel-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><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 class="sam-panel-title" id="panel-title">Sam is Idle</h3>
<p class="sam-panel-subtitle" id="panel-subtitle">Click "Call Sam" to start</p>
</div>
</div>
<!-- Suggestions -->
<div class="sam-suggestions">
<h4 class="sam-suggestions-title">Try Asking</h4>
<div class="sam-suggestions-list">
<button class="sam-suggestion">
<span class="sam-suggestion-icon">?</span>
"Show me the finance dashboard"
</button>
<button class="sam-suggestion">
<span class="sam-suggestion-icon">?</span>
"Find the Q3 report from October"
</button>
<button class="sam-suggestion">
<span class="sam-suggestion-icon">?</span>
"Settings 화면으로 이동해줘"
</button>
</div>
</div>
</div>
</div>
<!-- Error Toast -->
<div class="sam-toast" id="error-toast" style="display: none;"></div>
</main>
</div>
@endsection
@push('scripts')
<!-- Google GenAI SDK -->
<script type="module">
const loadGenAI = async () => {
try {
const module = await import('https://aistudiocdn.com/@google/genai@^1.31.0');
window.GoogleGenAI = module.GoogleGenAI;
window.Modality = module.Modality;
window.FunctionDeclaration = module.FunctionDeclaration;
window.Type = module.Type;
window.GenAILoaded = true;
window.dispatchEvent(new Event('genai-loaded'));
} catch (error) {
console.error('Failed to load Google GenAI SDK:', error);
window.GenAILoaded = false;
}
};
loadGenAI();
</script>
<script type="module">
import { LiveManager, AudioVisualizer, ConnectionStatus } from '/js/sam-ai-live.js';
// ========================================================================
// Mock Data
// ========================================================================
const MOCK_FILES = [
{ id: '1', name: 'Q3_Financial_Report.xlsx', type: 'xlsx', date: '2023-10-24', size: '2.4 MB', tags: ['finance', 'report', 'q3'] },
{ id: '2', name: 'Project_Alpha_Overview.pptx', type: 'pptx', date: '2023-10-22', size: '15 MB', tags: ['project', 'alpha', 'presentation'] },
{ id: '3', name: 'Employee_Handbook_2024.pdf', type: 'pdf', date: '2023-11-01', size: '4.1 MB', tags: ['hr', 'policy'] },
{ id: '4', name: 'Marketing_Strategy_Draft.docx', type: 'docx', date: '2023-10-28', size: '1.2 MB', tags: ['marketing', 'draft'] },
{ id: '5', name: 'Engineering_Sync_Notes.docx', type: 'docx', date: '2023-11-05', size: '0.5 MB', tags: ['engineering', 'meeting'] },
{ id: '6', name: 'Q4_Projections.xlsx', type: 'xlsx', date: '2023-11-02', size: '1.8 MB', tags: ['finance', 'forecast'] },
{ id: '7', name: 'Client_List_APAC.xlsx', type: 'xlsx', date: '2023-09-15', size: '3.2 MB', tags: ['sales', 'apac'] },
{ id: '8', name: 'Logo_Assets.folder', type: 'folder', date: '2023-08-10', size: '--', tags: ['design', 'brand'] },
];
const MENU_LABELS = {
dashboard: 'Dashboard',
files: 'My Files',
analytics: 'Analytics',
settings: 'Settings',
team: 'Team Directory',
calendar: 'Calendar',
};
// ========================================================================
// State
// ========================================================================
let activeMenuId = 'dashboard';
let fileFilter = '';
let apiKey = null;
let liveManager = null;
let visualizer = null;
// ========================================================================
// DOM Elements
// ========================================================================
const elements = {
nav: document.getElementById('sam-nav'),
pageTitle: document.getElementById('page-title'),
btnConnect: document.getElementById('btn-connect'),
btnConnectText: document.getElementById('btn-connect-text'),
btnDisconnect: document.getElementById('btn-disconnect'),
errorMsg: document.getElementById('error-msg'),
errorToast: document.getElementById('error-toast'),
viewDashboard: document.getElementById('view-dashboard'),
viewFiles: document.getElementById('view-files'),
filterBadge: document.getElementById('filter-badge'),
fileListBody: document.getElementById('file-list-body'),
samPanel: document.getElementById('sam-panel'),
panelTitle: document.getElementById('panel-title'),
panelSubtitle: document.getElementById('panel-subtitle'),
visualizerCanvas: document.getElementById('visualizer'),
};
// ========================================================================
// UI Functions
// ========================================================================
function updateActiveMenu(pageId) {
activeMenuId = pageId;
document.querySelectorAll('.sam-menu-item').forEach(item => {
item.classList.toggle('active', item.dataset.page === pageId);
});
elements.pageTitle.textContent = MENU_LABELS[pageId] || 'Dashboard';
// Show/hide views
elements.viewDashboard.style.display = pageId === 'dashboard' ? 'grid' : 'none';
elements.viewFiles.style.display = pageId !== 'dashboard' ? 'flex' : 'none';
}
function updateFileList(filter = '') {
fileFilter = filter;
const filteredFiles = MOCK_FILES.filter(f =>
f.name.toLowerCase().includes(filter.toLowerCase()) ||
f.tags.some(t => t.toLowerCase().includes(filter.toLowerCase()))
);
// Update filter badge
if (filter) {
elements.filterBadge.textContent = `Filtering by: "${filter}"`;
elements.filterBadge.style.display = 'inline-block';
} else {
elements.filterBadge.style.display = 'none';
}
// Render file list
if (filteredFiles.length === 0) {
elements.fileListBody.innerHTML = `
<tr>
<td colspan="4" class="sam-file-empty">No files found matching "${filter}"</td>
</tr>
`;
} else {
elements.fileListBody.innerHTML = filteredFiles.map(file => `
<tr>
<td class="sam-file-name">
<span class="sam-file-icon ${file.type}">${file.type.toUpperCase().slice(0, 3)}</span>
${file.name}
</td>
<td>${file.date}</td>
<td>${file.size}</td>
<td>
<div class="sam-file-tags">
${file.tags.map(tag => `<span class="sam-file-tag">#${tag}</span>`).join('')}
</div>
</td>
</tr>
`).join('');
}
}
function updateConnectionUI(status) {
const isConnected = status === ConnectionStatus.CONNECTED;
const isConnecting = status === ConnectionStatus.CONNECTING;
const isError = status === ConnectionStatus.ERROR;
elements.btnConnect.style.display = isConnected ? 'none' : 'flex';
elements.btnDisconnect.style.display = isConnected ? 'flex' : 'none';
elements.btnConnect.disabled = isConnecting;
elements.btnConnectText.textContent = isConnecting ? 'Connecting...' : 'Call Sam';
elements.samPanel.classList.toggle('active', isConnected);
elements.samPanel.classList.toggle('idle', !isConnected);
elements.panelTitle.textContent = isConnected ? 'Sam is Listening' : 'Sam is Idle';
elements.panelSubtitle.textContent = isConnected ? 'Say "Find the quarterly report"' : 'Click "Call Sam" to start';
if (isError) {
elements.errorMsg.textContent = 'Connection Error';
elements.errorMsg.style.display = 'inline';
} else {
elements.errorMsg.style.display = 'none';
}
// Visualizer
if (visualizer) {
visualizer.setActive(isConnected);
if (isConnected && liveManager) {
visualizer.setAnalyser(liveManager.getOutputAnalyser());
}
}
}
function showError(message) {
elements.errorToast.textContent = message;
elements.errorToast.style.display = 'block';
setTimeout(() => {
elements.errorToast.style.display = 'none';
}, 5000);
}
// ========================================================================
// Tool Handler
// ========================================================================
async function handleToolCall(name, args) {
console.log("Tool Call:", name, args);
if (name === 'navigateToPage') {
const pageId = args.pageId.toLowerCase();
const exists = Object.keys(MENU_LABELS).includes(pageId);
if (exists) {
updateActiveMenu(pageId);
if (pageId !== 'dashboard') {
updateFileList(fileFilter);
}
return { success: true, message: `Navigated to ${pageId}` };
} else {
return { success: false, message: `Page ${pageId} not found` };
}
} else if (name === 'searchDocuments') {
updateFileList(args.query);
updateActiveMenu('files');
return { success: true, count: 5 };
}
return { error: 'Unknown tool' };
}
// ========================================================================
// Connection Functions
// ========================================================================
async function connectToSam() {
if (!apiKey) {
showError("API Key가 설정되지 않았습니다.");
return;
}
if (liveManager) {
liveManager.disconnect();
}
liveManager = new LiveManager(
apiKey,
handleToolCall,
(status) => {
console.log('Status changed:', status);
updateConnectionUI(status);
},
(level) => {
// Audio level callback (for future use)
}
);
try {
await liveManager.connect();
} catch (err) {
console.error('Connection failed:', err);
showError('연결에 실패했습니다: ' + err.message);
}
}
function disconnect() {
if (liveManager) {
liveManager.disconnect();
liveManager = null;
}
}
// ========================================================================
// Initialize
// ========================================================================
async function init() {
// Initialize visualizer
visualizer = new AudioVisualizer(elements.visualizerCanvas);
visualizer.start();
// Load API key
try {
const response = await fetch('/api/gemini/api-key');
const data = await response.json();
if (data.success && data.apiKey) {
apiKey = data.apiKey;
console.log('API Key loaded successfully');
} else {
showError(data.error || 'API Key를 가져올 수 없습니다.');
}
} catch (err) {
console.error('API Key fetch error:', err);
showError('API Key를 가져오는 중 오류가 발생했습니다.');
}
// Initial file list
updateFileList('');
// Event listeners
elements.nav.addEventListener('click', (e) => {
const menuItem = e.target.closest('.sam-menu-item');
if (menuItem) {
const pageId = menuItem.dataset.page;
updateActiveMenu(pageId);
if (pageId !== 'dashboard') {
updateFileList(fileFilter);
}
}
});
elements.btnConnect.addEventListener('click', connectToSam);
elements.btnDisconnect.addEventListener('click', disconnect);
}
// Start
init();
</script>
@endpush