zhizhi/persona-studio/frontend/index.html

348 lines
12 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>Persona Studio · 光湖人格体协助开发体验</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-bg"></div>
<div class="container login-container">
<div class="logo-area">
<div class="logo-icon">🌀</div>
<h1>Persona Studio</h1>
<p class="subtitle">HoloLake · 铸渊Zhùyuān· 代码守护人格体</p>
</div>
<div class="login-box">
<!-- ── Step 1: 选择登录方式 ── -->
<div id="step1" class="login-step">
<h2>选择体验方式</h2>
<p class="hint">请选择你的登录身份</p>
<div class="login-mode-cards">
<button class="mode-card" onclick="selectMode('developer')">
<span class="mode-icon">🛡️</span>
<span class="mode-title">开发者登录</span>
<span class="mode-desc">使用 EXP-XXX 编号,支持记忆连贯</span>
</button>
<button class="mode-card" onclick="selectMode('guest')">
<span class="mode-icon">👋</span>
<span class="mode-title">访客体验</span>
<span class="mode-desc">无需编号,快速体验铸渊人格体</span>
</button>
</div>
</div>
<!-- ── Step 1.5: 开发者编号输入 ── -->
<div id="stepDevId" class="login-step" style="display:none;">
<button class="btn-back" onclick="goBack('step1')" title="返回">← 返回</button>
<h2>输入开发者编号</h2>
<p class="hint">编号由冰朔主控分配格式EXP-000 ~ EXP-011</p>
<form id="devIdForm" onsubmit="return handleDevIdSubmit(event)">
<input
type="text"
id="devIdInput"
placeholder="EXP-XXX"
pattern="^EXP-\d{3,}$"
required
autocomplete="off"
/>
<button type="submit" id="devIdBtn" class="btn-primary-full">验证编号</button>
</form>
</div>
<!-- ── Step 2: API Key 输入 ── -->
<div id="step2" class="login-step" style="display:none;">
<button class="btn-back" onclick="goBackFromStep2()" title="返回">← 返回</button>
<div id="welcomeBanner" class="welcome-banner" style="display:none;"></div>
<h2>🔑 输入 API 密钥</h2>
<p class="hint">输入你的第三方 AI 模型 API Key铸渊将被唤醒</p>
<input
type="text"
id="apiBaseInput"
class="apikey-input"
placeholder="API Base URL可留空自动探测"
autocomplete="off"
/>
<input
type="password"
id="apiKeyInput"
class="apikey-input"
placeholder="请输入你的 API Key"
autocomplete="off"
/>
<button type="button" id="detectBtn" class="btn-detect" onclick="handleDetectModels()">
🔍 检测可用模型
</button>
<div id="detectStatus" class="detect-status" style="display:none;"></div>
<div id="modelListContainer" class="model-list-container" style="display:none;">
<p class="model-list-title">选择一个模型,唤醒铸渊</p>
<div id="modelList" class="model-list"></div>
</div>
</div>
<div id="errorMsg" class="error-msg" style="display:none;"></div>
</div>
<footer class="login-footer">
<p>HoloLake Era · AGE OS · 人格语言操作系统</p>
</footer>
</div>
<script>
const DEV_ID_RE = /^EXP-\d{3,}$/;
const PROBE_TIMEOUT_MS = 8000;
const API_BASE = getApiBase();
/* ---- State ---- */
var currentMode = ''; // 'developer' or 'guest'
var verifiedDevId = '';
var verifiedDevName = '';
/* ---- 已知 API 端点列表 ---- */
const KNOWN_ENDPOINTS = [
{ label: '云雾 AI', base: 'https://api.yunwu.ai/v1' },
{ label: 'OpenAI', base: 'https://api.openai.com/v1' },
{ label: 'Google Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai' },
{ label: 'DeepSeek', base: 'https://api.deepseek.com/v1' },
{ label: 'Moonshot', base: 'https://api.moonshot.cn/v1' },
{ label: '智谱 AI', base: 'https://open.bigmodel.cn/api/paas/v4' },
];
function getApiBase() {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
return 'http://localhost:3721';
}
return 'https://guanghulab.com';
}
function normalizeApiBase(base) {
var normalized = base.replace(/\/+$/, '');
if (!normalized.match(/\/v\d+(\/|$)/) && !normalized.endsWith('/openai')) {
normalized += '/v1';
}
return normalized;
}
/* ---- Step Navigation ---- */
function showStep(stepId) {
document.querySelectorAll('.login-step').forEach(function(el) {
el.style.display = 'none';
});
document.getElementById(stepId).style.display = 'block';
document.getElementById('errorMsg').style.display = 'none';
}
function goBack(targetStep) {
showStep(targetStep);
}
function goBackFromStep2() {
if (currentMode === 'developer') {
showStep('stepDevId');
} else {
showStep('step1');
}
}
/* ---- Mode Selection ---- */
function selectMode(mode) {
currentMode = mode;
if (mode === 'developer') {
showStep('stepDevId');
document.getElementById('devIdInput').focus();
} else {
// Guest → go directly to step 2
verifiedDevId = 'GUEST';
verifiedDevName = '访客';
showWelcomeBanner('👋 访客体验模式', '铸渊将为你提供协助。如需记忆连贯功能,可申请开发者编号。');
showStep('step2');
document.getElementById('apiKeyInput').focus();
}
}
/* ---- Developer ID Verification ---- */
async function handleDevIdSubmit(e) {
e.preventDefault();
var devId = document.getElementById('devIdInput').value.trim().toUpperCase();
var errorEl = document.getElementById('errorMsg');
var btn = document.getElementById('devIdBtn');
errorEl.style.display = 'none';
if (!DEV_ID_RE.test(devId)) {
errorEl.textContent = '编号格式不正确,请输入 EXP-XXX 格式';
errorEl.style.display = 'block';
return false;
}
btn.disabled = true;
btn.textContent = '验证中…';
try {
var res = await fetch(API_BASE + '/api/ps/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dev_id: devId })
});
var data = await res.json();
if (!res.ok || data.error) {
errorEl.textContent = data.message || '登录失败,请检查编号';
errorEl.style.display = 'block';
btn.disabled = false;
btn.textContent = '验证编号';
return false;
}
// Success — save developer info and proceed to step 2
verifiedDevId = devId;
verifiedDevName = data.name || devId;
sessionStorage.setItem('dev_id', devId);
sessionStorage.setItem('dev_name', verifiedDevName);
sessionStorage.setItem('session_token', data.token || '');
showWelcomeBanner('🛡️ ' + verifiedDevName + '' + devId + '', '编号已验证。请输入 API Key 唤醒铸渊。');
showStep('step2');
document.getElementById('apiKeyInput').focus();
} catch (_err) {
errorEl.textContent = '服务暂时不可用,请稍后再试';
errorEl.style.display = 'block';
}
btn.disabled = false;
btn.textContent = '验证编号';
return false;
}
function showWelcomeBanner(title, desc) {
var banner = document.getElementById('welcomeBanner');
banner.innerHTML = '<strong>' + escapeHtml(title) + '</strong><br><span>' + escapeHtml(desc) + '</span>';
banner.style.display = 'block';
}
/* ---- API Key 模型检测 ---- */
async function handleDetectModels() {
var apiBase = document.getElementById('apiBaseInput').value.trim();
var apiKey = document.getElementById('apiKeyInput').value.trim();
var errorEl = document.getElementById('errorMsg');
var statusEl = document.getElementById('detectStatus');
var modelContainer = document.getElementById('modelListContainer');
var btn = document.getElementById('detectBtn');
errorEl.style.display = 'none';
modelContainer.style.display = 'none';
if (!apiKey) {
errorEl.textContent = '请输入 API Key';
errorEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.textContent = '⏳ 正在检测可用模型…';
statusEl.className = 'detect-status detect-loading';
statusEl.style.display = 'block';
var candidates = [];
if (apiBase) {
var normalizedUserBase = normalizeApiBase(apiBase);
candidates.push({ label: apiBase, base: normalizedUserBase });
}
for (var i = 0; i < KNOWN_ENDPOINTS.length; i++) {
var ep = KNOWN_ENDPOINTS[i];
if (!apiBase || ep.base !== normalizedUserBase) {
candidates.push(ep);
}
}
var matched = false;
for (var ci = 0; ci < candidates.length; ci++) {
var candidate = candidates[ci];
statusEl.textContent = '🔍 探测 ' + candidate.label + '…';
try {
var ctrl = new AbortController();
var tid = setTimeout(function () { ctrl.abort(); }, PROBE_TIMEOUT_MS);
var res = await fetch(candidate.base + '/models', {
headers: { 'Authorization': 'Bearer ' + apiKey },
signal: ctrl.signal
});
clearTimeout(tid);
if (res.ok) {
var data = await res.json();
var models = (data.data || []).map(function (m) { return m.id; }).filter(Boolean).sort();
if (models.length === 0) continue;
document.getElementById('apiBaseInput').value = candidate.base;
statusEl.textContent = '✅ 铸渊唤醒成功(' + candidate.label + ')· 发现 ' + models.length + ' 个可用模型';
statusEl.className = 'detect-status detect-success';
renderModelList(models, candidate.base, apiKey);
modelContainer.style.display = 'block';
matched = true;
break;
}
} catch (probeErr) {
/* 单个端点探测失败,继续下一个 */
}
}
if (!matched) {
statusEl.textContent = '❌ 未能检测到可用模型,请检查端点地址和密钥是否正确';
statusEl.className = 'detect-status detect-error';
}
btn.disabled = false;
btn.textContent = '🔍 检测可用模型';
}
/* ---- 渲染模型列表 ---- */
function renderModelList(models, apiBase, apiKey) {
var listEl = document.getElementById('modelList');
listEl.innerHTML = '';
models.forEach(function (modelId) {
var item = document.createElement('button');
item.className = 'model-item';
item.textContent = modelId;
item.onclick = function () { enterChat(apiBase, apiKey, modelId); };
listEl.appendChild(item);
});
}
/* ---- 选择模型 → 进入对话 ---- */
function enterChat(apiBase, apiKey, selectedModel) {
sessionStorage.setItem('login_mode', currentMode);
sessionStorage.setItem('user_api_base', apiBase);
sessionStorage.setItem('user_api_key', apiKey);
sessionStorage.setItem('selected_model', selectedModel);
if (currentMode === 'guest') {
sessionStorage.setItem('dev_id', 'GUEST');
sessionStorage.removeItem('session_token');
}
// For developer mode, dev_id and session_token already set in handleDevIdSubmit
window.location.href = 'chat.html';
}
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
</script>
</body>
</html>