레거시 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>
579 lines
31 KiB
PHP
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
|