diff --git a/.gitignore b/.gitignore index d37bf7a..f25012d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ frontend/dist/ # ---- 后端 ---- backend/bin/ +backend/server # ---- 环境变量 ---- .env @@ -20,3 +21,9 @@ Thumbs.db # ---- Obsidian ---- .obsidian/ + +# ---- Playwright MCP ---- +.playwright-mcp/ + +# ---- 截图 ---- +*.png diff --git a/frontend/src/App.css b/frontend/src/App.css index ca328d8..c3016ad 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,19 +1,23 @@ /* ============================================================ - CamTalk — 主应用样式 + CamTalk — Web 端应用样式 + 布局:顶部工具栏 + 左右分栏(视频 | 对话) ============================================================ */ :root { - --color-primary: #2563eb; - --color-primary-hover: #1d4ed8; - --color-bg: #0f172a; - --color-surface: #1e293b; - --color-text: #f1f5f9; - --color-text-muted: #94a3b8; - --color-border: #334155; + --color-primary: #3b82f6; + --color-primary-hover: #2563eb; + --color-primary-muted: rgba(59, 130, 246, 0.15); + --color-bg: #0b0f19; + --color-surface: #141927; + --color-surface-hover: #1c2235; + --color-text: #e2e8f0; + --color-text-muted: #64748b; + --color-border: #1e293b; --color-success: #22c55e; --color-warning: #f59e0b; --color-error: #ef4444; - --radius: 8px; + --radius: 6px; + --radius-lg: 10px; } * { @@ -23,142 +27,155 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--color-bg); color: var(--color-text); - min-height: 100vh; + height: 100vh; + overflow: hidden; } +#root { + height: 100vh; +} + +/* ---- App Shell ---- */ + .app { display: flex; flex-direction: column; - min-height: 100vh; - max-width: 900px; - margin: 0 auto; - padding: 0 16px; + height: 100vh; } -/* ---- Header ---- */ +/* ---- Toolbar ---- */ -.app-header { +.toolbar { display: flex; align-items: center; justify-content: space-between; - padding: 16px 0; + height: 48px; + padding: 0 20px; + background: var(--color-surface); border-bottom: 1px solid var(--color-border); -} - -.app-header h1 { - font-size: 1.5rem; - font-weight: 700; -} - -.header-right { - display: flex; - align-items: center; - gap: 8px; -} - -.btn-icon { - background: none; - border: none; - font-size: 1.2rem; - cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: background 0.2s; -} - -.btn-icon:hover { - background: var(--color-surface); -} - -.status { - font-size: 0.85rem; - padding: 4px 12px; - border-radius: 12px; - background: var(--color-surface); -} - -.status--connected { - color: var(--color-success); - border: 1px solid var(--color-success); -} - -.status--connecting { - color: var(--color-warning); - border: 1px solid var(--color-warning); -} - -.status--disconnected { - color: var(--color-text-muted); - border: 1px solid var(--color-border); -} - -/* ---- Main ---- */ - -.app-main { - flex: 1; - display: flex; - flex-direction: column; - gap: 16px; - padding: 16px 0; - overflow: hidden; -} - -.video-section { - position: relative; flex-shrink: 0; } -.vad-indicator { - position: absolute; - bottom: 12px; - left: 12px; - background: rgba(0, 0, 0, 0.7); - color: var(--color-success); - padding: 6px 12px; - border-radius: var(--radius); - font-size: 0.85rem; - animation: pulse 1.5s ease-in-out infinite; +.toolbar__left { + display: flex; + align-items: center; + gap: 16px; } -.vad-indicator--loading { - color: var(--color-warning); +.toolbar__logo { + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--color-text); } -.vad-indicator--error { - color: var(--color-error); - animation: none; -} - -.vad-indicator--audio { - left: auto; - right: 12px; +.toolbar__logo span { color: var(--color-primary); } -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.6; } +.toolbar__right { + display: flex; + align-items: center; + gap: 12px; } -.chat-section { +.toolbar__status { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.toolbar__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-text-muted); +} + +.toolbar__dot--connected { + background: var(--color-success); + box-shadow: 0 0 6px var(--color-success); +} + +.toolbar__dot--connecting { + background: var(--color-warning); + animation: pulse 1.5s ease-in-out infinite; +} + +.toolbar__stats { + font-size: 0.75rem; + color: var(--color-text-muted); + padding: 3px 10px; + background: var(--color-bg); + border-radius: 20px; +} + +.toolbar__btn { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border: none; + border-radius: var(--radius); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + background: transparent; + color: var(--color-text-muted); +} + +.toolbar__btn:hover { + background: var(--color-surface-hover); + color: var(--color-text); +} + +.toolbar__btn--active { + background: var(--color-primary-muted); + color: var(--color-primary); +} + +/* ---- Main Content ---- */ + +.app-content { flex: 1; display: flex; - flex-direction: column; - gap: 12px; overflow: hidden; } -/* ---- Video Preview ---- */ +/* ---- Left Panel (Video) ---- */ + +.panel-left { + display: flex; + flex-direction: column; + width: 480px; + min-width: 360px; + border-right: 1px solid var(--color-border); + background: var(--color-bg); +} + +.video-wrapper { + flex: 1; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + overflow: hidden; +} .video-preview { position: relative; width: 100%; + max-height: 100%; aspect-ratio: 4 / 3; background: var(--color-surface); - border-radius: var(--radius); + border-radius: var(--radius-lg); overflow: hidden; } @@ -172,91 +189,251 @@ body { position: absolute; inset: 0; display: flex; + flex-direction: column; align-items: center; justify-content: center; + gap: 8px; + color: var(--color-text-muted); + font-size: 0.9rem; +} + +.video-preview__placeholder svg { + width: 40px; + height: 40px; + opacity: 0.3; +} + +/* ---- Video Overlays ---- */ + +.video-badge { + position: absolute; + padding: 3px 10px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.3px; + pointer-events: none; +} + +.video-badge--hd { + top: 12px; + right: 12px; + background: rgba(59, 130, 246, 0.85); + color: white; +} + +.video-badge--observing { + top: 12px; + left: 12px; + background: rgba(34, 197, 94, 0.85); + color: white; + animation: pulse 2s ease-in-out infinite; +} + +.video-indicator { + position: absolute; + bottom: 16px; + left: 16px; + display: flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + border-radius: var(--radius); + font-size: 0.8rem; +} + +.video-indicator--speaking { + color: var(--color-success); +} + +.video-indicator--audio { + left: auto; + right: 16px; + color: var(--color-primary); +} + +.video-indicator--loading { + color: var(--color-warning); +} + +.video-indicator--error { + color: var(--color-error); +} + +.video-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.25); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-lg); +} + +.video-overlay__spinner { + width: 28px; + height: 28px; + border: 2.5px solid rgba(255, 255, 255, 0.2); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* ---- Left Panel Controls ---- */ + +.panel-controls { + display: flex; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--color-border); + background: var(--color-surface); +} + +.panel-controls .btn { + flex: 1; +} + +/* ---- Right Panel (Chat) ---- */ + +.panel-right { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + background: var(--color-bg); +} + +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 20px; + border-bottom: 1px solid var(--color-border); + background: var(--color-surface); + flex-shrink: 0; +} + +.chat-header__title { + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text); +} + +.chat-header__sub { + font-size: 0.75rem; color: var(--color-text-muted); } /* ---- Chat Panel ---- */ .chat-panel { + flex: 1; display: flex; flex-direction: column; - gap: 12px; + gap: 2px; overflow-y: auto; - flex: 1; + padding: 16px 20px; } .chat-panel--empty { display: flex; align-items: center; justify-content: center; - padding: 48px 0; + flex: 1; color: var(--color-text-muted); + font-size: 0.9rem; } .chat-message { - padding: 12px 16px; + padding: 10px 14px; border-radius: var(--radius); - background: var(--color-surface); + max-width: 85%; } .chat-message--user { - border-left: 3px solid var(--color-primary); + align-self: flex-end; + background: var(--color-primary); + color: white; } .chat-message--assistant { - border-left: 3px solid var(--color-success); + align-self: flex-start; + background: var(--color-surface); + border: 1px solid var(--color-border); } .chat-message--streaming { - opacity: 0.8; + opacity: 0.85; } .chat-message__role { - font-size: 0.75rem; + font-size: 0.65rem; font-weight: 600; text-transform: uppercase; - color: var(--color-text-muted); - margin-bottom: 4px; + letter-spacing: 0.5px; + margin-bottom: 3px; + opacity: 0.6; } .chat-message__content { - font-size: 0.95rem; - line-height: 1.5; + font-size: 0.88rem; + line-height: 1.55; white-space: pre-wrap; } -/* ---- Footer ---- */ - -.app-footer { - display: flex; - gap: 12px; - justify-content: center; - padding: 16px 0; - border-top: 1px solid var(--color-border); +.chat-message__meta { + margin-top: 5px; + font-size: 0.7rem; + opacity: 0.5; } -/* ---- Stats Bar ---- */ +.cursor { + display: inline; + animation: blink 0.8s step-end infinite; + color: var(--color-success); +} -.stats-bar { +/* ---- System Message ---- */ + +.system-message { text-align: center; - padding: 4px 0; - font-size: 0.75rem; - color: var(--color-text-muted); - border-top: 1px solid var(--color-border); + padding: 6px 14px; + border-radius: var(--radius); + font-size: 0.8rem; + margin: 4px 0; +} + +.system-message--warning { + background: rgba(245, 158, 11, 0.1); + color: var(--color-warning); + border: 1px solid rgba(245, 158, 11, 0.2); +} + +/* ---- Chat Section Wrapper ---- */ + +.chat-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; } /* ---- Buttons ---- */ .btn { - padding: 10px 24px; + padding: 8px 18px; border: none; border-radius: var(--radius); - font-size: 0.95rem; - font-weight: 600; + font-size: 0.82rem; + font-weight: 500; cursor: pointer; - transition: background 0.2s; + transition: all 0.15s; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; } .btn--primary { @@ -270,12 +447,13 @@ body { .btn--secondary { background: var(--color-surface); - color: var(--color-text); + color: var(--color-text-muted); border: 1px solid var(--color-border); } .btn--secondary:hover { - background: var(--color-border); + background: var(--color-surface-hover); + color: var(--color-text); } .btn--warning { @@ -288,59 +466,33 @@ body { } .btn--active { - background: var(--color-primary); - color: white; - box-shadow: 0 0 8px rgba(37, 99, 235, 0.5); - animation: pulseBtn 2s ease-in-out infinite; + background: var(--color-primary-muted); + color: var(--color-primary); + border: 1px solid rgba(59, 130, 246, 0.3); } -@keyframes pulseBtn { - 0%, 100% { box-shadow: 0 0 8px rgba(37, 99, 235, 0.5); } - 50% { box-shadow: 0 0 16px rgba(37, 99, 235, 0.8); } -} - -/* ---- Streaming Cursor ---- */ - -.cursor { - display: inline; - animation: blink 0.8s step-end infinite; - color: var(--color-success); -} - -@keyframes blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -/* ---- Message Meta ---- */ - -.chat-message__meta { - margin-top: 6px; - font-size: 0.75rem; +.btn--icon { + background: transparent; + border: none; color: var(--color-text-muted); -} - -/* ---- System Message ---- */ - -.system-message { - text-align: center; - padding: 8px 16px; + padding: 6px; border-radius: var(--radius); - font-size: 0.85rem; + cursor: pointer; + font-size: 1rem; + transition: all 0.15s; } -.system-message--warning { - background: rgba(245, 158, 11, 0.15); - color: var(--color-warning); - border: 1px solid rgba(245, 158, 11, 0.3); +.btn--icon:hover { + background: var(--color-surface-hover); + color: var(--color-text); } /* ---- Toast ---- */ .toast-container { position: fixed; - top: 16px; - right: 16px; + top: 60px; + right: 20px; z-index: 1000; display: flex; flex-direction: column; @@ -350,10 +502,11 @@ body { .toast { padding: 10px 16px; border-radius: var(--radius); - font-size: 0.85rem; + font-size: 0.82rem; cursor: pointer; animation: slideIn 0.3s ease-out; max-width: 320px; + backdrop-filter: blur(8px); } .toast--error { @@ -367,10 +520,84 @@ body { } .toast--info { - background: rgba(37, 99, 235, 0.9); + background: rgba(59, 130, 246, 0.9); color: white; } +/* ---- Config Panel ---- */ + +.config-panel { + position: absolute; + top: 48px; + right: 20px; + width: 280px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 16px; + z-index: 100; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.config-panel__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 14px; + font-size: 0.9rem; + font-weight: 600; +} + +.config-panel__close { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 0.9rem; + padding: 2px 6px; + border-radius: 4px; +} + +.config-panel__close:hover { + color: var(--color-text); + background: var(--color-surface-hover); +} + +.config-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + font-size: 0.82rem; + color: var(--color-text-muted); +} + +.config-row select, +.config-row input[type="checkbox"] { + background: var(--color-bg); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 4px 8px; + font-size: 0.8rem; +} + +/* ---- Animations ---- */ + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + @keyframes slideIn { from { opacity: 0; @@ -381,105 +608,3 @@ body { transform: translateX(0); } } - -/* ---- Video Overlay ---- */ - -.video-overlay { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius); -} - -.video-overlay__spinner { - width: 32px; - height: 32px; - border: 3px solid rgba(255, 255, 255, 0.3); - border-top-color: white; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* ---- Config Panel ---- */ - -.config-panel { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius); - padding: 16px; - margin: 8px 0; -} - -.config-panel__header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - font-weight: 600; -} - -.config-panel__close { - background: none; - border: none; - color: var(--color-text-muted); - cursor: pointer; - font-size: 1rem; - padding: 2px 6px; -} - -.config-panel__close:hover { - color: var(--color-text); -} - -.config-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - font-size: 0.9rem; -} - -.config-row select, -.config-row input[type="checkbox"] { - background: var(--color-bg); - color: var(--color-text); - border: 1px solid var(--color-border); - border-radius: 4px; - padding: 4px 8px; - font-size: 0.85rem; -} - -/* ---- Detail Badge ---- */ - -.detail-badge { - position: absolute; - top: 8px; - right: 8px; - background: rgba(37, 99, 235, 0.8); - color: white; - padding: 2px 8px; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.5px; -} - -.observation-badge { - position: absolute; - top: 8px; - left: 8px; - background: rgba(34, 197, 94, 0.8); - color: white; - padding: 2px 8px; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 700; - animation: pulse 2s ease-in-out infinite; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b23082c..8e72978 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ // ============================================================ -// CamTalk — 主应用组件 +// CamTalk — Web 端主应用 +// 布局:顶部工具栏 + 左右分栏(视频 | 对话) // ============================================================ import { useState } from "react"; @@ -39,111 +40,133 @@ function App() { return (