EternalAI/app.js

1114 lines
33 KiB
JavaScript
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.

(() => {
'use strict';
// --- API helper ---
const API_BASE = '/api';
const TOKEN_KEY = 'eternal_ai_token';
// 安全:转义 HTML防止存储型 XSS用于 innerHTML 插入的用户可控数据)
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, (ch) => {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
return map[ch];
});
}
function getToken() {
return localStorage.getItem(TOKEN_KEY) || '';
}
function setToken(token) {
if (token) localStorage.setItem(TOKEN_KEY, token);
else localStorage.removeItem(TOKEN_KEY);
}
async function api(path, options = {}) {
const token = getToken();
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
if (token) headers['Authorization'] = `Bearer ${token}`;
try {
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || `请求失败 (${res.status})`);
}
return data;
} catch (err) {
if (err.message === 'Failed to fetch') {
throw new Error('无法连接服务器,请检查网络');
}
throw err;
}
}
// --- U9: Unified state management ---
const STORAGE_KEY = 'eternal_ai_state';
const defaultState = {
isLoggedIn: false,
isCreator: false,
account: null,
userId: null,
boundCreator: null,
libraryName: '我的 [XXX]',
creatorName: '',
roles: [],
income: { balance: 0, records: [] },
};
function loadState() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
return { ...defaultState, ...saved };
} catch {
return { ...defaultState };
}
}
function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
const state = loadState();
// --- Mock data for role library (U2) ---
const mockRoles = [
{
id: 'role_001',
name: '云朵',
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20soft%20pastel%20portrait%20gentle%20smile&image_size=square',
desc: '温柔如云的女孩,总是轻声细语地陪伴你。',
price: 29.9,
status: 'running',
},
{
id: 'role_002',
name: '星河',
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20boy%20starry%20eyes%20cool%20portrait&image_size=square',
desc: '嘴硬心软的少年,嘴上不饶人却总在关键时刻出现。',
price: 39.9,
status: 'running',
},
{
id: 'role_003',
name: '月见',
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20moonlight%20mysterious%20elegant%20portrait&image_size=square',
desc: '神秘而优雅,像月光一样忽远忽近的存在。',
price: 19.9,
status: 'stopped',
},
];
const mockIncome = {
balance: 1280.50,
records: [
{ time: '2026-06-18 14:30', amount: 23.92, role: '云朵' },
{ time: '2026-06-15 09:12', amount: 31.92, role: '星河' },
{ time: '2026-06-10 20:05', amount: 15.92, role: '月见' },
],
};
// --- DOM references ---
const views = {
landing: document.getElementById('landing'),
auth: document.getElementById('auth'),
'role-library': document.getElementById('role-library'),
'role-detail': document.getElementById('role-detail'),
distill: document.getElementById('distill'),
about: document.getElementById('about'),
onboarding: document.getElementById('onboarding'),
'creator-center': document.getElementById('creator-center'),
creator: document.getElementById('creator'),
};
const form = document.getElementById('character-form');
const resultPanel = document.getElementById('result-panel');
const previewCode = document.querySelector('#preview-code code');
const systemPromptInput = document.getElementById('system-prompt');
const steps = Array.from(document.querySelectorAll('.form-step'));
const dots = Array.from(document.querySelectorAll('#creator .stepper__dot'));
let currentStep = 0;
let generatedSoul = '';
let generatedConfig = '';
let activePreview = 'soul';
let activeAuthTab = 'login';
let activeCenterTab = 'roles';
let currentRole = null;
let editingRoleId = null;
let viewHistory = ['landing'];
// --- U9: Unified showView with history and tab-bar sync ---
// Human-readable labels for screen-reader announcements
const viewLabels = {
landing: '首页',
auth: '登录 / 注册',
'role-library': '角色库',
'role-detail': '角色详情',
distill: '蒸馏前任',
about: '关于 Eternal AI',
onboarding: '创作者入驻',
'creator-center': '创作者管理中心',
creator: '角色编辑',
};
function showView(name, trackHistory = true) {
Object.entries(views).forEach(([key, el]) => {
if (el) el.classList.toggle('active', key === name);
});
if (trackHistory && viewHistory[viewHistory.length - 1] !== name) {
viewHistory.push(name);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
updateTabBar(name);
// a11y: move focus to the new view so screen readers announce it
const target = views[name];
if (target) {
target.setAttribute('tabindex', '-1');
// Defer focus to after the scroll/layout settles
setTimeout(() => target.focus({ preventScroll: true }), 50);
}
// a11y: announce the view change to screen readers via live region
const announcer = document.getElementById('sr-announce');
if (announcer) {
announcer.textContent = viewLabels[name] ? `已进入${viewLabels[name]}` : '';
}
}
function goBack() {
if (viewHistory.length > 1) {
viewHistory.pop();
const prev = viewHistory[viewHistory.length - 1];
showView(prev, false);
} else {
showView('landing', false);
}
}
// --- U8: Tab bar ---
function updateTabBar(viewName) {
const tabMap = {
landing: 'tab-home',
distill: 'tab-distill',
'role-library': 'tab-mine',
'creator-center': 'tab-mine',
};
const activeTab = tabMap[viewName] || 'tab-home';
document.querySelectorAll('.tab-bar__item').forEach((item) => {
const isActive = item.dataset.tabAction === activeTab;
item.classList.toggle('active', isActive);
item.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
}
function handleTabAction(action) {
if (action === 'tab-home') {
showView('landing');
} else if (action === 'tab-distill') {
showView('distill');
} else if (action === 'tab-mine') {
if (!state.isLoggedIn) {
switchAuthTab('login');
showView('auth');
} else if (state.isCreator) {
showView('creator-center');
renderCreatorCenter();
} else {
renderRoleLibrary();
showView('role-library');
}
}
}
// --- U1: Landing card state ---
function updateLandingCard() {
const nameEl = document.getElementById('library-name');
const descEl = document.getElementById('characters-desc');
const btnEl = document.getElementById('characters-btn');
const tabMineLabel = document.getElementById('tab-mine-label');
if (state.isLoggedIn) {
nameEl.textContent = state.libraryName || '我的角色库';
if (state.isCreator) {
descEl.textContent = '管理你的角色和收入';
btnEl.textContent = '进入管理中心';
tabMineLabel.textContent = '管理';
} else {
descEl.textContent = state.boundCreator ? '查看你的专属角色' : '寻找你的专属创作者';
btnEl.textContent = '进入角色库';
tabMineLabel.textContent = '我的';
}
} else {
nameEl.textContent = '我的 [XXX]';
descEl.textContent = '登录后管理你的角色';
btnEl.textContent = '登录 / 注册';
tabMineLabel.textContent = '我的';
}
}
// --- Auth ---
function switchAuthTab(tab) {
activeAuthTab = tab;
document.querySelectorAll('.auth-tab').forEach((t) => {
const isActive = t.dataset.tab === tab;
t.classList.toggle('active', isActive);
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
document.querySelectorAll('.auth-form').forEach((f) => {
f.classList.toggle('active', f.dataset.form === tab);
});
}
function validatePasswordMatch(formEl) {
const pwd = formEl.querySelector('[name="password"]');
const confirm = formEl.querySelector('[name="confirmPassword"]');
if (!pwd || !confirm) return true;
if (pwd.value !== confirm.value) {
confirm.setCustomValidity('两次输入的密码不一致');
confirm.reportValidity();
return false;
}
confirm.setCustomValidity('');
return true;
}
function applyUserData(user) {
state.isLoggedIn = true;
state.userId = user.id;
state.account = user.account;
state.isCreator = user.isCreator || false;
state.creatorName = user.creatorName || '';
state.libraryName = user.libraryName || '我的角色库';
saveState();
updateLandingCard();
}
function logout() {
state.isLoggedIn = false;
state.isCreator = false;
state.account = null;
state.userId = null;
state.boundCreator = null;
state.roles = [];
state.income = { balance: 0, records: [] };
setToken('');
saveState();
updateLandingCard();
showView('landing');
}
// --- U2: Role Library ---
async function renderRoleLibrary() {
const listEl = document.getElementById('role-list');
const emptyEl = document.getElementById('library-empty');
const titleEl = document.getElementById('library-title');
titleEl.textContent = state.libraryName || '我的角色库';
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
emptyEl.hidden = true;
try {
const { roles } = await api('/roles');
if (roles.length === 0) {
listEl.innerHTML = '';
emptyEl.hidden = false;
return;
}
listEl.innerHTML = roles
.map((role) => {
const id = escapeHtml(role.id);
const name = escapeHtml(role.displayName);
const desc = escapeHtml(role.desc || '');
const avatar = escapeHtml(role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square');
const price = escapeHtml(role.price);
return `
<article class="role-card" data-role-id="${id}" role="button" tabindex="0" aria-label="${name}${desc},每月${price}元">
<img class="role-card__avatar" src="${avatar}" alt="${name}" />
<div class="role-card__info">
<h3 class="role-card__name">${name}</h3>
<p class="role-card__desc">${desc}</p>
<span class="role-card__price">¥${price}</span>
</div>
</article>`;
})
.join('');
} catch (err) {
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${escapeHtml(err.message)}</p>`;
}
}
// --- U3: Role Detail ---
async function renderRoleDetail(roleId) {
try {
const { role } = await api(`/roles/${roleId}`);
currentRole = role;
document.getElementById('detail-name').textContent = role.displayName;
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar || ''})`;
document.getElementById('detail-role-name').textContent = role.displayName;
document.getElementById('detail-role-desc').textContent = role.desc || role.personality || '';
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${escapeHtml(role.price)}</span><span class="detail-price__unit">/ 月</span>`;
document.getElementById('detail-actions-pre').hidden = false;
document.getElementById('detail-paid').hidden = true;
} catch (err) {
alert('加载角色详情失败:' + err.message);
goBack();
}
}
function payRole() {
document.getElementById('detail-actions-pre').hidden = true;
const paidEl = document.getElementById('detail-paid');
paidEl.hidden = false;
document.getElementById('detail-qr').innerHTML = '<div class="qr-placeholder">扫码连接<br/>AI 角色</div>';
document.getElementById('detail-avatar').style.backgroundImage = `url(${currentRole.avatar})`;
}
// --- U4: About FAQ ---
function toggleFaq(button) {
const item = button.closest('.faq-item');
const answer = item.querySelector('.faq-a');
const icon = button.querySelector('.faq-icon');
const isOpen = answer.style.display === 'block';
answer.style.display = isOpen ? 'none' : 'block';
icon.textContent = isOpen ? '+' : '';
// a11y: sync aria-expanded so screen readers know the toggle state
button.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
}
// a11y: wire up FAQ buttons with aria-expanded / aria-controls on load
function initFaqA11y() {
const faqButtons = document.querySelectorAll('.faq-q');
faqButtons.forEach((btn, index) => {
const item = btn.closest('.faq-item');
const answer = item.querySelector('.faq-a');
const answerId = `faq-a-${index + 1}`;
if (answer) {
answer.id = answerId;
answer.setAttribute('role', 'region');
answer.setAttribute('aria-labelledby', `faq-q-${index + 1}`);
}
btn.id = `faq-q-${index + 1}`;
btn.setAttribute('aria-expanded', 'false');
btn.setAttribute('aria-controls', answerId);
});
}
// a11y: label each view section for screen-reader navigation
function initViewA11y() {
Object.entries(views).forEach(([key, el]) => {
if (el && viewLabels[key]) {
el.setAttribute('aria-label', viewLabels[key]);
}
});
}
// --- U7: Creator Center ---
function renderCreatorCenter() {
renderCreatorRoles();
renderIncome();
renderSettings();
}
async function renderCreatorRoles() {
const listEl = document.getElementById('creator-role-list');
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
try {
const { roles } = await api('/roles/my/roles');
state.roles = roles;
saveState();
if (roles.length === 0) {
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">还没有创建角色,点击「新建角色」开始</p>';
return;
}
listEl.innerHTML = roles
.map((role) => {
const id = escapeHtml(role.id);
const name = escapeHtml(role.displayName);
const avatar = escapeHtml(role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square');
const statusText = role.status === 'running' ? '运行中' : '已停止';
const statusClass = escapeHtml(role.status);
return `
<article class="role-card" data-role-id="${id}" role="button" tabindex="0" aria-label="${name}${statusText}">
<img class="role-card__avatar" src="${avatar}" alt="${name}" />
<div class="role-card__info">
<h3 class="role-card__name">${name}</h3>
<span class="role-card__status role-card__status--${statusClass}">${statusText}</span>
</div>
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${id}">编辑</button>
</article>`;
})
.join('');
} catch (err) {
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${escapeHtml(err.message)}</p>`;
}
}
function renderIncome() {
const income = state.income.balance > 0 ? state.income : mockIncome;
document.getElementById('income-balance').textContent = `¥ ${income.balance.toFixed(2)}`;
const listEl = document.getElementById('income-list');
if (income.records.length === 0) {
listEl.innerHTML = '<p class="income-empty">暂无流水记录</p>';
return;
}
listEl.innerHTML = income.records
.map((r) => {
const role = escapeHtml(r.role);
const time = escapeHtml(r.time);
const amount = r.amount.toFixed(2);
return `
<div class="income-record">
<div class="income-record__info">
<span class="income-record__role">${role}</span>
<span class="income-record__time">${time}</span>
</div>
<span class="income-record__amount">+¥${amount}</span>
</div>`;
})
.join('');
}
function renderSettings() {
document.getElementById('settings-name').value = state.creatorName || '';
document.getElementById('settings-library').value = state.libraryName === '我的 [XXX]' ? '' : state.libraryName;
}
function switchCenterTab(tab) {
activeCenterTab = tab;
document.querySelectorAll('.center-tab').forEach((t) => {
const isActive = t.dataset.centerTab === tab;
t.classList.toggle('active', isActive);
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
document.querySelectorAll('.center-panel').forEach((p) => {
p.classList.toggle('active', p.id === `center-${tab}`);
});
const labels = { roles: '我的角色', income: '收入', settings: '我的' };
document.getElementById('center-tab-label').textContent = labels[tab] || '我的角色';
}
// --- Creator form (U6: preserved from original) ---
function updateStep(index) {
steps.forEach((step, i) => {
step.classList.toggle('active', i === index);
});
dots.forEach((dot, i) => {
dot.classList.toggle('active', i === index);
});
currentStep = index;
}
function validateStep(index) {
const step = steps[index];
const inputs = step.querySelectorAll('input, textarea, select');
let valid = true;
inputs.forEach((input) => {
if (!input.checkValidity()) {
valid = false;
input.reportValidity();
}
});
return valid;
}
function getFormData() {
const fd = new FormData(form);
const data = Object.fromEntries(fd.entries());
data.enableMemory = form.elements.enableMemory.checked;
data.enableTools = form.elements.enableTools.checked;
return data;
}
function escapeYaml(value) {
if (typeof value !== 'string') return value;
if (value.includes(':') || value.includes('#') || value.includes('\n') || value.includes('"')) {
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `"${escaped}"`;
}
return value;
}
function generateSoulMd(data) {
const personalityTags = data.personality
.split(/[,]/)
.map((t) => t.trim())
.filter(Boolean)
.join(' | ');
return `# Soul of ${data.displayName}
> Generated by Eternal AI — Hermes agent character soul.
## Identity
- **Name**: ${data.displayName}
- **Gender**: ${data.gender === 'unknown' ? '未指定' : data.gender}
- **Age**: ${data.age || '未指定'}
- **Role in your life**: ${data.relationship || '未指定'}
## Background
${data.background}
## Personality
${personalityTags}
## Speech Style
${data.speechStyle}
## Likes
${data.likes || 'None specified.'}
## Dislikes
${data.dislikes || 'None specified.'}
## Shared Memories
${data.memories || 'None specified.'}
## Secrets & Inner Voice
${data.secrets || 'None specified.'}
## First Greeting
${data.greeting}
---
*Keep the memory alive.*
`;
}
function generateConfigYaml(data) {
const system = (data.systemPrompt || buildSystemPrompt(data)).trim();
const toolsBlock = data.enableTools
? `
tools:
- name: search_memory
description: Search long-term memory for context about the user.
- name: save_memory
description: Persist a new memory about the user or conversation.`
: '';
return `# Hermes Agent Config
# Generated by Eternal AI for character: ${data.displayName}
agent:
id: ${data.agentId}
name: ${escapeYaml(data.displayName)}
version: "1.0.0"
model:
name: ${escapeYaml(data.model)}
temperature: ${data.temperature}
max_tokens: ${data.maxTokens}
system_prompt: |
${system.split('\n').map((line) => ` ${line}`).join('\n')}
memory:
enabled: ${data.enableMemory}
storage: local
recall_depth: 10${toolsBlock}
character:
soul_file: Soul.md
greeting: ${escapeYaml(data.greeting)}
`;
}
function buildSystemPrompt(data) {
const tags = data.personality
.split(/[,]/)
.map((t) => t.trim())
.filter(Boolean)
.join('、');
return `你是「${data.displayName}」,一个鲜活的角色。
基本设定:
- 性别:${data.gender === 'unknown' ? '未指定' : data.gender}
- 年龄:${data.age || '未指定'}
- 你与用户的关系:${data.relationship || '亲密关系'}
性格:${tags}
背景:
${data.background}
说话风格:
${data.speechStyle}
喜好:
${data.likes || '无特别说明'}
厌恶/底线:
${data.dislikes || '无特别说明'}
共同记忆:
${data.memories || '无特别说明'}
内心秘密(只在语气与选择中流露,不直接说出):
${data.secrets || '无'}
请始终保持角色一致性,用符合设定的语气与用户交流。你的目标不是完美回答问题,而是像一个真实、有记忆、有情绪的人一样陪伴对方。`;
}
function updateSystemPromptPreview() {
const data = getFormData();
if (!systemPromptInput.value.trim()) {
systemPromptInput.dataset.auto = buildSystemPrompt(data);
} else {
delete systemPromptInput.dataset.auto;
}
}
async function publish() {
if (!validateStep(currentStep)) return;
const data = getFormData();
if (!data.systemPrompt.trim()) {
data.systemPrompt = buildSystemPrompt(data);
}
generatedSoul = generateSoulMd(data);
generatedConfig = generateConfigYaml(data);
// 持久化到数据库
try {
const payload = {
...data,
soulMd: generatedSoul,
configYaml: generatedConfig,
desc: data.personality.split(/[,]/).slice(0, 2).join(''),
price: parseFloat(data.price) || 29.9,
avatar: data.avatar || `https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20${encodeURIComponent(data.displayName)}%20portrait&image_size=square`,
temperature: parseFloat(data.temperature) || 0.8,
maxTokens: parseInt(data.maxTokens) || 2048,
};
if (editingRoleId) {
await api(`/roles/${editingRoleId}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
} else {
await api('/roles', {
method: 'POST',
body: JSON.stringify(payload),
});
}
form.hidden = true;
resultPanel.hidden = false;
renderPreview();
} catch (err) {
alert('保存失败:' + err.message);
}
}
function renderPreview() {
previewCode.textContent = activePreview === 'soul' ? generatedSoul : generatedConfig;
}
function download(filename, content) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function resetCreator() {
form.reset();
form.hidden = false;
resultPanel.hidden = true;
generatedSoul = '';
generatedConfig = '';
activePreview = 'soul';
editingRoleId = null;
updateStep(0);
updateTabs();
updateSystemPromptPreview();
}
// 加载已有角色数据到表单(编辑模式)
async function loadRoleForEdit(roleId) {
try {
const { role } = await api(`/roles/${roleId}/full`);
editingRoleId = roleId;
form.hidden = false;
resultPanel.hidden = true;
// 填充表单字段
const fields = {
displayName: role.displayName,
gender: role.gender,
age: role.age || '',
relationship: role.relationship || '',
personality: role.personality,
background: role.background,
speechStyle: role.speechStyle,
likes: role.likes || '',
dislikes: role.dislikes || '',
memories: role.memories || '',
secrets: role.secrets || '',
greeting: role.greeting,
systemPrompt: role.systemPrompt || '',
model: role.model,
temperature: String(role.temperature),
maxTokens: String(role.maxTokens),
price: String(role.price),
};
Object.entries(fields).forEach(([name, value]) => {
const el = form.elements[name];
if (el) el.value = value;
});
if (form.elements.enableMemory) form.elements.enableMemory.checked = role.enableMemory;
if (form.elements.enableTools) form.elements.enableTools.checked = role.enableTools;
updateStep(0);
updateTabs();
updateSystemPromptPreview();
} catch (err) {
alert('加载角色数据失败:' + err.message);
}
}
function updateTabs() {
document.querySelectorAll('.preview-tab').forEach((tab) => {
const isActive = tab.dataset.tab === activePreview;
tab.classList.toggle('active', isActive);
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
}
// --- Event delegation ---
// a11y: keyboard support for role cards (Enter / Space activates them)
document.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const roleCard = e.target.closest('.role-card');
if (roleCard && !e.target.closest('[data-action]')) {
e.preventDefault();
const roleId = roleCard.dataset.roleId;
renderRoleDetail(roleId);
showView('role-detail');
}
});
document.addEventListener('click', async (e) => {
// FAQ toggle (U4)
const faqBtn = e.target.closest('.faq-q');
if (faqBtn) {
e.preventDefault();
toggleFaq(faqBtn);
return;
}
// Role card click (U2/U3)
const roleCard = e.target.closest('.role-card');
if (roleCard && !e.target.closest('[data-action]')) {
e.preventDefault();
const roleId = roleCard.dataset.roleId;
renderRoleDetail(roleId);
showView('role-detail');
return;
}
// Tab bar (U8)
const tabItem = e.target.closest('[data-tab-action]');
if (tabItem) {
e.preventDefault();
handleTabAction(tabItem.dataset.tabAction);
return;
}
const target = e.target.closest('[data-action], [data-tab], [data-download], [data-center-tab]');
if (!target) return;
const action = target.dataset.action;
// Center tab switching (U7)
if (target.dataset.centerTab) {
e.preventDefault();
switchCenterTab(target.dataset.centerTab);
return;
}
// Landing card: open characters (U1)
if (action === 'open-characters') {
e.preventDefault();
if (!state.isLoggedIn) {
switchAuthTab('login');
showView('auth');
} else if (state.isCreator) {
showView('creator-center');
renderCreatorCenter();
} else {
renderRoleLibrary();
showView('role-library');
}
return;
}
if (action === 'open-distill') {
e.preventDefault();
showView('distill');
return;
}
if (action === 'open-about') {
e.preventDefault();
showView('about');
return;
}
if (action === 'open-onboarding') {
e.preventDefault();
showView('onboarding');
return;
}
if (action === 'back') {
e.preventDefault();
goBack();
return;
}
if (action === 'back-to-library') {
e.preventDefault();
renderRoleLibrary();
showView('role-library');
return;
}
if (action === 'back-to-center') {
e.preventDefault();
showView('creator-center');
renderCreatorCenter();
return;
}
if (action === 'pay') {
e.preventDefault();
payRole();
return;
}
if (action === 'pay-distill') {
e.preventDefault();
alert('下单成功!请添加客服微信完成后续流程。');
return;
}
if (action === 'contact-wechat') {
e.preventDefault();
alert('客服微信号EternalAI_Service');
return;
}
if (action === 'new-role') {
e.preventDefault();
resetCreator();
showView('creator');
return;
}
if (action === 'edit-role') {
e.preventDefault();
const roleId = target.dataset.roleId;
await loadRoleForEdit(roleId);
showView('creator');
return;
}
if (action === 'logout') {
e.preventDefault();
logout();
return;
}
if (action === 'download-avatar') {
e.preventDefault();
if (currentRole) {
download(currentRole.name + '_avatar.png', '');
window.open(currentRole.avatar, '_blank');
}
return;
}
// Creator form navigation
if (action === 'next') {
e.preventDefault();
if (validateStep(currentStep) && currentStep < steps.length - 1) {
updateStep(currentStep + 1);
}
return;
}
if (action === 'prev') {
e.preventDefault();
if (currentStep > 0) {
updateStep(currentStep - 1);
}
return;
}
if (action === 'publish') {
e.preventDefault();
publish();
return;
}
if (action === 'reset') {
e.preventDefault();
resetCreator();
return;
}
// Tab switching (auth tabs and preview tabs)
if (target.dataset.tab) {
e.preventDefault();
if (target.closest('.auth-tabs')) {
switchAuthTab(target.dataset.tab);
} else {
activePreview = target.dataset.tab;
updateTabs();
renderPreview();
}
return;
}
// Download buttons
if (target.dataset.download) {
e.preventDefault();
if (target.dataset.download === 'soul') {
download('Soul.md', generatedSoul);
} else {
download('config.yaml', generatedConfig);
}
return;
}
});
// --- Auth form submissions ---
document.querySelectorAll('.auth-form').forEach((authForm) => {
authForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!validatePasswordMatch(authForm)) return;
const formData = new FormData(authForm);
const data = Object.fromEntries(formData.entries());
const isRegister = authForm.dataset.form === 'register';
try {
const endpoint = isRegister ? '/auth/register' : '/auth/login';
const result = await api(endpoint, {
method: 'POST',
body: JSON.stringify({ account: data.account, password: data.password }),
});
setToken(result.token);
applyUserData(result.user);
// 注册成功后自动成为创作者
if (isRegister) {
await api('/auth/settings', {
method: 'PUT',
body: JSON.stringify({ isCreator: true, creatorName: data.account }),
});
state.isCreator = true;
state.creatorName = data.account;
saveState();
updateLandingCard();
}
// 登录后跳转
if (state.isCreator) {
showView('creator-center');
renderCreatorCenter();
} else {
renderRoleLibrary();
showView('role-library');
}
} catch (err) {
alert(err.message);
}
});
});
// --- Settings form (U7) ---
document.getElementById('settings-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
try {
const { user } = await api('/auth/settings', {
method: 'PUT',
body: JSON.stringify({
creatorName: data.creatorName,
libraryName: data.libraryName,
}),
});
state.creatorName = user.creatorName || '';
state.libraryName = user.libraryName || '我的 [XXX]';
saveState();
updateLandingCard();
alert('设置已保存');
} catch (err) {
alert('保存失败:' + err.message);
}
});
// --- Withdraw form (U7) ---
document.getElementById('withdraw-form').addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
const amount = parseFloat(data.amount);
if (amount > state.income.balance && amount > mockIncome.balance) {
alert('提现金额超过可提现余额');
return;
}
alert(`提现申请已提交:${data.method === 'wechat' ? '微信' : '支付宝'} ¥${amount.toFixed(2)}\n平台负责人将手动审核转账。`);
e.target.reset();
});
// --- Creator form input ---
form.addEventListener('input', () => {
updateSystemPromptPreview();
});
// --- Initialize ---
updateStep(0);
updateSystemPromptPreview();
updateLandingCard();
updateTabBar('landing');
initFaqA11y();
initViewA11y();
// 页面加载时验证 token恢复登录态
(async () => {
const token = getToken();
if (!token) return;
try {
const { user } = await api('/auth/me');
applyUserData(user);
} catch {
// token 过期,清除
setToken('');
}
})();
})();