387 lines
9.4 KiB
JavaScript
387 lines
9.4 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
|
||
const landing = document.getElementById('landing');
|
||
const creator = document.getElementById('creator');
|
||
const auth = document.getElementById('auth');
|
||
const distill = document.getElementById('distill');
|
||
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 views = { landing, creator, auth, distill };
|
||
const steps = Array.from(document.querySelectorAll('.form-step'));
|
||
const dots = Array.from(document.querySelectorAll('.stepper__dot'));
|
||
let currentStep = 0;
|
||
let generatedSoul = '';
|
||
let generatedConfig = '';
|
||
let activePreview = 'soul';
|
||
let activeAuthTab = 'login';
|
||
|
||
function showView(name) {
|
||
Object.entries(views).forEach(([key, el]) => {
|
||
if (el) el.classList.toggle('active', key === name);
|
||
});
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
function publish() {
|
||
if (!validateStep(currentStep)) return;
|
||
|
||
const data = getFormData();
|
||
if (!data.systemPrompt.trim()) {
|
||
data.systemPrompt = buildSystemPrompt(data);
|
||
}
|
||
|
||
generatedSoul = generateSoulMd(data);
|
||
generatedConfig = generateConfigYaml(data);
|
||
|
||
form.hidden = true;
|
||
resultPanel.hidden = false;
|
||
renderPreview();
|
||
}
|
||
|
||
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';
|
||
updateStep(0);
|
||
updateTabs();
|
||
}
|
||
|
||
function updateTabs() {
|
||
document.querySelectorAll('.preview-tab').forEach((tab) => {
|
||
tab.classList.toggle('active', tab.dataset.tab === activePreview);
|
||
});
|
||
}
|
||
|
||
function switchAuthTab(tab) {
|
||
activeAuthTab = tab;
|
||
document.querySelectorAll('.auth-tab').forEach((t) => {
|
||
t.classList.toggle('active', t.dataset.tab === tab);
|
||
});
|
||
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;
|
||
}
|
||
|
||
// Event delegation
|
||
document.addEventListener('click', (e) => {
|
||
const target = e.target.closest('[data-action], [data-tab], [data-download]');
|
||
if (!target) return;
|
||
|
||
const action = target.dataset.action;
|
||
|
||
if (action === 'open-creator') {
|
||
e.preventDefault();
|
||
resetCreator();
|
||
showView('creator');
|
||
return;
|
||
}
|
||
|
||
if (action === 'open-auth') {
|
||
e.preventDefault();
|
||
switchAuthTab('login');
|
||
showView('auth');
|
||
return;
|
||
}
|
||
|
||
if (action === 'open-distill') {
|
||
e.preventDefault();
|
||
showView('distill');
|
||
return;
|
||
}
|
||
|
||
if (action === 'back') {
|
||
e.preventDefault();
|
||
showView('landing');
|
||
return;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (target.dataset.tab) {
|
||
e.preventDefault();
|
||
if (target.closest('.auth-tabs')) {
|
||
switchAuthTab(target.dataset.tab);
|
||
} else {
|
||
activePreview = target.dataset.tab;
|
||
updateTabs();
|
||
renderPreview();
|
||
}
|
||
return;
|
||
}
|
||
|
||
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', (e) => {
|
||
e.preventDefault();
|
||
if (!validatePasswordMatch(authForm)) return;
|
||
const formData = new FormData(authForm);
|
||
const data = Object.fromEntries(formData.entries());
|
||
const action = authForm.dataset.form === 'login' ? '登录' : '注册';
|
||
alert(`${action}成功:${data.account}`);
|
||
showView('landing');
|
||
});
|
||
});
|
||
|
||
// Update auto-generated system prompt as user types
|
||
form.addEventListener('input', () => {
|
||
updateSystemPromptPreview();
|
||
});
|
||
|
||
// Initial state
|
||
updateStep(0);
|
||
updateSystemPromptPreview();
|
||
})();
|