Files
hunjing/frontend/design/admin-characters-preview.html
2026-08-21 16:00:13 +08:00

753 lines
41 KiB
HTML
Raw Permalink 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" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>魂镜 · 司命台 · 人物模板管理</title>
<link rel="stylesheet" href="tokens.css" />
<link rel="stylesheet" href="_base.css" />
<link rel="stylesheet" href="_chrome.css" />
<style>
/* ============================================================
* 铜镜鉴心 v2.3 — 司命台 · 人物模板管理(§19.8)
*
* 文档基准:docs/product/master-prd.md §2-§5
* · 铜镜四态 = MirrorState 健康度(void/breathing/steady/cracked),§2.3
* · 三签分层:星盘(空间·主角)/ 朱批(动作)/ 起居注(时间),§3
* · 司命台独立壳层(AdminShell 横向导航),§4
*
* data-view:char-templates
* ============================================================ */
/* ── 司命台壳层 ── */
.admin-shell { display: flex; flex-direction: column; min-height: 100vh; }
.topbar { position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: 10px; padding: 10px 28px; border-bottom: 1px solid var(--rule); background: var(--surface); backdrop-filter: blur(16px) saturate(140%); flex-wrap: wrap; }
.topbar-brand { font-family: "Songti SC", serif; font-size: 17px; font-weight: 700; letter-spacing: 0.06em; color: var(--fg); display: flex; align-items: center; gap: 7px; flex-shrink: 0; cursor: pointer; }
.topbar-brand .knob { width: 16px; height: 16px; }
.topbar-brand .knob circle:last-child { fill: var(--cinnabar); }
.topbar-sep { color: var(--fg-faint); opacity: 0.5; flex-shrink: 0; }
.topbar-nav { display: flex; align-items: center; gap: 1px; flex-wrap: wrap; }
.topbar-navitem { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 8px; font-size: 13px; font-weight: 500; color: var(--muted); text-decoration: none; transition: background 0.12s, color 0.12s; cursor: pointer; background: none; border: none; font-family: inherit; }
.topbar-navitem:hover { background: var(--overlay-hover); color: var(--fg); }
.topbar-navitem.active { background: var(--patina-wash); color: var(--patina); }
.topbar-navitem.pivot { font-size: 12px; padding: 4px 9px; }
.topbar-spacer { flex: 1; }
/* ── 视图与页头 ── */
main { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.page-header .spacer { flex: 1; }
.header-badge { display: inline-flex; align-items: center; gap: 6px; font-family: "JetBrains Mono", monospace; font-size: 11.5px; padding: 4px 12px; border-radius: 999px; border: 1px solid var(--patina-dim); color: var(--patina); background: var(--patina-wash); }
.view-body { flex: 1; padding: 16px 28px 48px; display: flex; flex-direction: column; gap: 22px; max-width: 1180px; width: 100%; overflow-y: auto; }
.section-label .glyph { color: var(--cinnabar); opacity: 0.85; }
.section-heading { font-family: "Songti SC", serif; font-size: 14px; font-weight: 600; color: var(--fg); margin-bottom: 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.section-heading .sub { font-size: 12px; font-weight: 400; color: var(--fg-dim); margin-left: 6px; }
.section-heading .glyph { color: var(--cinnabar); opacity: 0.85; }
/* ── 操作栏 ── */
.toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.toolbar .spacer { flex: 1; }
.btn-danger { border-color: var(--cinnabar-dim); background: var(--cinnabar-wash); color: var(--cinnabar); }
.btn-danger:hover { background: var(--cinnabar-dim); color: var(--fg); }
.btn-icon { padding: 6px; min-width: 0; }
.search-input { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; border: 1px solid var(--rule); border-radius: 10px; background: var(--surface); color: var(--fg); font-family: inherit; font-size: 13px; min-width: 200px; transition: border-color 0.15s; }
.search-input:focus { outline: none; border-color: var(--patina-dim); }
.search-input::placeholder { color: var(--fg-faint); }
/* ── 模板列表 ── */
.template-table { width: 100%; border-collapse: separate; border-spacing: 0; }
.template-table th { font-family: "JetBrains Mono", monospace; font-size: 11.5px; letter-spacing: 0.06em; color: var(--fg-faint); padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--rule); font-weight: 500; }
.template-table td { padding: 12px 14px; border-bottom: 1px solid var(--rule); font-size: 14px; vertical-align: top; }
.template-table tr:hover td { background: var(--surface-hi); }
.template-table .key-cell { font-family: "JetBrains Mono", monospace; font-size: 12px; color: var(--gilt); white-space: nowrap; }
.template-table .name-cell { font-weight: 600; }
.template-table .display-name-cell { color: var(--fg-dim); }
.template-table .desc-cell { color: var(--fg-dim); max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.template-table .date-cell { font-family: "JetBrains Mono", monospace; font-size: 12px; color: var(--fg-faint); white-space: nowrap; }
.template-table .actions-cell { white-space: nowrap; text-align: right; }
.template-table .actions-cell .btn { margin-left: 4px; }
/* ── 标签 ── */
.tag-default { background: var(--patina-wash); color: var(--patina); border: 1px solid var(--patina-dim); }
.tag-api { background: var(--blue-wash); color: var(--blue); border: 1px solid var(--blue-dim); }
.tag-yaml { background: var(--gilt-faint); color: var(--gilt); border: 1px solid var(--gilt-dim); }
/* ── 空状态 ── */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; text-align: center; }
.empty-state .glyph { font-size: 36px; margin-bottom: 12px; opacity: 0.3; }
.empty-state .title { font-family: "Songti SC", serif; font-size: 16px; color: var(--fg-dim); }
.empty-state .desc { font-size: 13px; color: var(--fg-faint); margin-top: 6px; }
/* ── Modal ── */
.modal-overlay { position: fixed; inset: 0; z-index: 100; background: rgba(0, 0, 0, 0.55); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: 20px; }
.modal { background: var(--ink); border: 1px solid var(--rule); border-radius: 18px; width: 100%; max-width: 620px; max-height: 88vh; display: flex; flex-direction: column; box-shadow: 0 12px 60px rgba(0, 0, 0, 0.6); }
.modal-header { display: flex; align-items: center; gap: 12px; padding: 18px 22px 10px; border-bottom: 1px solid var(--rule); }
.modal-title { font-family: "Songti SC", serif; font-size: 17px; font-weight: 600; color: var(--fg); flex: 1; }
.modal-close { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 8px; cursor: pointer; border: none; background: none; color: var(--fg-faint); font-size: 18px; transition: background 0.12s, color 0.12s; }
.modal-close:hover { background: var(--surface-hi); color: var(--fg); }
.modal-body { flex: 1; overflow-y: auto; padding: 18px 22px 22px; display: flex; flex-direction: column; gap: 16px; }
.modal-footer { display: flex; align-items: center; gap: 10px; padding: 12px 22px 18px; border-top: 1px solid var(--rule); justify-content: flex-end; }
.field { display: flex; flex-direction: column; gap: 5px; }
.field-label { font-family: "JetBrains Mono", monospace; font-size: 11.5px; letter-spacing: 0.06em; color: var(--fg-faint); }
.field-hint { font-size: 12px; color: var(--fg-faint); margin-top: 2px; }
.field input, .field textarea, .field select { padding: 8px 12px; border: 1px solid var(--rule); border-radius: 10px; background: var(--surface); color: var(--fg); font-family: inherit; font-size: 13px; transition: border-color 0.15s; width: 100%; }
.field input:focus, .field textarea:focus, .field select:focus { outline: none; border-color: var(--patina-dim); }
.field textarea { min-height: 72px; resize: vertical; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* ── Separator ── */
.form-section { margin-top: 4px; padding-top: 14px; border-top: 1px solid var(--rule); }
.form-section-title { font-family: "Songti SC", serif; font-size: 13px; font-weight: 600; color: var(--gilt); margin-bottom: 12px; display: flex; align-items: center; gap: 6px; }
.form-section-title .glyph { opacity: 0.6; }
/* ── Tag input (hobbies/values) ── */
.tag-input-wrap { display: flex; flex-wrap: wrap; gap: 5px; padding: 6px 8px; border: 1px solid var(--rule); border-radius: 10px; background: var(--surface); min-height: 36px; align-items: center; cursor: text; transition: border-color 0.15s; }
.tag-input-wrap:focus-within { border-color: var(--patina-dim); }
.tag-input-tag { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 12px; background: var(--patina-wash); color: var(--patina); border: 1px solid var(--patina-dim); }
.tag-input-tag .remove { cursor: pointer; opacity: 0.5; font-size: 14px; line-height: 1; margin-left: 2px; }
.tag-input-tag .remove:hover { opacity: 1; color: var(--cinnabar); }
.tag-input-native { border: none !important; background: transparent !important; outline: none !important; flex: 1; min-width: 80px; padding: 2px 4px; font-size: 13px; color: var(--fg); font-family: inherit; }
/* ── Detail modal extra ── */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.detail-item { border: 1px solid var(--rule); border-radius: 10px; padding: 12px; background: var(--surface); }
.detail-item.full { grid-column: 1 / -1; }
.detail-item .label { font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: 0.06em; color: var(--fg-faint); margin-bottom: 4px; }
.detail-item .value { font-size: 13px; color: var(--fg); white-space: pre-wrap; }
.detail-item .value.json { font-family: "JetBrains Mono", monospace; font-size: 11.5px; }
/* ── Delete confirmation ── */
.confirm-box { border: 1px solid var(--cinnabar-dim); border-radius: 12px; padding: 16px; background: var(--cinnabar-wash); display: flex; flex-direction: column; gap: 10px; }
.confirm-box .title { font-size: 14px; font-weight: 600; color: var(--cinnabar); }
.confirm-box .desc { font-size: 13px; color: var(--fg-dim); }
/* ── Toast ── */
.toast { position: fixed; bottom: 24px; right: 24px; z-index: 200; display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-radius: 12px; border: 1px solid var(--patina-dim); background: var(--surface); backdrop-filter: blur(12px); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5); font-size: 13px; color: var(--patina); }
.toast.error { border-color: var(--cinnabar-dim); color: var(--cinnabar); }
/* ── Responsive ── */
@media (max-width: 768px) {
.view-body { padding: 12px 16px 36px; }
.field-row { grid-template-columns: 1fr; }
.detail-grid { grid-template-columns: 1fr; }
.template-table th, .template-table td { padding: 8px 10px; }
.template-table .desc-cell { display: none; }
}
</style>
</head>
<body>
<div class="app-shell">
<div class="sidebar-mount" data-active="司命台"></div>
<div class="main-area">
<div class="admin-shell">
<!-- ═══ 司命台顶栏 ═══ -->
<header class="topbar">
<div class="topbar-brand">
<svg class="knob" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="var(--gilt)" stroke-width="1.3"/><circle cx="8" cy="8" r="2"/></svg>
司命台
</div>
<span class="topbar-sep">|</span>
<nav class="topbar-nav">
<span class="topbar-navitem">星盘</span>
<span class="topbar-navitem">朱批</span>
<span class="topbar-navitem">起居注</span>
<span class="topbar-navitem">太一</span>
<span class="topbar-navitem">烛照</span>
<span class="topbar-navitem">河图</span>
<span class="topbar-navitem">洛书</span>
<span class="topbar-navitem">天工阁</span>
<span class="topbar-navitem">用户</span>
<span class="topbar-navitem active">人物模板</span>
<span class="topbar-navitem">链路</span>
</nav>
</header>
<main>
<!-- ═══ 页头 ═══ -->
<div class="page-header">
<h1 class="page-title">人物模板</h1>
<span class="page-sub">Character Templates · §19.8</span>
<span class="header-badge">2 个模板</span>
<div class="spacer"></div>
<button class="btn btn-primary" onclick="openCreate()">✦ 新建模板</button>
</div>
<div class="view-body">
<!-- ── 操作栏 ── -->
<div class="toolbar">
<input class="search-input" type="text" placeholder="搜索模板 key 或名称…" />
<div class="spacer"></div>
<button class="btn btn-sm" onclick="syncFromYaml()">↻ 从 YAML 同步</button>
</div>
<!-- ── 模板列表 ── -->
<table class="template-table">
<thead>
<tr>
<th>Key</th>
<th>名称</th>
<th>显示名</th>
<th>来源</th>
<th>简介</th>
<th>创建时间</th>
<th></th>
</tr>
</thead>
<tbody>
<!-- 魂镜(YAML 管理) -->
<tr>
<td class="key-cell">default</td>
<td class="name-cell">魂镜</td>
<td class="display-name-cell">魂镜</td>
<td><span class="tag tag-yaml">YAML</span></td>
<td class="desc-cell">一面照见人心的镜子</td>
<td class="date-cell">2026-06-19</td>
<td class="actions-cell">
<button class="btn btn-sm" onclick="openDetail('default')">查看</button>
<button class="btn btn-sm" onclick="openEdit('default')">编辑</button>
<button class="btn btn-sm btn-danger" onclick="confirmDelete('default')">删除</button>
</td>
</tr>
<!-- 时崎狂三(YAML 管理) -->
<tr>
<td class="key-cell">tokisaki_kurumi</td>
<td class="name-cell">时崎狂三</td>
<td class="display-name-cell">时崎狂三</td>
<td><span class="tag tag-yaml">YAML</span></td>
<td class="desc-cell">被称为「梦魇」的时间精灵</td>
<td class="date-cell">2026-07-09</td>
<td class="actions-cell">
<button class="btn btn-sm" onclick="openDetail('tokisaki_kurumi')">查看</button>
<button class="btn btn-sm" onclick="openEdit('tokisaki_kurumi')">编辑</button>
<button class="btn btn-sm btn-danger" onclick="confirmDelete('tokisaki_kurumi')">删除</button>
</td>
</tr>
<!-- API 创建的模板 -->
<tr>
<td class="key-cell">api_gufeng</td>
<td class="name-cell">古风人物</td>
<td class="display-name-cell">墨语</td>
<td><span class="tag tag-api">API</span></td>
<td class="desc-cell">穿越千年而来的书卷气</td>
<td class="date-cell">2026-07-13</td>
<td class="actions-cell">
<button class="btn btn-sm" onclick="openDetail('api_gufeng')">查看</button>
<button class="btn btn-sm" onclick="openEdit('api_gufeng')">编辑</button>
<button class="btn btn-sm btn-danger" onclick="confirmDelete('api_gufeng')">删除</button>
</td>
</tr>
</tbody>
</table>
<!-- ── 来自 YAML 的提示 ── -->
<div class="detail-item" style="border-style:dashed;">
<div class="label">⚠ YAML 管理提示</div>
<div style="font-size:13px; color:var(--fg-dim);">
key 与 <code style="font-size:12px; color:var(--gilt);">config/character_templates.yaml</code> 中定义一致的模板,在服务重启时会从 YAML 文件覆盖同步。
API 创建的模板(来源标记为 API)不受影响。
</div>
</div>
</div><!-- /view-body -->
</main>
</div><!-- /admin-shell -->
<!-- ═══ 查看详情 Modal ═══ -->
<div class="modal-overlay" id="modal-detail" style="display:none;">
<div class="modal" style="max-width: 680px;">
<div class="modal-header">
<h2 class="modal-title">模板详情 · <span id="detail-key">default</span></h2>
<button class="modal-close" onclick="closeModal('modal-detail')">✕</button>
</div>
<div class="modal-body">
<div class="detail-grid">
<div class="detail-item">
<div class="label">Key</div>
<div class="value" id="d-key" style="font-family:monospace;">default</div>
</div>
<div class="detail-item">
<div class="label">来源</div>
<div><span class="tag tag-yaml">YAML</span></div>
</div>
<div class="detail-item">
<div class="label">名称</div>
<div class="value" id="d-name">魂镜</div>
</div>
<div class="detail-item">
<div class="label">显示名</div>
<div class="value" id="d-display">魂镜</div>
</div>
<div class="detail-item">
<div class="label">语言</div>
<div class="value" id="d-lang">zh-CN</div>
</div>
<div class="detail-item">
<div class="label">私隐等级</div>
<div class="value" id="d-privacy">normal</div>
</div>
<div class="detail-item full">
<div class="label">人物档案(profile)</div>
<div class="value json" id="d-profile">{
"description": "一面照见人心的镜子",
"background": "默认人物",
"speaking_style": "温和、清晰、简洁",
"temperament": "沉静而温暖",
"hobbies": ["哲学", "文学", "围棋"],
"values": ["真诚", "同理心", "独立思考"],
"birthday": "2026-06-19",
}</div>
</div>
<div class="detail-item full">
<div class="label">模型策略(model_policy)</div>
<div class="value json" id="d-policy">{
"memory_extraction_model_hint": "memory-extraction",
"personality_analysis_model_hint": "personality-analysis",
"embedding_model_hint": "embedding",
"default_chat_model_hint": "deepseek-v4-flash"
}</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('modal-detail')">关闭</button>
<button class="btn btn-primary" onclick="closeModal('modal-detail'); openEdit('default')">编辑</button>
</div>
</div>
</div>
<!-- ═══ 创建/编辑 Modal ═══ -->
<div class="modal-overlay" id="modal-edit" style="display:none;">
<div class="modal" style="max-width: 720px;">
<div class="modal-header">
<h2 class="modal-title" id="edit-title">新建模板</h2>
<button class="modal-close" onclick="closeModal('modal-edit')">✕</button>
</div>
<div class="modal-body">
<!-- ── 基础信息 ── -->
<div class="field-row">
<div class="field">
<label class="field-label">Key *</label>
<input id="f-key" type="text" placeholder="唯一标识,如 my_template" />
<span class="field-hint">仅小写字母、数字、下划线、连字符。创建后不可修改。</span>
</div>
<div class="field">
<label class="field-label">是否默认</label>
<select id="f-is-default">
<option value="false">否</option>
<option value="true">是(新用户以此创建初始角色)</option>
</select>
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label">名称 *</label>
<input id="f-name" type="text" placeholder="内部名称" />
</div>
<div class="field">
<label class="field-label">显示名</label>
<input id="f-display" type="text" placeholder="显示昵称" />
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label">语言</label>
<select id="f-lang">
<option value="zh-CN">zh-CN</option>
<option value="en">en</option>
<option value="ja">ja</option>
</select>
</div>
<div class="field">
<label class="field-label">私隐等级</label>
<select id="f-privacy">
<option value="normal">normal</option>
<option value="sensitive">sensitive</option>
<option value="local_only">local_only</option>
</select>
</div>
</div>
<!-- ── 人物档案 ── -->
<div class="form-section">
<div class="form-section-title"><span class="glyph">📋</span> 人物档案(Profile)</div>
<div class="field" style="margin-bottom:12px;">
<label class="field-label">一句话简介</label>
<input id="f-description" type="text" placeholder="例如:一面照见人心的镜子" />
</div>
<div class="field" style="margin-bottom:12px;">
<label class="field-label">背景故事</label>
<textarea id="f-background" rows="4" placeholder="人物背景故事设定…"></textarea>
</div>
<div class="field-row">
<div class="field">
<label class="field-label">说话风格</label>
<input id="f-speaking-style" type="text" placeholder="例如:温和、清晰、简洁" />
</div>
<div class="field">
<label class="field-label">性情气质</label>
<input id="f-temperament" type="text" placeholder="例如:沉静而温暖" />
</div>
</div>
<div class="field-row" style="margin-top:12px;">
<div class="field">
<label class="field-label">生日</label>
<input id="f-birthday" type="date" />
<span class="field-hint">留空则系统用创建时间自动兜底</span>
</div>
<div class="field-row" style="grid-template-columns:1fr 1fr; gap:10px; margin:0;">
<div class="field">
<label class="field-label">身高</label>
<input id="f-height" type="number" placeholder="cm" />
</div>
<div class="field">
<label class="field-label">体重</label>
<input id="f-weight" type="number" placeholder="kg" />
</div>
</div>
</div>
<div class="field-row" style="margin-top:12px;">
<div class="field">
<label class="field-label">兴趣爱好</label>
<div class="tag-input-wrap" id="hobbies-wrap">
<span class="tag-input-tag">抚琴<span class="remove" onclick="removeTag(this)">×</span></span>
<span class="tag-input-tag">书法<span class="remove" onclick="removeTag(this)">×</span></span>
<span class="tag-input-tag">品茶<span class="remove" onclick="removeTag(this)">×</span></span>
<span class="tag-input-tag">对弈<span class="remove" onclick="removeTag(this)">×</span></span>
<input class="tag-input-native" id="f-hobbies-input" type="text" placeholder="输入后按回车添加…" onkeydown="if(event.key==='Enter'){event.preventDefault();addTag(this,document.getElementById('hobbies-hidden'));}" />
</div>
<input id="hobbies-hidden" type="hidden" value='["抚琴","书法","品茶","对弈"]' />
</div>
<div class="field">
<label class="field-label">价值观</label>
<div class="tag-input-wrap" id="values-wrap">
<span class="tag-input-tag">中庸<span class="remove" onclick="removeTag(this)">×</span></span>
<span class="tag-input-tag">礼义<span class="remove" onclick="removeTag(this)">×</span></span>
<span class="tag-input-tag">自然<span class="remove" onclick="removeTag(this)">×</span></span>
<input class="tag-input-native" id="f-values-input" type="text" placeholder="输入后按回车添加…" onkeydown="if(event.key==='Enter'){event.preventDefault();addTag(this,document.getElementById('values-hidden'));}" />
</div>
<input id="values-hidden" type="hidden" value='["中庸","礼义","自然"]' />
</div>
</div>
</div>
<!-- ── 额外内容(JSON,补充人物档案) ── -->
<div class="form-section">
<div class="form-section-title"><span class="glyph">⋯</span> 额外内容(Extra JSON)<span style="font-weight:400;font-size:12px;color:var(--fg-faint);margin-left:6px;">以上字段未覆盖的自定义 profile 键值</span></div>
<div class="field">
<textarea id="f-extra-json" rows="3" style="font-family:monospace;font-size:12px;">{}</textarea>
</div>
</div>
<!-- ── 模型策略 ── -->
<div class="form-section">
<div class="form-section-title"><span class="glyph">⚙</span> 模型策略(Model Policy)<span style="font-weight:400;font-size:12px;color:var(--fg-faint);margin-left:6px;">不选则留空</span></div>
<div class="field-row" style="grid-template-columns:1fr 1fr;">
<div class="field">
<label class="field-label">对话模型</label>
<select id="f-chat-hint">
<option value="">— 不设置(使用系统默认)</option>
<option value="deepseek-v4-flash">deepseek-v4-flash</option>
<option value="deepseek-v3">deepseek-v3</option>
<option value="gpt-4o">gpt-4o</option>
<option value="claude-sonnet-5">claude-sonnet-5</option>
<option value="claude-opus-4-8">claude-opus-4-8</option>
</select>
</div>
<div class="field">
<label class="field-label">记忆提取模型</label>
<select id="f-memory-hint">
<option value="">— 不设置(空)</option>
<option value="memory-extraction">memory-extraction</option>
<option value="deepseek-v4-flash">deepseek-v4-flash</option>
<option value="deepseek-v3">deepseek-v3</option>
<option value="gpt-4o-mini">gpt-4o-mini</option>
</select>
</div>
<div class="field">
<label class="field-label">人格分析模型</label>
<select id="f-personality-hint">
<option value="">— 不设置(空)</option>
<option value="personality-analysis">personality-analysis</option>
<option value="deepseek-v4-flash">deepseek-v4-flash</option>
<option value="deepseek-v3">deepseek-v3</option>
<option value="gpt-4o">gpt-4o</option>
</select>
</div>
<div class="field">
<label class="field-label">向量嵌入模型</label>
<select id="f-embedding-hint">
<option value="">— 不设置(空)</option>
<option value="embedding">embedding</option>
<option value="text-embedding-3-small">text-embedding-3-small</option>
<option value="text-embedding-3-large">text-embedding-3-large</option>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('modal-edit')">取消</button>
<button class="btn btn-primary" id="edit-save-btn">✦ 保存</button>
</div>
</div>
</div>
<!-- ═══ 删除确认 Modal ═══ -->
<div class="modal-overlay" id="modal-delete" style="display:none;">
<div class="modal" style="max-width: 460px;">
<div class="modal-header">
<h2 class="modal-title">确认删除</h2>
<button class="modal-close" onclick="closeModal('modal-delete')">✕</button>
</div>
<div class="modal-body">
<div class="confirm-box">
<div class="title">⚠ 确定要删除此模板?</div>
<div class="desc">
将删除系统模板 <strong id="del-key-name">default · 魂镜</strong>。如果有用户人物引用了此模板将无法删除。
此操作不可撤销。
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('modal-delete')">取消</button>
<button class="btn btn-danger" onclick="performDelete()">✕ 确认删除</button>
</div>
</div>
</div>
<!-- ═══ Toast ═══ -->
<div class="toast" id="toast" style="display:none;">✔ 操作成功</div>
<script>
// ── Tag input helpers ──
function addTag(inputEl, hiddenEl) {
const val = inputEl.value.trim();
if (!val) return;
const tag = document.createElement('span');
tag.className = 'tag-input-tag';
tag.innerHTML = val + '<span class="remove" onclick="removeTag(this)">×</span>';
inputEl.parentNode.insertBefore(tag, inputEl);
inputEl.value = '';
syncTags(hiddenEl);
}
function removeTag(removeEl) {
const tag = removeEl.parentNode;
const wrap = tag.parentNode;
tag.remove();
const hidden = wrap.parentNode.querySelector('input[type="hidden"]') || wrap.nextElementSibling;
if (hidden) syncTags(hidden);
}
function syncTags(hiddenEl) {
const wrap = hiddenEl.parentNode.querySelector('.tag-input-wrap') || hiddenEl.previousElementSibling;
const items = [];
wrap.querySelectorAll('.tag-input-tag').forEach(t => items.push(t.textContent.replace('×','').trim()));
hiddenEl.value = JSON.stringify(items);
}
function populateTags(wrapId, hiddenId, values) {
const wrap = document.getElementById(wrapId);
const hidden = document.getElementById(hiddenId);
// clear existing tags (keep native input)
wrap.querySelectorAll('.tag-input-tag').forEach(t => t.remove());
values.forEach(v => {
const tag = document.createElement('span');
tag.className = 'tag-input-tag';
tag.innerHTML = v + '<span class="remove" onclick="removeTag(this)">×</span>';
wrap.insertBefore(tag, wrap.querySelector('.tag-input-native'));
});
hidden.value = JSON.stringify(values);
}
// ── Simple state ──
const state = { deleteKey: null, deleteName: null };
function closeModal(id) { document.getElementById(id).style.display = 'none'; }
function openCreate() {
document.getElementById('edit-title').textContent = '✦ 新建模板';
document.getElementById('edit-save-btn').textContent = '✦ 创建';
document.getElementById('f-key').disabled = false;
document.getElementById('f-key').value = '';
document.getElementById('f-name').value = '';
document.getElementById('f-display').value = '';
document.getElementById('f-lang').value = 'zh-CN';
document.getElementById('f-privacy').value = 'normal';
document.getElementById('f-is-default').value = 'false';
document.getElementById('f-description').value = '';
document.getElementById('f-background').value = '';
document.getElementById('f-speaking-style').value = '';
document.getElementById('f-temperament').value = '';
document.getElementById('f-birthday').value = '';
document.getElementById('f-height').value = '';
document.getElementById('f-weight').value = '';
populateTags('hobbies-wrap', 'hobbies-hidden', []);
populateTags('values-wrap', 'values-hidden', []);
document.getElementById('f-chat-hint').value = '';
document.getElementById('f-memory-hint').value = '';
document.getElementById('f-personality-hint').value = '';
document.getElementById('f-embedding-hint').value = '';
document.getElementById('f-extra-json').value = '{}';
document.getElementById('modal-edit').style.display = 'flex';
}
const templateData = {
default: {
name: '魂镜', display: '魂镜', desc: '一面照见人心的镜子',
background: '默认人物', speaking: '温和、清晰、简洁',
temper: '沉静而温暖', hobbies: ['哲学','文学','围棋'],
values: ['真诚','同理心','独立思考'],
bday: '', height: '', weight: '',
chatHint: 'deepseek-v4-flash', memHint: 'memory-extraction',
persHint: 'personality-analysis', embHint: 'embedding'
},
tokisaki_kurumi: {
name: '时崎狂三', display: '时崎狂三', desc: '被称为「梦魇」的时间精灵',
background: '拥有时间之力的精灵「Nightmare(梦魇)」…',
speaking: '优雅从容的大小姐口气', temper: '优雅腹黑、孤独偏执、温柔而危险',
hobbies: ['与猫相处','阅读','钢琴','收藏古董时钟','摄影','品尝红茶与甜点'],
values: ['自由','尊严','独立','记忆'],
bday: '2000-12-25', height: '157', weight: '43',
chatHint: 'deepseek-v4-flash', memHint: 'memory-extraction',
persHint: 'personality-analysis', embHint: 'embedding'
},
gufeng: {
name: '古风人物', display: '墨语', desc: '穿越千年而来的书卷气',
background: '生于江南世家,自幼饱读诗书,后遇变故隐居山林…',
speaking: '文雅、含蓄、引经据典', temper: '淡泊通透',
hobbies: ['抚琴','书法','品茶','对弈'],
values: ['中庸','礼义','自然'],
bday: '', height: '', weight: '',
chatHint: 'deepseek-v4-flash', memHint: 'memory-extraction',
persHint: 'personality-analysis', embHint: 'embedding'
},
baize: {
name: '白泽', display: '白泽', desc: '上古瑞兽,通晓万物之理的博学者',
background: '上古瑞兽白泽的化身…',
speaking: '语速适中偏快,喜欢引用典籍和典故', temper: '博学谦和、言语灵动',
hobbies: ['收集珍稀典籍','整理知识图谱','翻译古文字','观星与占星术','与智者辩论'],
values: ['求真','传递','谦逊','包容'],
bday: '', height: '176', weight: '68',
chatHint: 'deepseek-v4-flash', memHint: 'memory-extraction',
persHint: 'personality-analysis', embHint: 'embedding'
},
qingnv: {
name: '青女', display: '青女', desc: '掌霜雪之神,清冷而孤独的月宫仙子',
background: '青女乃霜雪之神,每逢深秋便降下白霜…',
speaking: '语调清冷平稳,惜字如金', temper: '清冷孤高、外表疏离内心温柔',
hobbies: ['编织霜花','在月宫散步','聆听凡间的祈祷','收集露水制茶','书写霜雪日记'],
values: ['秩序','纯净','美','守护'],
bday: '', height: '170', weight: '',
chatHint: 'deepseek-v4-flash', memHint: 'memory-extraction',
persHint: 'personality-analysis', embHint: 'embedding'
},
api_gufeng: {
name: '古风人物', display: '墨语', desc: '穿越千年而来的书卷气',
background: '生于江南世家,自幼饱读诗书…',
speaking: '文雅、含蓄、引经据典', temper: '淡泊通透',
hobbies: ['抚琴','书法','品茶','对弈'],
values: ['中庸','礼义','自然'],
bday: '', height: '', weight: '',
chatHint: 'deepseek-v4-flash', memHint: 'memory-extraction',
persHint: 'personality-analysis', embHint: 'embedding'
}
};
function openEdit(key) {
const d = templateData[key] || templateData.default;
document.getElementById('edit-title').textContent = '✎ 编辑模板 · ' + key;
document.getElementById('edit-save-btn').textContent = '✎ 保存';
document.getElementById('f-key').disabled = true;
document.getElementById('f-key').value = key;
document.getElementById('f-name').value = d.name;
document.getElementById('f-display').value = d.display;
document.getElementById('f-lang').value = 'zh-CN';
document.getElementById('f-privacy').value = 'normal';
document.getElementById('f-is-default').value = key === 'default' ? 'true' : 'false';
document.getElementById('f-description').value = d.desc;
document.getElementById('f-background').value = d.background;
document.getElementById('f-speaking-style').value = d.speaking;
document.getElementById('f-temperament').value = d.temper;
document.getElementById('f-birthday').value = d.bday || '';
document.getElementById('f-height').value = d.height || '';
document.getElementById('f-weight').value = d.weight || '';
populateTags('hobbies-wrap', 'hobbies-hidden', d.hobbies);
populateTags('values-wrap', 'values-hidden', d.values);
document.getElementById('f-chat-hint').value = d.chatHint || '';
document.getElementById('f-memory-hint').value = d.memHint || '';
document.getElementById('f-personality-hint').value = d.persHint || '';
document.getElementById('f-embedding-hint').value = d.embHint || '';
document.getElementById('f-extra-json').value = '{}';
document.getElementById('modal-edit').style.display = 'flex';
}
function openDetail(key) {
const d = templateData[key] || templateData.default;
document.getElementById('detail-key').textContent = key;
document.getElementById('d-key').textContent = key;
document.getElementById('d-name').textContent = d.name;
document.getElementById('d-display').textContent = d.display;
document.getElementById('d-lang').textContent = 'zh-CN';
document.getElementById('d-privacy').textContent = 'normal';
document.getElementById('modal-detail').style.display = 'flex';
}
function confirmDelete(key) {
state.deleteKey = key;
const d = templateData[key] || templateData.default;
state.deleteName = d.name;
document.getElementById('del-key-name').textContent = key + ' · ' + d.name;
document.getElementById('modal-delete').style.display = 'flex';
}
function performDelete() {
closeModal('modal-delete');
showToast('✔ 已删除模板 · ' + state.deleteKey);
state.deleteKey = null;
}
function syncFromYaml() {
showToast('↻ 正在从 YAML 同步模板…');
setTimeout(() => showToast('✔ 同步完成'), 1500);
}
function showToast(msg, isError) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast' + (isError ? ' error' : '');
t.style.display = 'flex';
setTimeout(() => { t.style.display = 'none'; }, 3000);
}
// ── Escape to close modals ──
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay').forEach(m => m.style.display = 'none');
}
});
// Click overlay to close
document.querySelectorAll('.modal-overlay').forEach(m => {
m.addEventListener('click', function(e) {
if (e.target === this) this.style.display = 'none';
});
});
</script>
</div>
</div>
<script src="_theme.js" defer></script>
</body>
</html>