SignApp / src /sign_app /ui /index.html
SearingShot's picture
Deploy SignApp main app
bb5dd0a
Raw
History Blame Contribute Delete
16.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SignApp - Speech to Sign Language</title>
<meta name="description" content="Real-time speech to sign language avatar powered by AI">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.158.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.158.0/examples/jsm/"
}
}
</script>
<style>
:root {
--bg-color-top: #0c0a1a;
--bg-color-mid: #1a1333;
--bg-color-bot: #0d1b2a;
--text-main: #e2e8f0;
--text-main-muted: #cbd5e1;
--text-muted: #64748b;
--topbar-bg: rgba(12,10,26,0.9);
--bottom-panel-bg1: rgba(12,10,26,0.85);
--bottom-panel-bg2: rgba(12,10,26,0.4);
--chip-bg: rgba(255,255,255,0.06);
--chip-border: rgba(255,255,255,0.08);
--input-bg: rgba(255,255,255,0.05);
--input-border: rgba(255,255,255,0.1);
--input-focus: rgba(167,139,250,0.4);
--accent-color: #a78bfa;
--accent-hover: rgba(167,139,250,0.3);
--btn-bg: rgba(167,139,250,0.2);
--btn-border: rgba(167,139,250,0.3);
--btn-text: #c4b5fd;
--btn-hover-bg: rgba(167,139,250,0.3);
--btn-hover-text: #fff;
--btn-sec-bg: rgba(255,255,255,0.05);
--btn-sec-border: rgba(255,255,255,0.1);
--btn-sec-hover-bg: rgba(255,255,255,0.1);
--btn-sec-hover-border: rgba(255,255,255,0.2);
--btn-sec-text: #94a3b8;
--btn-sec-hover-text: #fff;
--status-dot-processing: #f59e0b;
--gloss-token-bg: rgba(167,139,250,0.15);
--gloss-token-border: rgba(167,139,250,0.25);
--gloss-token-text: #c4b5fd;
--gloss-token-active-bg: rgba(167,139,250,0.35);
--gloss-token-active-border: #a78bfa;
--gloss-token-active-text: #fff;
}
.light-mode {
--bg-color-top: #f8fafc;
--bg-color-mid: #e2e8f0;
--bg-color-bot: #cbd5e1;
--text-main: #1e293b;
--text-main-muted: #334155;
--text-muted: #475569;
--topbar-bg: rgba(248, 250, 252, 0.9);
--bottom-panel-bg1: rgba(248, 250, 252, 0.95);
--bottom-panel-bg2: rgba(248, 250, 252, 0.6);
--chip-bg: rgba(0,0,0,0.05);
--chip-border: rgba(0,0,0,0.1);
--input-bg: rgba(0,0,0,0.05);
--input-border: rgba(0,0,0,0.1);
--input-focus: rgba(124,58,237,0.4);
--accent-color: #7c3aed;
--accent-hover: rgba(124, 58, 237, 0.15);
--btn-bg: rgba(124, 58, 237, 0.1);
--btn-border: rgba(124, 58, 237, 0.2);
--btn-text: #6d28d9;
--btn-hover-bg: rgba(124, 58, 237, 0.2);
--btn-hover-text: #1e293b;
--btn-sec-bg: rgba(0,0,0,0.05);
--btn-sec-border: rgba(0,0,0,0.1);
--btn-sec-hover-bg: rgba(0,0,0,0.1);
--btn-sec-hover-border: rgba(0,0,0,0.2);
--btn-sec-text: #475569;
--btn-sec-hover-text: #1e293b;
--status-dot-processing: #d97706;
--gloss-token-bg: rgba(124, 58, 237, 0.1);
--gloss-token-border: rgba(124, 58, 237, 0.2);
--gloss-token-text: #6d28d9;
--gloss-token-active-bg: rgba(124, 58, 237, 0.25);
--gloss-token-active-border: #7c3aed;
--gloss-token-active-text: #1e293b;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, var(--bg-color-top) 0%, var(--bg-color-mid) 40%, var(--bg-color-bot) 100%);
color: var(--text-main);
overflow: hidden;
height: 100vh;
width: 100vw;
}
/* ── 3D Canvas ─────────────────────────────── */
#avatar-canvas {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
/* ── Top Bar ───────────────────────────────── */
#top-bar {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 28px;
background: linear-gradient(180deg, var(--topbar-bg) 0%, transparent 100%);
}
.logo {
font-weight: 700;
font-size: 20px;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
background: var(--chip-bg);
border: 1px solid var(--chip-border);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #4ade80;
transition: background 0.3s;
}
.status-dot.recording {
background: #ef4444;
animation: pulse-dot 1s ease-in-out infinite;
}
.status-dot.processing {
background: var(--status-dot-processing);
animation: pulse-dot 0.6s ease-in-out infinite;
}
.status-dot.signing {
background: var(--accent-color);
animation: pulse-dot 1.2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.3); }
}
/* ── Bottom Panel ──────────────────────────── */
#bottom-panel {
position: fixed;
bottom: 0; left: 0; right: 0;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px 28px 32px;
background: linear-gradient(0deg, var(--bottom-panel-bg1) 0%, var(--bottom-panel-bg2) 70%, transparent 100%);
pointer-events: none;
}
#bottom-panel > * {
pointer-events: auto;
}
/* ── Transcript Area ───────────────────────── */
#transcript-area {
width: 100%;
max-width: 640px;
text-align: center;
min-height: 50px;
}
.transcript-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
margin-bottom: 6px;
}
.transcript-text {
font-size: 15px;
color: var(--text-main-muted);
line-height: 1.5;
opacity: 0;
transform: translateY(8px);
transition: all 0.4s ease;
}
.transcript-text.visible {
opacity: 1;
transform: translateY(0);
}
.gloss-tokens {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
margin-top: 8px;
}
.gloss-token {
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', monospace;
background: var(--gloss-token-bg);
border: 1px solid var(--gloss-token-border);
color: var(--gloss-token-text);
transition: all 0.3s ease;
}
.gloss-token.active {
background: var(--gloss-token-active-bg);
border-color: var(--gloss-token-active-border);
color: #fff;
transform: scale(1.08);
box-shadow: 0 0 12px rgba(167,139,250,0.3);
}
.gloss-token.done {
opacity: 0.4;
}
/* ── Mic Button ────────────────────────────── */
#mic-btn {
width: 72px; height: 72px;
border-radius: 50%;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #7c3aed, #3b82f6);
box-shadow: 0 4px 24px rgba(124,58,237,0.4), 0 0 0 0 rgba(124,58,237,0);
transition: all 0.3s ease;
position: relative;
}
#mic-btn:hover {
transform: scale(1.06);
box-shadow: 0 6px 32px rgba(124,58,237,0.5);
}
#mic-btn:active { transform: scale(0.97); }
#mic-btn.recording {
background: linear-gradient(135deg, #ef4444, #dc2626);
box-shadow: 0 4px 24px rgba(239,68,68,0.4);
animation: pulse-ring 1.5s ease-out infinite;
}
#mic-btn.processing {
background: linear-gradient(135deg, #f59e0b, #d97706);
pointer-events: none;
animation: spin-slow 2s linear infinite;
}
@keyframes pulse-ring {
0% { box-shadow: 0 4px 24px rgba(239,68,68,0.4), 0 0 0 0 rgba(239,68,68,0.3); }
100% { box-shadow: 0 4px 24px rgba(239,68,68,0.4), 0 0 0 20px rgba(239,68,68,0); }
}
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.mic-icon {
width: 28px; height: 28px;
fill: white;
}
/* ── Audio Waveform Visualizer ──────────────── */
#waveform {
width: 200px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
opacity: 0;
transition: opacity 0.3s;
}
#waveform.active { opacity: 1; }
.wave-bar {
width: 3px;
height: 8px;
border-radius: 2px;
background: var(--accent-color);
transition: height 0.1s ease;
}
/* ── Signing Progress ──────────────────────── */
#sign-label {
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
opacity: 0;
transition: opacity 0.3s;
}
#sign-label.visible { opacity: 1; }
/* ── Text Input Area ────────────────────────── */
#text-input-container {
width: 100%;
max-width: 500px;
display: flex;
gap: 10px;
padding: 4px;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 12px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
#text-input-container:focus-within {
background: var(--input-bg);
border-color: var(--input-focus);
box-shadow: 0 0 15px var(--accent-hover);
}
#text-input {
color: var(--text-main);
flex: 1;
background: transparent;
border: none;
outline: none;
color: #fff;
padding: 10px 16px;
font-family: inherit;
font-size: 14px;
}
#text-input::placeholder { color: var(--text-muted); }
#send-btn {
background: var(--btn-bg);
border: 1px solid var(--btn-border);
color: var(--gloss-token-text);
padding: 0 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s ease;
}
#send-btn:hover {
background: var(--btn-hover-bg);
color: #fff;
}
/* ── Controls Layout ────────────────────────── */
.controls-row {
display: flex;
align-items: center;
gap: 20px;
}
/* ── Secondary Button (Replay) ──────────────── */
.secondary-btn {
width: 48px; height: 48px;
border-radius: 50%;
border: 1px solid var(--input-border);
background: var(--input-bg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
color: var(--text-muted);
}
.secondary-btn:hover {
background: rgba(255,255,255,0.1);
border-color: rgba(255,255,255,0.2);
color: #fff;
transform: translateY(-2px);
}
.secondary-btn:active { transform: translateY(0); }
.secondary-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
transform: none;
}
.btn-icon { width: 20px; height: 20px; fill: currentColor; }
/* ── Responsive adjustments ─────────────────── */
@media (max-width: 600px), (max-height: 700px) {
.transcript-text { font-size: 13px; line-height: 1.3; }
.gloss-token { font-size: 11px; padding: 3px 8px; }
#bottom-panel { padding: 12px 16px 20px; gap: 8px; }
#text-input-container { max-width: 100%; margin-bottom: 4px; }
#mic-btn { width: 60px; height: 60px; }
#waveform { height: 30px; }
}
</style>
</head>
<body>
<!-- Top Bar -->
<div id="top-bar">
<div style="display: flex; gap: 16px; align-items: center;">
<div class="logo">SignApp</div>
<button id="theme-btn" class="secondary-btn" style="width: 36px; height: 36px;" title="Toggle Theme">
<svg class="btn-icon" viewBox="0 0 24 24" id="theme-icon" style="width: 18px; height: 18px;">
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41.39.39 1.03.39 1.41 0l1.06-1.06z"/>
</svg>
</button>
</div>
<div class="status-chip">
<span class="status-dot" id="status-dot"></span>
<span id="status-text">Ready</span>
</div>
</div>
<!-- Bottom Panel -->
<div id="bottom-panel">
<!-- Transcript -->
<div id="transcript-area">
<div class="transcript-label" id="transcript-label"></div>
<div class="transcript-text" id="transcript-text"></div>
<div class="gloss-tokens" id="gloss-tokens"></div>
</div>
<!-- Text Input -->
<div id="text-input-container">
<input type="text" id="text-input" placeholder="Or type something here..." autocomplete="off">
<button id="send-btn">Send</button>
</div>
<!-- Waveform -->
<div id="waveform"></div>
<!-- Sign Progress -->
<div id="sign-label"></div>
<div style="font-size: 10px; color: var(--text-muted); opacity: 0.8; text-align: center; max-width: 80%; margin-top: -8px; margin-bottom: 4px;">Note: Generated signs are AI approximations and may not be 100% accurate.</div>
<!-- Controls -->
<div class="controls-row">
<!-- Replay Button -->
<button id="replay-btn" class="secondary-btn" title="Replay last signs" disabled>
<svg class="btn-icon" viewBox="0 0 24 24">
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
</svg>
</button>
<!-- Microphone Button -->
<button id="mic-btn" title="Click to speak">
<svg class="mic-icon" viewBox="0 0 24 24" id="mic-svg">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
</button>
<!-- Placeholder to balance the row if needed, or other future buttons -->
<div style="width: 48px;"></div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>