| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> |
| <title>Gemma Avatar - Chat & Voice</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| <link |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Geist+Mono:wght@500&display=swap" |
| rel="stylesheet" |
| /> |
| <link rel="stylesheet" href="./src/style.css" /> |
| <script type="importmap"> |
| { |
| "imports": { |
| "three": "https://esm.sh/three@0.180.0", |
| "three/": "https://esm.sh/three@0.180.0/", |
| "@met4citizen/talkinghead": "https://esm.sh/@met4citizen/talkinghead@1.7.0" |
| } |
| } |
| </script> |
| </head> |
| <body> |
| <main id="app"> |
| <div id="stage" aria-label="3D avatar"></div> |
| <div id="loading">Loading avatar…</div> |
| <div id="start-progress" class="start-progress"> |
| <div class="start-progress__msg">Chờ tôi một lát! Tôi sẽ đến với bạn ngay!</div> |
| <div class="start-progress__bar"> |
| <div class="start-progress__fill" style="width: 0%"></div> |
| </div> |
| </div> |
|
|
| <header id="topbar"> |
| <div id="identity"> |
| <h1>Gemma Avatar</h1> |
| <p>Chat or talk to <strong>Gemma 4</strong>. Text → Qwen3-TTS → lip-sync avatar.</p> |
| </div> |
| <button id="settings-btn" class="icon-btn" aria-label="Settings" title="Settings"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
| <circle cx="12" cy="12" r="3"></circle> |
| <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path> |
| </svg> |
| </button> |
| </header> |
|
|
| <div id="subtitles" aria-live="polite"></div> |
|
|
| <div id="text-chat" hidden> |
| <div id="chat-header"> |
| <span id="chat-title">Text Chat</span> |
| <div id="chat-header-actions"> |
| <select id="chat-avatar-select" title="Switch avatar"></select> |
| <img id="avatar-thumb-preview" class="avatar-thumb-preview" alt="" hidden /> |
| <button id="chat-close-btn" class="icon-btn small" aria-label="Close text chat" title="Close"> |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="18" y1="6" x2="6" y2="18"></line> |
| <line x1="6" y1="6" x2="18" y2="18"></line> |
| </svg> |
| </button> |
| </div> |
| </div> |
| <div id="chat-messages"></div> |
| <div id="chat-input-container"> |
| <input id="chat-input" type="text" placeholder="Type a message..." autocomplete="off" /> |
| <button id="chat-mic-btn" class="icon-btn" aria-label="Switch to voice chat" title="Chuyển sang chat giọng nói" hidden> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path> |
| <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> |
| <line x1="12" y1="19" x2="12" y2="23"></line> |
| <line x1="8" y1="23" x2="16" y2="23"></line> |
| </svg> |
| </button> |
| <button id="chat-send-btn" class="icon-btn" aria-label="Send message"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="22" y1="2" x2="11" y2="13"></line> |
| <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> |
| </svg> |
| </button> |
| </div> |
| <div id="chat-resize-handle"></div> |
| </div> |
|
|
| |
| <div id="news-panel" hidden> |
| <div id="news-header"> |
| <span id="news-title">📰 Tin tức</span> |
| <button id="news-close-btn" class="icon-btn small" aria-label="Close news" title="Close"> |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="18" y1="6" x2="6" y2="18"></line> |
| <line x1="6" y1="6" x2="18" y2="18"></line> |
| </svg> |
| </button> |
| </div> |
| <div id="news-list"></div> |
| <div id="news-loadmore" hidden> |
| <button id="news-loadmore-btn" class="news-loadmore-btn">Xem thêm tin tức ↓</button> |
| <div id="news-sentinel"></div> |
| </div> |
| <div id="news-resize-handle"></div> |
| </div> |
|
|
| |
| <div id="catalogue-panel"> |
| <div class="catalogue-header"> |
| <span style="position:relative;display:flex;width:100%"> |
| <i class="fas fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--g);font-size:14px"></i> |
| <input id="cat-search" type="text" placeholder="Tìm sản phẩm trong catalogue... (tên, mã, thông số)" oninput="debounceCatSearch()" onkeydown="if(event.key==='Enter')doCatSearch()"/> |
| </span> |
| <button id="cat-close" class="catalogue-close">Đóng</button> |
| </div> |
| <div class="catalogue-filters"> |
| <select id="cat-filter-brand" onchange="doCatSearch()"><option value="all">Tất cả thương hiệu</option></select> |
| <select id="cat-filter-cat" onchange="doCatSearch()"><option value="all">Tất cả danh mục</option></select> |
| <span id="cat-count" class="catalogue-count"></span> |
| </div> |
| <div class="catalogue-grid" id="cat-grid"></div> |
| </div> |
|
|
| <footer id="controls"> |
| <div id="caption">LOADING…</div> |
| <div id="buttons"> |
| <button id="main-btn" disabled> |
| <span id="main-btn-label">Loading…</span> |
| </button> |
| <button id="mute-btn" class="icon-btn" hidden aria-label="Mute microphone" title="Mute microphone"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
| <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path> |
| <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> |
| <line x1="12" y1="19" x2="12" y2="23"></line> |
| <line x1="8" y1="23" x2="16" y2="23"></line> |
| </svg> |
| </button> |
| <button id="text-mode-btn" class="icon-btn" hidden aria-label="Text chat mode" title="Text chat mode"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> |
| </svg> |
| </button> |
| <button id="snap-btn" class="icon-btn" aria-label="Chụp ảnh đại diện" title="Chụp PNG"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path> |
| <circle cx="12" cy="13" r="4"></circle> |
| </svg> |
| </button> |
| <button id="catalogue-btn" class="icon-btn" aria-label="Product catalogue" title="Product catalogue"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path> |
| <line x1="3" y1="6" x2="21" y2="6"></line> |
| <path d="M16 10a4 4 0 0 1-8 0"></path> |
| </svg> |
| </button> |
| <button id="news-toggle-btn" class="icon-btn" aria-label="Toggle news" title="Tin tức"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"></path> |
| <path d="M18 14h-8M15 18h-5M10 6h8v4h-8V6Z"></path> |
| </svg> |
| </button> |
| </div> |
| <dialog id="snap-dialog"> |
| <form method="dialog"> |
| <h2>📸 Ảnh đại diện</h2> |
| <p id="snap-preview-wrap"><img id="snap-preview" alt="preview" /></p> |
| <div class="dialog-actions"> |
| <button id="snap-download" class="primary" type="button">⬇️ Tải về PNG</button> |
| <button id="snap-setavatar" type="button">🖼️ Đặt làm ảnh đại diện</button> |
| <button value="cancel">Đóng</button> |
| </div> |
| </form> |
| </dialog> |
| </footer> |
|
|
| <dialog id="settings"> |
| <form method="dialog"> |
| <h2>Settings</h2> |
| <label>Voice<select id="voice"></select></label> |
| <label>Avatar model<select id="settings-avatar-select"></select></label> |
| <label class="check-row"><input type="checkbox" id="subtitles-toggle" />Show subtitles while she speaks</label> |
| <label>Extra instructions<textarea id="instructions" rows="4" placeholder="Anything you want her to know or do (persona stays)."></textarea></label> |
| <label id="direct-url-row" hidden>Direct server URL<input id="direct-url" type="url" placeholder="ws://localhost:8080/v1/realtime" spellcheck="false" /></label> |
| <div class="dialog-actions"><button value="close" class="primary">Done</button></div> |
| </form> |
| </dialog> |
| |
|
|
| </main> |
| <script type="module" src="./src/app.js"></script> |
| </body> |
| </html> |