Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Kimi K2.7-Code — Terminal</title> | |
| <meta name="description" content="Kimi K2.7-Code terminal — Moonshot AI multimodal code model."> | |
| <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=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,700;1,300&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #030303; | |
| --surface: #0a0a0a; | |
| --surface2: #111; | |
| --border: rgba(255,255,255,0.07); | |
| --border-hi: rgba(0, 230, 118, 0.35); | |
| --text: #dcdcdc; | |
| --text-dim: #555; | |
| --text-muted:#333; | |
| --green: #00e676; | |
| --green-dim: rgba(0, 230, 118, 0.12); | |
| --green-glow:rgba(0, 230, 118, 0.08); | |
| --error: #ff5252; | |
| --radius: 12px; | |
| --mono: 'JetBrains Mono', 'Fira Code', monospace; | |
| } | |
| html, body { | |
| height: 100%; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 13.5px; | |
| line-height: 1.65; | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| /* Subtle scanline overlay — terminal feel */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0,0,0,0.03) 2px, | |
| rgba(0,0,0,0.03) 4px | |
| ); | |
| pointer-events: none; | |
| z-index: 9999; | |
| } | |
| /* ─── Shell layout ─── */ | |
| .shell { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| max-width: 880px; | |
| margin: 0 auto; | |
| padding: 0 28px; | |
| } | |
| /* ─── Header ─── */ | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 18px 0 14px; | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| /* Moon SVG */ | |
| .moon-wrap { | |
| position: relative; | |
| width: 30px; | |
| height: 30px; | |
| flex-shrink: 0; | |
| } | |
| .logo-info { display: flex; flex-direction: column; gap: 1px; } | |
| .logo-name { | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text); | |
| letter-spacing: 0.02em; | |
| } | |
| .logo-sub { | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| font-weight: 300; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .status-pill { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| letter-spacing: 0.04em; | |
| } | |
| .status-dot { | |
| width: 5px; | |
| height: 5px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| transition: all 0.3s; | |
| } | |
| .status-dot.on { | |
| background: var(--green); | |
| box-shadow: 0 0 6px var(--green); | |
| } | |
| /* ─── Toolbar ─── */ | |
| .toolbar { | |
| display: flex; | |
| align-items: center; | |
| gap: 0; | |
| padding: 6px 0; | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .tb-item { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| padding: 4px 12px; | |
| cursor: pointer; | |
| border-right: 1px solid var(--border); | |
| letter-spacing: 0.04em; | |
| transition: color 0.15s; | |
| user-select: none; | |
| } | |
| .tb-item:first-child { padding-left: 0; } | |
| .tb-item:hover { color: var(--green); } | |
| .tb-sep { flex: 1; } | |
| .tb-model { | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| padding-right: 0; | |
| } | |
| /* ─── Chat area ─── */ | |
| .chat-area { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px 0 8px; | |
| display: flex; | |
| flex-direction: column; | |
| scrollbar-width: thin; | |
| scrollbar-color: #1a1a1a transparent; | |
| } | |
| .chat-area::-webkit-scrollbar { width: 3px; } | |
| .chat-area::-webkit-scrollbar-thumb { background: #1a1a1a; } | |
| /* ─── Hero ─── */ | |
| .hero { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| flex: 1; | |
| gap: 24px; | |
| padding-bottom: 80px; | |
| text-align: center; | |
| } | |
| .hero-moon { | |
| position: relative; | |
| width: 72px; | |
| height: 72px; | |
| animation: moonFloat 6s ease-in-out infinite; | |
| } | |
| @keyframes moonFloat { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-6px); } | |
| } | |
| .hero-title { | |
| font-size: 15px; | |
| font-weight: 300; | |
| color: var(--text-dim); | |
| letter-spacing: 0.04em; | |
| max-width: 400px; | |
| } | |
| .hero-title span { | |
| color: var(--green); | |
| font-weight: 400; | |
| } | |
| .hero-chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| justify-content: center; | |
| margin-top: 4px; | |
| } | |
| .chip { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 100px; | |
| padding: 5px 14px; | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-family: var(--mono); | |
| letter-spacing: 0.02em; | |
| } | |
| .chip:hover { | |
| border-color: var(--border-hi); | |
| color: var(--green); | |
| background: var(--green-dim); | |
| } | |
| /* ─── Message rows ─── */ | |
| .msg-row { | |
| padding: 18px 0; | |
| border-bottom: 1px solid var(--border); | |
| animation: msgIn 0.2s ease; | |
| } | |
| .msg-row:last-child { border-bottom: none; } | |
| @keyframes msgIn { | |
| from { opacity: 0; transform: translateY(6px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Prompt prefix bar */ | |
| .msg-prefix { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| font-size: 11px; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| } | |
| .prefix-sigil { | |
| color: var(--green); | |
| font-size: 13px; | |
| font-weight: 700; | |
| } | |
| .prefix-label { color: var(--text-muted); } | |
| /* Message body */ | |
| .msg-body { | |
| font-size: 13.5px; | |
| line-height: 1.75; | |
| font-weight: 300; | |
| color: var(--text); | |
| } | |
| .msg-body.user { | |
| color: rgba(220,220,220,0.9); | |
| padding-left: 18px; | |
| border-left: 1px solid var(--border); | |
| } | |
| .msg-body.kimi { | |
| padding-left: 18px; | |
| border-left: 1px solid var(--border-hi); | |
| } | |
| /* Markdown inside kimi responses */ | |
| .msg-body p { margin-bottom: 10px; } | |
| .msg-body p:last-child { margin-bottom: 0; } | |
| .msg-body h1,.msg-body h2,.msg-body h3 { | |
| font-weight: 500; | |
| margin: 18px 0 8px; | |
| color: #eee; | |
| } | |
| .msg-body h1 { font-size: 16px; } | |
| .msg-body h2 { font-size: 14px; } | |
| .msg-body h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--green); } | |
| .msg-body ul, .msg-body ol { | |
| padding-left: 18px; | |
| margin-bottom: 10px; | |
| } | |
| .msg-body li { margin-bottom: 3px; } | |
| .msg-body code { | |
| font-family: var(--mono); | |
| font-size: 12.5px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 1px 5px; | |
| color: var(--green); | |
| } | |
| .msg-body pre { | |
| background: #050505 ; | |
| border: 1px solid var(--border) ; | |
| border-left: 2px solid var(--green) ; | |
| border-radius: 6px 6px 0 0 ; | |
| padding: 14px 16px ; | |
| overflow-x: auto; | |
| margin: 12px 0 0 ; | |
| position: relative; | |
| } | |
| .msg-body pre code { | |
| background: none ; | |
| border: none ; | |
| padding: 0 ; | |
| color: #b0b0b0 ; | |
| font-size: 12.5px ; | |
| line-height: 1.7 ; | |
| } | |
| /* Code block action bar */ | |
| .code-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: #080808; | |
| border: 1px solid var(--border); | |
| border-top: none; | |
| border-radius: 0 0 6px 6px; | |
| padding: 5px 12px; | |
| margin: 0 0 12px; | |
| gap: 8px; | |
| } | |
| .code-lang { | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| font-family: var(--mono); | |
| } | |
| .code-actions { display: flex; gap: 6px; } | |
| .code-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| color: var(--text-dim); | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| padding: 3px 9px; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| letter-spacing: 0.04em; | |
| } | |
| .code-btn:hover { | |
| border-color: var(--border-hi); | |
| color: var(--green); | |
| background: var(--green-dim); | |
| } | |
| .code-btn.active { | |
| border-color: var(--green); | |
| color: var(--green); | |
| background: var(--green-dim); | |
| } | |
| /* Live preview iframe panel */ | |
| .preview-panel { | |
| display: none; | |
| border: 1px solid var(--border-hi); | |
| border-top: 2px solid var(--green); | |
| border-radius: 0 0 8px 8px; | |
| margin: -12px 0 12px; | |
| overflow: hidden; | |
| background: #fff; | |
| position: relative; | |
| } | |
| .preview-panel.open { display: block; } | |
| .preview-topbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 5px 12px; | |
| background: #0d0d0d; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .preview-url { | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| font-family: var(--mono); | |
| letter-spacing: 0.03em; | |
| } | |
| .preview-resize { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .resize-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| border-radius: 3px; | |
| color: var(--text-muted); | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| padding: 2px 7px; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| } | |
| .resize-btn:hover { color: var(--green); border-color: var(--border-hi); } | |
| .resize-btn.active { color: var(--green); border-color: var(--green); background: var(--green-dim); } | |
| .preview-frame-wrap { | |
| width: 100%; | |
| overflow: hidden; | |
| transition: height 0.3s ease; | |
| } | |
| .preview-iframe { | |
| width: 100%; | |
| height: 100%; | |
| border: none; | |
| display: block; | |
| background: #fff; | |
| } | |
| .msg-body blockquote { | |
| border-left: 2px solid var(--green); | |
| padding-left: 14px; | |
| margin: 10px 0; | |
| color: var(--text-dim); | |
| font-style: italic; | |
| } | |
| .msg-body table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 10px 0; | |
| font-size: 12.5px; | |
| } | |
| .msg-body th, .msg-body td { | |
| padding: 7px 12px; | |
| border: 1px solid var(--border); | |
| text-align: left; | |
| } | |
| .msg-body th { | |
| background: var(--surface2); | |
| font-weight: 500; | |
| color: var(--green); | |
| letter-spacing: 0.04em; | |
| } | |
| .msg-body a { color: var(--green); text-decoration: none; } | |
| .msg-body a:hover { text-decoration: underline; } | |
| /* Image pill */ | |
| .img-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| padding: 5px 10px; | |
| margin-bottom: 8px; | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| } | |
| .img-pill img { | |
| width: 32px; height: 32px; | |
| object-fit: cover; | |
| border-radius: 3px; | |
| } | |
| /* Blinking block cursor while streaming */ | |
| .cursor { | |
| display: inline-block; | |
| width: 7px; | |
| height: 13px; | |
| background: var(--green); | |
| margin-left: 2px; | |
| vertical-align: middle; | |
| animation: blink 0.9s step-end infinite; | |
| } | |
| @keyframes blink { | |
| 0%,100% { opacity: 1; } | |
| 50% { opacity: 0; } | |
| } | |
| /* System line */ | |
| .sys-line { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| padding: 6px 0; | |
| font-style: italic; | |
| } | |
| .sys-line.err { color: var(--error); font-style: normal; } | |
| /* ─── Input zone ─── */ | |
| .input-zone { | |
| flex-shrink: 0; | |
| padding: 12px 0 24px; | |
| } | |
| /* Staged image preview */ | |
| .staged-bar { | |
| display: none; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 8px 14px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-bottom: none; | |
| border-radius: var(--radius) var(--radius) 0 0; | |
| } | |
| .staged-bar.show { display: flex; } | |
| .staged-bar img { | |
| width: 32px; height: 32px; | |
| object-fit: cover; | |
| border-radius: 4px; | |
| border: 1px solid var(--border); | |
| } | |
| .staged-bar-name { | |
| flex: 1; | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .staged-rm { | |
| width: 18px; height: 18px; | |
| border-radius: 50%; | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| font-size: 10px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.15s; | |
| } | |
| .staged-rm:hover { background: var(--error); border-color: var(--error); color: #fff; } | |
| /* Input box */ | |
| .input-box { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 0; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 12px 14px; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .input-box.has-img { | |
| border-top-left-radius: 0; | |
| border-top-right-radius: 0; | |
| } | |
| .input-box:focus-within { | |
| border-color: var(--border-hi); | |
| box-shadow: 0 0 0 1px var(--green-glow), 0 0 20px var(--green-glow); | |
| } | |
| /* Terminal prompt prefix inside input */ | |
| .input-prompt { | |
| font-size: 13px; | |
| color: var(--green); | |
| font-weight: 500; | |
| user-select: none; | |
| flex-shrink: 0; | |
| padding-right: 8px; | |
| padding-bottom: 2px; | |
| letter-spacing: 0.02em; | |
| } | |
| .input-ta { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 13.5px; | |
| font-weight: 300; | |
| line-height: 1.5; | |
| resize: none; | |
| min-height: 22px; | |
| max-height: 180px; | |
| overflow-y: auto; | |
| scrollbar-width: none; | |
| } | |
| .input-ta::placeholder { color: var(--text-muted); } | |
| .input-ta::-webkit-scrollbar { display: none; } | |
| .input-btns { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding-left: 10px; | |
| flex-shrink: 0; | |
| } | |
| .icon-btn { | |
| width: 30px; height: 30px; | |
| border-radius: 6px; | |
| background: transparent; | |
| border: 1px solid transparent; | |
| color: var(--text-dim); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| } | |
| .icon-btn:hover { | |
| background: var(--surface2); | |
| border-color: var(--border); | |
| color: var(--text); | |
| } | |
| .icon-btn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } | |
| .send-btn { | |
| width: 30px; height: 30px; | |
| border-radius: 6px; | |
| background: var(--green); | |
| border: none; | |
| color: #000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover { background: #33eb91; } | |
| .send-btn:disabled { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| cursor: not-allowed; | |
| } | |
| .send-btn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; } | |
| .input-foot { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 8px; | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.04em; | |
| } | |
| /* Drag overlay */ | |
| .drag-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,0.92); | |
| z-index: 9998; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 14px; | |
| border: 1px dashed rgba(0,230,118,0.25); | |
| } | |
| .drag-overlay.on { display: flex; } | |
| .drag-overlay svg { width: 40px; height: 40px; opacity: 0.35; } | |
| .drag-overlay p { font-size: 13px; color: var(--text-muted); letter-spacing: 0.04em; } | |
| #file-in { display: none; } | |
| @media (max-width: 600px) { | |
| .shell { padding: 0 14px; } | |
| .toolbar { display: none; } | |
| .hero-chips { display: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Drag overlay --> | |
| <div class="drag-overlay" id="drag-overlay"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="rgba(0,230,118,0.5)" stroke-width="1.5"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> | |
| <polyline points="17 8 12 3 7 8"/> | |
| <line x1="12" y1="3" x2="12" y2="15"/> | |
| </svg> | |
| <p>drop image to attach</p> | |
| </div> | |
| <div class="shell"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="logo"> | |
| <!-- Moon eclipse SVG --> | |
| <svg class="moon-wrap" viewBox="0 0 30 30" fill="none"> | |
| <circle cx="15" cy="15" r="14" fill="#080808" stroke="rgba(255,255,255,0.06)" stroke-width="1"/> | |
| <circle cx="15" cy="15" r="9" fill="#0f0f0f" stroke="rgba(255,255,255,0.08)" stroke-width="0.5"/> | |
| <path d="M11 11 Q15 6 19 11 Q23 15 19 19 Q15 24 11 19 Q7 15 11 11Z" | |
| fill="url(#hg)" opacity="0.95"/> | |
| <defs> | |
| <radialGradient id="hg" cx="62%" cy="28%" r="70%"> | |
| <stop offset="0%" stop-color="#fff" stop-opacity="0.95"/> | |
| <stop offset="40%" stop-color="#888" stop-opacity="0.3"/> | |
| <stop offset="100%" stop-color="#000" stop-opacity="0"/> | |
| </radialGradient> | |
| </defs> | |
| </svg> | |
| <div class="logo-info"> | |
| <div class="logo-name">Kimi K2.7-Code</div> | |
| <div class="logo-sub">Moonshot AI · Novita</div> | |
| </div> | |
| </div> | |
| <div class="header-right"> | |
| <div class="status-pill"> | |
| <div class="status-dot" id="sdot"></div> | |
| <span id="stxt">connecting…</span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Toolbar --> | |
| <div class="toolbar"> | |
| <span class="tb-item" onclick="cmd('/clear')">clear</span> | |
| <span class="tb-item" onclick="cmd('/history')">history</span> | |
| <span class="tb-item" onclick="cmd('/model')">model info</span> | |
| <div class="tb-sep"></div> | |
| <span class="tb-model">moonshotai/Kimi-K2.7-Code:novita</span> | |
| </div> | |
| <!-- Chat area --> | |
| <div class="chat-area" id="chat-area"> | |
| <div class="hero" id="hero"> | |
| <!-- Large moon --> | |
| <svg class="hero-moon" viewBox="0 0 72 72" fill="none"> | |
| <circle cx="36" cy="36" r="35" fill="#070707" stroke="rgba(255,255,255,0.05)" stroke-width="1"/> | |
| <circle cx="36" cy="36" r="22" fill="#0e0e0e" stroke="rgba(255,255,255,0.07)" stroke-width="0.5"/> | |
| <path d="M26 26 Q36 14 46 26 Q56 36 46 46 Q36 58 26 46 Q16 36 26 26Z" | |
| fill="url(#hm)" opacity="0.95"/> | |
| <defs> | |
| <radialGradient id="hm" cx="60%" cy="26%" r="72%"> | |
| <stop offset="0%" stop-color="#fff" stop-opacity="1"/> | |
| <stop offset="38%" stop-color="#777" stop-opacity="0.45"/> | |
| <stop offset="100%" stop-color="#000" stop-opacity="0"/> | |
| </radialGradient> | |
| </defs> | |
| </svg> | |
| <p class="hero-title">Seeking the optimal conversion from<br><span>energy to intelligence</span></p> | |
| <div class="hero-chips"> | |
| <div class="chip" onclick="fillPrompt('Write a Python async web scraper with aiohttp')">python async scraper</div> | |
| <div class="chip" onclick="fillPrompt('Explain this code and find bugs')">code review</div> | |
| <div class="chip" onclick="fillPrompt('Write a Rust CLI tool for file compression')">rust cli tool</div> | |
| <div class="chip" onclick="fillPrompt('What do you see in this image?')">vision analysis</div> | |
| <div class="chip" onclick="fillPrompt('Design a REST API with FastAPI and SQLAlchemy')">fastapi design</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input zone --> | |
| <div class="input-zone"> | |
| <div class="staged-bar" id="staged-bar"> | |
| <img id="staged-thumb" src="" alt=""> | |
| <span class="staged-bar-name" id="staged-name">—</span> | |
| <button class="staged-rm" onclick="clearImg()">✕</button> | |
| </div> | |
| <div class="input-box" id="input-box"> | |
| <span class="input-prompt">>_</span> | |
| <textarea | |
| class="input-ta" | |
| id="pta" | |
| placeholder="Throw me a hard one, I'm ready." | |
| rows="1" | |
| autofocus | |
| ></textarea> | |
| <div class="input-btns"> | |
| <button class="icon-btn" onclick="triggerFile()" title="Attach image"> | |
| <svg viewBox="0 0 24 24"> | |
| <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/> | |
| </svg> | |
| </button> | |
| <button class="send-btn" id="send-btn" onclick="handleSend()" title="Send (Enter)"> | |
| <svg viewBox="0 0 24 24"> | |
| <line x1="12" y1="19" x2="12" y2="5"/> | |
| <polyline points="5 12 12 5 19 12"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="input-foot"> | |
| <span>enter to send · shift+enter for newline · /clear /history /model</span> | |
| <span id="foot-time">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| <input type="file" id="file-in" accept="image/*" onchange="onFileChange(event)"> | |
| <!-- NOTE: using lowercase { client } as per Gradio Server docs demo --> | |
| <script type="module"> | |
| import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| /* ── State ── */ | |
| let app = null; | |
| let chatHistory = []; | |
| let stagedFile = null; | |
| let stagedB64 = ""; | |
| let isGenerating = false; | |
| /* ── Clock ── */ | |
| setInterval(() => { | |
| const el = document.getElementById("foot-time"); | |
| if (el) el.textContent = new Date().toTimeString().slice(0,8); | |
| }, 1000); | |
| /* ── Marked config ── */ | |
| marked.setOptions({ breaks: true }); | |
| /* ── Connect ── */ | |
| async function connect() { | |
| try { | |
| // Use lowercase `client` as in Gradio Server docs | |
| app = await client(window.location.origin); | |
| setStatus(true); | |
| appendSys("session established · model ready"); | |
| } catch(e) { | |
| setStatus(false); | |
| appendSys("connection failed: " + e.message, true); | |
| } | |
| } | |
| function setStatus(on) { | |
| document.getElementById("sdot").className = "status-dot" + (on ? " on" : ""); | |
| document.getElementById("stxt").textContent = on ? "online" : "offline"; | |
| } | |
| /* ── Toolbar commands ── */ | |
| window.cmd = function(c) { | |
| if (c === "/clear") { | |
| const ca = document.getElementById("chat-area"); | |
| ca.innerHTML = ""; | |
| chatHistory = []; | |
| appendSys("terminal cleared"); | |
| return; | |
| } | |
| if (c === "/history") { | |
| appendSys(`history buffer: ${chatHistory.length} messages`); | |
| return; | |
| } | |
| if (c === "/model") { | |
| appendSys("model: moonshotai/Kimi-K2.7-Code:novita"); | |
| appendSys("host: https://router.huggingface.co/v1"); | |
| appendSys("context: 128k tokens · multimodal: yes"); | |
| return; | |
| } | |
| }; | |
| /* ── Chip fill ── */ | |
| window.fillPrompt = function(t) { | |
| const ta = document.getElementById("pta"); | |
| ta.value = t; | |
| ta.dispatchEvent(new Event("input")); | |
| ta.focus(); | |
| }; | |
| /* ── File handling ── */ | |
| window.triggerFile = function() { document.getElementById("file-in").click(); }; | |
| window.onFileChange = function(e) { const f = e.target.files[0]; if(f) stageFile(f); }; | |
| function stageFile(file) { | |
| if (!file.type.startsWith("image/")) return; | |
| stagedFile = file; | |
| const reader = new FileReader(); | |
| reader.onload = ev => { | |
| stagedB64 = ev.target.result; | |
| document.getElementById("staged-thumb").src = stagedB64; | |
| document.getElementById("staged-name").textContent = | |
| file.name + " (" + (file.size/1024).toFixed(1) + " KB)"; | |
| document.getElementById("staged-bar").classList.add("show"); | |
| document.getElementById("input-box").classList.add("has-img"); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| window.clearImg = function() { | |
| stagedFile = null; stagedB64 = ""; | |
| document.getElementById("file-in").value = ""; | |
| document.getElementById("staged-bar").classList.remove("show"); | |
| document.getElementById("input-box").classList.remove("has-img"); | |
| }; | |
| /* ── Drag & drop ── */ | |
| const overlay = document.getElementById("drag-overlay"); | |
| window.addEventListener("dragenter", e => { e.preventDefault(); overlay.classList.add("on"); }); | |
| overlay.addEventListener("dragover", e => e.preventDefault()); | |
| overlay.addEventListener("dragleave", e => { if (!overlay.contains(e.relatedTarget)) overlay.classList.remove("on"); }); | |
| overlay.addEventListener("drop", e => { | |
| e.preventDefault(); overlay.classList.remove("on"); | |
| const f = e.dataTransfer.files[0]; if (f) stageFile(f); | |
| }); | |
| /* ── Textarea ── */ | |
| const pta = document.getElementById("pta"); | |
| pta.addEventListener("input", function() { | |
| this.style.height = "auto"; | |
| this.style.height = Math.min(this.scrollHeight, 180) + "px"; | |
| }); | |
| pta.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } | |
| }); | |
| /* ── DOM helpers ── */ | |
| const ca = () => document.getElementById("chat-area"); | |
| function hideHero() { | |
| const h = document.getElementById("hero"); | |
| if (h) h.remove(); | |
| } | |
| function scrollBot() { | |
| const el = ca(); el.scrollTop = el.scrollHeight; | |
| } | |
| function appendSys(text, isErr = false) { | |
| const d = document.createElement("div"); | |
| d.className = "sys-line" + (isErr ? " err" : ""); | |
| d.textContent = "// " + text; | |
| ca().appendChild(d); | |
| scrollBot(); | |
| } | |
| function addUserRow(prompt, imgSrc, imgName) { | |
| hideHero(); | |
| const row = document.createElement("div"); | |
| row.className = "msg-row"; | |
| let imgHtml = ""; | |
| if (imgSrc) { | |
| imgHtml = `<div class="img-pill"><img src="${imgSrc}" alt=""><span>${escHtml(imgName)}</span></div><br>`; | |
| } | |
| row.innerHTML = ` | |
| <div class="msg-prefix"> | |
| <span class="prefix-sigil">›</span> | |
| <span class="prefix-label">you</span> | |
| </div> | |
| <div class="msg-body user">${imgHtml}${escHtml(prompt)}</div> | |
| `; | |
| ca().appendChild(row); | |
| scrollBot(); | |
| } | |
| function addKimiRow() { | |
| const row = document.createElement("div"); | |
| row.className = "msg-row"; | |
| row.innerHTML = ` | |
| <div class="msg-prefix"> | |
| <span class="prefix-sigil" style="color:#888;">◎</span> | |
| <span class="prefix-label">kimi</span> | |
| </div> | |
| <div class="msg-body kimi" id="kimi-active"><span class="cursor"></span></div> | |
| `; | |
| ca().appendChild(row); | |
| scrollBot(); | |
| return document.getElementById("kimi-active"); | |
| } | |
| function escHtml(t) { | |
| return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); | |
| } | |
| /* ── Stream renderer ── */ | |
| function makeRenderer(el) { | |
| let buf = ""; | |
| let pending = false; | |
| function render() { | |
| pending = false; | |
| el.innerHTML = marked.parse(buf) + '<span class="cursor"></span>'; | |
| scrollBot(); | |
| } | |
| return { | |
| push(token) { | |
| buf += token; | |
| if (!pending) { pending = true; requestAnimationFrame(render); } | |
| }, | |
| finish() { | |
| el.id = ""; | |
| el.innerHTML = marked.parse(buf); | |
| injectCodeBars(el); | |
| scrollBot(); | |
| return buf; | |
| } | |
| }; | |
| } | |
| /* ── Code bar + live preview injector ── */ | |
| function injectCodeBars(container) { | |
| const pres = container.querySelectorAll("pre"); | |
| pres.forEach(pre => { | |
| // Avoid double-processing | |
| if (pre.nextElementSibling && pre.nextElementSibling.classList.contains("code-bar")) return; | |
| const codeEl = pre.querySelector("code"); | |
| const rawCode = codeEl ? codeEl.textContent : pre.textContent; | |
| // Detect language from class (e.g. language-html) | |
| const cls = codeEl ? codeEl.className : ""; | |
| const langMatch = cls.match(/language-(\w+)/); | |
| const lang = langMatch ? langMatch[1].toLowerCase() : ""; | |
| // Determine if this is a web-previewable block | |
| const isHtml = lang === "html" || lang === "svg"; | |
| const isPreviewable = isHtml || | |
| (!lang && rawCode.trim().startsWith("<") && rawCode.includes("</")); | |
| // Build action bar | |
| const bar = document.createElement("div"); | |
| bar.className = "code-bar"; | |
| let previewBtn = ""; | |
| if (isPreviewable || lang === "css" || lang === "js" || lang === "javascript") { | |
| previewBtn = `<button class="code-btn preview-toggle" title="Render preview">▶ Preview</button>`; | |
| } | |
| bar.innerHTML = ` | |
| <span class="code-lang">${lang || "code"}</span> | |
| <div class="code-actions"> | |
| ${previewBtn} | |
| <button class="code-btn copy-btn" title="Copy">Copy</button> | |
| </div> | |
| `; | |
| pre.after(bar); | |
| // Copy button | |
| const copyBtn = bar.querySelector(".copy-btn"); | |
| copyBtn.addEventListener("click", () => { | |
| navigator.clipboard.writeText(rawCode).then(() => { | |
| copyBtn.textContent = "Copied!"; | |
| copyBtn.classList.add("active"); | |
| setTimeout(() => { copyBtn.textContent = "Copy"; copyBtn.classList.remove("active"); }, 1800); | |
| }); | |
| }); | |
| // Preview toggle | |
| const pvBtn = bar.querySelector(".preview-toggle"); | |
| if (!pvBtn) return; | |
| // Build preview panel (hidden by default) | |
| const panel = document.createElement("div"); | |
| panel.className = "preview-panel"; | |
| // Compose the HTML to render | |
| let renderSrc = rawCode; | |
| if (lang === "css") { | |
| renderSrc = `<!DOCTYPE html><html><head><style>${rawCode}</style></head><body><p style='font-family:sans-serif;padding:20px;color:#333'>CSS preview — add HTML to see layout</p></body></html>`; | |
| } else if (lang === "js" || lang === "javascript") { | |
| renderSrc = `<!DOCTYPE html><html><body><script>${rawCode}<\/script></body></html>`; | |
| } | |
| panel.innerHTML = ` | |
| <div class="preview-topbar"> | |
| <span class="preview-url">▣ live preview — sandbox</span> | |
| <div class="preview-resize"> | |
| <button class="resize-btn" data-h="260">S</button> | |
| <button class="resize-btn active" data-h="420">M</button> | |
| <button class="resize-btn" data-h="620">L</button> | |
| <button class="resize-btn" data-h="900">XL</button> | |
| </div> | |
| </div> | |
| <div class="preview-frame-wrap" style="height:420px"> | |
| <iframe class="preview-iframe" | |
| sandbox="allow-scripts allow-forms allow-modals" | |
| srcdoc="" | |
| ></iframe> | |
| </div> | |
| `; | |
| bar.after(panel); | |
| // Resize buttons | |
| panel.querySelectorAll(".resize-btn").forEach(rb => { | |
| rb.addEventListener("click", () => { | |
| panel.querySelectorAll(".resize-btn").forEach(b => b.classList.remove("active")); | |
| rb.classList.add("active"); | |
| panel.querySelector(".preview-frame-wrap").style.height = rb.dataset.h + "px"; | |
| }); | |
| }); | |
| let loaded = false; | |
| pvBtn.addEventListener("click", () => { | |
| const isOpen = panel.classList.toggle("open"); | |
| pvBtn.textContent = isOpen ? "▼ Hide" : "▶ Preview"; | |
| pvBtn.classList.toggle("active", isOpen); | |
| if (isOpen && !loaded) { | |
| loaded = true; | |
| const iframe = panel.querySelector(".preview-iframe"); | |
| iframe.srcdoc = renderSrc; | |
| } | |
| scrollBot(); | |
| }); | |
| }); | |
| } | |
| /* ── Send ── */ | |
| window.handleSend = async function() { | |
| if (isGenerating) return; | |
| const prompt = pta.value.trim(); | |
| if (!prompt && !stagedFile) return; | |
| const imgSrc = stagedB64; | |
| const imgName = stagedFile ? stagedFile.name : ""; | |
| const imgB64 = stagedB64; | |
| pta.value = ""; pta.style.height = "auto"; | |
| clearImg(); | |
| addUserRow(prompt, imgSrc, imgName); | |
| if (!app) { | |
| appendSys("not connected — retrying…", true); | |
| await connect(); | |
| if (!app) return; | |
| } | |
| isGenerating = true; | |
| document.getElementById("send-btn").disabled = true; | |
| const kimiEl = addKimiRow(); | |
| const renderer = makeRenderer(kimiEl); | |
| let fullReply = ""; | |
| try { | |
| // Use app.submit for streaming — matches Gradio Server @app.api generator | |
| const job = app.submit("/chat", { | |
| prompt, | |
| history_json: JSON.stringify(chatHistory), | |
| image_b64: imgB64 | |
| }); | |
| for await (const msg of job) { | |
| if (msg.type === "data") { | |
| const token = msg.data[0]; | |
| if (token) { renderer.push(token); fullReply += token; } | |
| } else if (msg.type === "status" && msg.stage === "error") { | |
| renderer.push("\n\n[endpoint error — check logs]"); | |
| } | |
| } | |
| fullReply = renderer.finish(); | |
| if (fullReply) { | |
| chatHistory.push({ role: "user", content: prompt }); | |
| chatHistory.push({ role: "assistant", content: fullReply }); | |
| } | |
| } catch(err) { | |
| renderer.finish(); | |
| appendSys("error: " + err.message, true); | |
| } finally { | |
| isGenerating = false; | |
| document.getElementById("send-btn").disabled = false; | |
| pta.focus(); | |
| } | |
| }; | |
| connect(); | |
| </script> | |
| </body> | |
| </html> | |