初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,752 @@
|
||||
<!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>
|
||||
Reference in new issue
Block a user