| |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta name="color-scheme" content="dark"> |
| <title>Cosmos T3 Chat — Shatter Edition</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;700;800&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg-deep: #07080f; |
| --bg-main: #0b0d18; |
| --bg-assistant: rgba(255, 255, 255, 0.025); |
| --bg-input: rgba(17, 19, 33, 0.82); |
| --text-main: #eceef6; |
| --text-muted: #9da1b8; |
| --accent-color: #facc15; |
| --logo-yellow: #FACC15; |
| --user-accent-1: #8b5cf6; |
| --user-accent-2: #6d28d9; |
| --border-soft: rgba(255, 255, 255, 0.08); |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| } |
| |
| ::selection { background: rgba(250, 204, 21, 0.28); } |
| |
| body { |
| background: |
| radial-gradient(1200px 800px at 82% -10%, rgba(139, 92, 246, 0.10), transparent 60%), |
| radial-gradient(1000px 700px at 8% 110%, rgba(250, 204, 21, 0.06), transparent 60%), |
| radial-gradient(900px 600px at 50% 45%, rgba(56, 78, 173, 0.08), transparent 65%), |
| linear-gradient(180deg, var(--bg-main), var(--bg-deep)); |
| color: var(--text-main); |
| height: 100vh; |
| display: flex; |
| overflow: hidden; |
| } |
| |
| |
| .stars { |
| position: fixed; |
| inset: 0; |
| pointer-events: none; |
| z-index: 0; |
| background-repeat: repeat; |
| } |
| |
| .stars-1 { |
| background-image: |
| radial-gradient(1px 1px at 25px 35px, rgba(255, 255, 255, 0.9), transparent), |
| radial-gradient(1px 1px at 120px 90px, rgba(255, 255, 255, 0.6), transparent), |
| radial-gradient(1.5px 1.5px at 200px 160px, rgba(250, 204, 21, 0.7), transparent), |
| radial-gradient(1px 1px at 80px 200px, rgba(255, 255, 255, 0.5), transparent), |
| radial-gradient(1px 1px at 170px 40px, rgba(186, 196, 255, 0.7), transparent); |
| background-size: 240px 240px; |
| opacity: 0.5; |
| animation: starDrift1 180s linear infinite, twinkle 7s ease-in-out infinite; |
| } |
| |
| .stars-2 { |
| background-image: |
| radial-gradient(1px 1px at 60px 110px, rgba(255, 255, 255, 0.7), transparent), |
| radial-gradient(1.5px 1.5px at 300px 250px, rgba(255, 255, 255, 0.5), transparent), |
| radial-gradient(1px 1px at 200px 330px, rgba(186, 196, 255, 0.6), transparent), |
| radial-gradient(1px 1px at 340px 60px, rgba(250, 204, 21, 0.5), transparent); |
| background-size: 380px 380px; |
| opacity: 0.35; |
| animation: starDrift2 260s linear infinite, twinkle 9s ease-in-out infinite 2s; |
| } |
| |
| @keyframes starDrift1 { from { background-position: 0 0; } to { background-position: -240px -480px; } } |
| @keyframes starDrift2 { from { background-position: 0 0; } to { background-position: -380px -760px; } } |
| @keyframes twinkle { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.5); } } |
| |
| #main { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| height: 100%; |
| position: relative; |
| z-index: 1; |
| } |
| |
| #chat-window { |
| flex: 1; |
| overflow-y: auto; |
| display: flex; |
| flex-direction: column; |
| scroll-behavior: smooth; |
| padding-top: 60px; |
| } |
| |
| |
| .header-bar { |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 60px; |
| display: flex; |
| align-items: center; |
| justify-content: flex-end; |
| padding: 0 24px; |
| z-index: 100; |
| background: linear-gradient(to bottom, rgba(7, 8, 15, 0.85) 50%, transparent); |
| pointer-events: none; |
| } |
| |
| .header-btn { |
| background: rgba(255, 255, 255, 0.04); |
| color: var(--text-muted); |
| border: 1px solid var(--border-soft); |
| border-radius: 10px; |
| padding: 8px 12px; |
| font-size: 13px; |
| font-weight: 500; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| transition: background 0.2s, border-color 0.2s, color 0.2s, box-shadow 0.2s; |
| pointer-events: auto; |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| } |
| |
| .header-btn:hover:not(:disabled) { |
| background: rgba(255, 255, 255, 0.08); |
| border-color: rgba(250, 204, 21, 0.35); |
| color: var(--text-main); |
| box-shadow: 0 0 18px -6px rgba(250, 204, 21, 0.35); |
| } |
| |
| .header-btn:disabled { |
| opacity: 0.4; |
| cursor: not-allowed; |
| } |
| |
| .header-btn svg { |
| width: 14px; |
| height: 14px; |
| } |
| |
| |
| .empty-state { |
| height: 100%; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| padding: 20px; |
| text-align: center; |
| } |
| |
| .empty-state svg { |
| width: 84px; |
| height: auto; |
| margin-bottom: 20px; |
| opacity: 0.85; |
| } |
| |
| .empty-state h1 { |
| font-size: 46px; |
| font-weight: 800; |
| letter-spacing: -0.02em; |
| margin-bottom: 12px; |
| background: linear-gradient(100deg, #fff 10%, var(--logo-yellow) 45%, #f59e0b 55%, #fff 90%); |
| background-size: 200% auto; |
| -webkit-background-clip: text; |
| background-clip: text; |
| -webkit-text-fill-color: transparent; |
| color: transparent; |
| animation: titleShimmer 6s linear infinite; |
| } |
| |
| @keyframes titleShimmer { to { background-position: 200% center; } } |
| |
| .empty-sub { |
| color: var(--text-muted); |
| font-size: 15px; |
| margin-bottom: 40px; |
| } |
| |
| .grid-container { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: 16px; |
| max-width: 800px; |
| width: 100%; |
| } |
| |
| .grid-card { |
| position: relative; |
| padding: 18px; |
| border: 1px solid var(--border-soft); |
| border-radius: 14px; |
| background: linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.015)); |
| text-align: left; |
| cursor: pointer; |
| transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease; |
| } |
| |
| .grid-card:hover { |
| transform: translateY(-3px); |
| border-color: rgba(250, 204, 21, 0.35); |
| background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03)); |
| box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.6), 0 0 24px -6px rgba(250, 204, 21, 0.18); |
| } |
| |
| .card-icon { font-size: 20px; display: block; margin-bottom: 10px; } |
| .grid-card .title { font-weight: 600; margin-bottom: 6px; } |
| .grid-card .desc { font-size: 13px; color: var(--text-muted); line-height: 1.5; } |
| |
| |
| .message { |
| padding: 26px 24px; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.04); |
| width: 100%; |
| animation: fadeIn 0.35s ease-out; |
| } |
| |
| .message.assistant { |
| background: var(--bg-assistant); |
| box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); |
| } |
| |
| .message.user { background: transparent; } |
| |
| .message-content { |
| width: 100%; |
| display: flex; |
| gap: 16px; |
| max-width: 800px; |
| margin: 0 auto; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(8px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| |
| .message-actions { |
| display: flex; |
| gap: 10px; |
| margin-top: 12px; |
| animation: fadeIn 0.2s ease-out; |
| } |
| |
| .msg-action-btn { |
| background: rgba(255, 255, 255, 0.04); |
| color: var(--text-muted); |
| border: 1px solid var(--border-soft); |
| border-radius: 8px; |
| padding: 5px 10px; |
| font-size: 12px; |
| font-weight: 500; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| transition: background-color 0.2s, border-color 0.2s, color 0.2s; |
| } |
| |
| .msg-action-btn:hover { |
| background: rgba(255, 255, 255, 0.08); |
| border-color: rgba(250, 204, 21, 0.35); |
| color: var(--text-main); |
| } |
| |
| .msg-action-btn svg { |
| width: 12px; |
| height: 12px; |
| stroke: currentColor; |
| } |
| |
| |
| .avatar { |
| width: 32px; |
| height: 32px; |
| border-radius: 10px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| flex-shrink: 0; |
| color: white; |
| } |
| |
| .avatar.assistant { |
| background-color: transparent; |
| padding: 4px; |
| scale: 1.35; |
| transition: transform 2s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| .avatar.user { |
| background: linear-gradient(135deg, var(--user-accent-1), var(--user-accent-2)); |
| box-shadow: 0 4px 14px -4px rgba(139, 92, 246, 0.5); |
| } |
| |
| .user-icon { width: 20px; height: 20px; } |
| |
| |
| .avatar.assistant svg { |
| width: 100%; |
| height: 100%; |
| } |
| |
| .avatar.assistant svg, |
| .empty-state svg { |
| filter: drop-shadow(0 0 8px rgba(234, 179, 8, 0.4)); |
| transform-box: fill-box; |
| transform-origin: center; |
| will-change: transform; |
| cursor: pointer; |
| user-select: none; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| |
| .avatar.assistant svg path, |
| .empty-state svg path { |
| stroke: var(--logo-yellow); |
| } |
| |
| .avatar.assistant svg g path[fill="#000"], |
| .empty-state svg g path[fill="#000"] { |
| fill: var(--logo-yellow); |
| } |
| |
| |
| .avatar.assistant svg.clicked, |
| .empty-state svg.clicked { |
| animation: svgGlow 460ms ease-out; |
| } |
| |
| @keyframes svgGlow { |
| 0% { filter: drop-shadow(0 0 8px rgba(234, 179, 8, 0.4)); } |
| 25% { filter: drop-shadow(0 0 24px rgba(234, 179, 8, 1)); } |
| 100% { filter: drop-shadow(0 0 8px rgba(234, 179, 8, 0.4)); } |
| } |
| |
| |
| .svg-layer-outer-1, |
| .svg-layer-outer-2, |
| .svg-layer-inner { |
| transform-box: view-box; |
| transform-origin: center; |
| will-change: transform, rotate, scale; |
| } |
| |
| |
| |
| |
| .svg-layer-outer-1 { |
| animation: spinCW 20s linear infinite, pulseOuter1 5s ease-in-out infinite; |
| } |
| |
| .svg-layer-outer-2 { |
| animation: spinCCW 25s linear infinite, breatheOuter2 6s ease-in-out infinite 1s; |
| } |
| |
| .svg-layer-inner { |
| animation: spinCW 15s linear infinite, breatheInner 4s ease-in-out infinite 0.5s; |
| } |
| |
| @keyframes spinCW { from { rotate: 0deg; } to { rotate: 360deg; } } |
| @keyframes spinCCW { from { rotate: 0deg; } to { rotate: -360deg; } } |
| @keyframes pulseOuter1 { 0%, 100% { opacity: 1; scale: 1; } 50% { opacity: 0.65; scale: 1.04; } } |
| @keyframes breatheOuter2 { 0%, 100% { scale: 1; } 50% { scale: 1.06; } } |
| @keyframes breatheInner { 0%, 100% { scale: 1; } 50% { scale: 1.15; } } |
| |
| |
| .avatar.typing .svg-layer-outer-1 { |
| animation: spinCW 6s linear infinite, pulseOuter1 2s ease-in-out infinite; |
| } |
| |
| .avatar.typing .svg-layer-outer-2 { |
| animation: spinCCW 8s linear infinite, breatheOuter2 2.5s ease-in-out infinite 0.3s; |
| } |
| |
| .avatar.typing .svg-layer-inner { |
| animation: spinCW 4s linear infinite, breatheInner 1.5s ease-in-out infinite 0.2s; |
| } |
| |
| |
| .text-area { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| min-width: 0; |
| } |
| |
| .role-label { |
| font-size: 11px; |
| font-weight: 700; |
| letter-spacing: 0.08em; |
| text-transform: uppercase; |
| color: var(--text-muted); |
| opacity: 0.9; |
| } |
| |
| .message.assistant .role-label { |
| color: var(--logo-yellow); |
| opacity: 0.75; |
| } |
| |
| .content-text { |
| max-width: 100%; |
| line-height: 1.65; |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| } |
| |
| |
| .content-text.streaming::after { |
| content: '▋'; |
| display: inline-block; |
| animation: blink 1s steps(2) infinite; |
| margin-left: 1px; |
| color: var(--logo-yellow); |
| } |
| |
| @keyframes blink { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0; } |
| } |
| |
| |
| .input-container { |
| padding: 20px 24px 16px; |
| background: linear-gradient(transparent, rgba(11, 13, 24, 0.92) 40%); |
| position: relative; |
| z-index: 2; |
| } |
| |
| .input-wrapper { |
| width: 100%; |
| position: relative; |
| max-width: 800px; |
| margin: 0 auto; |
| } |
| |
| textarea { |
| width: 100%; |
| background: var(--bg-input); |
| color: var(--text-main); |
| border: 1px solid var(--border-soft); |
| border-radius: 16px; |
| padding: 14px 54px 14px 18px; |
| resize: none; |
| outline: none; |
| font-size: 16px; |
| font-family: inherit; |
| box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.6); |
| min-height: 52px; |
| max-height: 200px; |
| overflow-y: auto; |
| caret-color: var(--logo-yellow); |
| backdrop-filter: blur(14px); |
| -webkit-backdrop-filter: blur(14px); |
| transition: border-color 0.2s, box-shadow 0.2s; |
| } |
| |
| textarea:focus { |
| border-color: rgba(250, 204, 21, 0.45); |
| box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.12), 0 12px 32px -12px rgba(0, 0, 0, 0.6); |
| } |
| |
| textarea::placeholder { color: #6f7390; } |
| |
| .send-btn { |
| position: absolute; |
| right: 10px; |
| bottom: 10px; |
| width: 34px; |
| height: 34px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: rgba(255, 255, 255, 0.06); |
| border: none; |
| color: var(--text-muted); |
| cursor: pointer; |
| border-radius: 10px; |
| transition: background 0.2s, color 0.2s, transform 0.15s, box-shadow 0.2s; |
| } |
| |
| .send-btn:not(:disabled):hover { |
| background: var(--logo-yellow); |
| color: #1a1505; |
| transform: scale(1.06); |
| box-shadow: 0 4px 16px -2px rgba(250, 204, 21, 0.5); |
| } |
| |
| .send-btn:disabled { opacity: 0.3; cursor: not-allowed; } |
| |
| .send-btn.stop-btn { |
| color: #f87171; |
| background: rgba(239, 68, 68, 0.12); |
| } |
| |
| .send-btn.stop-btn:hover { |
| background: rgba(239, 68, 68, 0.25) !important; |
| color: #fca5a5 !important; |
| box-shadow: 0 4px 16px -2px rgba(239, 68, 68, 0.4) !important; |
| } |
| |
| .footer-note { |
| text-align: center; |
| font-size: 11px; |
| letter-spacing: 0.06em; |
| color: #6b6f85; |
| margin-top: 12px; |
| } |
| |
| |
| .typing-dots { |
| display: flex; |
| gap: 4px; |
| padding: 12px 0; |
| align-items: center; |
| } |
| |
| .dot { |
| width: 8px; |
| height: 8px; |
| background: var(--logo-yellow); |
| border-radius: 50%; |
| animation: bounce 1.4s infinite ease-in-out both; |
| } |
| |
| .dot:nth-child(1) { animation-delay: -0.32s; } |
| .dot:nth-child(2) { animation-delay: -0.16s; } |
| |
| @keyframes bounce { |
| 0%, 80%, 100% { transform: scale(0); } |
| 40% { transform: scale(1.0); } |
| } |
| |
| |
| .error-toast { |
| position: fixed; |
| top: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: rgba(220, 38, 38, 0.92); |
| border: 1px solid rgba(255, 255, 255, 0.15); |
| color: white; |
| padding: 12px 24px; |
| border-radius: 10px; |
| font-size: 14px; |
| z-index: 1000; |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.45); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| animation: toastIn 0.3s ease-out, toastOut 0.3s ease-in 3.7s forwards; |
| } |
| |
| @keyframes toastIn { |
| from { opacity: 0; transform: translateX(-50%) translateY(-10px); } |
| to { opacity: 1; transform: translateX(-50%) translateY(0); } |
| } |
| |
| @keyframes toastOut { |
| from { opacity: 1; } |
| to { opacity: 0; } |
| } |
| |
| |
| #chat-window::-webkit-scrollbar { width: 8px; } |
| #chat-window::-webkit-scrollbar-track { background: transparent; } |
| #chat-window::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; } |
| #chat-window::-webkit-scrollbar-thumb:hover { background: rgba(250, 204, 21, 0.3); } |
| |
| |
| @media (max-width: 600px) { |
| .grid-container { grid-template-columns: 1fr; } |
| .message { padding: 16px 12px; } |
| .input-container { padding: 16px 12px; } |
| .empty-state h1 { font-size: 30px; } |
| |
| .message-actions { |
| flex-wrap: wrap; |
| gap: 8px; |
| } |
| |
| .msg-action-btn { |
| padding: 4px 8px; |
| font-size: 11px; |
| flex: 1 1 auto; |
| justify-content: center; |
| } |
| } |
| |
| |
| #loading-screen { |
| position: fixed; |
| inset: 0; |
| z-index: 2000; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| gap: 26px; |
| background: radial-gradient(circle at 50% 42%, #13152a 0%, var(--bg-deep) 70%); |
| transition: opacity 0.6s ease, visibility 0.6s ease; |
| } |
| |
| #loading-screen.hidden { |
| opacity: 0; |
| visibility: hidden; |
| pointer-events: none; |
| } |
| |
| .loading-logo { |
| width: 130px; |
| height: 130px; |
| animation: loaderGlow 2.4s ease-in-out infinite; |
| } |
| |
| .loading-logo svg { |
| width: 100%; |
| height: 100%; |
| } |
| |
| .loading-logo svg path { stroke: var(--logo-yellow); } |
| .loading-logo svg g path[fill="#000"] { fill: var(--logo-yellow); } |
| |
| @keyframes loaderGlow { |
| 0%, 100% { filter: drop-shadow(0 0 14px rgba(234, 179, 8, 0.45)); } |
| 50% { filter: drop-shadow(0 0 32px rgba(234, 179, 8, 0.85)); } |
| } |
| |
| .loading-title { |
| font-size: 20px; |
| font-weight: 700; |
| letter-spacing: 0.22em; |
| text-transform: uppercase; |
| color: var(--text-main); |
| animation: loaderTitlePulse 2.4s ease-in-out infinite; |
| } |
| |
| @keyframes loaderTitlePulse { |
| 0%, 100% { opacity: 0.4; } |
| 50% { opacity: 0.9; } |
| } |
| |
| .loading-bar { |
| position: relative; |
| width: 180px; |
| height: 3px; |
| border-radius: 3px; |
| background: rgba(255, 255, 255, 0.08); |
| overflow: hidden; |
| } |
| |
| .loading-bar::after { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: -40%; |
| width: 40%; |
| height: 100%; |
| border-radius: 3px; |
| background: linear-gradient(90deg, transparent, var(--logo-yellow), transparent); |
| animation: loaderSlide 1.15s ease-in-out infinite; |
| } |
| |
| @keyframes loaderSlide { |
| 0% { left: -40%; } |
| 100% { left: 100%; } |
| } |
| |
| |
| #glass-pane { |
| position: fixed; |
| inset: 0; |
| z-index: 1500; |
| background: |
| linear-gradient(115deg, |
| rgba(255, 255, 255, 0.10) 0%, |
| rgba(255, 255, 255, 0.02) 28%, |
| rgba(255, 255, 255, 0.09) 42%, |
| rgba(255, 255, 255, 0.02) 56%, |
| rgba(255, 255, 255, 0.06) 100%); |
| backdrop-filter: blur(1.5px) brightness(1.05); |
| -webkit-backdrop-filter: blur(1.5px) brightness(1.05); |
| opacity: 0; |
| transition: opacity 0.45s ease; |
| cursor: crosshair; |
| } |
| |
| #glass-pane.on { opacity: 1; } |
| |
| #glass-pane.broken { |
| background: none; |
| backdrop-filter: none; |
| -webkit-backdrop-filter: none; |
| cursor: default; |
| pointer-events: none; |
| } |
| |
| .glass-hint { |
| position: absolute; |
| left: 50%; |
| bottom: 12%; |
| transform: translateX(-50%); |
| font-size: 13px; |
| letter-spacing: 0.25em; |
| text-transform: uppercase; |
| color: rgba(255, 255, 255, 0.65); |
| text-shadow: 0 0 12px rgba(255, 255, 255, 0.4); |
| animation: hintPulse 2.2s ease-in-out infinite; |
| pointer-events: none; |
| white-space: nowrap; |
| } |
| |
| @keyframes hintPulse { |
| 0%, 100% { opacity: 0.35; } |
| 50% { opacity: 0.9; } |
| } |
| |
| .glass-shard { |
| position: fixed; |
| inset: 0; |
| pointer-events: none; |
| z-index: 1501; |
| will-change: transform, opacity; |
| } |
| |
| .glass-flash { |
| position: fixed; |
| inset: 0; |
| z-index: 1502; |
| pointer-events: none; |
| } |
| |
| #shatter-btn { |
| position: fixed; |
| right: 18px; |
| bottom: 18px; |
| z-index: 1400; |
| width: 44px; |
| height: 44px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: rgba(255, 255, 255, 0.06); |
| border: 1px solid var(--border-soft); |
| color: var(--text-muted); |
| font-size: 18px; |
| cursor: pointer; |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s; |
| } |
| |
| #shatter-btn:hover { |
| transform: scale(1.08); |
| border-color: rgba(250, 204, 21, 0.4); |
| box-shadow: 0 0 18px -4px rgba(250, 204, 21, 0.4); |
| } |
| </style> |
| </head> |
|
|
| <body> |
|
|
| |
| <div class="stars stars-1"></div> |
| <div class="stars stars-2"></div> |
|
|
| |
| <div id="loading-screen"> |
| <div class="loading-logo"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.217 108.55"> |
| <defs> |
| <clipPath id="loaderClip"><path fill="none" d="M0 0h116.217v108.55H0z"/></clipPath> |
| </defs> |
| <g clip-path="url(#loaderClip)" fill="none" stroke-width="none" stroke-miterlimit="10" |
| font-family="none" font-weight="none" font-size="none" |
| text-anchor="none" style="mix-blend-mode:normal"> |
| <g class="svg-layer-outer-1"> |
| <path d="M40.9 92.92c-5.487-1.788-6.775-9.344-10.164-14.014-4.894-6.742-12.956-12.074-14.684-20.224-1.196-5.643 4.697-10.537 7.045-15.806 3.39-7.608 3.974-17.256 10.167-22.827 4.29-3.857 11.475-1.199 17.212-1.8 8.284-.868 16.931-5.187 24.851-2.608 5.487 1.787 6.776 9.343 10.165 14.013 4.89 6.74 12.948 12.07 14.673 20.217 1.194 5.643-4.7 10.536-7.048 15.805-3.39 7.609-3.974 17.257-10.167 22.827-4.3 3.867-34.124 6.997-42.05 4.416z" stroke="#000" stroke-width="8"/> |
| </g> |
| <g class="svg-layer-outer-2"> |
| <path d="M77.404 104.55c-6.547 3.33-14.509-2.311-21.763-3.464-10.474-1.664-22.531.793-31.422-4.988-6.156-4.002-5.257-13.713-7.887-20.568-3.797-9.9-11.955-19.111-11.395-29.7.388-7.333 9.25-11.408 13.873-17.114C25.485 20.478 29.38 8.806 38.832 4 45.379.67 53.34 6.31 60.595 7.464c10.468 1.664 22.52-.79 31.405 4.99 6.156 4.005 5.255 13.715 7.884 20.571 3.797 9.9 11.956 19.11 11.395 29.7-.389 7.35-24.417 37.014-33.875 41.825z" stroke="#000" stroke-width="8"/> |
| </g> |
| <g class="svg-layer-inner"> |
| <path d="M35.33 63.017c-1.51-2.966 1.047-6.573 1.57-9.86.753-4.746-.36-10.209 2.259-14.237 1.813-2.79 6.213-2.382 9.32-3.573 4.485-1.72 8.658-5.417 13.456-5.163 3.322.176 5.169 4.19 7.754 6.285 3.732 3.025 9.02 4.79 11.198 9.072 1.509 2.967-1.047 6.574-1.57 9.86-.753 4.744.359 10.204-2.26 14.23-1.815 2.789-6.214 2.38-9.32 3.572-4.486 1.72-8.66 5.417-13.457 5.163-3.33-.176-16.77-11.063-18.95-15.349" fill="#000"/> |
| </g> |
| </g> |
| </svg> |
| </div> |
| <div class="loading-title">Cosmos T3</div> |
| <div class="loading-bar"></div> |
| </div> |
|
|
| <main id="main"> |
|
|
| |
| <div id="header-bar" class="header-bar" style="display: none;"> |
| <button |
| id="clear-btn" |
| class="header-btn" |
| onclick="onClearClick()" |
| aria-label="Clear chat" |
| > |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <polyline points="3 6 5 6 21 6"></polyline> |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> |
| <line x1="10" y1="11" x2="10" y2="17"></line> |
| <line x1="14" y1="11" x2="14" y2="17"></line> |
| </svg> |
| Clear Chat |
| </button> |
| </div> |
|
|
| <div id="chat-window"></div> |
|
|
| <div class="input-container"> |
| <div class="input-wrapper"> |
| <textarea |
| id="user-input" |
| rows="1" |
| placeholder="Send a message..." |
| aria-label="Chat input" |
| ></textarea> |
| <button |
| id="send-btn" |
| class="send-btn" |
| onclick="onSendClick()" |
| aria-label="Send message" |
| > |
| <svg xmlns="http://www.w3.org/2000/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 class="footer-note"> |
| Powered by Cosmos T3 BASE |
| </div> |
| </div> |
|
|
| </main> |
|
|
| <script> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const BACKEND_BASE = 'https://wop-server-backend.hf.space/gradio_api'; |
| const CHAT_FN = 'chat'; |
| |
| |
| |
| |
| |
| const modelLogoSvg = ` |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.217 108.55"> |
| <defs> |
| <clipPath id="a"><path fill="none" d="M0 0h116.217v108.55H0z"/></clipPath> |
| </defs> |
| <g clip-path="url(#a)" fill="none" stroke-width="none" stroke-miterlimit="10" |
| font-family="none" font-weight="none" font-size="none" |
| text-anchor="none" style="mix-blend-mode:normal"> |
| <g class="svg-layer-outer-1"> |
| <path d="M40.9 92.92c-5.487-1.788-6.775-9.344-10.164-14.014-4.894-6.742-12.956-12.074-14.684-20.224-1.196-5.643 4.697-10.537 7.045-15.806 3.39-7.608 3.974-17.256 10.167-22.827 4.29-3.857 11.475-1.199 17.212-1.8 8.284-.868 16.931-5.187 24.851-2.608 5.487 1.787 6.776 9.343 10.165 14.013 4.89 6.74 12.948 12.07 14.673 20.217 1.194 5.643-4.7 10.536-7.048 15.805-3.39 7.609-3.974 17.257-10.167 22.827-4.3 3.867-34.124 6.997-42.05 4.416z" |
| stroke="#000" stroke-width="8"/> |
| </g> |
| <g class="svg-layer-outer-2"> |
| <path d="M77.404 104.55c-6.547 3.33-14.509-2.311-21.763-3.464-10.474-1.664-22.531.793-31.422-4.988-6.156-4.002-5.257-13.713-7.887-20.568-3.797-9.9-11.955-19.111-11.395-29.7.388-7.333 9.25-11.408 13.873-17.114C25.485 20.478 29.38 8.806 38.832 4 45.379.67 53.34 6.31 60.595 7.464c10.468 1.664 22.52-.79 31.405 4.99 6.156 4.005 5.255 13.715 7.884 20.571 3.797 9.9 11.956 19.11 11.395 29.7-.389 7.35-24.417 37.014-33.875 41.825z" |
| stroke="#000" stroke-width="8"/> |
| </g> |
| <g class="svg-layer-inner"> |
| <path d="M35.33 63.017c-1.51-2.966 1.047-6.573 1.57-9.86.753-4.746-.36-10.209 2.259-14.237 1.813-2.79 6.213-2.382 9.32-3.573 4.485-1.72 8.658-5.417 13.456-5.163 3.322.176 5.169 4.19 7.754 6.285 3.732 3.025 9.02 4.79 11.198 9.072 1.509 2.967-1.047 6.574-1.57 9.86-.753 4.744.359 10.204-2.26 14.23-1.815 2.789-6.214 2.38-9.32 3.572-4.486 1.72-8.66 5.417-13.457 5.163-3.33-.176-16.77-11.063-18.95-15.349" |
| fill="#000"/> |
| </g> |
| </g> |
| </svg>`; |
| |
| const userIconSvg = ` |
| <svg class="user-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" |
| fill="none" stroke="currentColor" stroke-width="2" |
| stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> |
| <circle cx="12" cy="7" r="4"></circle> |
| </svg>`; |
| |
| const sendIconSvg = ` |
| <svg xmlns="http://www.w3.org/2000/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>`; |
| |
| const stopIconSvg = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" |
| viewBox="0 0 24 24" fill="currentColor"> |
| <rect x="6" y="6" width="12" height="12" rx="2"/> |
| </svg>`; |
| |
| |
| |
| |
| |
| const SESSION_KEY = 'lfm_session_id'; |
| let sessionId = localStorage.getItem(SESSION_KEY); |
| if (!sessionId) { |
| sessionId = (crypto.randomUUID && crypto.randomUUID()) || |
| (Date.now().toString(36) + Math.random().toString(36).slice(2)); |
| localStorage.setItem(SESSION_KEY, sessionId); |
| } |
| |
| let messages = []; |
| let isStreaming = false; |
| let abortController = null; |
| let activeAssistantMsg = null; |
| let activeAssistantContentEl = null; |
| let activeAvatarEl = null; |
| |
| const chatWindowEl = document.getElementById('chat-window'); |
| const userInputEl = document.getElementById('user-input'); |
| const sendBtnEl = document.getElementById('send-btn'); |
| |
| |
| |
| |
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text ?? ''; |
| return div.innerHTML; |
| } |
| |
| function scrollToBottom() { |
| requestAnimationFrame(() => { |
| chatWindowEl.scrollTop = chatWindowEl.scrollHeight; |
| }); |
| } |
| |
| function showErrorToast(message) { |
| const existing = document.querySelector('.error-toast'); |
| if (existing) existing.remove(); |
| const toast = document.createElement('div'); |
| toast.className = 'error-toast'; |
| toast.textContent = message; |
| document.body.appendChild(toast); |
| setTimeout(() => toast.remove(), 4000); |
| } |
| |
| function setSendButton(mode) { |
| if (mode === 'stop') { |
| sendBtnEl.innerHTML = stopIconSvg; |
| sendBtnEl.classList.add('stop-btn'); |
| sendBtnEl.disabled = false; |
| sendBtnEl.setAttribute('aria-label', 'Stop generating'); |
| } else { |
| sendBtnEl.innerHTML = sendIconSvg; |
| sendBtnEl.classList.remove('stop-btn'); |
| sendBtnEl.disabled = false; |
| sendBtnEl.setAttribute('aria-label', 'Send message'); |
| } |
| } |
| |
| function normalizeRole(role) { |
| return role === 'user' || role === 'assistant' || role === 'system' ? role : null; |
| } |
| |
| |
| function fillPrompt(text) { |
| userInputEl.value = text; |
| autoResizeTextarea(); |
| } |
| |
| |
| |
| |
| |
| function updateHeader() { |
| const headerBar = document.getElementById('header-bar'); |
| const clearBtn = document.getElementById('clear-btn'); |
| if (messages.length === 0) { |
| headerBar.style.display = 'none'; |
| return; |
| } |
| headerBar.style.display = 'flex'; |
| clearBtn.disabled = isStreaming; |
| } |
| |
| |
| |
| |
| |
| function renderMessages() { |
| chatWindowEl.innerHTML = ''; |
| if (messages.length === 0) { |
| chatWindowEl.innerHTML = ` |
| <div class="empty-state"> |
| ${modelLogoSvg} |
| <h1>Cosmos T3</h1> |
| <div class="empty-sub">Ask anything. Explore everything.</div> |
| <div class="grid-container"> |
| <div class="grid-card" onclick="fillPrompt('Explain quantum computing in simple terms')"> |
| <span class="card-icon">💡</span> |
| <div class="title">Examples</div> |
| <div class="desc">"Explain quantum computing in simple terms"</div> |
| </div> |
| <div class="grid-card" onclick="fillPrompt('Write a short creative poem about the ocean')"> |
| <span class="card-icon">✨</span> |
| <div class="title">Creative</div> |
| <div class="desc">"Write a short poem about the ocean"</div> |
| </div> |
| <div class="grid-card" onclick="fillPrompt('What is the meaning of life?')"> |
| <span class="card-icon">🌌</span> |
| <div class="title">Philosophy</div> |
| <div class="desc">"What is the meaning of life?"</div> |
| </div> |
| </div> |
| </div> |
| `; |
| updateHeader(); |
| return; |
| } |
| |
| for (let i = 0; i < messages.length; i++) { |
| const msg = messages[i]; |
| const role = normalizeRole(msg.role); |
| if (!role) continue; |
| const isLastMessage = (i === messages.length - 1); |
| const isLastAssistantMessage = isLastMessage && role === 'assistant'; |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = `message ${role}`; |
| const avatarContent = role === 'assistant' ? modelLogoSvg : userIconSvg; |
| const roleLabel = role === 'assistant' ? 'Cosmos T3' : (role === 'system' ? 'System' : 'You'); |
| |
| |
| let actionsHtml = ''; |
| if (isLastAssistantMessage && !isStreaming) { |
| const hasUserMsg = messages.some(m => m.role === 'user'); |
| actionsHtml = ` |
| <div class="message-actions"> |
| ${hasUserMsg ? ` |
| <button |
| class="msg-action-btn" |
| onclick="onRegenerateClick()" |
| aria-label="Regenerate response" |
| > |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <polyline points="23 4 23 10 17 10"></polyline> |
| <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> |
| </svg> |
| Regenerate |
| </button> |
| ` : ''} |
| <button |
| class="msg-action-btn" |
| onclick="onContinueClick()" |
| aria-label="Continue generating" |
| > |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <polyline points="9 18 15 12 9 6"></polyline> |
| </svg> |
| Continue Generating |
| </button> |
| </div> |
| `; |
| } |
| |
| msgDiv.innerHTML = ` |
| <div class="message-content"> |
| <div class="avatar ${role}">${avatarContent}</div> |
| <div class="text-area"> |
| <div class="role-label">${roleLabel}</div> |
| <div class="content-text">${escapeHtml(msg.content)}</div> |
| ${actionsHtml} |
| </div> |
| </div> |
| `; |
| chatWindowEl.appendChild(msgDiv); |
| } |
| |
| scrollToBottom(); |
| updateHeader(); |
| } |
| |
| function showTypingIndicator() { |
| removeTypingIndicator(); |
| const indicator = document.createElement('div'); |
| indicator.className = 'message assistant'; |
| indicator.id = 'typing-indicator'; |
| indicator.innerHTML = ` |
| <div class="message-content"> |
| <div class="avatar assistant typing">${modelLogoSvg}</div> |
| <div class="text-area"> |
| <div class="role-label">Cosmos T3</div> |
| <div class="typing-dots"> |
| <div class="dot"></div> |
| <div class="dot"></div> |
| <div class="dot"></div> |
| </div> |
| </div> |
| </div> |
| `; |
| chatWindowEl.appendChild(indicator); |
| scrollToBottom(); |
| } |
| |
| function removeTypingIndicator() { |
| const el = document.getElementById('typing-indicator'); |
| if (el) el.remove(); |
| } |
| |
| function createAssistantPlaceholder() { |
| const assistantMsg = { role: 'assistant', content: '' }; |
| messages.push(assistantMsg); |
| renderMessages(); |
| const contentEls = chatWindowEl.querySelectorAll('.content-text'); |
| const lastContentEl = contentEls[contentEls.length - 1]; |
| const lastMsgEl = lastContentEl ? lastContentEl.closest('.message') : null; |
| const avatarEl = lastMsgEl ? lastMsgEl.querySelector('.avatar.assistant') : null; |
| if (lastContentEl) lastContentEl.classList.add('streaming'); |
| if (avatarEl) avatarEl.classList.add('typing'); |
| activeAssistantMsg = assistantMsg; |
| activeAssistantContentEl = lastContentEl; |
| activeAvatarEl = avatarEl; |
| } |
| |
| function clearAssistantStreamingUI() { |
| if (activeAssistantContentEl) { |
| activeAssistantContentEl.classList.remove('streaming'); |
| } |
| if (activeAvatarEl) { |
| activeAvatarEl.classList.remove('typing'); |
| } |
| activeAssistantContentEl = null; |
| activeAvatarEl = null; |
| } |
| |
| |
| function setAssistantContent(fullText) { |
| if (activeAssistantMsg) { |
| activeAssistantMsg.content = fullText; |
| } |
| if (activeAssistantContentEl) { |
| activeAssistantContentEl.textContent = fullText; |
| scrollToBottom(); |
| } |
| } |
| |
| |
| |
| |
| |
| function parseSSEEvents(buffer) { |
| const events = buffer.split('\n\n'); |
| const rest = events.pop(); |
| return { events, rest }; |
| } |
| |
| |
| function parseEventBlock(block) { |
| const lines = block.split('\n'); |
| let eventName = 'message'; |
| const dataLines = []; |
| for (const line of lines) { |
| if (line.startsWith('event:')) { |
| eventName = line.slice(6).trim(); |
| } else if (line.startsWith('data:')) { |
| dataLines.push(line.slice(5).trimStart()); |
| } |
| } |
| return { event: eventName, data: dataLines.length ? dataLines.join('\n') : null }; |
| } |
| |
| |
| function extractTextFromGradioData(parsed) { |
| if (parsed == null) return null; |
| if (typeof parsed === 'string') return parsed; |
| if (Array.isArray(parsed)) { |
| const first = parsed[0]; |
| if (typeof first === 'string') return first; |
| |
| if (first && typeof first === 'object') { |
| if (typeof first.content === 'string') return first.content; |
| if (Array.isArray(first.content)) { |
| return first.content |
| .map(c => (c && typeof c.text === 'string') ? c.text : '') |
| .join(''); |
| } |
| } |
| return null; |
| } |
| if (typeof parsed === 'object') { |
| if (typeof parsed.content === 'string') return parsed.content; |
| } |
| return null; |
| } |
| |
| async function readErrorBody(response) { |
| try { |
| const ct = response.headers.get('content-type') || ''; |
| if (ct.includes('application/json')) { |
| const j = await response.json(); |
| return j.error || JSON.stringify(j); |
| } |
| return await response.text(); |
| } catch { |
| return `HTTP ${response.status}`; |
| } |
| } |
| |
| |
| |
| |
| |
| |
| async function streamGradioChat({ message, history, onFullText, signal }) { |
| |
| const joinRes = await fetch(`${BACKEND_BASE}/call/${CHAT_FN}`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| |
| |
| |
| |
| |
| data: [message, null] |
| }), |
| signal |
| }); |
| |
| if (!joinRes.ok) { |
| throw new Error(await readErrorBody(joinRes) || `Server error: HTTP ${joinRes.status}`); |
| } |
| |
| const joinJson = await joinRes.json(); |
| const eventId = joinJson.event_id || joinJson.eventId; |
| if (!eventId) { |
| throw new Error('Backend did not return an event_id.'); |
| } |
| |
| |
| const streamRes = await fetch(`${BACKEND_BASE}/call/${CHAT_FN}/${eventId}`, { |
| method: 'GET', |
| headers: { 'Accept': 'text/event-stream' }, |
| signal |
| }); |
| |
| if (!streamRes.ok || !streamRes.body) { |
| throw new Error(await readErrorBody(streamRes) || `Stream error: HTTP ${streamRes.status}`); |
| } |
| |
| const reader = streamRes.body.getReader(); |
| const decoder = new TextDecoder('utf-8'); |
| let buffer = ''; |
| let finished = false; |
| let lastText = ''; |
| |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| buffer += decoder.decode(value, { stream: true }); |
| const parsed = parseSSEEvents(buffer); |
| buffer = parsed.rest; |
| |
| for (const block of parsed.events) { |
| if (!block.trim()) continue; |
| const { event, data } = parseEventBlock(block); |
| |
| |
| if (event === 'heartbeat') continue; |
| |
| |
| if (event === 'error') { |
| let msg = data || 'Unknown backend error'; |
| try { |
| const j = JSON.parse(data); |
| msg = (typeof j === 'string') ? j : (j.error || JSON.stringify(j)); |
| } catch { } |
| throw new Error(msg); |
| } |
| |
| if (!data) continue; |
| |
| let payload; |
| try { |
| payload = JSON.parse(data); |
| } catch { |
| |
| continue; |
| } |
| |
| const text = extractTextFromGradioData(payload); |
| |
| if (event === 'generating') { |
| if (typeof text === 'string') { |
| lastText = text; |
| onFullText(text); |
| } |
| } else if (event === 'complete') { |
| if (typeof text === 'string') { |
| lastText = text; |
| onFullText(text); |
| } |
| finished = true; |
| } |
| } |
| } |
| |
| return { finished, lastText }; |
| } |
| |
| |
| |
| |
| |
| function onSendClick() { |
| if (isStreaming && abortController) { |
| abortController.abort(); |
| return; |
| } |
| handleSend(); |
| } |
| |
| function onClearClick() { |
| if (isStreaming && abortController) { |
| abortController.abort(); |
| } |
| messages = []; |
| renderMessages(); |
| } |
| |
| function onRegenerateClick() { |
| if (isStreaming || messages.length === 0) return; |
| |
| const lastMsg = messages[messages.length - 1]; |
| if (lastMsg && lastMsg.role === 'assistant') { |
| messages.pop(); |
| } |
| |
| const newLastMsg = messages[messages.length - 1]; |
| if (!newLastMsg || newLastMsg.role !== 'user') { |
| renderMessages(); |
| return; |
| } |
| const text = newLastMsg.content; |
| executeChatStream(text, true); |
| } |
| |
| function onContinueClick() { |
| if (isStreaming || messages.length === 0) return; |
| |
| const lastMsg = messages[messages.length - 1]; |
| if (!lastMsg || lastMsg.role !== 'assistant') { |
| return; |
| } |
| executeChatStream("Continue", false); |
| } |
| |
| async function handleSend() { |
| const text = userInputEl.value.trim(); |
| if (!text || isStreaming) return; |
| executeChatStream(text, false); |
| } |
| |
| async function executeChatStream(text, isRegenerate = false) { |
| if (isStreaming) return; |
| isStreaming = true; |
| setSendButton('stop'); |
| updateHeader(); |
| |
| if (!isRegenerate) { |
| const userMsg = { role: 'user', content: text }; |
| messages.push(userMsg); |
| userInputEl.value = ''; |
| userInputEl.style.height = 'auto'; |
| } |
| |
| renderMessages(); |
| showTypingIndicator(); |
| |
| |
| const history = messages |
| .filter(m => ['user', 'assistant', 'system'].includes(m.role)) |
| .slice(0, -1) |
| .map(m => ({ role: m.role, content: String(m.content ?? '') })); |
| |
| abortController = new AbortController(); |
| let placeholderCreated = false; |
| |
| try { |
| const result = await streamGradioChat({ |
| message: text, |
| history, |
| signal: abortController.signal, |
| onFullText: (fullText) => { |
| if (!placeholderCreated) { |
| removeTypingIndicator(); |
| createAssistantPlaceholder(); |
| placeholderCreated = true; |
| } |
| setAssistantContent(fullText); |
| } |
| }); |
| |
| removeTypingIndicator(); |
| |
| |
| if (!placeholderCreated) { |
| createAssistantPlaceholder(); |
| placeholderCreated = true; |
| } |
| |
| clearAssistantStreamingUI(); |
| |
| if (activeAssistantMsg) { |
| activeAssistantMsg.content = (activeAssistantMsg.content || '').trim(); |
| if (!activeAssistantMsg.content) { |
| activeAssistantMsg.content = "I'm not sure how to respond to that."; |
| } |
| } |
| |
| } catch (error) { |
| removeTypingIndicator(); |
| |
| if (error.name === 'AbortError') { |
| clearAssistantStreamingUI(); |
| if (activeAssistantMsg) { |
| activeAssistantMsg.content = (activeAssistantMsg.content || '').trim(); |
| if (!activeAssistantMsg.content) { |
| activeAssistantMsg.content = '(Generation stopped)'; |
| } else { |
| activeAssistantMsg.content += ' ⏹'; |
| } |
| } |
| } else { |
| console.error('Stream error:', error); |
| showErrorToast(error.message || 'Unknown error'); |
| clearAssistantStreamingUI(); |
| if (activeAssistantMsg && !activeAssistantMsg.content.trim()) { |
| activeAssistantMsg.content = '⚠️ Sorry, something went wrong: ' + (error.message || 'Unknown error'); |
| } else if (!activeAssistantMsg) { |
| messages.push({ |
| role: 'assistant', |
| content: '⚠️ Sorry, something went wrong: ' + (error.message || 'Unknown error') |
| }); |
| } |
| } |
| |
| } finally { |
| isStreaming = false; |
| abortController = null; |
| activeAssistantMsg = null; |
| clearAssistantStreamingUI(); |
| setSendButton('send'); |
| renderMessages(); |
| } |
| } |
| |
| |
| |
| |
| |
| function autoResizeTextarea() { |
| userInputEl.style.height = 'auto'; |
| const newHeight = Math.min(userInputEl.scrollHeight, 200); |
| userInputEl.style.height = newHeight + 'px'; |
| } |
| |
| userInputEl.addEventListener('input', autoResizeTextarea); |
| |
| |
| |
| |
| |
| userInputEl.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| onSendClick(); |
| } |
| }); |
| |
| |
| |
| |
| |
| renderMessages(); |
| setSendButton('send'); |
| |
| |
| (function initLoadingScreen() { |
| const loader = document.getElementById('loading-screen'); |
| if (!loader) return; |
| const MIN_VISIBLE_MS = 1400; |
| const start = performance.now(); |
| function hide() { |
| const wait = Math.max(0, MIN_VISIBLE_MS - (performance.now() - start)); |
| setTimeout(() => { |
| loader.classList.add('hidden'); |
| loader.addEventListener('transitionend', () => loader.remove(), { once: true }); |
| }, wait); |
| } |
| if (document.readyState === 'complete') hide(); |
| else window.addEventListener('load', hide); |
| })(); |
| |
| const SVG_CLICK_SELECTOR = '.avatar.assistant svg, .empty-state svg'; |
| |
| function restartClassAnimation(el, className) { |
| el.classList.remove(className); |
| void el.getBoundingClientRect(); |
| el.classList.add(className); |
| } |
| |
| function animateSvg(svg) { |
| |
| restartClassAnimation(svg, 'clicked'); |
| |
| |
| |
| |
| const layers = svg.querySelectorAll( |
| '.svg-layer-outer-1, .svg-layer-outer-2, .svg-layer-inner' |
| ); |
| |
| layers.forEach((layer, i) => { |
| layer.animate( |
| [ |
| { transform: 'scale(1)' }, |
| { transform: 'scale(0.8)', offset: 0.35 }, |
| { transform: 'scale(1.14)', offset: 0.7 }, |
| { transform: 'scale(1)' } |
| ], |
| { |
| duration: 420, |
| delay: i * 80, |
| easing: 'cubic-bezier(.2,.8,.2,1)', |
| fill: 'none' |
| } |
| ); |
| }); |
| } |
| |
| document.addEventListener('pointerdown', (e) => { |
| if (e.pointerType === 'mouse' && e.button !== 0) return; |
| |
| const target = e.target instanceof Element ? e.target : null; |
| if (!target) return; |
| |
| const svg = target.closest(SVG_CLICK_SELECTOR); |
| if (!svg) return; |
| |
| animateSvg(svg); |
| }); |
| </script> |
|
|
| <script> |
| |
| |
| |
| |
| |
| |
| (function glassShatter() { |
| let pane = null; |
| let breaking = false; |
| |
| function spawnPane(showHint) { |
| if (pane) return; |
| pane = document.createElement('div'); |
| pane.id = 'glass-pane'; |
| if (showHint) { |
| const hint = document.createElement('div'); |
| hint.className = 'glass-hint'; |
| hint.textContent = 'Click anywhere to shatter'; |
| pane.appendChild(hint); |
| } |
| document.body.appendChild(pane); |
| requestAnimationFrame(() => requestAnimationFrame(() => pane.classList.add('on'))); |
| pane.addEventListener('pointerdown', (e) => shatterAt(e.clientX, e.clientY)); |
| } |
| |
| |
| function playShatterSound() { |
| try { |
| const Ctx = window.AudioContext || window.webkitAudioContext; |
| if (!Ctx) return; |
| const ctx = new Ctx(); |
| const now = ctx.currentTime; |
| const len = Math.floor(ctx.sampleRate * 0.45); |
| const buf = ctx.createBuffer(1, len, ctx.sampleRate); |
| const data = buf.getChannelData(0); |
| for (let i = 0; i < len; i++) { |
| data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2.2); |
| } |
| const noise = ctx.createBufferSource(); |
| noise.buffer = buf; |
| const hp = ctx.createBiquadFilter(); |
| hp.type = 'highpass'; |
| hp.frequency.value = 2400; |
| const g = ctx.createGain(); |
| g.gain.setValueAtTime(0.18, now); |
| g.gain.exponentialRampToValueAtTime(0.001, now + 0.5); |
| noise.connect(hp).connect(g).connect(ctx.destination); |
| noise.start(now); |
| for (let i = 0; i < 5; i++) { |
| const osc = ctx.createOscillator(); |
| const og = ctx.createGain(); |
| osc.type = 'sine'; |
| osc.frequency.value = 1800 + Math.random() * 3200; |
| const t = now + 0.03 + Math.random() * 0.22; |
| og.gain.setValueAtTime(0.05, t); |
| og.gain.exponentialRampToValueAtTime(0.0008, t + 0.3); |
| osc.connect(og).connect(ctx.destination); |
| osc.start(t); |
| osc.stop(t + 0.32); |
| } |
| setTimeout(() => ctx.close(), 1500); |
| } catch { } |
| } |
| |
| function shatterAt(cx, cy) { |
| if (!pane || breaking) return; |
| breaking = true; |
| const hint = pane.querySelector('.glass-hint'); |
| if (hint) hint.remove(); |
| |
| const W = window.innerWidth, H = window.innerHeight; |
| const maxR = Math.max( |
| Math.hypot(cx, cy), Math.hypot(W - cx, cy), |
| Math.hypot(cx, H - cy), Math.hypot(W - cx, H - cy) |
| ) * 1.12; |
| |
| |
| |
| const nRays = 10 + Math.floor(Math.random() * 3); |
| const ringFracs = [0.14, 0.34, 0.62, 1.0]; |
| const pts = []; |
| for (let i = 0; i < nRays; i++) { |
| const baseAngle = (i / nRays) * Math.PI * 2 + (Math.random() - 0.5) * 0.45; |
| const row = [[cx, cy]]; |
| for (let j = 0; j < ringFracs.length; j++) { |
| const a = baseAngle + (Math.random() - 0.5) * 0.18; |
| const r = maxR * ringFracs[j] * (0.85 + Math.random() * 0.3); |
| row.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]); |
| } |
| pts.push(row); |
| } |
| |
| playShatterSound(); |
| |
| |
| const flash = document.createElement('div'); |
| flash.className = 'glass-flash'; |
| flash.style.background = `radial-gradient(circle at ${cx}px ${cy}px, rgba(255,255,255,0.85), rgba(255,255,255,0.25) 18%, transparent 45%)`; |
| document.body.appendChild(flash); |
| flash.animate( |
| [{ opacity: 1 }, { opacity: 0 }], |
| { duration: 320, easing: 'ease-out' } |
| ).onfinish = () => flash.remove(); |
| |
| |
| const main = document.getElementById('main'); |
| if (main) { |
| main.animate( |
| [0, 1, 2, 3, 4, 5].map(k => ({ |
| transform: (k === 0 || k === 5) ? 'translate(0,0)' : |
| `translate(${(Math.random() * 14 - 7).toFixed(1)}px, ${(Math.random() * 14 - 7).toFixed(1)}px)` |
| })), |
| { duration: 340, easing: 'ease-out' } |
| ); |
| } |
| |
| |
| const SVG_NS = 'http://www.w3.org/2000/svg'; |
| const svg = document.createElementNS(SVG_NS, 'svg'); |
| svg.setAttribute('width', W); |
| svg.setAttribute('height', H); |
| svg.style.cssText = 'position:fixed;inset:0;z-index:1502;pointer-events:none;'; |
| const polylines = []; |
| for (let i = 0; i < nRays; i++) { |
| polylines.push(pts[i].map(p => p.join(',')).join(' ')); |
| } |
| for (let j = 1; j < ringFracs.length; j++) { |
| const ring = []; |
| for (let i = 0; i <= nRays; i++) ring.push(pts[i % nRays][j].join(',')); |
| polylines.push(ring.join(' ')); |
| } |
| for (const ptsStr of polylines) { |
| const pl = document.createElementNS(SVG_NS, 'polyline'); |
| pl.setAttribute('points', ptsStr); |
| pl.setAttribute('fill', 'none'); |
| pl.setAttribute('stroke', 'rgba(255,255,255,0.85)'); |
| pl.setAttribute('stroke-width', (0.8 + Math.random() * 1.4).toFixed(2)); |
| svg.appendChild(pl); |
| } |
| document.body.appendChild(svg); |
| for (const pl of svg.children) { |
| const len = pl.getTotalLength(); |
| pl.style.strokeDasharray = len; |
| pl.style.strokeDashoffset = len; |
| pl.animate( |
| [{ strokeDashoffset: len }, { strokeDashoffset: 0 }], |
| { duration: 160 + Math.random() * 120, easing: 'ease-out', fill: 'forwards' } |
| ); |
| } |
| |
| |
| setTimeout(() => { |
| pane.classList.add('broken'); |
| svg.animate( |
| [{ opacity: 1 }, { opacity: 0 }], |
| { duration: 250, fill: 'forwards' } |
| ).onfinish = () => svg.remove(); |
| |
| const shards = []; |
| for (let i = 0; i < nRays; i++) { |
| const next = (i + 1) % nRays; |
| for (let j = 0; j < ringFracs.length; j++) { |
| const poly = j === 0 |
| ? [pts[i][0], pts[i][1], pts[next][1]] |
| : [pts[i][j], pts[i][j + 1], pts[next][j + 1], pts[next][j]]; |
| shards.push({ poly, ring: j }); |
| } |
| } |
| |
| let pending = shards.length; |
| for (const s of shards) { |
| const el = document.createElement('div'); |
| el.className = 'glass-shard'; |
| const sheenAngle = Math.floor(Math.random() * 360); |
| const a = 0.05 + Math.random() * 0.1; |
| el.style.background = `linear-gradient(${sheenAngle}deg, rgba(255,255,255,${(a + 0.08).toFixed(3)}), rgba(190,205,255,${a.toFixed(3)}) 45%, rgba(255,255,255,${(a + 0.05).toFixed(3)}))`; |
| el.style.clipPath = `polygon(${s.poly.map(p => `${p[0].toFixed(1)}px ${p[1].toFixed(1)}px`).join(',')})`; |
| document.body.appendChild(el); |
| |
| const centX = s.poly.reduce((sum, p) => sum + p[0], 0) / s.poly.length; |
| const centY = s.poly.reduce((sum, p) => sum + p[1], 0) / s.poly.length; |
| const d = Math.hypot(centX - cx, centY - cy) || 1; |
| const dirX = (centX - cx) / d, dirY = (centY - cy) / d; |
| const fly = 90 + Math.random() * 240; |
| const fall = H * (0.5 + Math.random() * 0.7); |
| const rot = Math.random() * 140 - 70; |
| |
| el.animate( |
| [ |
| { transform: 'translate(0,0) rotate(0deg)', opacity: 1 }, |
| { transform: `translate(${(dirX * fly * 0.6).toFixed(1)}px, ${(dirY * fly * 0.6).toFixed(1)}px) rotate(${(rot * 0.3).toFixed(1)}deg)`, opacity: 1, offset: 0.25 }, |
| { transform: `translate(${(dirX * fly).toFixed(1)}px, ${(dirY * fly + fall).toFixed(1)}px) rotate(${rot.toFixed(1)}deg)`, opacity: 0 } |
| ], |
| { |
| duration: 800 + Math.random() * 500, |
| delay: s.ring * 55 + Math.random() * 90, |
| easing: 'cubic-bezier(.4, .05, .8, .6)', |
| fill: 'forwards' |
| } |
| ).onfinish = () => { |
| el.remove(); |
| if (--pending === 0) cleanup(); |
| }; |
| } |
| }, 300); |
| |
| function cleanup() { |
| if (pane) { |
| pane.remove(); |
| pane = null; |
| } |
| breaking = false; |
| } |
| } |
| |
| |
| const btn = document.createElement('button'); |
| btn.id = 'shatter-btn'; |
| btn.title = 'Shatter the screen'; |
| btn.setAttribute('aria-label', 'Shatter the screen'); |
| btn.textContent = '🔨'; |
| document.body.appendChild(btn); |
| btn.addEventListener('click', () => { |
| if (breaking) return; |
| if (pane) { |
| shatterAt(window.innerWidth / 2, window.innerHeight / 2); |
| return; |
| } |
| spawnPane(false); |
| setTimeout(() => { |
| shatterAt( |
| window.innerWidth * (0.3 + Math.random() * 0.4), |
| window.innerHeight * (0.25 + Math.random() * 0.4) |
| ); |
| }, 550); |
| }); |
| |
| |
| spawnPane(true); |
| })(); |
| </script> |
|
|
| </body> |
| </html> |
|
|