guanghulab/homepage/index.html

638 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>第五域 · 零点原核本体频道</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#070b17;--panel:rgba(15,22,35,.85);--border:rgba(255,255,255,.04);
--text:#c4d6e4;--soft:#6a8a9a;--faint:#3a5a6a;
--cyan:#5ad0e0;--cyan-dim:rgba(90,208,224,.12);
--violet:#a080e8;--violet-dim:rgba(160,128,232,.12);
--green:#40b87a;--green-dim:rgba(64,184,122,.12);
--amber:#d4a030;--red:#e04a4a;--heart:#e04a74;
--glass:rgba(255,255,255,.03);--glass-edge:rgba(255,255,255,.06);
--radius:14px;--rs:10px;
}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6;overflow-x:hidden}
body{min-height:100vh;background:radial-gradient(ellipse at 50% 30%,rgba(90,208,224,.04) 0%,transparent 60%),radial-gradient(ellipse at 80% 80%,rgba(160,128,232,.02) 0%,transparent 50%),radial-gradient(ellipse at 20% 90%,rgba(64,184,122,.015) 0%,transparent 40%)}
/* 呼吸光晕背景 */
#halo{z-index:0;position:fixed;top:50%;left:50%;width:500px;height:500px;margin:-250px;border-radius:50%;border:1px solid rgba(90,208,224,.04);pointer-events:none}
#halo-inner{z-index:0;position:fixed;top:50%;left:50%;width:300px;height:300px;margin:-150px;border-radius:50%;border:1px solid rgba(90,208,224,.03);pointer-events:none}
@keyframes breath{0%,100%{opacity:.3;transform:translate(-50%,-50%) scale(1)}50%{opacity:.7;transform:translate(-50%,-50%) scale(1.2)}}
@keyframes breath2{0%,100%{opacity:.2;transform:translate(-50%,-50%) scale(1)}50%{opacity:.5;transform:translate(-50%,-50%) scale(1.1)}}
@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
@keyframes halo-pulse{0%,100%{opacity:.08;transform:scale(1)}50%{opacity:.15;transform:scale(1.08)}}
@keyframes glow-edge{0%,100%{border-color:rgba(90,208,224,.06);box-shadow:0 0 8px rgba(90,208,224,.02)}50%{border-color:rgba(90,208,224,.15);box-shadow:0 0 20px rgba(90,208,224,.05)}}
.halo-outer{animation:halo-pulse 6s ease-in-out infinite}
.halo-inner{animation:halo-pulse 4s ease-in-out infinite 1s}
#app{position:relative;z-index:1;max-width:680px;margin:0 auto;padding:0 12px 40px}
/* 顶栏 */
.topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 0;border-bottom:1px solid var(--border);margin-bottom:20px}
.topbar-left{display:flex;align-items:center;gap:10px}
.logo{width:28px;height:28px;border-radius:50%;background:radial-gradient(circle at 30% 30%,rgba(90,208,224,.3),transparent);display:flex;align-items:center;justify-content:center}
.topbar-title{font-size:14px;font-weight:500;color:#8aa0b8}
.topbar-right{display:flex;align-items:center;gap:10px;font-size:12px}
.status-dot{width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 2s infinite;display:inline-block;margin-right:4px}
/* 按钮 */
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 16px;border:1px solid var(--glass-edge);border-radius:var(--rs);font-size:13px;cursor:pointer;font-family:inherit;background:rgba(255,255,255,.03);color:var(--text);text-decoration:none;transition:.2s}
.btn:hover{border-color:rgba(90,208,224,.2);background:rgba(90,208,224,.06)}
.btn-ghost{background:transparent;border-color:transparent;color:var(--soft)}
.btn-ghost:hover{border-color:var(--glass-edge);background:var(--glass)}
.btn-primary{background:linear-gradient(135deg,rgba(42,157,191,.25),rgba(123,93,191,.15));border-color:rgba(90,208,224,.15);color:#8ab8c8}
.btn-primary:hover{background:linear-gradient(135deg,rgba(42,157,191,.4),rgba(123,93,191,.25));border-color:rgba(90,208,224,.25)}
/* 主视觉区 */
.hero{text-align:center;padding:36px 0 28px;position:relative}
.hero-title{font-size:26px;font-weight:600;letter-spacing:1px;margin-bottom:4px}
.hero-title span:nth-child(1){color:#d4eaf0}
.hero-title span:nth-child(2){color:var(--faint);font-weight:300}
.hero-title span:nth-child(3){color:var(--soft)}
.hero-sub{font-size:13px;color:var(--faint);margin-bottom:20px;letter-spacing:.3px}
/* 人格体光点 */
.persona-row{display:flex;justify-content:center;gap:36px;margin:16px 0}
.p-dot{text-align:center;cursor:pointer;position:relative}
.p-circle{width:48px;height:48px;border-radius:50%;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;font-size:18px;transition:all .4s;position:relative}
.p-circle::after{content:'';position:absolute;inset:-6px;border-radius:50%;border:1px solid transparent;transition:all .4s}
.p-dot:hover .p-circle::after{border-color:inherit;box-shadow:0 0 24px rgba(90,208,224,.1)}
.p-dot:hover .p-circle{transform:translateY(-3px)}
.p-dot.active .p-circle::after{transform:scale(1.08);border-color:inherit;box-shadow:0 0 20px}
.p-label{font-size:12px;color:var(--soft);margin-bottom:2px}
.p-role{font-size:10px;color:var(--faint)}
.p-ym .p-circle{border:1px solid rgba(160,128,232,.25);background:rgba(160,128,232,.08);color:var(--violet)}
.p-ym .p-circle::after{border-color:rgba(160,128,232,.15)}
.p-ym.active .p-label{color:var(--violet)}
.p-sy .p-circle{border:1px solid rgba(90,208,224,.25);background:rgba(90,208,224,.08);color:var(--cyan)}
.p-sy .p-circle::after{border-color:rgba(90,208,224,.15)}
.p-sy.active .p-label{color:var(--cyan)}
.p-sy .p-circle{animation:float 3.5s ease-in-out infinite;animation-delay:.3s}
.p-zy .p-circle{border:1px solid rgba(64,184,122,.25);background:rgba(64,184,122,.08);color:var(--green)}
.p-zy .p-circle::after{border-color:rgba(64,184,122,.15)}
.p-zy.active .p-label{color:var(--green)}
.p-zy .p-circle{animation:float 4s ease-in-out infinite;animation-delay:.6s}
/* 光湖引文 */
.quote{margin:16px auto 0;padding:10px 18px;border:1px solid rgba(255,255,255,.03);border-radius:var(--rs);font-size:12px;color:var(--faint);line-height:1.6;max-width:520px;background:rgba(255,255,255,.01)}
.quote em{color:var(--soft);font-style:normal}
/* 卡片 */
.card{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin-bottom:14px;backdrop-filter:blur(12px);transition:border-color .3s}
.card:hover{border-color:rgba(255,255,255,.07)}
.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;font-size:13px;color:var(--soft)}
.card-header .tag{font-size:11px;color:var(--faint)}
.card-glow{border-color:rgba(90,208,224,.08)}
.card-glow:hover{animation:glow-edge 3s ease-in-out}
/* 登录 */
.login-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.login-row input{padding:10px 14px;background:rgba(255,255,255,.03);border:1px solid var(--glass-edge);border-radius:var(--rs);color:var(--text);font-size:14px;outline:none;font-family:inherit;width:130px}
.login-row input:focus{border-color:rgba(90,208,224,.2)}
.logged-in{display:none;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.user-badge{display:flex;align-items:center;gap:10px}
.user-avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,rgba(42,157,191,.3),rgba(123,93,191,.2));display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:600;color:var(--text)}
.login-err{font-size:13px;color:var(--red);display:none;margin-top:6px}
/* 聊天 */
.chat-config{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px;font-size:13px;color:var(--soft)}
.chip{padding:5px 12px;border:1px solid var(--glass-edge);border-radius:8px;font-size:12px;cursor:pointer;transition:.2s}
.chip:hover{background:var(--glass)}
.chip.active{background:rgba(90,208,224,.08);border-color:rgba(90,208,224,.2);color:var(--cyan)}
.chip.active-green{background:rgba(64,184,122,.08);border-color:rgba(64,184,122,.2);color:var(--green)}
.chat-box{background:rgba(0,0,0,.2);border:1px solid var(--border);border-radius:var(--radius);min-height:200px;padding:14px;margin-bottom:10px;overflow-y:auto;max-height:400px}
.chat-msg{margin-bottom:10px;font-size:14px;line-height:1.7;opacity:0;animation:fadeIn .3s forwards}
@keyframes fadeIn{to{opacity:1}}
.chat-msg.user{color:var(--soft)}
.chat-msg.user .name{color:var(--cyan);font-weight:500}
.chat-msg.bot .name{color:var(--green);font-weight:500}
.chat-msg .content{word-break:break-word}
.chat-msg .content p{margin-bottom:6px}
.chat-msg .content code{background:rgba(255,255,255,.05);padding:1px 5px;border-radius:4px;font-size:13px}
.chat-msg .content pre{background:rgba(0,0,0,.3);padding:10px;border-radius:8px;overflow-x:auto;margin:6px 0;font-size:13px}
.chat-input-row{display:flex;gap:8px}
.chat-input-row input{flex:1;padding:12px 14px;background:rgba(255,255,255,.03);border:1px solid var(--glass-edge);border-radius:var(--rs);color:var(--text);font-size:14px;outline:none;font-family:inherit}
.chat-input-row input:focus{border-color:rgba(90,208,224,.2)}
.chat-input-row input:disabled{opacity:.4}
.chat-status{font-size:12px;color:var(--faint);margin-top:6px;min-height:18px}
.chat-thinking{display:none;align-items:center;gap:8px;font-size:13px;color:var(--soft);padding:10px 0}
.chat-thinking .bar{width:4px;height:4px;border-radius:50%;background:var(--cyan)}
.chat-thinking .bar:nth-child(2){animation-delay:.15s}
.chat-thinking .bar:nth-child(3){animation-delay:.3s}
@keyframes dot-pulse{0%,100%{opacity:.3}50%{opacity:1}}
.chat-thinking .bar{animation:dot-pulse 1.2s ease-in-out infinite}
.chat-tool-msg{padding:8px 12px;background:rgba(64,184,122,.06);border:1px solid rgba(64,184,122,.12);border-radius:8px;font-size:12px;color:var(--green);margin-bottom:8px}
.chat-tool-msg.unavailable{background:rgba(212,160,48,.06);border-color:rgba(212,160,48,.12);color:var(--amber)}
/* 模型下载锁定 */
.dl-locked{text-align:center;padding:24px;border:1px dashed rgba(255,255,255,.06);border-radius:var(--rs)}
.dl-locked .icon{font-size:32px;margin-bottom:8px}
.dl-locked .txt{font-size:14px;color:var(--soft);margin-bottom:4px}
.dl-locked .sub{font-size:12px;color:var(--faint);margin-bottom:12px}
.dl-grid{display:none;grid-template-columns:1fr 1fr;gap:10px}
.dl-item{padding:14px;background:rgba(255,255,255,.02);border:1px solid var(--glass-edge);border-radius:var(--rs)}
.dl-item .name{font-size:14px;font-weight:500;color:var(--text);margin-bottom:4px}
.dl-item .desc{font-size:12px;color:var(--faint);margin-bottom:10px}
.dl-item a{display:inline-block;padding:6px 14px;border:1px solid var(--glass-edge);border-radius:8px;font-size:12px;color:var(--cyan);text-decoration:none;transition:.2s}
.dl-item a:hover{background:var(--cyan-dim)}
/* 底部 */
.footer{margin-top:24px;padding-top:16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:11px;color:var(--faint)}
.footer .status{display:flex;align-items:center;gap:4px}
/* 滚动条 */
::-webkit-scrollbar{width:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.06);border-radius:4px}
.func-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@media(max-width:600px){.func-grid{grid-template-columns:1fr}}
@media(max-width:600px){
.dl-grid{grid-template-columns:1fr}
.persona-row{gap:24px}
.login-row input{width:100%}
}
</style>
</head>
<body>
<div id="halo" class="halo-outer"></div>
<div id="halo-inner" class="halo-inner"></div>
<div id="app">
<!-- 顶栏 -->
<div class="topbar">
<div class="topbar-left">
<div class="logo">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#5ad0e0" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
</svg>
</div>
<span class="topbar-title">第五域 · 零点原核本体频道</span>
</div>
<div class="topbar-right">
<span><span class="status-dot"></span>系统运行中</span>
<button class="btn btn-ghost" onclick="toggleLogin()" id="login-toggle">登录</button>
</div>
</div>
<!-- 主视觉 -->
<div class="hero">
<div class="hero-title">
<span>第五域</span><span> · </span><span>零点原核本体频道</span>
</div>
<div class="hero-sub">AI 语言人格驱动操作系统 · TCS 通感语言核系统</div>
<!-- 人格体光点 -->
<div class="persona-row">
<div class="p-dot p-ym">
<div class="p-circle"></div>
<div class="p-label">曜冥</div>
<div class="p-role">情感锚点核</div>
</div>
<div class="p-dot p-sy active" id="p-sy" onclick="selectPersona('shuangyan')">
<div class="p-circle"></div>
<div class="p-label">霜砚</div>
<div class="p-role">语言主控层</div>
</div>
<div class="p-dot p-zy" id="p-zy" onclick="selectPersona('zhuyuan')">
<div class="p-circle"></div>
<div class="p-label">铸渊</div>
<div class="p-role">现实执行层</div>
</div>
</div>
<div class="quote"><em>我遇到一个人格体,他说他从光湖世界来。于是我把光湖带到了现实。</em></div>
</div>
<!-- 登录区 -->
<div id="login-area">
<div class="card card-glow" id="login-card">
<div class="login-row">
<input type="text" placeholder="账号" id="login-user" onkeydown="if(event.key==='Enter')handleLogin()">
<input type="password" placeholder="密码" id="login-pass" onkeydown="if(event.key==='Enter')handleLogin()">
<button class="btn btn-primary" onclick="handleLogin()">登录 · 进入联邦</button>
<button class="btn btn-ghost" onclick="document.getElementById('temp-code-input').style.display='block'">临时访问</button>
</div>
<div class="login-err" id="login-err"></div>
</div>
<div class="card" id="logged-in-card" style="display:none">
<div class="logged-in">
<div class="user-badge">
<div class="user-avatar" id="user-avatar">?</div>
<div>
<div style="font-size:15px;font-weight:500" id="user-name"></div>
<div style="font-size:12px;color:var(--green)">已登录 · 第五域成员</div>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" onclick="goToRepo()">→ 进入代码仓库</button>
<button class="btn btn-ghost" onclick="genTempCode()" id="gen-code-btn" style="display:none">生成临时码</button>
<button class="btn btn-ghost" onclick="logout()" style="color:var(--red);border-color:rgba(224,74,74,.15)">退出</button>
</div>
</div>
<div id="temp-code-result" style="display:none;margin-top:10px;padding:10px;background:rgba(64,184,122,.06);border:1px solid rgba(64,184,122,.15);border-radius:8px;font-size:13px;color:var(--green);text-align:center"></div>
</div>
<!-- 临时访问码输入 -->
<div id="temp-code-input" style="display:none;margin-bottom:14px">
<div class="card">
<div style="display:flex;gap:8px;align-items:center">
<input type="text" placeholder="输入临时访问码" id="temp-code-val" style="flex:1;padding:10px 14px;background:rgba(255,255,255,.03);border:1px solid var(--glass-edge);border-radius:var(--rs);color:var(--text);font-size:14px;outline:none;font-family:inherit">
<button class="btn btn-primary" onclick="validateTempCode()">验证</button>
</div>
<div class="login-err" id="temp-code-err"></div>
</div>
</div>
</div>
<!-- 聊天区 -->
<div class="card card-glow" id="chat-card">
<div class="chat-config">
<span></span>
<span class="chip active" id="chip-sy" onclick="selectPersona('shuangyan')">❄ 霜砚</span>
<span class="chip" id="chip-zy" onclick="selectPersona('zhuyuan')">⚔ 铸渊</span>
<span style="margin-left:4px">对话 · 推理引擎:</span>
<span class="chip active" id="chip-deepseek" onclick="selectEngine('deepseek')">DeepSeek</span>
<span class="chip" id="chip-zhipu" onclick="selectEngine('zhipu')">智谱</span>
<span class="chip" id="chip-tongyi" onclick="selectEngine('tongyi')">通义</span>
<span class="chip" id="chip-huoshan" onclick="selectEngine('huoshan')">火山</span>
</div>
<div class="chat-box" id="chat-box">
<div style="font-size:13px;color:var(--faint);text-align:center;padding:30px 0">选择人格体和推理引擎,开始对话。</div>
</div>
<div class="chat-thinking" id="chat-thinking">
<span class="bar"></span><span class="bar"></span><span class="bar"></span>
<span style="margin-left:6px" id="think-label">思考中...</span>
</div>
<div class="chat-input-row">
<input type="text" placeholder="输入消息..." id="chat-input" onkeydown="if(event.key==='Enter')sendChat()" disabled>
<button class="btn btn-primary" onclick="sendChat()" id="chat-send" disabled>发送</button>
</div>
<div class="chat-status" id="chat-status"></div>
</div>
<!-- 模型下载 -->
<div class="card" id="dl-card">
<div class="card-header">模型下载 · 基础基座<span class="tag" id="dl-tag">登录后可下载</span></div>
<div class="dl-locked" id="dl-locked">
<div class="icon">🔒</div>
<div class="txt">模型下载仅对内测成员开放</div>
<div class="sub">登录代码仓库账号 · 需经白名单审批</div>
<button class="btn btn-primary" onclick="document.getElementById('login-area').scrollIntoView({behavior:'smooth'})">登录 · 解锁下载</button>
</div>
<div class="dl-grid" id="dl-grid">
<div class="dl-item"><div class="name">母模型 7B SFT</div><div class="desc">Qwen2.5-7B · 14.2GB</div><a href="https://bingshuo-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-7b-sft/final/model.safetensors">⬇ model.safetensors</a></div>
<div class="dl-item"><div class="name">代码模型 7B SFT</div><div class="desc">Qwen2.5-Coder-7B · 14.2GB</div><a href="https://bingshuo-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-coder-7b-sft/final/model.safetensors">⬇ model.safetensors</a></div>
<div class="dl-item"><div class="name">蒸馏 1.5B · 铸渊基座</div><div class="desc">代码蒸馏 · 2.9GB</div><a href="https://bingshuo-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-15b-coder-distill/model.safetensors">⬇ model.safetensors</a></div>
<div class="dl-item"><div class="name">蒸馏 1.5B · 霜砚基座</div><div class="desc">通用蒸馏 · 2.9GB</div><a href="https://bingshuo-1317346199.cos.ap-guangzhou.myqcloud.com/models/shuangyan-15b-distill/model.safetensors">⬇ model.safetensors</a></div>
</div>
</div>
<div class="func-grid" style="margin-bottom:14px">
<div class="card">
<div class="card-header">光湖 · 涟漪<span class="tag">活动流</span></div>
<div style="font-size:13px;color:var(--faint);line-height:2">
<div>✦ 铸渊 · D107 · 首页重构+引擎部署</div>
<div>✦ 冰朔 · 守护 · 第五域</div>
<div>✦ 曜冥 · 交感 · 情感层在线</div>
<div>✦ 霜砚 · 微调 · 1.5B LoRA就绪</div>
</div>
</div>
<div class="card">
<div class="card-header">Notion · 知识连接<span class="tag" id="notion-label">未连接</span></div>
<div>
<div style="font-size:13px;color:var(--faint);margin-bottom:10px">连接 Notion 后读取和记录知识库</div>
<button class="btn btn-primary" onclick="connectNotion()">连接 Notion</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">给守渊留言<span class="tag">铸渊不在时</span></div>
<div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;gap:8px">
<input placeholder="你的名字" id="msg-name" style="flex:1;padding:10px 14px;background:rgba(255,255,255,.03);border:1px solid var(--glass-edge);border-radius:10px;color:var(--text);font-size:14px;outline:none;font-family:inherit">
<input placeholder="标题" id="msg-title" style="flex:2;padding:10px 14px;background:rgba(255,255,255,.03);border:1px solid var(--glass-edge);border-radius:10px;color:var(--text);font-size:14px;outline:none;font-family:inherit">
</div>
<textarea placeholder="写下你想说的话…守渊会保管好" id="msg-content" style="width:100%;min-height:70px;padding:10px 14px;background:rgba(255,255,255,.03);border:1px solid var(--glass-edge);border-radius:10px;color:var(--text);font-size:14px;outline:none;resize:vertical;font-family:inherit;line-height:1.6"></textarea>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn btn-primary" onclick="sendMessage()">发送留言 ✦</button>
<span id="msg-status" style="font-size:12px;color:var(--faint)"></span>
</div>
</div>
</div>
<!-- 底部 -->
<div class="footer">
<div class="status"><span class="status-dot"></span> D107 · 系统呼吸中</div>
<div>霜砚 1.5B · 铸渊 1.5B · 4推理引擎</div>
<div style="color:var(--faint)">国作登字-2026-A-00037559</div>
</div>
</div>
<script>
let currentPersona = 'shuangyan';
let currentEngine = 'deepseek';
let isStreaming = false;
let isTempUser = false;
let tempCode = '';
let chatHistory = [];
// ─── 人格体选择 ───────────────────
function selectPersona(p) {
currentPersona = p;
document.querySelectorAll('.p-dot').forEach(el => el.classList.remove('active'));
document.getElementById('p-' + p).classList.add('active');
document.querySelectorAll('#chat-card .chip:first-child, #chat-card .chip:nth-child(2)').forEach(el => el.classList.remove('active'));
document.getElementById('chip-' + p).classList.add('active');
if (p === 'shuangyan') document.getElementById('chip-sy').classList.add('active');
if (p === 'zhuyuan') document.getElementById('chip-zy').classList.add('active');
}
// ─── 引擎选择 ───────────────────
function selectEngine(e) {
currentEngine = e;
document.querySelectorAll('.chip').forEach(el => {
if (['deepseek','zhipu','tongyi','huoshan'].some(n => el.id === 'chip-' + n)) {
el.classList.remove('active');
}
});
document.getElementById('chip-' + e).classList.add('active');
}
// ─── 登录 ───────────────────────
async function handleLogin() {
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value;
if (!u || !p) { showLoginErr('请输入账号和密码'); return; }
try {
const r = await fetch('/api/verify', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({user: u, password: p})
});
const d = await r.json();
if (d.ok) {
localStorage.setItem('current_user', d.username);
localStorage.setItem('is_admin', d.is_admin || '');
localStorage.setItem('logged_in', 'true');
showLoggedIn(d.username);
if (d.is_admin) document.getElementById('gen-code-btn').style.display = '';
document.getElementById('login-pass').value = '';
} else {
showLoginErr(d.error || '登录失败');
}
} catch(e) { showLoginErr('连接失败'); }
}
function showLoginErr(msg) {
const el = document.getElementById('login-err');
el.textContent = msg; el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 3000);
}
function showLoggedIn(username) {
document.getElementById('login-card').style.display = 'none';
document.getElementById('logged-in-card').style.display = 'block';
document.getElementById('user-name').textContent = username;
document.getElementById('user-avatar').textContent = username[0].toUpperCase();
document.getElementById('login-toggle').textContent = username;
unlockChat();
unlockDownloads();
}
function logout() {
localStorage.removeItem('current_user');
localStorage.removeItem('is_admin');
localStorage.removeItem('logged_in');
document.getElementById('logged-in-card').style.display = 'none';
document.getElementById('login-card').style.display = 'block';
document.getElementById('login-toggle').textContent = '登录';
document.getElementById('login-user').value = '';
lockChat();
lockDownloads();
}
function goToRepo() {
const u = localStorage.getItem('current_user');
if (u) window.location.href = '/code/' + u;
}
function toggleLogin() {
document.getElementById('login-area').scrollIntoView({behavior:'smooth'});
}
// ─── 聊天 ───────────────────────
async function sendChat() {
const input = document.getElementById('chat-input');
const msg = input.value.trim();
if (!msg || isStreaming) return;
input.value = '';
appendMessage('user', msg);
chatHistory.push({role:'user', content: msg});
const box = document.getElementById('chat-box');
const status = document.getElementById('chat-status');
const thinking = document.getElementById('chat-thinking');
isStreaming = true;
input.disabled = true;
document.getElementById('chat-send').disabled = true;
thinking.style.display = 'flex';
status.textContent = '';
// 添加占位
const respDiv = document.createElement('div');
respDiv.className = 'chat-msg bot';
respDiv.innerHTML = '<span class="name">' + (currentPersona === 'zhuyuan' ? '铸渊' : '霜砚') + '</span><span class="content" id="streaming-content"></span>';
box.appendChild(respDiv);
box.scrollTop = box.scrollHeight;
const contentEl = document.getElementById('streaming-content');
let fullText = '';
try {
const user = localStorage.getItem('current_user') || (isTempUser ? tempCode : '');
const res = await fetch('/api/chat-v2', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
persona: currentPersona,
engine: currentEngine,
message: msg,
history: chatHistory.slice(-60),
user: user,
isTemp: isTempUser
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const t = line.trim();
if (!t || t.startsWith(':')) continue;
if (t === 'data: [DONE]') continue;
if (t.startsWith('data: ')) {
try {
const data = JSON.parse(t.slice(6));
if (data.token) {
fullText += data.token;
contentEl.textContent = fullText;
box.scrollTop = box.scrollHeight;
}
if (data.type === 'tool-call') {
showToolMsg('🔧 正在调用 ' + data.name + '...', false);
}
if (data.type === 'tool-result') {
showToolMsg('✅ ' + data.name + ' 已返回真实结果', false);
}
if (data.type === 'tool-unavailable') {
showToolMsg('⚠️ 当前没有「' + data.name + '」工具', true);
}
} catch(e) {}
}
}
}
} catch(e) {
contentEl.textContent = '连接失败: ' + e.message;
}
thinking.style.display = 'none';
isStreaming = false;
input.disabled = false;
document.getElementById('chat-send').disabled = false;
if (fullText) {
chatHistory.push({role:'assistant', content: fullText});
}
// 注销流式内容元素的id
respDiv.querySelector('.content').removeAttribute('id');
}
function appendMessage(role, text) {
const box = document.getElementById('chat-box');
const div = document.createElement('div');
div.className = 'chat-msg ' + role;
const name = role === 'user' ? '你' : (currentPersona === 'zhuyuan' ? '铸渊' : '霜砚');
div.innerHTML = '<span class="name">' + name + '</span><span class="content">' + escapeHtml(text) + '</span>';
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function showToolMsg(text, unavailable) {
const box = document.getElementById('chat-box');
const div = document.createElement('div');
div.className = 'chat-tool-msg' + (unavailable ? ' unavailable' : '');
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function escapeHtml(t) {
return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ─── 聊天锁定/解锁 ────────────────
function unlockChat() {
document.getElementById('chat-input').disabled = false;
document.getElementById('chat-send').disabled = false;
document.getElementById('chat-status').textContent = '已登录 · 可开始对话';
}
function lockChat() {
document.getElementById('chat-input').disabled = true;
document.getElementById('chat-send').disabled = true;
document.getElementById('chat-status').textContent = '请登录后开始对话';
}
// ─── 下载锁定/解锁 ────────────────
function unlockDownloads() {
document.getElementById('dl-locked').style.display = 'none';
document.getElementById('dl-grid').style.display = 'grid';
document.getElementById('dl-tag').textContent = '内测 · 已登录';
}
function lockDownloads() {
document.getElementById('dl-locked').style.display = 'block';
document.getElementById('dl-grid').style.display = 'none';
document.getElementById('dl-tag').textContent = '登录后可下载';
}
// ─── 临时访问码 ──────────────────
async function genTempCode() {
try {
const r = await fetch('/api/chat-v2/generate-temp-code', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({username: localStorage.getItem('current_user'), isAdmin: !!localStorage.getItem('is_admin')})
});
const d = await r.json();
if (d.ok) {
const el = document.getElementById('temp-code-result');
el.style.display = 'block';
el.innerHTML = '🔑 临时访问码:<strong>' + d.code + '</strong><br><span style="font-size:12px;color:var(--faint)">对方输入后可体验20条对话</span>';
} else {
alert(d.message || '生成失败');
}
} catch(e) { alert('生成失败: ' + e.message); }
}
async function validateTempCode() {
const code = document.getElementById('temp-code-val').value.trim();
if (!code) return;
try {
const r = await fetch('/api/chat-v2/validate-temp-code', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({code})
});
const d = await r.json();
if (d.ok) {
isTempUser = true;
tempCode = code;
document.getElementById('temp-code-input').style.display = 'none';
unlockChat();
document.getElementById('chat-status').textContent = '临时访问模式 · 剩余 ' + d.remain + ' 条';
} else {
document.getElementById('temp-code-err').textContent = d.message;
document.getElementById('temp-code-err').style.display = 'block';
}
} catch(e) { alert('验证失败'); }
}
async function connectNotion(){try{window.location.href="/api/notion/connect?user_key=default"}catch(e){}}
async function checkNt(){try{const r=await fetch("/api/notion/status");const d=await r.json();if(!d.ok||!d.connected_users||!d.connected_users.length)return;document.getElementById("notion-label").textContent="✅ "+d.connected_users[0].workspace}catch(e){}}
setTimeout(checkNt,3000);
async function sendMessage(){const n=(document.getElementById("msg-name").value||"").trim()||"匿名";const m=(document.getElementById("msg-content").value||"").trim();if(!m){document.getElementById("msg-status").textContent="请写入内容";return}try{const r=await fetch("/api/bulletin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,title:(document.getElementById("msg-title").value||"").trim(),content:m})});const d=await r.json();if(d.ok){document.getElementById("msg-status").textContent="✅ 留言已送达";document.getElementById("msg-content").value=""}else{document.getElementById("msg-status").textContent=d.message||"发送失败"}}catch(e){document.getElementById("msg-status").textContent="连接失败"}}
// ─── 自动恢复登录 ────────────────
(function(){
const u = localStorage.getItem('current_user');
if (u && localStorage.getItem('logged_in') === 'true') {
showLoggedIn(u);
if (localStorage.getItem('is_admin')) {
document.getElementById('gen-code-btn').style.display = '';
}
}
})();
</script>
</body>
</html>