M6:缘契、人格与世界——契约版本链、铸魂分灵、河图境界与三界分流
- 缘契域 domains/yuanqi:covenants(当前版)+ covenant_events(不可变历史);提议—问心—铭契—撤回全链;边界变更立即生效;多维关系并存,服务层拒绝 affinity/好感度单一键;浪漫维度成人资格标记位 - 道体铸魂与分灵 domains/jingsheng/soul.py:soul_forgings 确认流(锚点变更 pending→confirmed 应用并签发新魂籍版本,确认前人物锚点不变);分灵创建新人物(继承资料与谱系 lineage 链、不继承原身份,魂籍从 v1 起) - 河图域 domains/hetu:persistent_worlds 境界 + world_entities/world_relations 境界作用域重建 + 世谱挂接;境界正史视图(scope=persistent_world 归真集合);canonize 后置推进正史计数 - 三界分流:reality→息壤提取(不变);persistent_world→境界实录+正史推进;simulation→只记试演实录;factual_records 增 scope 列——境界/幻境实录与真界本相隔离 - 正影打通:引文 target_type=memory 时被引记忆 superseded → 返回 successor_memory_id 提示,不静默替换 - 前端:人物详情页缘契面板(提议维度选择/铭契/撤回/事件史)+ 世界页重建(境界目录/实体/正史概览)+ covenants/worlds api 库 - 退役:旧 personality/drive/evolution/world 模块、服务、端点、仓储、schema、前端五页与组件样式;迁移 007 退役九张旧表与 characters.current_personality_version_id - 验收(test_m6_domains 六用例):多维无单一好感度/铭契版本链/事件不可抹除/撤回立即生效/边界即时/铸魂确认流+魂籍版本/分灵谱系/境界正史不进真界(实录 scope 隔离+真界候选=0)/引文正影后继 - 四套件:后端 379、前端 199、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 007 净库+±1 干净
This commit is contained in:
1 parent
684261f4b4
commit
7e23d6fa84
108 files changed
+2600
-9732
No files matched your search
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { CharacterIdentityCard } from "@/components/characters/CharacterIdentityCard";
|
||||
import { CovenantPanel } from "@/components/characters/CovenantPanel";
|
||||
import { CharacterLifecycleBar } from "@/components/characters/CharacterLifecycleBar";
|
||||
import { ModelPolicyEditForm } from "@/components/characters/ModelPolicyEditForm";
|
||||
import { ProfileEditForm } from "@/components/characters/ProfileEditForm";
|
||||
@@ -12,6 +13,7 @@ import { CharacterStatusBadge } from "@/components/ui/StatusBadge";
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { formatDateTime } from "@/lib/utils/format";
|
||||
import type { Covenant } from "@/lib/api/covenants";
|
||||
import type { Character } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -28,12 +30,21 @@ export default async function CharacterDetailPage({
|
||||
const { characterId } = await params;
|
||||
|
||||
let character = null;
|
||||
let covenant: Covenant | null = null;
|
||||
let error: string | null = null;
|
||||
try {
|
||||
character = await serverApi.get<Character>(`/characters/${characterId}`);
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
// 缘契独立拉取,失败静默(无缘契是合法状态)
|
||||
try {
|
||||
covenant = await serverApi.get<Covenant | null>(
|
||||
`/covenants/by-character/${characterId}`,
|
||||
);
|
||||
} catch {
|
||||
covenant = null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -62,6 +73,10 @@ export default async function CharacterDetailPage({
|
||||
|
||||
<CharacterIdentityCard character={character} />
|
||||
|
||||
<div className="mt-6">
|
||||
<CovenantPanel characterId={character.id} initialCovenant={covenant} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<ProfileEditForm character={character} />
|
||||
<ModelPolicyEditForm character={character} />
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,244 +0,0 @@
|
||||
import { DriveView } from "@/components/drive/DriveView";
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { ErrorState } from "@/components/shared/StateViews";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { GOAL_TYPE_LABEL, GOAL_STATUS_LABEL } from "@/lib/constants/labels";
|
||||
import type {
|
||||
Character,
|
||||
CuriosityQuestion,
|
||||
ExploratorySubgoal,
|
||||
GoalNode,
|
||||
Paginated,
|
||||
} from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* 内驱力页 · 大周天生成的好奇问题与探索性次生目标 + 太一目标进度总览。
|
||||
* 设计稿 drive-preview.html — 太一目标 + 好奇萤火 + 探索天平 + 阴阳制衡。
|
||||
*/
|
||||
export default async function DrivePage() {
|
||||
let characterId: string | null = null;
|
||||
let characterName = "";
|
||||
let goals: GoalNode[] = [];
|
||||
let curiosities: CuriosityQuestion[] = [];
|
||||
let subgoals: ExploratorySubgoal[] = [];
|
||||
let counts = { candidate: 0, approved: 0, deferred: 0, rejected: 0 };
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const active = await serverApi.get<Character>("/characters/active");
|
||||
characterId = active.id;
|
||||
characterName = active.display_name || active.name;
|
||||
const [goalRes, curios, cand, approved, deferred, rejected] =
|
||||
await Promise.all([
|
||||
serverApi
|
||||
.get<GoalNode[]>("/admin/goals", { character_id: active.id })
|
||||
.catch(() => [] as GoalNode[]),
|
||||
serverApi.get<Paginated<CuriosityQuestion>>(
|
||||
`/characters/${active.id}/curiosities`,
|
||||
{ page_size: 20 },
|
||||
),
|
||||
serverApi.get<Paginated<ExploratorySubgoal>>(
|
||||
`/characters/${active.id}/subgoals`,
|
||||
{ status: "candidate", page_size: 50 },
|
||||
),
|
||||
serverApi.get<Paginated<ExploratorySubgoal>>(
|
||||
`/characters/${active.id}/subgoals`,
|
||||
{ status: "approved", page_size: 1 },
|
||||
),
|
||||
serverApi.get<Paginated<ExploratorySubgoal>>(
|
||||
`/characters/${active.id}/subgoals`,
|
||||
{ status: "deferred", page_size: 1 },
|
||||
),
|
||||
serverApi.get<Paginated<ExploratorySubgoal>>(
|
||||
`/characters/${active.id}/subgoals`,
|
||||
{ status: "rejected", page_size: 1 },
|
||||
),
|
||||
]);
|
||||
goals = goalRes;
|
||||
curiosities = curios.items;
|
||||
subgoals = cand.items;
|
||||
counts = {
|
||||
candidate: cand.total,
|
||||
approved: approved.total,
|
||||
deferred: deferred.total,
|
||||
rejected: rejected.total,
|
||||
};
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
const totalCurios = curiosities.length;
|
||||
const totalSubgoals = subgoals.length;
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="内驱力"
|
||||
description="大周天 · 好奇与探索"
|
||||
meta={
|
||||
<>
|
||||
{characterName && (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
fontSize: 12.5,
|
||||
color: "var(--patina)",
|
||||
padding: "3px 11px 3px 4px",
|
||||
border: "1px solid var(--patina-dim)",
|
||||
borderRadius: 9999,
|
||||
background: "var(--patina-wash)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: "50%",
|
||||
background: "var(--patina)",
|
||||
color: "var(--ink)",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: '"Songti SC", serif',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{characterName.charAt(0)}
|
||||
</span>
|
||||
{characterName}
|
||||
</span>
|
||||
)}
|
||||
<span className="page-meta">
|
||||
好奇 <b>{totalCurios}</b> · 待探 <b>{totalSubgoals}</b> · 已延{" "}
|
||||
<b>{counts.deferred}</b>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
maxWidth="max-w-5xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : characterId ? (
|
||||
<div className="space-y-5">
|
||||
{/* 太一 · 目标进度 */}
|
||||
{goals.length > 0 && (
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 太一{" "}
|
||||
<span className="sub">最高目标 · 内驱力在其下生成次生探索</span>
|
||||
</div>
|
||||
{goals.map((g) => (
|
||||
<div
|
||||
key={g.id}
|
||||
className="taiyi-card"
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<div className="taiyi-row">
|
||||
<span
|
||||
className={
|
||||
g.status === "active"
|
||||
? "taiyi-dot active"
|
||||
: g.status === "completed"
|
||||
? "taiyi-dot completed"
|
||||
: "taiyi-dot"
|
||||
}
|
||||
style={{
|
||||
background:
|
||||
g.status === "blocked"
|
||||
? "var(--cinnabar)"
|
||||
: undefined,
|
||||
boxShadow:
|
||||
g.status === "blocked"
|
||||
? "0 0 0 3px rgba(184,57,46,0.2)"
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
<span className="taiyi-title">{g.title}</span>
|
||||
{g.type && (
|
||||
<span className="taiyi-tag">
|
||||
{GOAL_TYPE_LABEL[g.type] ?? g.type}
|
||||
</span>
|
||||
)}
|
||||
<span className="taiyi-status">
|
||||
{GOAL_STATUS_LABEL[g.status] ?? g.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="taiyi-prog">
|
||||
<div className="track">
|
||||
<div
|
||||
className="fill"
|
||||
style={{
|
||||
width: `${Math.round(Math.max(0, Math.min(1, g.progress)) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="tv">
|
||||
{Math.round(Math.max(0, Math.min(1, g.progress)) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
{g.description && (
|
||||
<p className="text-[12.5px] leading-relaxed text-fg-dim/90">
|
||||
{g.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<DriveView
|
||||
characterId={characterId}
|
||||
curiosities={curiosities}
|
||||
subgoals={subgoals}
|
||||
counts={counts}
|
||||
/>
|
||||
|
||||
{/* 阴阳制衡(太极图) */}
|
||||
<div className="yin-yang">
|
||||
<svg className="yy-taiji" viewBox="0 0 200 200" aria-hidden="true">
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="96"
|
||||
fill="none"
|
||||
stroke="var(--gilt-dim)"
|
||||
strokeWidth="0.8"
|
||||
/>
|
||||
<circle cx="100" cy="100" r="88" fill="var(--patina)" />
|
||||
<path
|
||||
d="M 100,12 A 88,88 0 0,1 100,188 A 44,44 0 0,1 100,100 A 44,44 0 0,0 100,12 Z"
|
||||
fill="var(--cinnabar)"
|
||||
opacity="0.88"
|
||||
/>
|
||||
<circle cx="100" cy="144" r="13" fill="var(--patina)" />
|
||||
<circle
|
||||
cx="100"
|
||||
cy="56"
|
||||
r="13"
|
||||
fill="var(--cinnabar)"
|
||||
opacity="0.88"
|
||||
/>
|
||||
</svg>
|
||||
<div className="yy-text">
|
||||
<b>阴阳制衡</b> · 内驱力持续生成候选(
|
||||
<span className="yang">阳 · 生发</span>
|
||||
),司命分级过滤、你按需确认(
|
||||
<span className="yin">阴 · 约束</span>
|
||||
)。候选零产生则僵化(阴盛),悉数被驳则阳亢——生成节奏与治理粒度需相称。
|
||||
<div className="yy-meta">
|
||||
阴阳平衡度 = 通过率 / (生成率 + ε) · 当前 <b>0.94 · 动态平衡</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { PersonalityWorkspace } from "@/components/personality/PersonalityWorkspace";
|
||||
import { EmptyState, ErrorState } from "@/components/shared/StateViews";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Paginated, PersonalityVersion } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* 人格页 · 当前人物的人格版本,字段以提议审核方式留痕写入。
|
||||
* 预取当前版本与版本历史,交由 PersonalityWorkspace 组合编辑 / 即时审批 / 时间线。
|
||||
*/
|
||||
export default async function PersonalityPage() {
|
||||
let personality: PersonalityVersion | null = null;
|
||||
let versions: PersonalityVersion[] = [];
|
||||
let error: string | null = null;
|
||||
try {
|
||||
personality = await serverApi.get<PersonalityVersion>(
|
||||
"/personality/current",
|
||||
);
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
// 版本历史独立拉取,失败静默(不影响编辑区)
|
||||
try {
|
||||
versions = (
|
||||
await serverApi.get<Paginated<PersonalityVersion>>(
|
||||
"/personality/versions",
|
||||
{ page_size: 50 },
|
||||
)
|
||||
).items;
|
||||
} catch {
|
||||
/* 版本历史可选 */
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="人格"
|
||||
description="当前人物的人格版本;字段修改以提议审核方式留痕写入。"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : personality ? (
|
||||
<PersonalityWorkspace personality={personality} versions={versions} />
|
||||
) : (
|
||||
<EmptyState
|
||||
title="尚未生成人格"
|
||||
description="当前人物尚未初始化人格版本,待后端写入后即可查看与编辑。"
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { RunTraceView } from "@/components/runs/RunTraceView";
|
||||
import { ErrorState } from "@/components/shared/StateViews";
|
||||
import { ErrorTraceLink } from "@/components/shared/ErrorTraceLink";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Run, RunEvent } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function RunPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ runId: string }>;
|
||||
}) {
|
||||
const { runId } = await params;
|
||||
|
||||
let run: Run | null = null;
|
||||
let events: RunEvent[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const [fetchedRun, fetchedEvents] = await Promise.all([
|
||||
serverApi.get<Run>(`/runs/${runId}`),
|
||||
serverApi
|
||||
.get<RunEvent[]>(`/runs/${runId}/events`)
|
||||
.catch(() => [] as RunEvent[]),
|
||||
]);
|
||||
run = fetchedRun;
|
||||
events = fetchedEvents;
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
// 取该 run 首个带 trace_id 的事件作为链路入口(同一 run 共享一条链路)
|
||||
const traceId = events.find((ev) => ev.trace_id)?.trace_id;
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="运行轨迹"
|
||||
description={`Run ${runId}`}
|
||||
maxWidth="max-w-4xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : (
|
||||
<>
|
||||
{traceId && (
|
||||
<div className="mb-4 flex items-center gap-2 text-xs text-muted">
|
||||
<span>本次链路</span>
|
||||
<ErrorTraceLink traceId={traceId} />
|
||||
</div>
|
||||
)}
|
||||
<RunTraceView runId={runId} initialRun={run} initialEvents={events} />
|
||||
</>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { RunStatusBadge } from "@/components/ui/StatusBadge";
|
||||
import { EmptyState, ErrorState } from "@/components/shared/StateViews";
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { formatDateTime } from "@/lib/utils/format";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Paginated, Run } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/** 运行记录入口:与 /runs/[runId] 详情页组成完整轨迹路径。 */
|
||||
export default async function RunsPage() {
|
||||
let runs: Paginated<Run> | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
runs = await serverApi.get<Paginated<Run>>("/runs", { page_size: 50 });
|
||||
} catch (cause) {
|
||||
error = describeError(cause);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="运行轨迹"
|
||||
description="查看每一次推演、工具调用与模型响应的执行状态。"
|
||||
meta={
|
||||
runs && (
|
||||
<span className="rounded-full border border-border-glass bg-glass px-2.5 py-0.5 text-xs text-muted">
|
||||
共 {runs.total} 条
|
||||
</span>
|
||||
)
|
||||
}
|
||||
maxWidth="max-w-5xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : !runs || runs.items.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无运行记录"
|
||||
description="发起对话或执行任务后,这里会保留可追溯的运行轨迹。"
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-2xl border border-border-glass bg-glass backdrop-blur-sm">
|
||||
<ul className="divide-y divide-border-glass">
|
||||
{runs.items.map((run) => (
|
||||
<li key={run.id}>
|
||||
<Link
|
||||
href={`/runs/${run.id}`}
|
||||
className="group flex min-h-20 items-center gap-4 px-4 py-3 transition-colors hover:bg-overlay-hover focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-patina"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-mono text-sm text-fg group-hover:text-patina">
|
||||
{run.goal || run.id}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-muted">
|
||||
{[
|
||||
run.id,
|
||||
run.model_hint ? `模型:${run.model_hint}` : null,
|
||||
`创建:${formatDateTime(run.created_at ?? run.started_at)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
</div>
|
||||
<RunStatusBadge status={run.status} />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,235 +1,22 @@
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { WorldEntityList } from "@/components/admin/WorldEntityList";
|
||||
import { WorldRelationManager } from "@/components/world/WorldRelationManager";
|
||||
import { WorldRelationGraph } from "@/components/world/WorldRelationGraph";
|
||||
import { EmptyState, ErrorState } from "@/components/shared/StateViews";
|
||||
import { WorldWorkbench } from "@/components/world/WorldWorkbench";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type {
|
||||
Character,
|
||||
Paginated,
|
||||
WorldEntity,
|
||||
WorldRelation,
|
||||
} from "@/types/api";
|
||||
import type { PersistentWorld } from "@/lib/api/worlds";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* 世界页 · 数字生命对世界的认知(河图枢主域视图)。
|
||||
* 设计稿 world-preview.html — 人物 pill + section-label + 关系图谱 + 实体名册 + 不确定性 + 世界观共振。
|
||||
* 实体 CRUD 走 admin 域(WorldEntityList,保留编辑/删除);关系图谱走独立 /world/relations。
|
||||
* 世界页(M6 河图重建):境界列表 + 境界详情(实体/关系/正史)。
|
||||
* 境界正史不进真界本相——正史条目只读呈现。
|
||||
*/
|
||||
export default async function WorldPage() {
|
||||
let entities: WorldEntity[] = [];
|
||||
let relations: WorldRelation[] = [];
|
||||
let character: Character | null = null;
|
||||
let characterName = "";
|
||||
let worlds: PersistentWorld[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
entities = await serverApi.get<WorldEntity[]>("/admin/world/entities");
|
||||
worlds = await serverApi.get<PersistentWorld[]>("/worlds");
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
relations = (
|
||||
await serverApi.get<Paginated<WorldRelation>>("/world/relations", {
|
||||
page_size: 50,
|
||||
})
|
||||
).items;
|
||||
} catch {
|
||||
/* 关系可选 */
|
||||
}
|
||||
|
||||
try {
|
||||
character = await serverApi
|
||||
.get<Character>("/characters/active")
|
||||
.catch(() => null);
|
||||
if (character) {
|
||||
characterName = character.display_name || character.name;
|
||||
}
|
||||
} catch {
|
||||
/* 人物信息可选 */
|
||||
}
|
||||
|
||||
/* 按 state_type 统计(用于待验断言计数) */
|
||||
const hypotheses = entities.filter(
|
||||
(e) => e.state_type === "hypothesis",
|
||||
).length;
|
||||
|
||||
/* 待验断言(静态示例,与设计稿对齐) */
|
||||
const uncertainties = [
|
||||
{
|
||||
claim: "云岚 与 青梧 同属旧族旁支,可能存在亲属关系。",
|
||||
reason: "来源:对话中的姓氏推测 · 证据单一且间接",
|
||||
confidence: 0.42,
|
||||
},
|
||||
{
|
||||
claim: "用户当前正在推进的「辰正项目」目标 为时态感知子系统。",
|
||||
reason: "来源:关键词频率推断 · 缺乏用户明确表述",
|
||||
confidence: 0.55,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="世界"
|
||||
description="河图 · 世界模型"
|
||||
meta={
|
||||
<>
|
||||
{characterName && (
|
||||
<span className="char-pill">
|
||||
<span className="avatar-mini">{characterName.charAt(0)}</span>
|
||||
{characterName}视角
|
||||
</span>
|
||||
)}
|
||||
<span className="page-meta">
|
||||
实体 <b>{entities.length}</b> · 关系 <b>{relations.length}</b> ·
|
||||
待验 <b style={{ color: "var(--cinnabar)" }}>{hypotheses}</b>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
maxWidth="max-w-5xl"
|
||||
>
|
||||
{error ? (
|
||||
<ErrorState message={error} />
|
||||
) : entities.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无世界实体"
|
||||
description="尚未建模任何实体。点击「新增世界实体」开始。"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{/* 关系图谱 */}
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 关系图谱{" "}
|
||||
<span className="sub">
|
||||
河图 · 实体为节点,关系为边,置信度定粗细,假设态虚线待验
|
||||
</span>
|
||||
</div>
|
||||
<WorldRelationGraph entities={entities} relations={relations} />
|
||||
</section>
|
||||
|
||||
{/* 实体名册 */}
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 实体名册{" "}
|
||||
<span className="sub">按类型 · 事实性着色 · 推断召回时降权</span>
|
||||
</div>
|
||||
<WorldEntityList entities={entities} />
|
||||
</section>
|
||||
|
||||
{/* 待验断言(不确定性) */}
|
||||
{uncertainties.length > 0 && (
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 待验断言{" "}
|
||||
<span className="sub">
|
||||
UncertaintyState · 低置信,需复核方可固化为事实
|
||||
</span>
|
||||
</div>
|
||||
<div className="unc-list">
|
||||
{uncertainties.map((u, i) => (
|
||||
<div key={i} className="unc-card">
|
||||
<div className="unc-claim">
|
||||
<div className="unc-text">{u.claim}</div>
|
||||
<div className="unc-reason">{u.reason}</div>
|
||||
</div>
|
||||
<div className="unc-conf">
|
||||
<div className="v">{u.confidence.toFixed(2)}</div>
|
||||
<div className="k">置信</div>
|
||||
</div>
|
||||
<span className="unc-verify">需验证</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 世界观共振 */}
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 世界观共振{" "}
|
||||
<span className="sub">
|
||||
见地镜 · 每人物独立的世界观立场,可同境可异境
|
||||
</span>
|
||||
</div>
|
||||
<div className="wv-card">
|
||||
<div className="wv-pair">
|
||||
<div className="wv-side qingwu">
|
||||
<div className="wv-head">
|
||||
<div className="wv-disc">
|
||||
{characterName ? characterName.charAt(0) : "?"}
|
||||
</div>
|
||||
<span className="wv-name">{characterName || "人物"}</span>
|
||||
<span className="wv-tag shared">同境 · 主脉</span>
|
||||
</div>
|
||||
<div className="wv-stance">
|
||||
万物流转无常,变化的规律本身比任何固定结论更可依凭。
|
||||
</div>
|
||||
<div className="wv-anchors">
|
||||
<span className="wv-anchor">
|
||||
面对未知优先观察与适应,而非强求确定性。
|
||||
</span>
|
||||
<span className="wv-anchor">
|
||||
把推断与事实分开,承认不知也是一种知。
|
||||
</span>
|
||||
</div>
|
||||
<span className="wv-conf">
|
||||
spiritual_core.worldview · confidence 0.82 · v7
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="wv-bridge">
|
||||
<div className="b-disc">
|
||||
<div className="b-fill"></div>
|
||||
<span className="b-val">0.41</span>
|
||||
</div>
|
||||
<div className="b-label">
|
||||
共振度
|
||||
<br />
|
||||
divergent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wv-side yunlan">
|
||||
<div className="wv-head">
|
||||
<div className="wv-disc">岚</div>
|
||||
<span className="wv-name">云岚</span>
|
||||
<span className="wv-tag divergent">异境 · 旁支</span>
|
||||
</div>
|
||||
<div className="wv-stance">
|
||||
人如其名,聚散如岚——来去之间留痕即可,不必执守长久。
|
||||
</div>
|
||||
<div className="wv-anchors">
|
||||
<span className="wv-anchor">
|
||||
珍惜每次相遇,不强求每段关系都有归处。
|
||||
</span>
|
||||
<span className="wv-anchor">
|
||||
在变动中自洽,不为稳定而委屈当下。
|
||||
</span>
|
||||
</div>
|
||||
<span className="wv-conf">
|
||||
spiritual_core.worldview · confidence 0.38 · v3 · 初成
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 关系管理 */}
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 关系管理{" "}
|
||||
<span className="sub">新增 / 编辑实体间关系</span>
|
||||
</div>
|
||||
<WorldRelationManager relations={relations} entities={entities} />
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
return <WorldWorkbench initialWorlds={worlds} error={error} />;
|
||||
}
|
||||
@@ -1,920 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
|
||||
/* ── 五行色(组件作用域 CSS 变量,未映射到 @theme → 用任意值引用)── */
|
||||
const WUXING_VARS = {
|
||||
"--wood": "#6fa389",
|
||||
"--wood-dim": "rgba(111, 163, 137, 0.34)",
|
||||
"--fire": "#b8392e",
|
||||
"--fire-dim": "rgba(184, 57, 46, 0.38)",
|
||||
"--earth": "#b08a3c",
|
||||
"--earth-dim": "rgba(176, 138, 60, 0.36)",
|
||||
"--metal": "#c9a24a",
|
||||
"--metal-dim": "rgba(201, 162, 74, 0.34)",
|
||||
"--water": "#5a7fa6",
|
||||
"--water-dim": "rgba(90, 127, 166, 0.38)",
|
||||
} as const;
|
||||
|
||||
/* ── 枢健康维度数据 ── */
|
||||
const DIMS = [
|
||||
{
|
||||
key: "wood",
|
||||
seal: "木",
|
||||
label: "成长性",
|
||||
sub: "演化候选 · 学习速度",
|
||||
width: 64,
|
||||
val: "0.64",
|
||||
},
|
||||
{
|
||||
key: "fire",
|
||||
seal: "火",
|
||||
label: "表达力",
|
||||
sub: "事件发射 · 模型密度",
|
||||
width: 78,
|
||||
val: "0.78",
|
||||
},
|
||||
{
|
||||
key: "earth",
|
||||
seal: "土",
|
||||
label: "稳定性",
|
||||
sub: "数据一致 · 基线回归",
|
||||
width: 88,
|
||||
val: "0.88",
|
||||
},
|
||||
{
|
||||
key: "metal",
|
||||
seal: "金",
|
||||
label: "约束强度",
|
||||
sub: "审核驳回 · 确认触发",
|
||||
width: 71,
|
||||
val: "0.71",
|
||||
},
|
||||
{
|
||||
key: "water",
|
||||
seal: "水",
|
||||
label: "适应性",
|
||||
sub: "上下文切换 · 异常恢复",
|
||||
width: 55,
|
||||
val: "0.55",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/* ── 九枢数据 ── */
|
||||
const PIVOTS = [
|
||||
{
|
||||
name: "灵核枢",
|
||||
eng: "Personality Core",
|
||||
score: 89,
|
||||
trend: "↑ 土 +2",
|
||||
desc: "人格连续性稳定",
|
||||
bars: [55, 70, 96, 60, 58] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "太一枢",
|
||||
eng: "Goal Orchestrator",
|
||||
score: 85,
|
||||
trend: "↑ 土 +1",
|
||||
desc: "目标调度基线稳固",
|
||||
bars: [48, 52, 90, 75, 62] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "司命枢",
|
||||
eng: "Safety Governor",
|
||||
score: 73,
|
||||
trend: "↓ 木 -3",
|
||||
desc: "金过盛克木",
|
||||
bars: [32, 50, 84, 94, 40] as const,
|
||||
alert: true,
|
||||
tag: "金→木",
|
||||
},
|
||||
{
|
||||
name: "烛照枢",
|
||||
eng: "Perception Layer",
|
||||
score: 81,
|
||||
trend: "↑ 火 +4",
|
||||
desc: "多模态输入活跃",
|
||||
bars: [50, 92, 78, 55, 70] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "河图枢",
|
||||
eng: "World Model",
|
||||
score: 87,
|
||||
trend: "↑ 土 +1",
|
||||
desc: "世界实体一致",
|
||||
bars: [58, 60, 95, 66, 64] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "洛书枢",
|
||||
eng: "Planner",
|
||||
score: 79,
|
||||
trend: "↑ 水 +2",
|
||||
desc: "规划切换敏捷",
|
||||
bars: [62, 74, 80, 58, 82] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "息壤枢",
|
||||
eng: "Memory System",
|
||||
score: 84,
|
||||
trend: "↑ 木 +3",
|
||||
desc: "知识沉淀加速",
|
||||
bars: [80, 54, 91, 52, 60] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "百工枢",
|
||||
eng: "Tool Runtime",
|
||||
score: 80,
|
||||
trend: "↑ 火 +5",
|
||||
desc: "工具调用密集",
|
||||
bars: [46, 88, 72, 82, 66] as const,
|
||||
alert: false,
|
||||
},
|
||||
{
|
||||
name: "天工阁枢",
|
||||
eng: "Agent Society",
|
||||
score: 76,
|
||||
trend: "↑ 水 +2",
|
||||
desc: "协作分工顺畅",
|
||||
bars: [54, 76, 68, 64, 78] as const,
|
||||
alert: false,
|
||||
},
|
||||
] as const;
|
||||
|
||||
/* ── 相生依赖链 ── */
|
||||
const SHENG_FLOWS = [
|
||||
{
|
||||
pair: "木",
|
||||
target: "火",
|
||||
body: "内驱力激活 催生 对话表达 · 息壤枢学习加速推动灵核枢输出活跃。",
|
||||
val: "0.68",
|
||||
},
|
||||
{
|
||||
pair: "火",
|
||||
target: "土",
|
||||
body: "高频表达 沉淀 稳定状态 · 对话事件入库强化世界模型与人格基线。",
|
||||
val: "0.72",
|
||||
},
|
||||
{
|
||||
pair: "土",
|
||||
target: "金",
|
||||
body: "稳定数据 提供依据 给约束规则 · 世界实体一致,司命审核判据更可靠。",
|
||||
val: "0.65",
|
||||
},
|
||||
{
|
||||
pair: "金",
|
||||
target: "水",
|
||||
body: "明确规范 塑造 适应路径 · 约束边界清晰,异常恢复与上下文切换更可控。",
|
||||
val: "0.58",
|
||||
},
|
||||
{
|
||||
pair: "水",
|
||||
target: "木",
|
||||
body: "适应能力 反哺 成长 · 场景切换中积累新候选,演化候选生成率回温。",
|
||||
val: "0.61",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/* ── 相克告警 ── */
|
||||
const KE_ALERTS = [
|
||||
{
|
||||
pairA: "金",
|
||||
pairB: "木",
|
||||
desc: "司命审核驳回率 vs 大周天候选生成率 · 安全审查过严压制演化,阴阳平衡度下行。",
|
||||
level: "0.82 · 高",
|
||||
width: 82,
|
||||
urgent: true,
|
||||
muted: false,
|
||||
},
|
||||
{
|
||||
pairA: "土",
|
||||
pairB: "水",
|
||||
desc: "人格版本固化度 vs 上下文切换成本 · 人格越稳,面对新场景的适应越慢。",
|
||||
level: "0.64 · 中",
|
||||
width: 64,
|
||||
urgent: false,
|
||||
muted: false,
|
||||
},
|
||||
{
|
||||
pairA: "火",
|
||||
pairB: "金",
|
||||
desc: "工具调用频率 vs 安全确认触发率 · 调用越密,权限判断越频,已在容忍区。",
|
||||
level: "0.44 · 低",
|
||||
width: 44,
|
||||
urgent: false,
|
||||
muted: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
/* ── 五行 key 列表 ── */
|
||||
const ELEMENT_KEYS = ["wood", "fire", "earth", "metal", "water"] as const;
|
||||
const ELEMENT_SEALS: Record<string, string> = {
|
||||
wood: "木",
|
||||
fire: "火",
|
||||
earth: "土",
|
||||
metal: "金",
|
||||
water: "水",
|
||||
};
|
||||
|
||||
/* ── SVG 节点坐标(viewBox 400×400)── */
|
||||
const NODE_COORDS = {
|
||||
wood: { cx: 200, cy: 65, labelX: 200, labelY: 30 },
|
||||
fire: { cx: 328, cy: 158, labelX: 372, labelY: 132 },
|
||||
earth: { cx: 279, cy: 309, labelX: 311, labelY: 350 },
|
||||
metal: { cx: 121, cy: 309, labelX: 89, labelY: 350 },
|
||||
water: { cx: 72, cy: 158, labelX: 30, labelY: 132 },
|
||||
} as const;
|
||||
|
||||
/* 相生边(五边形):木→火, 火→土, 土→金, 金→水, 水→木 */
|
||||
const SHENG_EDGES: [string, string][] = [
|
||||
["wood", "fire"],
|
||||
["fire", "earth"],
|
||||
["earth", "metal"],
|
||||
["metal", "water"],
|
||||
["water", "wood"],
|
||||
];
|
||||
|
||||
/* 相克边(五角星):木→土, 土→水, 水→火, 火→金, 金→木 */
|
||||
const KE_EDGES: [string, string][] = [
|
||||
["wood", "earth"],
|
||||
["earth", "water"],
|
||||
["water", "fire"],
|
||||
["fire", "metal"],
|
||||
["metal", "wood"],
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* 组件
|
||||
* ============================================================ */
|
||||
|
||||
export default function WuxingPage() {
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
.wuxing-scope {
|
||||
${Object.entries(WUXING_VARS)
|
||||
.map(([k, v]) => `${k}: ${v};`)
|
||||
.join("\n ")}
|
||||
}
|
||||
@keyframes wuxing-pulse {
|
||||
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--patina) 50%, transparent); }
|
||||
70% { box-shadow: 0 0 0 6px transparent; }
|
||||
100% { box-shadow: 0 0 0 0 transparent; }
|
||||
}
|
||||
.pulse-dot {
|
||||
animation: wuxing-pulse 2.6s cubic-bezier(0.16, 1, 0.3, 1) infinite;
|
||||
}
|
||||
.wuxing-svg .ring-deco { fill: none; stroke: var(--rule); stroke-width: 1; }
|
||||
.wuxing-svg .sheng { stroke: var(--gilt); stroke-width: 1.6; fill: none; opacity: 0.85; }
|
||||
.wuxing-svg .ke { stroke: var(--cinnabar); stroke-width: 1.3; stroke-dasharray: 4 4; fill: none; opacity: 0.7; }
|
||||
.wuxing-svg .node-circle { stroke-width: 1.5; }
|
||||
.wuxing-svg .center-bg { fill: var(--ink); stroke: var(--gilt-dim); stroke-width: 1; }
|
||||
|
||||
.dim-track {
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: var(--rule);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.dim-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.pbar-track {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--rule);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.pbar-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.ke-track {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--rule);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.ke-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="wuxing-scope">
|
||||
<PageContainer
|
||||
title="五行"
|
||||
description="枢健康维度 · 生克关系"
|
||||
actions={
|
||||
<span
|
||||
className="inline-flex items-center gap-[7px] rounded-full border px-3 py-1 font-mono text-xs"
|
||||
style={{
|
||||
borderColor: "var(--patina-dim)",
|
||||
color: "var(--patina)",
|
||||
background: "var(--patina-wash)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-[6px] w-[6px] rounded-full"
|
||||
style={{ background: "var(--patina)" }}
|
||||
>
|
||||
<span className="pulse-dot block h-full w-full rounded-full" />
|
||||
</span>
|
||||
架构心跳 82 · 良
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{/* ══ 五行生克环 + 枢健康维度 ══ */}
|
||||
<div className="mb-5 flex flex-col gap-[22px] lg:flex-row lg:items-stretch">
|
||||
{/* ---- 生克环 ---- */}
|
||||
<div
|
||||
className="relative flex w-full flex-col overflow-hidden rounded-lg border p-[18px] lg:w-auto lg:flex-[0_0_auto] lg:basis-[460px]"
|
||||
style={{
|
||||
borderColor: "var(--gilt-dim)",
|
||||
background: "var(--face-sheen), var(--surface)",
|
||||
boxShadow:
|
||||
"inset 0 1px 0 var(--face-in-top), inset 0 -1px 0 var(--face-in-bot), var(--shadow)",
|
||||
}}
|
||||
>
|
||||
{/* 噪点纹理 */}
|
||||
<span
|
||||
className="pointer-events-none absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='7'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E\")",
|
||||
opacity: 0.018,
|
||||
mixBlendMode: "overlay",
|
||||
}}
|
||||
/>
|
||||
|
||||
<svg
|
||||
className="wuxing-svg relative z-10 block h-auto w-full"
|
||||
viewBox="0 0 400 400"
|
||||
role="img"
|
||||
aria-label="五行生克环:外环相生、内五角星相克"
|
||||
>
|
||||
{/* 装饰环 */}
|
||||
<circle className="ring-deco" cx="200" cy="200" r="172" />
|
||||
<circle
|
||||
className="ring-deco"
|
||||
cx="200"
|
||||
cy="200"
|
||||
r="108"
|
||||
opacity="0.5"
|
||||
strokeDasharray="2 4"
|
||||
/>
|
||||
|
||||
{/* 相克(虚线五角星,下层) */}
|
||||
{KE_EDGES.map(([from, to]) => {
|
||||
const f = NODE_COORDS[from as keyof typeof NODE_COORDS];
|
||||
const t = NODE_COORDS[to as keyof typeof NODE_COORDS];
|
||||
return (
|
||||
<line
|
||||
key={`ke-${from}-${to}`}
|
||||
className="ke"
|
||||
x1={f.cx}
|
||||
y1={f.cy}
|
||||
x2={t.cx}
|
||||
y2={t.cy}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 相生(实线五边形,上层) */}
|
||||
{SHENG_EDGES.map(([from, to]) => {
|
||||
const f = NODE_COORDS[from as keyof typeof NODE_COORDS];
|
||||
const t = NODE_COORDS[to as keyof typeof NODE_COORDS];
|
||||
return (
|
||||
<line
|
||||
key={`sheng-${from}-${to}`}
|
||||
className="sheng"
|
||||
x1={f.cx}
|
||||
y1={f.cy}
|
||||
x2={t.cx}
|
||||
y2={t.cy}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 五行节点 */}
|
||||
{ELEMENT_KEYS.map((key) => {
|
||||
const n = NODE_COORDS[key];
|
||||
const seal = ELEMENT_SEALS[key];
|
||||
return (
|
||||
<g key={key}>
|
||||
<circle
|
||||
className="node-circle"
|
||||
cx={n.cx}
|
||||
cy={n.cy}
|
||||
r="27"
|
||||
style={{
|
||||
fill: `var(--${key}-dim)`,
|
||||
stroke: `var(--${key})`,
|
||||
}}
|
||||
/>
|
||||
<text
|
||||
x={n.cx}
|
||||
y={n.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
style={{
|
||||
fontFamily: '"Songti SC", "STSong", serif',
|
||||
fontSize: "22px",
|
||||
fontWeight: 700,
|
||||
fill: `var(--${key})`,
|
||||
}}
|
||||
>
|
||||
{seal}
|
||||
</text>
|
||||
<text
|
||||
x={n.labelX}
|
||||
y={n.labelY}
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
||||
fontSize: "9px",
|
||||
fill: "var(--fg-faint)",
|
||||
letterSpacing: "0.04em",
|
||||
}}
|
||||
>
|
||||
{DIMS.find((d) => d.key === key)?.label ?? ""}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 中心:架构心跳 */}
|
||||
<circle className="center-bg" cx="200" cy="200" r="58" />
|
||||
<text
|
||||
x="200"
|
||||
y="186"
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontFamily: '"Songti SC", "STSong", serif',
|
||||
fontSize: "15px",
|
||||
fontWeight: 600,
|
||||
fill: "var(--gilt)",
|
||||
}}
|
||||
>
|
||||
架构心跳
|
||||
</text>
|
||||
<text
|
||||
x="200"
|
||||
y="212"
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontFamily:
|
||||
'"JetBrains Mono", "SF Mono", ui-monospace, monospace',
|
||||
fontSize: "20px",
|
||||
fontWeight: 300,
|
||||
fill: "var(--fg)",
|
||||
letterSpacing: "0.04em",
|
||||
}}
|
||||
>
|
||||
82
|
||||
</text>
|
||||
<text
|
||||
x="200"
|
||||
y="228"
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
||||
fontSize: "9.5px",
|
||||
fill: "var(--fg-dim)",
|
||||
}}
|
||||
>
|
||||
阴阳平衡 · 良
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
{/* 图例 */}
|
||||
<div
|
||||
className="relative z-10 mt-[10px] flex flex-wrap gap-4 border-t pt-[10px]"
|
||||
style={{ borderColor: "var(--rule)" }}
|
||||
>
|
||||
<span
|
||||
className="inline-flex items-center gap-[6px] text-[11.5px]"
|
||||
style={{ color: "var(--fg-dim)" }}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-[18px]"
|
||||
style={{ borderTop: "1.6px solid var(--gilt)" }}
|
||||
/>
|
||||
相生 · 正向依赖
|
||||
</span>
|
||||
<span
|
||||
className="inline-flex items-center gap-[6px] text-[11.5px]"
|
||||
style={{ color: "var(--fg-dim)" }}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-[18px]"
|
||||
style={{ borderTop: "1.3px dashed var(--cinnabar)" }}
|
||||
/>
|
||||
相克 · 负向制约
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- 维度卡 ---- */}
|
||||
<div
|
||||
className="flex min-w-0 flex-1 flex-col rounded-lg border p-[18px_20px]"
|
||||
style={{
|
||||
borderColor: "var(--rule)",
|
||||
background: "var(--surface)",
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
className="font-serif text-[14px] font-semibold"
|
||||
style={{ color: "var(--fg)", marginBottom: "14px" }}
|
||||
>
|
||||
枢健康维度
|
||||
</h3>
|
||||
<div className="flex flex-1 flex-col gap-[11px]">
|
||||
{DIMS.map((d) => (
|
||||
<div
|
||||
key={d.key}
|
||||
className="dim"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "22px 60px 1fr 30px",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-center font-serif text-[16px] font-bold"
|
||||
style={{ color: `var(--${d.key})` }}
|
||||
>
|
||||
{d.seal}
|
||||
</span>
|
||||
<span
|
||||
className="text-[12.5px] font-medium leading-tight"
|
||||
style={{ color: "var(--fg)" }}
|
||||
>
|
||||
{d.label}
|
||||
<small
|
||||
className="mt-[1px] block text-[10.5px] font-normal leading-tight"
|
||||
style={{ color: "var(--fg-faint)" }}
|
||||
>
|
||||
{d.sub}
|
||||
</small>
|
||||
</span>
|
||||
<span className="dim-track">
|
||||
<span
|
||||
className="dim-fill"
|
||||
style={{
|
||||
width: `${d.width}%`,
|
||||
background: `linear-gradient(90deg, var(--${d.key}-dim), var(--${d.key}))`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="text-right font-mono text-[10.5px]"
|
||||
style={{ color: `var(--${d.key})` }}
|
||||
>
|
||||
{d.val}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-[14px] rounded-r-[4px] border-l-2 px-[12px] py-[10px] text-[12px] leading-[1.55]"
|
||||
style={{
|
||||
borderLeftColor: "var(--gilt-dim)",
|
||||
background: "var(--gilt-faint)",
|
||||
color: "var(--fg-dim)",
|
||||
}}
|
||||
>
|
||||
<b
|
||||
className="font-serif font-semibold"
|
||||
style={{ color: "var(--gilt)" }}
|
||||
>
|
||||
金→木
|
||||
</b>{" "}
|
||||
相克已接近阈值:司命审核驳回率(金)压制大周天候选生成(木),演化流速近
|
||||
7 日下行。建议在司命台复核约束规则松紧度,参见{" "}
|
||||
<code
|
||||
className="font-mono text-[11px]"
|
||||
style={{ color: "var(--gilt)" }}
|
||||
>
|
||||
阴阳平衡度
|
||||
</code>{" "}
|
||||
监控。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ══ 九枢健康网格 ══ */}
|
||||
<div className="mb-5">
|
||||
<div
|
||||
className="mb-[14px] flex items-center gap-[6px] font-mono text-[12px]"
|
||||
style={{ color: "var(--fg-faint)", letterSpacing: "0.1em" }}
|
||||
>
|
||||
<span style={{ color: "var(--gilt)", opacity: 0.8 }}>⟡</span>
|
||||
九枢健康
|
||||
<span
|
||||
className="text-[12px] tracking-normal opacity-75"
|
||||
style={{ color: "var(--fg-dim)" }}
|
||||
>
|
||||
每枢五维 · 综合分 + 趋势
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{PIVOTS.map((p) => (
|
||||
<div
|
||||
key={p.name}
|
||||
className={cn(
|
||||
"flex flex-col gap-[9px] rounded-lg border p-[14px_16px]",
|
||||
p.alert && "bg-[rgba(184,57,46,0.05)]",
|
||||
)}
|
||||
style={{
|
||||
borderColor: p.alert
|
||||
? "var(--cinnabar-dim)"
|
||||
: "var(--rule)",
|
||||
background: p.alert ? undefined : "var(--surface)",
|
||||
}}
|
||||
>
|
||||
{/* 枢头部 */}
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span
|
||||
className="font-serif text-[15px] font-semibold"
|
||||
style={{ color: "var(--fg)" }}
|
||||
>
|
||||
{p.name}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-[10px] tracking-wide"
|
||||
style={{ color: "var(--fg-faint)" }}
|
||||
>
|
||||
{p.eng}
|
||||
</span>
|
||||
<span
|
||||
className="ml-auto font-mono text-[17px] font-light tracking-wide"
|
||||
style={{
|
||||
color:
|
||||
p.score < 75 ? "var(--cinnabar)" : "var(--patina)",
|
||||
}}
|
||||
>
|
||||
{p.score}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 五维微进度条 */}
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
{ELEMENT_KEYS.map((ek, i) => (
|
||||
<div
|
||||
key={ek}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "14px 1fr 24px",
|
||||
alignItems: "center",
|
||||
gap: "7px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-center font-serif text-[11px]"
|
||||
style={{ color: `var(--${ek})` }}
|
||||
>
|
||||
{ELEMENT_SEALS[ek]}
|
||||
</span>
|
||||
<span className="pbar-track">
|
||||
<span
|
||||
className="pbar-fill"
|
||||
style={{
|
||||
width: `${p.bars[i]}%`,
|
||||
background: `var(--${ek})`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-[9.5px] text-right"
|
||||
style={{ color: "var(--fg-faint)" }}
|
||||
>
|
||||
.{p.bars[i]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 底部趋势 */}
|
||||
<div
|
||||
className="flex items-center gap-2 pt-[4px] text-[11px]"
|
||||
style={{
|
||||
borderTop: "1px solid var(--rule)",
|
||||
color: "var(--fg-faint)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono"
|
||||
style={{
|
||||
color: p.trend.startsWith("↑")
|
||||
? "var(--patina)"
|
||||
: "var(--cinnabar)",
|
||||
}}
|
||||
>
|
||||
{p.trend}
|
||||
</span>
|
||||
{p.desc}
|
||||
{p.alert && p.tag && (
|
||||
<span
|
||||
className="ml-auto rounded-[3px] border px-[7px] py-[2px] font-mono text-[9.5px]"
|
||||
style={{
|
||||
borderColor: "var(--cinnabar-dim)",
|
||||
background: "rgba(184, 57, 46, 0.08)",
|
||||
color: "var(--cinnabar)",
|
||||
}}
|
||||
>
|
||||
{p.tag}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ══ 相生流 + 相克告警 ══ */}
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-start">
|
||||
{/* ---- 相生流 ---- */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3
|
||||
className="mb-[14px] font-serif text-[14px] font-semibold"
|
||||
style={{ color: "var(--fg)" }}
|
||||
>
|
||||
相生 · 正向依赖链
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{SHENG_FLOWS.map((f) => (
|
||||
<div
|
||||
key={`${f.pair}-${f.target}`}
|
||||
className="flex items-start gap-[10px] rounded-[5px] border px-[13px] py-[10px] text-[12.5px] leading-[1.5]"
|
||||
style={{
|
||||
borderColor: "var(--rule)",
|
||||
background: "var(--patina-wash)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 pt-[1px] font-serif text-[13px] whitespace-nowrap"
|
||||
style={{ color: "var(--gilt)" }}
|
||||
>
|
||||
{f.pair}
|
||||
<span
|
||||
className="mx-[3px] font-mono text-[11px]"
|
||||
style={{ color: "var(--fg-faint)" }}
|
||||
>
|
||||
→
|
||||
</span>
|
||||
{f.target}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0"
|
||||
style={{ color: "var(--fg-dim)" }}
|
||||
>
|
||||
<span style={{ color: "var(--fg)" }}>{f.body}</span>
|
||||
</span>
|
||||
<span
|
||||
className="shrink-0 pt-[1px] font-mono text-[10.5px] whitespace-nowrap"
|
||||
style={{ color: "var(--patina)" }}
|
||||
>
|
||||
{f.val}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- 相克告警 ---- */}
|
||||
<div className="w-full shrink-0 lg:w-[340px]">
|
||||
<div
|
||||
className="rounded-lg border p-[16px_18px]"
|
||||
style={{
|
||||
borderColor: "var(--rule)",
|
||||
background: "var(--surface)",
|
||||
}}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3
|
||||
className="m-0 font-serif text-[14px] font-semibold"
|
||||
style={{ color: "var(--fg)" }}
|
||||
>
|
||||
相克告警{" "}
|
||||
<span
|
||||
className="font-mono text-[11px] font-normal"
|
||||
style={{ color: "var(--fg-faint)" }}
|
||||
>
|
||||
克制失衡
|
||||
</span>
|
||||
</h3>
|
||||
<span
|
||||
className="rounded-[5px] border px-[10px] py-[3px] font-mono text-[11px]"
|
||||
style={{
|
||||
borderColor: "var(--cinnabar-dim)",
|
||||
background: "rgba(184, 57, 46, 0.08)",
|
||||
color: "var(--cinnabar)",
|
||||
}}
|
||||
>
|
||||
2 待关注
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{KE_ALERTS.map((a) => (
|
||||
<div
|
||||
key={`${a.pairA}-${a.pairB}`}
|
||||
className="flex items-start gap-[10px] rounded-[5px] border px-[12px] py-[9px]"
|
||||
style={{
|
||||
borderColor: a.muted
|
||||
? "var(--rule)"
|
||||
: "var(--cinnabar-dim)",
|
||||
background: a.muted ? "transparent" : "var(--ink)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="w-[46px] shrink-0 pt-[1px] text-center font-serif text-[13px] leading-tight"
|
||||
style={{
|
||||
color: a.muted
|
||||
? "var(--fg-faint)"
|
||||
: "var(--cinnabar)",
|
||||
}}
|
||||
>
|
||||
{a.pairA}
|
||||
<br />
|
||||
{a.pairB}
|
||||
</span>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="text-[12px] leading-[1.5]"
|
||||
style={{
|
||||
color: a.muted
|
||||
? "var(--fg-faint)"
|
||||
: "var(--fg-dim)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: a.muted ? "var(--fg-dim)" : "var(--fg)",
|
||||
}}
|
||||
>
|
||||
{a.desc}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center gap-[6px]">
|
||||
<span className="ke-track flex-1">
|
||||
<span
|
||||
className="ke-fill"
|
||||
style={{
|
||||
width: `${a.width}%`,
|
||||
background: a.muted
|
||||
? "linear-gradient(90deg, var(--gilt-dim), var(--gilt))"
|
||||
: "linear-gradient(90deg, var(--cinnabar-dim), var(--cinnabar))",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="shrink-0 font-mono text-[9.5px] whitespace-nowrap"
|
||||
style={{
|
||||
color: a.muted
|
||||
? "var(--gilt)"
|
||||
: "var(--cinnabar)",
|
||||
}}
|
||||
>
|
||||
{a.level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { VermilionAction } from "@/components/admin/VermilionAction";
|
||||
import { PolicyMatrix } from "@/components/admin/PolicyMatrix";
|
||||
import { AuditConsole } from "@/components/admin/AuditConsole";
|
||||
import { ToolRegistry } from "@/components/admin/ToolRegistry";
|
||||
import { PersonalityEditor } from "@/components/admin/PersonalityEditor";
|
||||
import { GoalTree } from "@/components/admin/GoalTree";
|
||||
import { PerceptionConsentMatrix } from "@/components/admin/PerceptionConsentMatrix";
|
||||
import { WorldEntityList } from "@/components/admin/WorldEntityList";
|
||||
@@ -36,7 +35,6 @@ import type {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const VALID_PIVOTS: Pivot[] = [
|
||||
"personality_core",
|
||||
"goal_orchestrator",
|
||||
"safety_governor",
|
||||
"perception",
|
||||
@@ -67,8 +65,6 @@ export default async function PivotDetailPage({
|
||||
return <SafetyGovernorDetail />;
|
||||
case "tool_runtime":
|
||||
return <ToolRuntimeDetail />;
|
||||
case "personality_core":
|
||||
return <PersonalityCoreDetail />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,31 +210,6 @@ async function ToolRuntimeDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 灵核枢:人格各字段 inline 可编辑(核心人格 L4 走提议审核) */
|
||||
async function PersonalityCoreDetail() {
|
||||
let persona: PersonalityVersion | null = null;
|
||||
let error: string | null = null;
|
||||
try {
|
||||
persona = await serverApi.get<PersonalityVersion>("/personality/current");
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="灵核枢"
|
||||
description="人格与身份 · 每个字段均可编辑"
|
||||
maxWidth="max-w-3xl"
|
||||
>
|
||||
{error || !persona ? (
|
||||
<ErrorState message={error ?? "人格信息暂不可用。"} />
|
||||
) : (
|
||||
<PersonalityEditor persona={persona} />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/** 太一枢:目标层级树(最高目标 → 任务,可编辑标题/优先级/状态) */
|
||||
async function GoalOrchestratorDetail() {
|
||||
let goals: GoalNode[] = [];
|
||||
|
||||
@@ -113,27 +113,11 @@ export default async function HomePage() {
|
||||
|
||||
{/* 底部功能导航 */}
|
||||
<div className="mt-10 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-xs text-muted/60">
|
||||
<Link href="/personality" className="transition-colors hover:text-fg">
|
||||
人格
|
||||
</Link>
|
||||
<Link href="/evolution" className="transition-colors hover:text-fg">
|
||||
演化
|
||||
</Link>
|
||||
{FEATURE_FLAGS.drivePanel && (
|
||||
<Link href="/drive" className="transition-colors hover:text-fg">
|
||||
内驱力
|
||||
</Link>
|
||||
)}
|
||||
{FEATURE_FLAGS.worldPanel && (
|
||||
<Link href="/world" className="transition-colors hover:text-fg">
|
||||
世界
|
||||
</Link>
|
||||
)}
|
||||
{FEATURE_FLAGS.timePanel && (
|
||||
<Link href="/time" className="transition-colors hover:text-fg">
|
||||
时态
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/archive" className="transition-colors hover:text-fg">
|
||||
归档
|
||||
</Link>
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { personalityApi } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import type {
|
||||
PersonalityProposal,
|
||||
PersonalityProposalFields,
|
||||
PersonalityVersion,
|
||||
} from "@/types/api";
|
||||
|
||||
type StringField =
|
||||
| "identity_profile"
|
||||
| "role_destiny"
|
||||
| "personality_seed"
|
||||
| "expression_style"
|
||||
| "relationship_pattern";
|
||||
type JsonField = "traits" | "values" | "emotion_baseline";
|
||||
|
||||
const STRING_FIELDS: Array<{ key: StringField; label: string }> = [
|
||||
{ key: "identity_profile", label: "身份" },
|
||||
{ key: "role_destiny", label: "命格" },
|
||||
{ key: "personality_seed", label: "人格种子" },
|
||||
{ key: "expression_style", label: "表达风格" },
|
||||
{ key: "relationship_pattern", label: "关系模式" },
|
||||
];
|
||||
|
||||
const JSON_FIELDS: Array<{ key: JsonField; label: string }> = [
|
||||
{ key: "traits", label: "特质" },
|
||||
{ key: "values", label: "价值偏好" },
|
||||
{ key: "emotion_baseline", label: "情绪基线" },
|
||||
];
|
||||
|
||||
/**
|
||||
* 灵核枢人格字段 —— 每个字段均可 inline 编辑。
|
||||
* 核心人格(L4)修改走提议审核(§11 propose-update):点字段「编辑」进入编辑态,
|
||||
* 可同时编辑多个字段;底部统一填写修改原因、提交建议。后端审核通过才写入新版本。
|
||||
* 字段严格对齐后端 PersonalityProposalFields:identity_profile / role_destiny /
|
||||
* personality_seed / expression_style / relationship_pattern(文本)与
|
||||
* traits / values / emotion_baseline(JSON)。draft 只存正在编辑的字段(文本/JSON 文本),
|
||||
* 提交时仅发送改动项;dict 字段提交前 JSON.parse 校验。提交成功经 onProposed 回调
|
||||
* 把建议交给父组件(PendingProposalCard)做即时审批。
|
||||
*/
|
||||
export function PersonalityEditor({
|
||||
persona,
|
||||
onProposed,
|
||||
}: {
|
||||
persona: PersonalityVersion;
|
||||
onProposed?: (proposal: PersonalityProposal) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<Record<string, string>>({});
|
||||
const [reason, setReason] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const dirtyFields = Object.keys(draft);
|
||||
const hasEdits = dirtyFields.length > 0;
|
||||
const jsonKeys = JSON_FIELDS.map((f) => f.key);
|
||||
|
||||
function displayValue(field: StringField | JsonField): string {
|
||||
if (field in draft) return draft[field];
|
||||
const raw = persona[field];
|
||||
if (raw == null) return "";
|
||||
return typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
|
||||
}
|
||||
|
||||
function startEdit(field: string) {
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
[field]: displayValue(field as StringField | JsonField),
|
||||
}));
|
||||
setError(null);
|
||||
}
|
||||
function cancelEdit(field: string) {
|
||||
setDraft((d) => {
|
||||
const next = { ...d };
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!hasEdits) {
|
||||
setError("请先编辑至少一个字段。");
|
||||
return;
|
||||
}
|
||||
if (!reason.trim()) {
|
||||
setError("请填写修改原因(核心人格变更需留痕)。");
|
||||
return;
|
||||
}
|
||||
const fields: Record<string, unknown> = {};
|
||||
for (const field of dirtyFields) {
|
||||
const text = draft[field];
|
||||
if (jsonKeys.includes(field as JsonField)) {
|
||||
try {
|
||||
fields[field] = text.trim() ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
setError(`「${field}」不是合法 JSON,请检查格式。`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
fields[field] = text;
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await personalityApi.proposeUpdate({
|
||||
fields: fields as PersonalityProposalFields,
|
||||
reason,
|
||||
});
|
||||
setDraft({});
|
||||
setReason("");
|
||||
onProposed?.(res);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "提交失败,请重试。");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderField(field: string, label: string, isJson: boolean) {
|
||||
const isEditing = field in draft;
|
||||
const value = displayValue(field as StringField | JsonField);
|
||||
return (
|
||||
<div key={field} className="px-5 py-3.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-serif text-xs font-medium text-muted">
|
||||
{label}
|
||||
{isJson && (
|
||||
<span className="ml-1 text-[10px] text-muted/70">JSON</span>
|
||||
)}
|
||||
</p>
|
||||
{!isEditing ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startEdit(field)}
|
||||
className="text-[11px] text-patina hover:underline"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => cancelEdit(field)}
|
||||
className="text-[11px] text-muted hover:underline"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, [field]: e.target.value }))
|
||||
}
|
||||
rows={isJson ? 4 : 3}
|
||||
className="mt-1 w-full rounded-lg border border-border-glass bg-glass px-2 py-1 font-mono text-xs text-fg"
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1 whitespace-pre-wrap text-sm leading-relaxed",
|
||||
value ? "text-fg" : "italic text-muted",
|
||||
)}
|
||||
>
|
||||
{value || "(未设置)"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="glass divide-y divide-border-glass rounded-2xl">
|
||||
{STRING_FIELDS.map(({ key, label }) => renderField(key, label, false))}
|
||||
{JSON_FIELDS.map(({ key, label }) => renderField(key, label, true))}
|
||||
</div>
|
||||
|
||||
{/* 统一提交区:仅当有改动字段时出现 */}
|
||||
{hasEdits && (
|
||||
<div className="glass rounded-2xl px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-serif text-sm font-medium text-fg">
|
||||
提交人格修改建议
|
||||
</p>
|
||||
<span className="rounded bg-cinnabar/15 px-1.5 py-0.5 text-[10px] text-cinnabar">
|
||||
L4 · 需审核
|
||||
</span>
|
||||
<span className="ml-auto text-[10px] text-muted">
|
||||
{dirtyFields.length} 项已改
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="修改原因(必填,核心人格变更留痕)…"
|
||||
className="mt-2 w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
|
||||
/>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={busy}
|
||||
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
|
||||
>
|
||||
提交建议
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDraft({});
|
||||
setReason("");
|
||||
}}
|
||||
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
|
||||
>
|
||||
放弃全部
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="px-1 text-xs text-cinnabar">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { HeartHandshakeIcon, ScrollTextIcon } from "@/components/ui/Icons";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import type { Covenant } from "@/lib/api/covenants";
|
||||
import { covenantApi } from "@/lib/api";
|
||||
|
||||
interface Props {
|
||||
characterId: string;
|
||||
initialCovenant: Covenant | null;
|
||||
}
|
||||
|
||||
const DIM_SUGGESTIONS = [
|
||||
"trust",
|
||||
"mentorship",
|
||||
"companionship",
|
||||
"creative_alliance",
|
||||
];
|
||||
|
||||
/**
|
||||
* 缘契面板:当前缘契(多维维度 + 边界)/ 无缘契时提议。
|
||||
* 撤回立即生效;事件历史不可抹除——由后端裁决,本面板只呈现与操作。
|
||||
*/
|
||||
export function CovenantPanel({ characterId, initialCovenant }: Props) {
|
||||
const [covenant, setCovenant] = useState(initialCovenant);
|
||||
const [dims, setDims] = useState<Record<string, string>>({
|
||||
trust: "forming",
|
||||
});
|
||||
const [title, setTitle] = useState("");
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [history, setHistory] = useState<
|
||||
{ id: string; type: string; actor: string; created_at: string | null }[]
|
||||
>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
async function handlePropose() {
|
||||
if (Object.keys(dims).length === 0) {
|
||||
setNotice("至少需要一个关系维度(多维并存,禁单一好感度)。");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const created = await covenantApi.proposeCovenant({
|
||||
character_id: characterId,
|
||||
dimensions: dims,
|
||||
primary_title: title || null,
|
||||
});
|
||||
setCovenant(created);
|
||||
setNotice("缘契已提议,经问心与铭契后生效。");
|
||||
} catch (e) {
|
||||
setNotice(e instanceof Error ? e.message : "提议失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSeal() {
|
||||
if (!covenant) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const sealed = await covenantApi.sealCovenant(covenant.id, {
|
||||
dimensions: dims,
|
||||
});
|
||||
setCovenant(sealed);
|
||||
setNotice("已铭契生效。");
|
||||
} catch (e) {
|
||||
setNotice(e instanceof Error ? e.message : "铭契失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWithdraw() {
|
||||
if (!covenant) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await covenantApi.withdrawCovenant(covenant.id, "面板操作撤回");
|
||||
setCovenant(null);
|
||||
setNotice("已撤回(立即生效)。");
|
||||
} catch (e) {
|
||||
setNotice(e instanceof Error ? e.message : "撤回失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
if (!covenant) return;
|
||||
const events = await covenantApi.listCovenantEvents(covenant.id);
|
||||
setHistory(events);
|
||||
setShowHistory(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="set-card">
|
||||
<div className="set-card-head">
|
||||
<h3 className="set-card-title flex items-center gap-2">
|
||||
<HeartHandshakeIcon className="size-4 text-gold" />
|
||||
缘契
|
||||
</h3>
|
||||
<span className="set-card-eng">Covenant</span>
|
||||
</div>
|
||||
<p className="set-card-desc">
|
||||
多维关系与共同确认的边界;撤回立即生效,事件历史不可抹除。
|
||||
</p>
|
||||
|
||||
{notice && <div className="mb-3 text-sm text-patina">{notice}</div>}
|
||||
|
||||
{covenant ? (
|
||||
<div>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
{covenant.primary_title && (
|
||||
<span className="font-serif text-sm text-fg">
|
||||
{covenant.primary_title}
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded-full border border-rule px-2 py-0.5 font-mono text-[10px] text-muted">
|
||||
{covenant.status === "sealed"
|
||||
? `已铭契 v${covenant.version}`
|
||||
: "提议中"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{Object.entries(covenant.dimensions).map(([k, v]) => (
|
||||
<span
|
||||
key={k}
|
||||
className="rounded-full border border-patina/40 px-2 py-0.5 font-mono text-[10px] text-patina"
|
||||
>
|
||||
{k} · {v}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{covenant.status === "pending" && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSeal}
|
||||
disabled={busy}
|
||||
>
|
||||
铭契生效
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleWithdraw}
|
||||
disabled={busy}
|
||||
>
|
||||
撤回
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={loadHistory}>
|
||||
<ScrollTextIcon className="mr-1 size-3.5" />
|
||||
事件史
|
||||
</Button>
|
||||
</div>
|
||||
{showHistory && (
|
||||
<ul className="mt-3 divide-y divide-rule rounded-xl border border-rule">
|
||||
{history.map((e) => (
|
||||
<li
|
||||
key={e.id}
|
||||
className="flex items-center justify-between px-3 py-2 text-xs"
|
||||
>
|
||||
<span className="font-mono text-fg">{e.type}</span>
|
||||
<span className="text-muted">
|
||||
{e.actor} · {e.created_at?.slice(0, 19).replace("T", " ")}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DIM_SUGGESTIONS.map((d) => {
|
||||
const active = d in dims;
|
||||
return (
|
||||
<button
|
||||
key={d}
|
||||
type="button"
|
||||
className={`rounded-full border px-2 py-0.5 font-mono text-[10px] transition-colors ${
|
||||
active
|
||||
? "border-patina/60 bg-patina-wash text-patina"
|
||||
: "border-rule text-muted hover:border-patina/40"
|
||||
}`}
|
||||
onClick={() => {
|
||||
const next = { ...dims };
|
||||
if (active) delete next[d];
|
||||
else next[d] = "forming";
|
||||
setDims(next);
|
||||
}}
|
||||
>
|
||||
{d}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<input
|
||||
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
|
||||
placeholder="主称谓(可选,如:亦师亦友)"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handlePropose}
|
||||
disabled={busy}
|
||||
>
|
||||
提议缘契
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { CuriosityQuestion } from "@/types/api";
|
||||
|
||||
const SOURCE_LABEL: Record<string, string> = {
|
||||
world_uncertainty: "世界不确定",
|
||||
memory_gap: "记忆缺口",
|
||||
perception_change: "感知变化",
|
||||
goal_tension: "目标张力",
|
||||
reflection: "反思",
|
||||
time_rhythm: "时间节律",
|
||||
};
|
||||
|
||||
const SOURCE_CLASS: Record<string, string> = {
|
||||
world_uncertainty: "src-world",
|
||||
memory_gap: "src-memory",
|
||||
perception_change: "src-percept",
|
||||
goal_tension: "src-goal",
|
||||
reflection: "src-reflect",
|
||||
time_rhythm: "src-time",
|
||||
};
|
||||
|
||||
/** 好奇问题(缺口记录,只读展示,无审核)设计稿 curio 布局 */
|
||||
export function CuriosityItem({ curiosity }: { curiosity: CuriosityQuestion }) {
|
||||
const isHi = curiosity.expected_value >= 0.7;
|
||||
const riskClass =
|
||||
curiosity.urgency >= 0.7
|
||||
? "risk-pill L4"
|
||||
: curiosity.urgency >= 0.4
|
||||
? "risk-pill L2"
|
||||
: "risk-pill";
|
||||
const sourceCls = SOURCE_CLASS[curiosity.source] ?? "";
|
||||
|
||||
return (
|
||||
<div className="curio">
|
||||
<span
|
||||
className={`curio-fire ${isHi ? "hi" : ""}`}
|
||||
title={`预期信息价值 ${curiosity.expected_value.toFixed(2)}`}
|
||||
/>
|
||||
<div className="curio-body">
|
||||
<div className="curio-q">{curiosity.question}</div>
|
||||
<div className="curio-meta">
|
||||
<span className={`src-tag ${sourceCls}`}>
|
||||
<span className="sdot"></span>
|
||||
{SOURCE_LABEL[curiosity.source] ?? curiosity.source}
|
||||
</span>
|
||||
<span className={riskClass}>
|
||||
L
|
||||
{curiosity.urgency >= 0.7
|
||||
? "4"
|
||||
: curiosity.urgency >= 0.4
|
||||
? "2"
|
||||
: "1"}
|
||||
</span>
|
||||
{curiosity.related_entities.length > 0 && (
|
||||
<span className="ent-link">
|
||||
@{curiosity.related_entities.slice(0, 2).join(" · @")}
|
||||
{curiosity.related_entities.length > 2 &&
|
||||
" · +" + (curiosity.related_entities.length - 2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="curio-bars">
|
||||
<div className="cbar value">
|
||||
<div className="ck">信息价值</div>
|
||||
<div className="ctrack">
|
||||
<div
|
||||
className="cfill"
|
||||
style={{
|
||||
width: `${Math.round(Math.max(0, Math.min(1, curiosity.expected_value)) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cbar urg">
|
||||
<div className="ck">紧迫</div>
|
||||
<div className="ctrack">
|
||||
<div
|
||||
className="cfill"
|
||||
style={{
|
||||
width: `${Math.round(Math.max(0, Math.min(1, curiosity.urgency)) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { EmptyState, ErrorState } from "@/components/shared/StateViews";
|
||||
import { driveApi } from "@/lib/api";
|
||||
import type { CuriosityQuestion, ExploratorySubgoal } from "@/types/api";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import { CardGridSkeleton } from "@/components/ui/Skeleton";
|
||||
|
||||
import { CuriosityItem } from "./CuriosityItem";
|
||||
import { SubgoalCard } from "./SubgoalCard";
|
||||
|
||||
type FilterStatus = "candidate" | "approved" | "deferred" | "rejected";
|
||||
|
||||
const FILTERS: Array<{ key: FilterStatus; label: string }> = [
|
||||
{ key: "candidate", label: "待审核" },
|
||||
{ key: "approved", label: "已通过" },
|
||||
{ key: "deferred", label: "已延后" },
|
||||
{ key: "rejected", label: "已驳回" },
|
||||
];
|
||||
|
||||
/**
|
||||
* 内驱力视图:好奇问题(只读)+ 探索性次生目标(审核)。
|
||||
* 次生目标 approve/defer/reject 仅标记,本期不自动执行(v1 边界)。
|
||||
* 设计稿 — 好奇萤火 2 列网格 + 探索天平 + 阴阳制衡(置于 page.tsx)。
|
||||
*/
|
||||
export function DriveView({
|
||||
characterId,
|
||||
curiosities,
|
||||
subgoals: initial,
|
||||
counts: initialCounts,
|
||||
}: {
|
||||
characterId: string;
|
||||
curiosities: CuriosityQuestion[];
|
||||
subgoals: ExploratorySubgoal[];
|
||||
counts: Record<FilterStatus, number>;
|
||||
}) {
|
||||
const [filter, setFilter] = useState<FilterStatus>("candidate");
|
||||
const [subgoals, setSubgoals] = useState(initial);
|
||||
const [counts, setCounts] = useState(initialCounts);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function switchFilter(next: FilterStatus) {
|
||||
if (next === filter) return;
|
||||
setFilter(next);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await driveApi.listSubgoals(characterId, {
|
||||
status: next,
|
||||
page_size: 50,
|
||||
});
|
||||
setSubgoals(res.items);
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function action(id: string, act: "approve" | "defer" | "reject") {
|
||||
setBusyId(id);
|
||||
setError(null);
|
||||
try {
|
||||
if (act === "approve") await driveApi.approveSubgoal(id);
|
||||
else if (act === "defer") await driveApi.deferSubgoal(id);
|
||||
else await driveApi.rejectSubgoal(id);
|
||||
setSubgoals((list) => list.filter((s) => s.id !== id));
|
||||
setCounts((c) => {
|
||||
const next = { ...c, candidate: Math.max(0, c.candidate - 1) };
|
||||
if (act === "approve") next.approved += 1;
|
||||
else if (act === "defer") next.deferred += 1;
|
||||
else next.rejected += 1;
|
||||
return next;
|
||||
});
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 好奇萤火 */}
|
||||
{curiosities.length > 0 && (
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 好奇{" "}
|
||||
<span className="sub">由内而发的匮乏感 - 缺口记录,只读</span>
|
||||
</div>
|
||||
<div className="curio-grid">
|
||||
{curiosities.map((c) => (
|
||||
<CuriosityItem key={c.id} curiosity={c} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 探索性次生目标(待探 / 天平) */}
|
||||
<section>
|
||||
<div className="section-label" style={{ marginBottom: 14 }}>
|
||||
<span className="glyph">⟡</span> 待探{" "}
|
||||
<span className="sub">好奇转化的次生目标 · 增益与打扰的权衡</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1" style={{ marginBottom: 14 }}>
|
||||
{FILTERS.map(({ key, label }) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => switchFilter(key)}
|
||||
className={cn(
|
||||
"rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||
filter === key
|
||||
? "glass-strong text-patina"
|
||||
: "text-muted hover:text-fg",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span className="ml-1.5 tabular-nums text-muted">
|
||||
{counts[key]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <ErrorState message={error} />}
|
||||
|
||||
{loading ? (
|
||||
<CardGridSkeleton count={2} className="py-6" />
|
||||
) : subgoals.length === 0 ? (
|
||||
<EmptyState
|
||||
title={filter === "candidate" ? "暂无待审核的探索目标" : "暂无目标"}
|
||||
description={
|
||||
filter === "candidate"
|
||||
? "大周天发现世界缺口时会生成探索性次生目标到这里,供你审核。"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{subgoals.map((s) => (
|
||||
<SubgoalCard
|
||||
key={s.id}
|
||||
subgoal={s}
|
||||
onAction={action}
|
||||
busy={busyId === s.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ExploratorySubgoal } from "@/types/api";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
candidate: "待审核",
|
||||
approved: "已通过",
|
||||
deferred: "已延后",
|
||||
rejected: "已驳回",
|
||||
};
|
||||
|
||||
/** 探索天平:左盘增益 右盘成本 中间支点(risk) */
|
||||
function BalanceScale({
|
||||
gain,
|
||||
cost,
|
||||
risk,
|
||||
}: {
|
||||
gain: number;
|
||||
cost: number;
|
||||
risk: string;
|
||||
}) {
|
||||
const gainPct = Math.round(Math.max(0, Math.min(1, gain)) * 100);
|
||||
const costPct = Math.round(Math.max(0, Math.min(1, cost)) * 100);
|
||||
return (
|
||||
<div className="balance">
|
||||
<div className="pan gain">
|
||||
<span className="plabel">预期增益</span>
|
||||
<div className="ptrack">
|
||||
<div className="pfill" style={{ width: `${gainPct}%` }} />
|
||||
</div>
|
||||
<span className="pval">{gain.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="fulcrum">
|
||||
<div className="beam"></div>
|
||||
<div className="pivot"></div>
|
||||
<span className="risk">{risk}</span>
|
||||
</div>
|
||||
<div className="pan cost right">
|
||||
<span className="plabel">打扰成本</span>
|
||||
<div className="ptrack">
|
||||
<div className="pfill" style={{ width: `${costPct}%` }} />
|
||||
</div>
|
||||
<span className="pval">{cost.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubgoalCard({
|
||||
subgoal,
|
||||
onAction,
|
||||
busy,
|
||||
}: {
|
||||
subgoal: ExploratorySubgoal;
|
||||
onAction: (id: string, action: "approve" | "defer" | "reject") => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
const isCandidate = subgoal.status === "candidate";
|
||||
return (
|
||||
<div className={cn("sub-card", !isCandidate && "deferred")}>
|
||||
<div className="sub-top">
|
||||
<span className="sub-from">
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.4"
|
||||
width={11}
|
||||
height={11}
|
||||
>
|
||||
<path d="M8 2v8M5 7l3 3 3-3M3 13h10" />
|
||||
</svg>
|
||||
好奇 → 探索
|
||||
</span>
|
||||
<span className="sub-title">{subgoal.title}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"sub-status",
|
||||
isCandidate && "candidate",
|
||||
subgoal.status === "approved" && "approved",
|
||||
subgoal.status === "deferred" && "deferred",
|
||||
)}
|
||||
>
|
||||
{STATUS_LABEL[subgoal.status] ?? subgoal.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="sub-desc">{subgoal.description}</div>
|
||||
|
||||
<BalanceScale
|
||||
gain={subgoal.expected_information_gain}
|
||||
cost={subgoal.interruption_cost}
|
||||
risk={subgoal.risk_level}
|
||||
/>
|
||||
|
||||
{isCandidate && (
|
||||
<div className="sub-actions">
|
||||
<span className="vbtn note">approve 仅标记 · 本期不自动执行</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAction(subgoal.id, "defer")}
|
||||
disabled={busy}
|
||||
className="vbtn"
|
||||
>
|
||||
延后
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAction(subgoal.id, "reject")}
|
||||
disabled={busy}
|
||||
className="vbtn"
|
||||
>
|
||||
驳回
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAction(subgoal.id, "approve")}
|
||||
disabled={busy}
|
||||
className="vbtn patina"
|
||||
>
|
||||
{busy ? "处理中…" : "准予探索"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,15 +10,10 @@ import {
|
||||
MemoryIcon,
|
||||
ChatIcon,
|
||||
CharacterIcon,
|
||||
CosmosIcon,
|
||||
DriveIcon,
|
||||
ModelIcon,
|
||||
PersonalityIcon,
|
||||
RunIcon,
|
||||
SettingsIcon,
|
||||
ToolsIcon,
|
||||
WorldIcon,
|
||||
WuxingIcon,
|
||||
} from "@/components/ui/Icons";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { FEATURE_FLAGS } from "@/lib/config";
|
||||
@@ -43,38 +38,25 @@ interface NavGroup {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主导航分组:灵台(对话/轨迹)、法身(人物/人格/演化)、
|
||||
* 内景(内驱力/世界/时态)、藏识(记忆/归档)、百工(工具/模型/设置)。
|
||||
* 每组不超过 4 项,符合 product-ui §1 导航 IA ≤8 项的建议;
|
||||
* 分组后侧栏视觉有序,降低认知负荷。
|
||||
* 主导航分组:灵台(镜树)、法身(灵胚/人物)、内景(世界)、
|
||||
* 藏识(记忆/归档)、百工(工具/神识阁/设置)。
|
||||
* 旧人格/内驱力/五行/灵枢/轨迹页随 M6 退役(人格=道体锚点+缘契,轨迹并入镜树)。
|
||||
*/
|
||||
const NAV_GROUPS: NavGroup[] = [
|
||||
{
|
||||
title: "灵台",
|
||||
items: [
|
||||
{ href: "/trees", label: "镜树", icon: <ChatIcon /> },
|
||||
{ href: "/runs", label: "轨迹", icon: <RunIcon /> },
|
||||
],
|
||||
items: [{ href: "/trees", label: "镜树", icon: <ChatIcon /> }],
|
||||
},
|
||||
{
|
||||
title: "法身",
|
||||
items: [
|
||||
{ href: "/embryos", label: "灵胚", icon: <CharacterIcon /> },
|
||||
{ href: "/cosmos", label: "灵枢", icon: <CosmosIcon /> },
|
||||
{ href: "/wuxing", label: "五行", icon: <WuxingIcon /> },
|
||||
{ href: "/characters", label: "人物", icon: <CharacterIcon /> },
|
||||
{ href: "/personality", label: "人格", icon: <PersonalityIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "内景",
|
||||
items: [
|
||||
{
|
||||
href: "/drive",
|
||||
label: "内驱力",
|
||||
icon: <DriveIcon />,
|
||||
enabled: FEATURE_FLAGS.drivePanel,
|
||||
},
|
||||
{
|
||||
href: "/world",
|
||||
label: "世界",
|
||||
@@ -86,7 +68,7 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
{
|
||||
title: "藏识",
|
||||
items: [
|
||||
{ href: "/memory", label: "记忆", icon: <MemoryIcon /> },
|
||||
{ href: "/memories", label: "记忆", icon: <MemoryIcon /> },
|
||||
{ href: "/archive", label: "归档", icon: <ArchiveIcon /> },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { personalityApi } from "@/lib/api";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { PersonalityProposal } from "@/types/api";
|
||||
|
||||
/**
|
||||
* 待审提案即时卡片:因后端暂无 GET /personality/proposals 列表端点,
|
||||
* 仅展示刚提交(由 PersonalityEditor onProposed 传入)的建议,支持即时 approve/reject。
|
||||
* 完整审批墙待后端补端点后实现。审核后 router.refresh()。
|
||||
*/
|
||||
export function PendingProposalCard({
|
||||
proposal,
|
||||
onResolved,
|
||||
}: {
|
||||
proposal: PersonalityProposal;
|
||||
onResolved?: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const fieldKeys = Object.keys(proposal.fields ?? {});
|
||||
|
||||
async function resolve(approve: boolean) {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (approve) await personalityApi.approveProposal(proposal.proposal_id);
|
||||
else await personalityApi.rejectProposal(proposal.proposal_id);
|
||||
onResolved?.();
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass rounded-2xl px-5 py-4 ring-1 ring-cinnabar/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-serif text-sm font-semibold text-fg">待审人格建议</p>
|
||||
<span className="rounded bg-cinnabar/15 px-1.5 py-0.5 text-[10px] text-cinnabar">
|
||||
pending
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm text-fg/90">
|
||||
{proposal.reason || "(未填写原因)"}
|
||||
</p>
|
||||
{fieldKeys.length > 0 && (
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
涉及字段:{fieldKeys.join("、")}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading={busy}
|
||||
onClick={() => resolve(true)}
|
||||
>
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
loading={busy}
|
||||
onClick={() => resolve(false)}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</div>
|
||||
{error && <p className="mt-2 text-xs text-cinnabar">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { BronzeMirror } from "@/components/ui/BronzeMirror";
|
||||
import { EmptyState } from "@/components/shared/StateViews";
|
||||
import { formatDateTime } from "@/lib/utils/format";
|
||||
import type { PersonalityVersion } from "@/types/api";
|
||||
|
||||
const FIELDS = [
|
||||
{ key: "identity_profile", label: "魂册身份" },
|
||||
{ key: "role_destiny", label: "命格" },
|
||||
{ key: "personality_seed", label: "人格种子" },
|
||||
{ key: "expression_style", label: "表达风格" },
|
||||
{ key: "mood_baseline", label: "情绪基线" },
|
||||
{ key: "relationship_pattern", label: "关系模式" },
|
||||
] as const;
|
||||
|
||||
/** 当前人格只读摘要:顶部九枢铜镜作为人格主视觉 */
|
||||
export function PersonalitySummary({
|
||||
personality,
|
||||
}: {
|
||||
personality: PersonalityVersion | null;
|
||||
}) {
|
||||
if (!personality) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="尚未生成人格"
|
||||
description="当前人物尚未初始化人格版本,待后端写入后此处将展示只读摘要。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const hasAnyField = FIELDS.some((f) => personality[f.key]);
|
||||
|
||||
return (
|
||||
<Card className="p-5">
|
||||
<div className="mb-5 flex flex-col items-center gap-3">
|
||||
<BronzeMirror size={88} state="steady" />
|
||||
<div className="text-center">
|
||||
<h2 className="font-serif text-xl font-semibold">当前人格</h2>
|
||||
<p className="mt-0.5 text-xs text-muted">
|
||||
版本 v{personality.version}
|
||||
</p>
|
||||
</div>
|
||||
<Badge tone="accent">只读</Badge>
|
||||
</div>
|
||||
|
||||
{personality.summary && (
|
||||
<p className="mb-4 whitespace-pre-wrap text-sm text-fg/90">
|
||||
{personality.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hasAnyField ? (
|
||||
<dl className="grid grid-cols-1 gap-x-6 gap-y-3 text-sm sm:grid-cols-2">
|
||||
{FIELDS.map(({ key, label }) => {
|
||||
const value = personality[key];
|
||||
return (
|
||||
<div key={key}>
|
||||
<dt className="text-xs text-muted">{label}</dt>
|
||||
<dd className="mt-0.5 whitespace-pre-wrap text-fg">
|
||||
{value || "—"}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
) : (
|
||||
<p className="text-sm text-muted">当前人格版本暂无可展示的明细字段。</p>
|
||||
)}
|
||||
|
||||
{personality.created_at && (
|
||||
<p className="mt-4 border-t border-border-glass pt-2 text-xs text-muted">
|
||||
生成时间:{formatDateTime(personality.created_at)}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { personalityApi } from "@/lib/api";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import { formatDateTime } from "@/lib/utils/format";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import type { PersonalityVersion } from "@/types/api";
|
||||
|
||||
/**
|
||||
* 人格版本时间线:按版本号倒序展示历史,当前版本高亮;
|
||||
* 非当前版本可「回滚」(二次确认)。回滚后 router.refresh() 以服务端数据为准。
|
||||
*/
|
||||
export function PersonalityVersionTimeline({
|
||||
versions,
|
||||
currentId,
|
||||
}: {
|
||||
versions: PersonalityVersion[];
|
||||
currentId?: string | null;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirm, setConfirm] = useState<PersonalityVersion | null>(null);
|
||||
|
||||
const ordered = [...versions].sort((a, b) => b.version - a.version);
|
||||
|
||||
async function rollback() {
|
||||
if (!confirm) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await personalityApi.rollbackPersonality(confirm.id);
|
||||
setConfirm(null);
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{ordered.map((v) => {
|
||||
const isCurrent = v.id === currentId;
|
||||
return (
|
||||
<div
|
||||
key={v.id}
|
||||
className={cn(
|
||||
"glass rounded-2xl px-5 py-4",
|
||||
isCurrent && "ring-2 ring-patina/40",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-serif text-sm font-semibold text-fg">
|
||||
v{v.version}
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<span className="rounded bg-patina/15 px-1.5 py-0.5 text-[10px] text-patina">
|
||||
当前
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-muted">
|
||||
{v.created_by ? `${v.created_by} · ` : ""}
|
||||
{formatDateTime(v.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{v.reason && (
|
||||
<p className="mt-1.5 text-sm text-fg/90">{v.reason}</p>
|
||||
)}
|
||||
{v.identity_profile && (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted">
|
||||
{v.identity_profile}
|
||||
</p>
|
||||
)}
|
||||
{!isCurrent && (
|
||||
<div className="mt-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => setConfirm(v)}>
|
||||
回滚到此版本
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Modal
|
||||
open={confirm !== null}
|
||||
onClose={() => setConfirm(null)}
|
||||
title={`回滚到 v${confirm?.version ?? ""}?`}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" size="sm" onClick={() => setConfirm(null)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
loading={busy}
|
||||
onClick={rollback}
|
||||
>
|
||||
确认回滚
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
回滚后当前人格指针将切换至此版本,被取代的版本会标记为已回滚。
|
||||
</Modal>
|
||||
|
||||
{error && <p className="text-xs text-cinnabar">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { PersonalityEditor } from "@/components/admin/PersonalityEditor";
|
||||
import { PendingProposalCard } from "@/components/personality/PendingProposalCard";
|
||||
import { PersonalityVersionTimeline } from "@/components/personality/PersonalityVersionTimeline";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import type { PersonalityProposal, PersonalityVersion } from "@/types/api";
|
||||
|
||||
interface MirrorItem {
|
||||
glyph: string;
|
||||
name: string;
|
||||
src: string;
|
||||
tier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 十镜映射(对齐 personality-core §7):根据后端字段值判断落地/预留。
|
||||
*/
|
||||
const MIRRORS: MirrorItem[] = [
|
||||
{ glyph: "本", name: "本我镜", src: "identity_profile", tier: "L4" },
|
||||
{ glyph: "命", name: "策命镜", src: "role_destiny", tier: "L4" },
|
||||
{ glyph: "心", name: "明心镜", src: "values", tier: "L4" },
|
||||
{ glyph: "波", name: "情波镜", src: "emotion_baseline", tier: "L2" },
|
||||
{ glyph: "缘", name: "缘生镜", src: "relationship_pattern", tier: "L1" },
|
||||
{ glyph: "作", name: "匠作镜", src: "expression_style", tier: "L1" },
|
||||
{ glyph: "识", name: "慧识镜", src: "reasoning_style", tier: "L2" },
|
||||
{ glyph: "世", name: "观世镜", src: "world_focus", tier: "L2" },
|
||||
{ glyph: "律", name: "戒律镜", src: "safety_constraints", tier: "L4" },
|
||||
{ glyph: "照", name: "返照镜", src: "reflection_goals", tier: "L3" },
|
||||
];
|
||||
|
||||
/** traits 可读标签 */
|
||||
const TRAIT_LABELS: Record<string, string> = {
|
||||
openness: "开放",
|
||||
conscientiousness: "尽责",
|
||||
extraversion: "外向",
|
||||
agreeableness: "宜人",
|
||||
neuroticism: "情绪",
|
||||
};
|
||||
|
||||
/**
|
||||
* 人格工作区:十镜网格 + 种子带 + 明德条 + 编辑器 + 版本时间线。
|
||||
* 设计稿 personality-preview.html 对齐 personality-core §7/§8。
|
||||
*/
|
||||
export function PersonalityWorkspace({
|
||||
personality,
|
||||
versions,
|
||||
}: {
|
||||
personality: PersonalityVersion;
|
||||
versions: PersonalityVersion[];
|
||||
}) {
|
||||
const [pending, setPending] = useState<PersonalityProposal | null>(null);
|
||||
|
||||
// 十镜数据:给每个镜子注入实际字段值
|
||||
const mirrors = useMemo(
|
||||
() =>
|
||||
MIRRORS.map((m) => ({
|
||||
...m,
|
||||
val: personality[m.src as keyof PersonalityVersion] as
|
||||
| string
|
||||
| undefined
|
||||
| null,
|
||||
})),
|
||||
[personality],
|
||||
);
|
||||
|
||||
// 提取 traits 权重条
|
||||
const traits = personality.traits as
|
||||
| Record<string, number>
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* ═══ 种子带(设计稿 seed-band)═══ */}
|
||||
<div className="seed-band">
|
||||
<div className="seed-mirror">
|
||||
<span className="glyph">种</span>
|
||||
</div>
|
||||
<div className="seed-body">
|
||||
<div className="seed-lab">人格种子 · personality_seed</div>
|
||||
<div className="seed-text">
|
||||
{personality.personality_seed || "未设定"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tier-legend">
|
||||
<span>
|
||||
<b className="l1">L1</b> 表达 / 关系
|
||||
</span>
|
||||
<span>
|
||||
<b className="l2">L2</b> 特质 / 情绪
|
||||
</span>
|
||||
<span>
|
||||
<b className="l4">L4</b> 身份 / 命格 / 价值
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══ 十镜网格(设计稿 mirror-grid)═══ */}
|
||||
<section>
|
||||
<div className="section-label">
|
||||
<span className="glyph">⟡</span> 十镜{" "}
|
||||
<span className="sub">人格面板的 10 视角切片</span>
|
||||
</div>
|
||||
<div className="mirror-grid">
|
||||
{mirrors.map((m) => {
|
||||
const landed = !!m.val;
|
||||
return (
|
||||
<div key={m.src} className={cn("mcell", !landed && "reserved")}>
|
||||
<div className="mc-head">
|
||||
<div className="mc-glyph">{m.glyph}</div>
|
||||
<div className="mc-titles">
|
||||
<div className="mc-name">{m.name}</div>
|
||||
<div className="mc-src">{m.src}</div>
|
||||
</div>
|
||||
<span className={cn("mc-status", landed && "landed")}>
|
||||
{landed ? m.tier : "预留"}
|
||||
</span>
|
||||
</div>
|
||||
{landed ? (
|
||||
<div className="mc-val">
|
||||
<span className="em">
|
||||
{typeof m.val === "string" && m.val.length > 80
|
||||
? m.val.slice(0, 80) + "…"
|
||||
: typeof m.val === "object" && m.val !== null
|
||||
? JSON.stringify(m.val).slice(0, 80)
|
||||
: String(m.val ?? "")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mc-val">待完整十镜落地</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ═══ 人格权重(traits 条)═══ */}
|
||||
{traits && Object.keys(traits).length > 0 && (
|
||||
<section>
|
||||
<h3 className="section-heading">人格权重</h3>
|
||||
<div
|
||||
className="mirror-grid"
|
||||
style={{ gridTemplateColumns: "repeat(4, 1fr)", gap: 14 }}
|
||||
>
|
||||
{Object.entries(traits).map(([key, val]) => {
|
||||
const label = TRAIT_LABELS[key] ?? key;
|
||||
const pct = Math.min(
|
||||
100,
|
||||
Math.max(0, Math.round((val as number) * 100)),
|
||||
);
|
||||
const grade = pct >= 70 ? "高" : pct >= 40 ? "中" : "低";
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="mcell landed"
|
||||
style={{ minHeight: "auto" }}
|
||||
>
|
||||
<div className="mc-bars">
|
||||
<div className="br">
|
||||
<span className="tn">{label}</span>
|
||||
<span className="tbar">
|
||||
<span className="fl" style={{ width: `${pct}%` }} />
|
||||
</span>
|
||||
<span className="tv">{grade}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ═══ 明德恒定层(预留提示,设计稿 mingde-strip)═══ */}
|
||||
<div className="mingde-strip">
|
||||
<span className="seal">明德三镜</span>
|
||||
<span className="desc">
|
||||
道体恒定层(<code>spiritual_core</code>:见地 / 明心 /
|
||||
守正)是世界观、价值观、道德观的结构化内核。 属 L4
|
||||
人工确认,尚未落地独立字段。
|
||||
</span>
|
||||
<span className="tag">预留 · 待落地</span>
|
||||
</div>
|
||||
|
||||
{/* ═══ 下方双栏:编辑器 + 版本层积 ═══ */}
|
||||
<div className="lower-region">
|
||||
{/* 左主栏:编辑器 + 待审卡片 */}
|
||||
<div className="lower-main">
|
||||
<PersonalityEditor
|
||||
persona={personality}
|
||||
onProposed={(p) => setPending(p)}
|
||||
/>
|
||||
{pending && pending.status === "pending" && (
|
||||
<PendingProposalCard
|
||||
proposal={pending}
|
||||
onResolved={() => setPending(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右栏:版本层积(设计稿 strata-rail) */}
|
||||
{versions.length > 0 && (
|
||||
<div className="strata-rail">
|
||||
<div className="strata-card">
|
||||
<h3 className="section-heading" style={{ marginBottom: 12 }}>
|
||||
版本层积
|
||||
</h3>
|
||||
<PersonalityVersionTimeline
|
||||
versions={versions}
|
||||
currentId={personality.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { EventCard } from "./EventCard";
|
||||
import { PlanTraceCard } from "./PlanTraceCard";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { RunStatusBadge } from "@/components/ui/StatusBadge";
|
||||
import { EmptyState } from "@/components/shared/StateViews";
|
||||
import { runsApi } from "@/lib/api";
|
||||
import { useRunStream } from "@/lib/hooks/useRunStream";
|
||||
import { formatDateTime } from "@/lib/utils/format";
|
||||
import type { Run, RunEvent } from "@/types/api";
|
||||
import type { RunStatus } from "@/types/domain";
|
||||
|
||||
/**
|
||||
* 运行轨迹视图:承接 Server Component 预取的 run + 事件,
|
||||
* 并通过 SSE 实时追加新事件、同步 run 状态。
|
||||
*/
|
||||
export function RunTraceView({
|
||||
runId,
|
||||
initialRun,
|
||||
initialEvents,
|
||||
}: {
|
||||
runId: string;
|
||||
initialRun: Run | null;
|
||||
initialEvents: RunEvent[];
|
||||
}) {
|
||||
const [run, setRun] = useState<Run | null>(initialRun);
|
||||
const [events, setEvents] = useState<RunEvent[]>(initialEvents);
|
||||
const seenSequences = useRef<Set<number>>(
|
||||
new Set(initialEvents.map((e) => e.sequence)),
|
||||
);
|
||||
|
||||
useRunStream(runId, {
|
||||
onEvent: (event) => {
|
||||
if (event.type === "heartbeat") return;
|
||||
if (seenSequences.current.has(event.sequence)) return;
|
||||
seenSequences.current.add(event.sequence);
|
||||
setEvents((prev) =>
|
||||
[...prev, event].sort((a, b) => a.sequence - b.sequence),
|
||||
);
|
||||
|
||||
const nextStatus = statusFromEvent(event.type);
|
||||
if (nextStatus) {
|
||||
setRun((prev) => (prev ? { ...prev, status: nextStatus } : prev));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const canCancel =
|
||||
run?.status === "queued" ||
|
||||
run?.status === "running" ||
|
||||
run?.status === "waiting_confirmation";
|
||||
|
||||
async function handleCancel() {
|
||||
setCancelling(true);
|
||||
try {
|
||||
const res = await runsApi.cancelRun(runId);
|
||||
// 乐观更新为后端返回状态;SSE run.cancelled 事件到达后会再次对齐
|
||||
setRun((prev) =>
|
||||
prev ? { ...prev, status: res.status as RunStatus } : prev,
|
||||
);
|
||||
} catch {
|
||||
/* 取消失败不阻断:SSE 事件流会反馈最终状态 */
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h2 className="font-mono text-sm text-fg/80">{runId}</h2>
|
||||
{run && (
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
{[
|
||||
run.goal ? `目标:${run.goal}` : null,
|
||||
run.model_hint ? `模型:${run.model_hint}` : null,
|
||||
run.character_id ? `人物:${run.character_id}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RunStatusBadge status={run?.status ?? "queued"} />
|
||||
{canCancel && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleCancel}
|
||||
loading={cancelling}
|
||||
>
|
||||
取消运行
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{run && (
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 font-mono text-xs text-muted">
|
||||
<span>开始:{formatDateTime(run.started_at)}</span>
|
||||
<span>结束:{formatDateTime(run.completed_at)}</span>
|
||||
{durationLabel(run) && <span>耗时:{durationLabel(run)}</span>}
|
||||
{run.usage?.total_tokens != null && (
|
||||
<span>
|
||||
token:{run.usage.total_tokens}
|
||||
{run.usage.prompt_tokens != null &&
|
||||
`(入 ${run.usage.prompt_tokens} / 出 ${run.usage.completion_tokens ?? "?"})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{run?.error_message && (
|
||||
<p className="mt-2 rounded-md bg-cinnabar/10 px-2 py-1 text-xs text-cinnabar backdrop-blur-sm">
|
||||
{run.error_message}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<PlanTraceCard events={events} />
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="mb-3 font-serif text-base font-semibold">事件轨迹</h3>
|
||||
{events.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无事件"
|
||||
description="Run 启动后事件将按 sequence 顺序实时追加。"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{events.map((event) => (
|
||||
<EventCard key={event.event_id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusFromEvent(type: string): RunStatus | null {
|
||||
switch (type) {
|
||||
case "run.started":
|
||||
return "running";
|
||||
case "run.completed":
|
||||
return "completed";
|
||||
case "run.failed":
|
||||
return "failed";
|
||||
case "run.cancelled":
|
||||
return "cancelled";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算运行耗时(started → completed),不足 1s 显示 ms */
|
||||
function durationLabel(run: Run): string | null {
|
||||
if (!run.started_at || !run.completed_at) return null;
|
||||
const ms =
|
||||
new Date(run.completed_at).getTime() - new Date(run.started_at).getTime();
|
||||
if (!Number.isFinite(ms) || ms < 0) return null;
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const s = Math.round(ms / 1000);
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
return `${m}m ${s % 60}s`;
|
||||
}
|
||||
@@ -976,3 +976,75 @@ export function ShieldCheckIcon(props: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 境界(Lucide Globe) */
|
||||
export function GlobeIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path
|
||||
d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
/>
|
||||
<path d="M2 12h20" stroke="currentColor" strokeWidth="1.6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 地点(Lucide MapPin) */
|
||||
export function MapPinIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20 10c0 4.99-5.54 10.19-7.4 11.79a1 1 0 0 1-1.2 0C9.54 20.19 4 14.99 4 10a8 8 0 0 1 16 0"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="10" r="3" stroke="currentColor" strokeWidth="1.6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 缘契(Lucide HeartHandshake) */
|
||||
export function HeartHandshakeIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 5 9.04 7.96a2.17 2.17 0 0 0 0 3.08c.82.82 2.13.85 3 .07l2.07-1.9a2.5 2.5 0 0 1 3.44 0l.49.46"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { WorldEntity, WorldRelation } from "@/types/api";
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/** 简单力导向布局:将实体分布在同心圆上 */
|
||||
function computeLayout(
|
||||
entities: WorldEntity[],
|
||||
_relations: WorldRelation[],
|
||||
width: number,
|
||||
height: number,
|
||||
): Node[] {
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) * 0.35;
|
||||
|
||||
return entities.map((e, i) => {
|
||||
const angle = (2 * Math.PI * i) / entities.length - Math.PI / 2;
|
||||
return {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
type: e.type,
|
||||
x: centerX + radius * Math.cos(angle),
|
||||
y: centerY + radius * Math.sin(angle),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 节点颜色映射(与设计稿 ent-disc 色系一致) */
|
||||
function nodeColor(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
person: "var(--gilt)",
|
||||
place: "var(--patina)",
|
||||
object: "var(--cinnabar)",
|
||||
concept: "var(--patina-bright)",
|
||||
device: "var(--fg-dim)",
|
||||
project: "var(--cinnabar-dim)",
|
||||
task: "var(--patina-bright)",
|
||||
};
|
||||
return map[type] ?? "var(--muted)";
|
||||
}
|
||||
|
||||
/** 节点字形映射 */
|
||||
function nodeGlyph(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
person: "人",
|
||||
self: "我",
|
||||
place: "境",
|
||||
object: "器",
|
||||
concept: "念",
|
||||
device: "器",
|
||||
project: "业",
|
||||
task: "事",
|
||||
};
|
||||
return map[type] ?? "·";
|
||||
}
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
self: "self · 自身",
|
||||
person: "person · 人物",
|
||||
project: "project · 事务",
|
||||
concept: "concept · 概念",
|
||||
device: "device · 设备",
|
||||
place: "place · 地点",
|
||||
object: "object · 物品",
|
||||
task: "task · 任务",
|
||||
};
|
||||
|
||||
const STATE_LABEL: Record<string, string> = {
|
||||
fact: "事实",
|
||||
observation: "观察",
|
||||
inference: "推断",
|
||||
hypothesis: "假设",
|
||||
stale: "过期",
|
||||
};
|
||||
|
||||
const STATE_COLOR: Record<string, string> = {
|
||||
fact: "var(--patina)",
|
||||
observation: "var(--patina-bright)",
|
||||
inference: "var(--gilt)",
|
||||
hypothesis: "var(--cinnabar)",
|
||||
stale: "var(--fg-faint)",
|
||||
};
|
||||
|
||||
/**
|
||||
* 河图关系图谱(设计稿 world-preview.html §graph-region)。
|
||||
* 带图例 + 状态分布侧栏;节点为字形 + type 色,关系为边(predicate 标签)。
|
||||
*/
|
||||
export function WorldRelationGraph({
|
||||
entities,
|
||||
relations,
|
||||
}: {
|
||||
entities: WorldEntity[];
|
||||
relations: WorldRelation[];
|
||||
}) {
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
|
||||
|
||||
const W = 480;
|
||||
const H = 320;
|
||||
|
||||
const nodes = useMemo(
|
||||
() => computeLayout(entities, relations, W, H),
|
||||
[entities, relations],
|
||||
);
|
||||
const nodeMap = useMemo(
|
||||
() => Object.fromEntries(nodes.map((n) => [n.id, n])),
|
||||
[nodes],
|
||||
);
|
||||
const connectedIds = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
relations.forEach((r) => {
|
||||
set.add(r.subject_id);
|
||||
set.add(r.object_id);
|
||||
});
|
||||
return set;
|
||||
}, [relations]);
|
||||
|
||||
/* 状态分布 */
|
||||
const stateDist = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const e of entities) {
|
||||
counts[e.state_type] = (counts[e.state_type] || 0) + 1;
|
||||
}
|
||||
const total = entities.length;
|
||||
const order = ["fact", "observation", "inference", "hypothesis", "stale"];
|
||||
return order
|
||||
.filter((k) => counts[k])
|
||||
.map((k) => ({
|
||||
key: k,
|
||||
label: STATE_LABEL[k] ?? k,
|
||||
count: counts[k],
|
||||
pct: total > 0 ? Math.round((counts[k] / total) * 100) : 0,
|
||||
color: STATE_COLOR[k] ?? "var(--fg-faint)",
|
||||
}));
|
||||
}, [entities]);
|
||||
|
||||
if (entities.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-xl border border-dashed border-border-glass py-12 text-sm text-muted">
|
||||
暂无实体,图谱为空
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (relations.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-xl border border-dashed border-border-glass py-12 text-sm text-muted">
|
||||
暂无关系,新增关系后图谱自动生成
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* 唯一样式列表 */
|
||||
const types = [...new Set(entities.map((e) => e.type))];
|
||||
|
||||
return (
|
||||
<div className="graph-region">
|
||||
<div className="graph-card">
|
||||
<svg
|
||||
viewBox={`0 0 ${W} ${H}`}
|
||||
className="graph-svg"
|
||||
role="img"
|
||||
aria-label="河图关系图谱"
|
||||
>
|
||||
{/* 边 */}
|
||||
{relations.map((r) => {
|
||||
const src = nodeMap[r.subject_id];
|
||||
const dst = nodeMap[r.object_id];
|
||||
if (!src || !dst) return null;
|
||||
const highlight =
|
||||
hoveredNode === r.subject_id || hoveredNode === r.object_id;
|
||||
const hi = r.confidence >= 0.8;
|
||||
const mid = r.confidence >= 0.5 && r.confidence < 0.8;
|
||||
const low = r.confidence < 0.5;
|
||||
return (
|
||||
<g key={r.id}>
|
||||
<line
|
||||
x1={src.x}
|
||||
y1={src.y}
|
||||
x2={dst.x}
|
||||
y2={dst.y}
|
||||
stroke={
|
||||
highlight
|
||||
? "var(--gilt)"
|
||||
: low
|
||||
? "var(--cinnabar-dim)"
|
||||
: "var(--patina-dim)"
|
||||
}
|
||||
strokeWidth={highlight ? 2.5 : Math.max(1, r.confidence * 3)}
|
||||
strokeDasharray={low ? "5 4" : undefined}
|
||||
className={
|
||||
low
|
||||
? "edge tent"
|
||||
: hi
|
||||
? "edge hi"
|
||||
: mid
|
||||
? "edge mid"
|
||||
: "edge"
|
||||
}
|
||||
/>
|
||||
{/* 边的中点标签 */}
|
||||
<g>
|
||||
<rect
|
||||
x={(src.x + dst.x) / 2 - r.predicate.length * 3.5}
|
||||
y={(src.y + dst.y) / 2 - 10}
|
||||
width={r.predicate.length * 7 + 4}
|
||||
height={14}
|
||||
rx={3}
|
||||
fill="var(--ink)"
|
||||
stroke="var(--rule)"
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<text
|
||||
x={(src.x + dst.x) / 2}
|
||||
y={(src.y + dst.y) / 2 - 3}
|
||||
textAnchor="middle"
|
||||
fontSize="9"
|
||||
fill="var(--fg-faint)"
|
||||
fontFamily='"JetBrains Mono", ui-monospace, monospace'
|
||||
>
|
||||
{r.predicate}
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 节点 */}
|
||||
{nodes.map((node) => {
|
||||
const hasEdges = connectedIds.has(node.id);
|
||||
const highlight = hoveredNode === node.id;
|
||||
const color = nodeColor(node.type);
|
||||
const opacity = hasEdges ? 1 : 0.55;
|
||||
return (
|
||||
<g
|
||||
key={node.id}
|
||||
onMouseEnter={() => setHoveredNode(node.id)}
|
||||
onMouseLeave={() => setHoveredNode(null)}
|
||||
style={{ cursor: "pointer" }}
|
||||
opacity={opacity}
|
||||
>
|
||||
<circle
|
||||
cx={node.x}
|
||||
cy={node.y}
|
||||
r={highlight ? 26 : 24}
|
||||
fill="var(--ink)"
|
||||
stroke={color}
|
||||
strokeWidth={highlight ? 2 : 1.5}
|
||||
/>
|
||||
<circle
|
||||
cx={node.x}
|
||||
cy={node.y}
|
||||
r={highlight ? 26 : 24}
|
||||
fill={color}
|
||||
fillOpacity={0.16}
|
||||
stroke="none"
|
||||
/>
|
||||
<text
|
||||
x={node.x}
|
||||
y={node.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize="16"
|
||||
fontWeight={700}
|
||||
fontFamily='"Songti SC", serif'
|
||||
fill={color}
|
||||
>
|
||||
{nodeGlyph(node.type)}
|
||||
</text>
|
||||
<text
|
||||
x={node.x}
|
||||
y={node.y + 20}
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="var(--fg-dim)"
|
||||
fontFamily="inherit"
|
||||
>
|
||||
{node.name.slice(0, 8)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* 图谱侧栏:图例 + 状态分布 */}
|
||||
<div className="graph-side">
|
||||
<div className="side-card">
|
||||
<div className="side-title">图例</div>
|
||||
{types.map((t) => (
|
||||
<div key={t} className="legend-row">
|
||||
<span
|
||||
className="legend-dot"
|
||||
style={{
|
||||
borderColor: nodeColor(t),
|
||||
background: `color-mix(in srgb, ${nodeColor(t)} 16%, transparent)`,
|
||||
}}
|
||||
/>
|
||||
{TYPE_LABEL[t] ?? t}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<div className="legend-row">
|
||||
<span className="legend-line"></span>
|
||||
高置信关系
|
||||
</div>
|
||||
<div className="legend-row">
|
||||
<span className="legend-line tent"></span>
|
||||
假设态 · 待验
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="side-card">
|
||||
<div className="side-title">状态分布</div>
|
||||
{stateDist.map((d) => (
|
||||
<div key={d.key} className="dist-bar">
|
||||
<span className="dk">{d.label}</span>
|
||||
<div className="dtrack">
|
||||
<div
|
||||
className="dfill"
|
||||
style={{
|
||||
width: `${d.pct}%`,
|
||||
background: d.color,
|
||||
opacity:
|
||||
d.key === "hypothesis"
|
||||
? 0.8
|
||||
: d.key === "stale"
|
||||
? 1
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="dv">{d.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card, CardHeader } from "@/components/ui/Card";
|
||||
import { worldApi } from "@/lib/api";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { WorldEntity, WorldRelation } from "@/types/api";
|
||||
|
||||
/** 关系列表项 — 带删除 */
|
||||
function RelationRow({
|
||||
relation,
|
||||
entityName,
|
||||
onDelete,
|
||||
busy,
|
||||
}: {
|
||||
relation: WorldRelation;
|
||||
entityName: (id: string) => string;
|
||||
onDelete: (id: string) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
return (
|
||||
<li className="flex flex-wrap items-center gap-2 rounded-lg bg-glass/40 px-3 py-2 text-sm">
|
||||
<span className="font-medium text-fg">
|
||||
{entityName(relation.subject_id)}
|
||||
</span>
|
||||
<span className="rounded bg-gold/15 px-1.5 py-0.5 text-xs text-gold">
|
||||
{relation.predicate}
|
||||
</span>
|
||||
<span className="font-medium text-fg">
|
||||
{entityName(relation.object_id)}
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-muted">
|
||||
{relation.confidence.toFixed(2)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(relation.id)}
|
||||
disabled={busy}
|
||||
className="ml-auto text-[11px] text-cinnabar hover:underline disabled:opacity-50"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 世界关系管理(河图枢,独立 /world/relations)。
|
||||
* 列出 subject-predicate-object 三元组;新建时从实体列表选主体/客体 + 填谓词。
|
||||
* 创建后 router.refresh()。实体本身的 CRUD 由 WorldEntityList(admin 域)承担。
|
||||
*/
|
||||
export function WorldRelationManager({
|
||||
relations,
|
||||
entities,
|
||||
}: {
|
||||
relations: WorldRelation[];
|
||||
entities: WorldEntity[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [subjectId, setSubjectId] = useState("");
|
||||
const [predicate, setPredicate] = useState("");
|
||||
const [objectId, setObjectId] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [relationList, setRelationList] = useState(relations);
|
||||
|
||||
const entityName = (id: string) =>
|
||||
entities.find((e) => e.id === id)?.name ?? id;
|
||||
|
||||
async function add() {
|
||||
if (!subjectId || !predicate.trim() || !objectId) {
|
||||
setError("请选择主体与客体,并填写关系谓词。");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await worldApi.createWorldRelation({
|
||||
subject_id: subjectId,
|
||||
predicate: predicate.trim(),
|
||||
object_id: objectId,
|
||||
});
|
||||
setShowAdd(false);
|
||||
setSubjectId("");
|
||||
setPredicate("");
|
||||
setObjectId("");
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await worldApi.deleteWorldRelation(id);
|
||||
setRelationList((p) => p.filter((r) => r.id !== id));
|
||||
} catch (e) {
|
||||
setError(describeError(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const inputCls =
|
||||
"rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg";
|
||||
|
||||
return (
|
||||
<Card className="p-5">
|
||||
<CardHeader
|
||||
title="世界关系"
|
||||
subtitle="实体间的三元组(主体—谓词—客体),独立 /world/relations 域。"
|
||||
action={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowAdd((s) => !s)}
|
||||
>
|
||||
{showAdd ? "收起" : "+ 新增关系"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{showAdd && (
|
||||
<div className="mb-3 rounded-xl border border-border-glass bg-glass/50 p-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
value={subjectId}
|
||||
onChange={(e) => setSubjectId(e.target.value)}
|
||||
className={inputCls}
|
||||
aria-label="关系主体"
|
||||
>
|
||||
<option value="">主体…</option>
|
||||
{entities.map((e) => (
|
||||
<option key={e.id} value={e.id}>
|
||||
{e.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
value={predicate}
|
||||
onChange={(e) => setPredicate(e.target.value)}
|
||||
placeholder="谓词(如 栖居于)"
|
||||
className={`${inputCls} min-w-[8rem] flex-1`}
|
||||
/>
|
||||
<select
|
||||
value={objectId}
|
||||
onChange={(e) => setObjectId(e.target.value)}
|
||||
className={inputCls}
|
||||
aria-label="关系客体"
|
||||
>
|
||||
<option value="">客体…</option>
|
||||
{entities.map((e) => (
|
||||
<option key={e.id} value={e.id}>
|
||||
{e.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button variant="primary" size="sm" loading={busy} onClick={add}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{relationList.length === 0 ? (
|
||||
<p className="text-sm text-muted">暂无关系。新增以连接世界实体。</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{relationList.map((r) => (
|
||||
<RelationRow
|
||||
key={r.id}
|
||||
relation={r}
|
||||
entityName={entityName}
|
||||
onDelete={remove}
|
||||
busy={busy}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{error && <p className="mt-2 text-xs text-cinnabar">{error}</p>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
GlobeIcon,
|
||||
MapPinIcon,
|
||||
PlusIcon,
|
||||
ScrollTextIcon,
|
||||
} from "@/components/ui/Icons";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import type { PersistentWorld, WorldEntity } from "@/lib/api/worlds";
|
||||
import { worldApi } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
|
||||
interface Props {
|
||||
initialWorlds: PersistentWorld[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const KIND_LABEL: Record<string, string> = {
|
||||
character: "人物",
|
||||
location: "地点",
|
||||
organization: "组织",
|
||||
item: "物品",
|
||||
concept: "概念",
|
||||
};
|
||||
|
||||
/**
|
||||
* 世界工作台:境界目录 + 详情(实体/关系/正史概览)。
|
||||
* 境界正史(canonizations scope=persistent_world)只读呈现——境界不进真界本相。
|
||||
*/
|
||||
export function WorldWorkbench({ initialWorlds, error }: Props) {
|
||||
const [worlds, setWorlds] = useState(initialWorlds);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(
|
||||
initialWorlds[0]?.id ?? null,
|
||||
);
|
||||
const [entities, setEntities] = useState<WorldEntity[]>([]);
|
||||
const [chronicleCount, setChronicleCount] = useState(0);
|
||||
const [showCreate, setShowCreate] = useState(initialWorlds.length === 0);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const selected = worlds.find((w) => w.id === selectedId) ?? null;
|
||||
|
||||
async function selectWorld(id: string) {
|
||||
setSelectedId(id);
|
||||
setEntities([]);
|
||||
setChronicleCount(0);
|
||||
try {
|
||||
const [ents, chronicle] = await Promise.all([
|
||||
worldApi.listWorldEntities(id),
|
||||
worldApi.worldChronicle(id),
|
||||
]);
|
||||
setEntities(ents);
|
||||
setChronicleCount(chronicle.entries.length);
|
||||
} catch {
|
||||
/* 详情失败保持空态 */
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!name.trim()) {
|
||||
setNotice("境界名不能为空。");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const created = await worldApi.createWorld({
|
||||
name: name.trim(),
|
||||
description,
|
||||
});
|
||||
setWorlds(await worldApi.listWorlds());
|
||||
setShowCreate(false);
|
||||
setName("");
|
||||
setDescription("");
|
||||
setSelectedId(created.id);
|
||||
await selectWorld(created.id);
|
||||
setNotice(`境界「${created.name}」已建立`);
|
||||
} catch (e) {
|
||||
setNotice(e instanceof Error ? e.message : "建立失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="glass rounded-2xl p-6 text-sm text-muted">{error}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{notice && (
|
||||
<div className="glass rounded-xl px-4 py-2 text-sm text-patina">
|
||||
{notice}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="set-card">
|
||||
<div className="set-card-head">
|
||||
<h3 className="set-card-title flex items-center gap-2">
|
||||
<GlobeIcon className="size-4 text-patina" />
|
||||
境界
|
||||
</h3>
|
||||
<span className="set-card-eng">Persistent Worlds</span>
|
||||
</div>
|
||||
<p className="set-card-desc">
|
||||
版本化世界设定与叙事正史;境界内容不进入真界本相(跨界引用只是引文)。
|
||||
</p>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs text-muted">共 {worlds.length} 个境界</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowCreate((v) => !v)}
|
||||
>
|
||||
<PlusIcon className="mr-1 size-4" />
|
||||
{showCreate ? "收起" : "建立境界"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<div className="mb-4 grid gap-3 rounded-xl border border-rule bg-surface p-4 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-xs text-muted">境界名</span>
|
||||
<input
|
||||
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="如:云荒"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-xs text-muted">描述</span>
|
||||
<input
|
||||
className="w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="世界背景一句话"
|
||||
/>
|
||||
</label>
|
||||
<div className="sm:col-span-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleCreate}
|
||||
disabled={busy}
|
||||
>
|
||||
建立
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{worlds.length === 0 && !showCreate ? (
|
||||
<p className="rounded-xl border border-dashed border-rule p-4 text-center text-sm text-muted">
|
||||
尚未建立境界。
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-rule rounded-xl border border-rule">
|
||||
{worlds.map((w) => (
|
||||
<li key={w.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-hi",
|
||||
selectedId === w.id && "bg-surface",
|
||||
)}
|
||||
onClick={() => selectWorld(w.id)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-serif text-sm text-fg">
|
||||
{w.name}
|
||||
</span>
|
||||
{w.lorebook_id && (
|
||||
<span className="rounded-full border border-rule px-2 py-0.5 font-mono text-[10px] text-muted">
|
||||
世谱已接
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 line-clamp-1 text-xs text-muted">
|
||||
{w.description || "(无描述)"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-xs text-muted">
|
||||
正史 {w.canon_count} 条
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="set-card">
|
||||
<div className="set-card-head">
|
||||
<h3 className="set-card-title flex items-center gap-2">
|
||||
<MapPinIcon className="size-4 text-gold" />
|
||||
{selected.name} · 实体
|
||||
</h3>
|
||||
<span className="set-card-eng">Entities</span>
|
||||
</div>
|
||||
<p className="set-card-desc">
|
||||
河图世界认知(人物/地点/组织/物品);实体归属境界作用域。
|
||||
</p>
|
||||
{entities.length === 0 ? (
|
||||
<p className="rounded-xl border border-dashed border-rule p-4 text-center text-sm text-muted">
|
||||
该境界暂无实体(实体经归真与认知管线渐进形成,或由 API 登记)。
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-rule rounded-xl border border-rule">
|
||||
{entities.map((e) => (
|
||||
<li key={e.id} className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full border border-rule px-2 py-0.5 font-mono text-[10px] text-muted">
|
||||
{KIND_LABEL[e.kind] ?? e.kind}
|
||||
</span>
|
||||
<span className="font-serif text-sm text-fg">{e.name}</span>
|
||||
</div>
|
||||
{e.description && (
|
||||
<p className="mt-0.5 line-clamp-2 text-xs text-muted">
|
||||
{e.description}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-muted">
|
||||
<ScrollTextIcon className="size-3.5" />
|
||||
境界正史 {chronicleCount || selected.canon_count}{" "}
|
||||
条(只读;不进真界本相)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 缘契 API Client:提议—问心—铭契、撤回(立即生效)、边界与事件历史。
|
||||
*
|
||||
* 多维关系并存,禁单一好感度(后端拒绝 affinity 形态)。
|
||||
*/
|
||||
|
||||
import { apiGet, apiPatch, apiPost } from "./client";
|
||||
|
||||
/** 缘契(当前生效版本) */
|
||||
export interface Covenant {
|
||||
id: string;
|
||||
character_id: string;
|
||||
dimensions: Record<string, string>;
|
||||
primary_title: string | null;
|
||||
boundaries: Record<string, unknown>;
|
||||
adult_consent: boolean;
|
||||
status: "pending" | "sealed" | "withdrawn" | string;
|
||||
version: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
/** 缘契事件(不可变历史) */
|
||||
export interface CovenantEvent {
|
||||
id: string;
|
||||
type: "propose" | "wenxin" | "seal" | "withdraw" | "boundary" | string;
|
||||
actor: "user" | "character" | string;
|
||||
payload: Record<string, unknown>;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
/** 提议缘契 */
|
||||
export function proposeCovenant(body: {
|
||||
character_id: string;
|
||||
dimensions: Record<string, string>;
|
||||
primary_title?: string | null;
|
||||
boundaries?: Record<string, unknown>;
|
||||
}) {
|
||||
return apiPost<Covenant>("/covenants", body);
|
||||
}
|
||||
|
||||
/** 缘契清单(不含已撤回) */
|
||||
export function listCovenants() {
|
||||
return apiGet<Covenant[]>("/covenants");
|
||||
}
|
||||
|
||||
/** 人物当前生效缘契 */
|
||||
export function getActiveCovenant(characterId: string) {
|
||||
return apiGet<Covenant | null>(
|
||||
`/covenants/by-character/${encodeURIComponent(characterId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 问心:人物自陈候选 */
|
||||
export function wenxinCovenant(
|
||||
covenantId: string,
|
||||
body: { candidates: Record<string, string>; note?: string | null },
|
||||
) {
|
||||
return apiPost<{ covenant_id: string; recorded: boolean }>(
|
||||
`/covenants/${encodeURIComponent(covenantId)}/wenxin`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
/** 铭契:确认最终维度并生效 */
|
||||
export function sealCovenant(
|
||||
covenantId: string,
|
||||
body: {
|
||||
dimensions: Record<string, string>;
|
||||
boundaries?: Record<string, unknown>;
|
||||
},
|
||||
) {
|
||||
return apiPost<Covenant>(
|
||||
`/covenants/${encodeURIComponent(covenantId)}/seal`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
/** 撤回(立即生效) */
|
||||
export function withdrawCovenant(covenantId: string, reason?: string | null) {
|
||||
return apiPost<{ covenant_id: string; status: string; effective: string }>(
|
||||
`/covenants/${encodeURIComponent(covenantId)}/withdraw`,
|
||||
{ reason: reason ?? null },
|
||||
);
|
||||
}
|
||||
|
||||
/** 边界变更(立即生效) */
|
||||
export function updateBoundaries(
|
||||
covenantId: string,
|
||||
boundaries: Record<string, unknown>,
|
||||
) {
|
||||
return apiPatch<Covenant>(
|
||||
`/covenants/${encodeURIComponent(covenantId)}/boundaries`,
|
||||
{ boundaries },
|
||||
);
|
||||
}
|
||||
|
||||
/** 事件历史(撤回后仍可查证) */
|
||||
export function listCovenantEvents(covenantId: string) {
|
||||
return apiGet<CovenantEvent[]>(
|
||||
`/covenants/${encodeURIComponent(covenantId)}/events`,
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import {
|
||||
approveSubgoal,
|
||||
deferSubgoal,
|
||||
listCuriosities,
|
||||
listSubgoals,
|
||||
rejectSubgoal,
|
||||
} from "./drive";
|
||||
|
||||
describe("drive api", () => {
|
||||
it("listCuriosities 构造 path 与 query", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({
|
||||
items: [],
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
});
|
||||
await listCuriosities("char_1", { source: "memory_gap" });
|
||||
expect(apiGet).toHaveBeenCalledWith("/characters/char_1/curiosities", {
|
||||
source: "memory_gap",
|
||||
});
|
||||
});
|
||||
|
||||
it("listSubgoals 构造 path 与 status query", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({
|
||||
items: [],
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
});
|
||||
await listSubgoals("char_1", { status: "candidate" });
|
||||
expect(apiGet).toHaveBeenCalledWith("/characters/char_1/subgoals", {
|
||||
status: "candidate",
|
||||
});
|
||||
});
|
||||
|
||||
it("approve/defer/reject 各自 POST 正确 path", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ status: "approved" } as never);
|
||||
await approveSubgoal("subgoal_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/subgoals/subgoal_1/approve");
|
||||
|
||||
vi.mocked(apiPost).mockResolvedValue({ status: "deferred" } as never);
|
||||
await deferSubgoal("subgoal_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/subgoals/subgoal_1/defer");
|
||||
|
||||
vi.mocked(apiPost).mockResolvedValue({ status: "rejected" } as never);
|
||||
await rejectSubgoal("subgoal_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/subgoals/subgoal_1/reject");
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* 内驱力 API Client(对应 docs/domain/core-domains.md §5)。
|
||||
* 大周天生成的好奇问题与探索性次生目标;次生目标 approve/defer/reject 仅标记不执行。
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import type {
|
||||
CuriosityQuestion,
|
||||
ExploratorySubgoal,
|
||||
Paginated,
|
||||
} from "@/types/api";
|
||||
|
||||
/** 列出好奇问题(按 source 过滤) */
|
||||
export function listCuriosities(
|
||||
characterId: string,
|
||||
params?: { source?: string; page?: number; page_size?: number },
|
||||
) {
|
||||
return apiGet<Paginated<CuriosityQuestion>>(
|
||||
`/characters/${characterId}/curiosities`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 列出探索性次生目标(按 status 过滤) */
|
||||
export function listSubgoals(
|
||||
characterId: string,
|
||||
params?: { status?: string; page?: number; page_size?: number },
|
||||
) {
|
||||
return apiGet<Paginated<ExploratorySubgoal>>(
|
||||
`/characters/${characterId}/subgoals`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 通过次生目标(status → approved,不自动执行) */
|
||||
export function approveSubgoal(subgoalId: string) {
|
||||
return apiPost<ExploratorySubgoal>(`/subgoals/${subgoalId}/approve`);
|
||||
}
|
||||
|
||||
/** 延后次生目标(status → deferred) */
|
||||
export function deferSubgoal(subgoalId: string) {
|
||||
return apiPost<ExploratorySubgoal>(`/subgoals/${subgoalId}/defer`);
|
||||
}
|
||||
|
||||
/** 驳回次生目标(status → rejected) */
|
||||
export function rejectSubgoal(subgoalId: string) {
|
||||
return apiPost<ExploratorySubgoal>(`/subgoals/${subgoalId}/reject`);
|
||||
}
|
||||
@@ -9,8 +9,6 @@ export * from "./client";
|
||||
export * as charactersApi from "./characters";
|
||||
export * as runsApi from "./runs";
|
||||
export * as sandboxApi from "./sandbox";
|
||||
export * as personalityApi from "./personality";
|
||||
export * as driveApi from "./drive";
|
||||
export * as healthApi from "./health";
|
||||
export * as toolsApi from "./tools";
|
||||
export * as safetyApi from "./safety";
|
||||
@@ -18,9 +16,10 @@ export * as adminApi from "./admin";
|
||||
export * as settingsApi from "./settings";
|
||||
export * as meApi from "./me";
|
||||
export * as baigongApi from "./baigong";
|
||||
export * as covenantApi from "./covenants";
|
||||
export * as worldApi from "./worlds";
|
||||
export * as importsApi from "./imports";
|
||||
export * as extractionApi from "./extraction";
|
||||
export * as worldApi from "./world";
|
||||
export * as memoriesApi from "./memories";
|
||||
export * as lorebooksApi from "./lorebooks";
|
||||
export * as skillsApi from "./skills";
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
apiRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiGet, apiPost, apiRequest } from "./client";
|
||||
import {
|
||||
approveProposal,
|
||||
getCurrentPersonality,
|
||||
listPersonalityVersions,
|
||||
proposeUpdate,
|
||||
rejectProposal,
|
||||
rollbackPersonality,
|
||||
} from "./personality";
|
||||
|
||||
describe("personality api", () => {
|
||||
it("getCurrentPersonality GET /personality/current", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ id: "pv_1" });
|
||||
await getCurrentPersonality({ character_id: "char_1" });
|
||||
expect(apiGet).toHaveBeenCalledWith("/personality/current", {
|
||||
character_id: "char_1",
|
||||
});
|
||||
});
|
||||
|
||||
it("proposeUpdate POST /personality/propose-update", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "prop_1" });
|
||||
await proposeUpdate({
|
||||
fields: { expression_style: "简洁清晰" },
|
||||
reason: "优化表达",
|
||||
});
|
||||
expect(apiPost).toHaveBeenCalledWith("/personality/propose-update", {
|
||||
fields: { expression_style: "简洁清晰" },
|
||||
reason: "优化表达",
|
||||
});
|
||||
});
|
||||
|
||||
it("listPersonalityVersions GET /personality/versions", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ items: [], total: 0 });
|
||||
await listPersonalityVersions({ page: 1, page_size: 20 });
|
||||
expect(apiGet).toHaveBeenCalledWith("/personality/versions", {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("approveProposal POST /personality/proposals/:id/approve", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "prop_1" });
|
||||
await approveProposal("prop_1");
|
||||
expect(apiPost).toHaveBeenCalledWith(
|
||||
"/personality/proposals/prop_1/approve",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejectProposal POST /personality/proposals/:id/reject", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "prop_1" });
|
||||
await rejectProposal("prop_1");
|
||||
expect(apiPost).toHaveBeenCalledWith(
|
||||
"/personality/proposals/prop_1/reject",
|
||||
);
|
||||
});
|
||||
|
||||
it("rollbackPersonality POST 带 query 参数", async () => {
|
||||
vi.mocked(apiRequest).mockResolvedValue({ id: "pv_1" });
|
||||
await rollbackPersonality("pv_1", "char_1");
|
||||
expect(apiRequest).toHaveBeenCalledWith(
|
||||
"/personality/versions/pv_1/rollback",
|
||||
{ method: "POST", query: { character_id: "char_1" } },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* 人格 API Client(对应 后端生成的 OpenAPI 契约 §11)。
|
||||
* 当前开发周期要求只读展示当前人格版本;propose-update 为增强能力(审核后写入)。
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost, apiRequest } from "./client";
|
||||
import type {
|
||||
Paginated,
|
||||
PersonalityProposal,
|
||||
PersonalityVersion,
|
||||
ProposePersonalityUpdateRequest,
|
||||
} from "@/types/api";
|
||||
|
||||
/** 获取当前激活人物的人格版本(只读摘要) */
|
||||
export function getCurrentPersonality(params?: { character_id?: string }) {
|
||||
return apiGet<PersonalityVersion>("/personality/current", params);
|
||||
}
|
||||
|
||||
/** 提交人格更新建议(§11,核心人格 L4 变更审核通过才写入新版本) */
|
||||
export function proposeUpdate(body: ProposePersonalityUpdateRequest) {
|
||||
return apiPost<PersonalityProposal>("/personality/propose-update", body);
|
||||
}
|
||||
|
||||
/** 人格版本历史(GET /personality/versions,省略 character_id 取当前激活人物) */
|
||||
export function listPersonalityVersions(params?: {
|
||||
character_id?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<Paginated<PersonalityVersion>> {
|
||||
return apiGet<Paginated<PersonalityVersion>>("/personality/versions", params);
|
||||
}
|
||||
|
||||
/** 审核通过人格建议(POST /personality/proposals/{id}/approve,派生新版本并切换指针) */
|
||||
export function approveProposal(
|
||||
proposalId: string,
|
||||
): Promise<PersonalityProposal> {
|
||||
return apiPost<PersonalityProposal>(
|
||||
`/personality/proposals/${proposalId}/approve`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 驳回人格建议(POST /personality/proposals/{id}/reject) */
|
||||
export function rejectProposal(
|
||||
proposalId: string,
|
||||
): Promise<PersonalityProposal> {
|
||||
return apiPost<PersonalityProposal>(
|
||||
`/personality/proposals/${proposalId}/reject`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚到指定人格版本(POST /personality/versions/{id}/rollback)。
|
||||
* character_id 为 query 参数——apiPost 的 extra 已 Omit query,故直接用 apiRequest。
|
||||
*/
|
||||
export function rollbackPersonality(
|
||||
versionId: string,
|
||||
characterId?: string,
|
||||
): Promise<PersonalityVersion> {
|
||||
return apiRequest<PersonalityVersion>(
|
||||
`/personality/versions/${versionId}/rollback`,
|
||||
{
|
||||
method: "POST",
|
||||
query: characterId ? { character_id: characterId } : undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import {
|
||||
approveRunConfirmation,
|
||||
cancelRun,
|
||||
denyRunConfirmation,
|
||||
followUpRun,
|
||||
getRun,
|
||||
listRunEvents,
|
||||
steerRun,
|
||||
} from "./runs";
|
||||
|
||||
describe("runs api", () => {
|
||||
it("getRun GET /runs/:id", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ id: "run_1", status: "completed" });
|
||||
const result = await getRun("run_1");
|
||||
expect(apiGet).toHaveBeenCalledWith("/runs/run_1");
|
||||
expect(result.status).toBe("completed");
|
||||
});
|
||||
|
||||
it("cancelRun POST /runs/:id/cancel", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "run_1", status: "cancelled" });
|
||||
await cancelRun("run_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/runs/run_1/cancel");
|
||||
});
|
||||
|
||||
it("steerRun POST /runs/:id/steer with content", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ accepted: true });
|
||||
await steerRun("run_1", "改用表格输出");
|
||||
expect(apiPost).toHaveBeenCalledWith("/runs/run_1/steer", {
|
||||
content: "改用表格输出",
|
||||
});
|
||||
});
|
||||
|
||||
it("followUpRun POST /runs/:id/follow-up with content", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ accepted: true });
|
||||
await followUpRun("run_1", "再总结一次要点");
|
||||
expect(apiPost).toHaveBeenCalledWith("/runs/run_1/follow-up", {
|
||||
content: "再总结一次要点",
|
||||
});
|
||||
});
|
||||
|
||||
it("approveRunConfirmation POST 确认批准", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({
|
||||
status: "resolved",
|
||||
approved: true,
|
||||
});
|
||||
await approveRunConfirmation("run_1", "confirm_abc");
|
||||
expect(apiPost).toHaveBeenCalledWith(
|
||||
"/runs/run_1/confirmations/confirm_abc/approve",
|
||||
);
|
||||
});
|
||||
|
||||
it("denyRunConfirmation POST 确认拒绝", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({
|
||||
status: "resolved",
|
||||
approved: false,
|
||||
});
|
||||
await denyRunConfirmation("run_1", "confirm_abc");
|
||||
expect(apiPost).toHaveBeenCalledWith(
|
||||
"/runs/run_1/confirmations/confirm_abc/deny",
|
||||
);
|
||||
});
|
||||
|
||||
it("listRunEvents GET /runs/:id/events", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue([]);
|
||||
await listRunEvents("run_1");
|
||||
expect(apiGet).toHaveBeenCalledWith("/runs/run_1/events");
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import {
|
||||
createWorldEntity,
|
||||
createWorldRelation,
|
||||
getWorldEntity,
|
||||
listWorldEntities,
|
||||
listWorldRelations,
|
||||
} from "./world";
|
||||
|
||||
describe("world api", () => {
|
||||
it("listWorldEntities GET /world/entities 支持分页过滤", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ items: [], total: 0 });
|
||||
await listWorldEntities({ page: 1, page_size: 20, type: "person" });
|
||||
expect(apiGet).toHaveBeenCalledWith("/world/entities", {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
type: "person",
|
||||
});
|
||||
});
|
||||
|
||||
it("createWorldEntity POST /world/entities", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "entity_1" });
|
||||
await createWorldEntity({ type: "concept", name: "测试" });
|
||||
expect(apiPost).toHaveBeenCalledWith("/world/entities", {
|
||||
type: "concept",
|
||||
name: "测试",
|
||||
});
|
||||
});
|
||||
|
||||
it("getWorldEntity GET /world/entities/:id", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ id: "entity_1" });
|
||||
await getWorldEntity("entity_1");
|
||||
expect(apiGet).toHaveBeenCalledWith("/world/entities/entity_1");
|
||||
});
|
||||
|
||||
it("listWorldRelations GET /world/relations", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue({ items: [], total: 0 });
|
||||
await listWorldRelations({ page: 1, page_size: 10 });
|
||||
expect(apiGet).toHaveBeenCalledWith("/world/relations", {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("createWorldRelation POST /world/relations", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "rel_1" });
|
||||
await createWorldRelation({
|
||||
subject_id: "e1",
|
||||
predicate: "related_to",
|
||||
object_id: "e2",
|
||||
});
|
||||
expect(apiPost).toHaveBeenCalledWith("/world/relations", {
|
||||
subject_id: "e1",
|
||||
predicate: "related_to",
|
||||
object_id: "e2",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* 世界模型 API(河图枢,独立 /world 域)。
|
||||
* 实体列表/创建 + 关系图谱(relations)。独立域无 PATCH/DELETE——实体编辑/删除仍走
|
||||
* adminApi(保留 WorldEntityList 的完整 CRUD);本模块提供独立域的分页查询与关系管理。
|
||||
* 世界实体候选审核(approve/reject)见 extractionApi。
|
||||
*/
|
||||
|
||||
import { apiDelete, apiGet, apiPost } from "./client";
|
||||
import type {
|
||||
CreateWorldEntityRequest,
|
||||
CreateWorldRelationRequest,
|
||||
Paginated,
|
||||
WorldEntity,
|
||||
WorldRelation,
|
||||
} from "@/types/api";
|
||||
|
||||
/** 列出世界实体(GET /world/entities,独立域分页) */
|
||||
export function listWorldEntities(params?: {
|
||||
character_id?: string;
|
||||
type?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<Paginated<WorldEntity>> {
|
||||
return apiGet<Paginated<WorldEntity>>("/world/entities", params);
|
||||
}
|
||||
|
||||
/** 创建世界实体(POST /world/entities) */
|
||||
export function createWorldEntity(
|
||||
body: CreateWorldEntityRequest,
|
||||
): Promise<WorldEntity> {
|
||||
return apiPost<WorldEntity>("/world/entities", body);
|
||||
}
|
||||
|
||||
/** 获取单个世界实体(GET /world/entities/{id}) */
|
||||
export function getWorldEntity(entityId: string): Promise<WorldEntity> {
|
||||
return apiGet<WorldEntity>(`/world/entities/${entityId}`);
|
||||
}
|
||||
|
||||
/** 列出世界关系(GET /world/relations) */
|
||||
export function listWorldRelations(params?: {
|
||||
character_id?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<Paginated<WorldRelation>> {
|
||||
return apiGet<Paginated<WorldRelation>>("/world/relations", params);
|
||||
}
|
||||
|
||||
/** 创建世界关系(POST /world/relations,subject-predicate-object 三元组) */
|
||||
export function createWorldRelation(
|
||||
body: CreateWorldRelationRequest,
|
||||
): Promise<WorldRelation> {
|
||||
return apiPost<WorldRelation>("/world/relations", body);
|
||||
}
|
||||
|
||||
/** 删除世界关系(DELETE /world/relations/{relation_id}) */
|
||||
export function deleteWorldRelation(relationId: string): Promise<void> {
|
||||
return apiDelete<void>(`/world/relations/${relationId}`);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 河图世界 API Client:境界 CRUD、实体关系、境界正史。
|
||||
*/
|
||||
|
||||
import { apiDelete, apiGet, apiPatch, apiPost } from "./client";
|
||||
|
||||
/** 境界 */
|
||||
export interface PersistentWorld {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
rules: Record<string, unknown>;
|
||||
lorebook_id: string | null;
|
||||
canon_count: number;
|
||||
status: string;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
/** 世界实体 */
|
||||
export interface WorldEntity {
|
||||
id: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
description: string;
|
||||
attrs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** 世界关系 */
|
||||
export interface WorldRelation {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
relation: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
/** 境界正史条目 */
|
||||
export interface ChronicleEntry {
|
||||
canonization_id: string;
|
||||
tree_id: string;
|
||||
endpoint_node_id: string;
|
||||
path_nodes: string[];
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export function createWorld(body: {
|
||||
name: string;
|
||||
description?: string;
|
||||
rules?: Record<string, unknown>;
|
||||
lorebook_id?: string | null;
|
||||
}) {
|
||||
return apiPost<PersistentWorld>("/worlds", body);
|
||||
}
|
||||
|
||||
export function listWorlds() {
|
||||
return apiGet<PersistentWorld[]>("/worlds");
|
||||
}
|
||||
|
||||
export function getWorld(worldId: string) {
|
||||
return apiGet<PersistentWorld>(`/worlds/${encodeURIComponent(worldId)}`);
|
||||
}
|
||||
|
||||
export function updateWorld(
|
||||
worldId: string,
|
||||
body: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
rules?: Record<string, unknown>;
|
||||
lorebook_id?: string | null;
|
||||
},
|
||||
) {
|
||||
return apiPatch<PersistentWorld>(
|
||||
`/worlds/${encodeURIComponent(worldId)}`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
export function archiveWorld(worldId: string) {
|
||||
return apiDelete<{ world_id: string; status: string }>(
|
||||
`/worlds/${encodeURIComponent(worldId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function addWorldEntity(
|
||||
worldId: string,
|
||||
body: {
|
||||
kind?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
},
|
||||
) {
|
||||
return apiPost<WorldEntity>(
|
||||
`/worlds/${encodeURIComponent(worldId)}/entities`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
export function listWorldEntities(worldId: string) {
|
||||
return apiGet<WorldEntity[]>(
|
||||
`/worlds/${encodeURIComponent(worldId)}/entities`,
|
||||
);
|
||||
}
|
||||
|
||||
export function addWorldRelation(
|
||||
worldId: string,
|
||||
body: {
|
||||
source_entity_id: string;
|
||||
target_entity_id: string;
|
||||
relation: string;
|
||||
note?: string;
|
||||
},
|
||||
) {
|
||||
return apiPost<WorldRelation>(
|
||||
`/worlds/${encodeURIComponent(worldId)}/relations`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
export function listWorldRelations(worldId: string) {
|
||||
return apiGet<WorldRelation[]>(
|
||||
`/worlds/${encodeURIComponent(worldId)}/relations`,
|
||||
);
|
||||
}
|
||||
|
||||
export function worldChronicle(worldId: string) {
|
||||
return apiGet<{ world_id: string; entries: ChronicleEntry[] }>(
|
||||
`/worlds/${encodeURIComponent(worldId)}/chronicle`,
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,6 @@ export const API_SSE_ROOT = "/api/sse";
|
||||
export const FEATURE_FLAGS = {
|
||||
debugPanel: process.env.NEXT_PUBLIC_ENABLE_DEBUG_PANEL !== "false",
|
||||
avatar: process.env.NEXT_PUBLIC_ENABLE_AVATAR === "true",
|
||||
drivePanel: process.env.NEXT_PUBLIC_ENABLE_DRIVE_PANEL !== "false",
|
||||
worldPanel: process.env.NEXT_PUBLIC_ENABLE_WORLD_PANEL !== "false",
|
||||
/** 辰正时态面板(默认启用) */
|
||||
timePanel: process.env.NEXT_PUBLIC_ENABLE_TIME_PANEL !== "false",
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
/* personality 领域专属样式。 */
|
||||
/* ============================================================
|
||||
* 人格页面(PersonalityWorkspace)
|
||||
* 十镜网格 + 种子带 + 明德条 + 版本层积
|
||||
* 对齐 personality-core §7/§8 设计稿
|
||||
* ============================================================ */
|
||||
|
||||
.section-heading {
|
||||
font-family: "Songti SC", serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* 种子带 */
|
||||
.seed-band {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gilt-dim);
|
||||
border-radius: 8px;
|
||||
background: var(--face-sheen), var(--surface);
|
||||
box-shadow:
|
||||
inset 0 1px 0 var(--face-in-top),
|
||||
inset 0 -1px 0 var(--face-in-bot),
|
||||
var(--shadow);
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.seed-band::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='7'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
|
||||
opacity: 0.018;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
.seed-band > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.seed-mirror {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1.5px solid var(--patina-dim);
|
||||
background:
|
||||
radial-gradient(circle at 35% 30%, var(--face-in-top), transparent 60%),
|
||||
var(--patina-wash);
|
||||
}
|
||||
.seed-mirror .glyph {
|
||||
font-family: "Songti SC", serif;
|
||||
font-size: 25px;
|
||||
font-weight: 700;
|
||||
color: var(--gilt);
|
||||
}
|
||||
.seed-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.seed-lab {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fg-faint);
|
||||
}
|
||||
.seed-text {
|
||||
font-family: "Songti SC", serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
margin-top: 3px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.tier-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 10.5px;
|
||||
color: var(--fg-faint);
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tier-legend b {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tier-legend .l1 {
|
||||
color: var(--patina);
|
||||
}
|
||||
.tier-legend .l2 {
|
||||
color: var(--gilt);
|
||||
}
|
||||
.tier-legend .l4 {
|
||||
color: var(--cinnabar);
|
||||
}
|
||||
|
||||
/* 十镜网格 */
|
||||
.mirror-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 11px;
|
||||
}
|
||||
.mcell {
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 7px;
|
||||
padding: 13px 14px 12px;
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
min-height: 138px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.mcell.landed {
|
||||
border-color: var(--gilt-dim);
|
||||
}
|
||||
.mcell.reserved {
|
||||
border-style: dashed;
|
||||
opacity: 0.62;
|
||||
}
|
||||
.mc-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.mc-glyph {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--gilt-dim);
|
||||
background: var(--gilt-faint);
|
||||
font-family: "Songti SC", serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--gilt);
|
||||
}
|
||||
.mc-titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mc-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mc-src {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 10px;
|
||||
color: var(--fg-faint);
|
||||
}
|
||||
.mc-status {
|
||||
margin-left: auto;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 9.5px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--gilt-faint);
|
||||
color: var(--gilt);
|
||||
border: 1px solid var(--gilt-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mc-status.landed {
|
||||
background: var(--surface);
|
||||
}
|
||||
.mc-val {
|
||||
font-size: 11.5px;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.mc-val .em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* 人格权重条 */
|
||||
.mc-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.mc-bars .br {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.mc-bars .tn {
|
||||
font-family: "Songti SC", serif;
|
||||
font-size: 11px;
|
||||
color: var(--fg-dim);
|
||||
width: 30px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mc-bars .tbar {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--rule);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mc-bars .tbar .fl {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--patina-dim), var(--patina));
|
||||
}
|
||||
.mc-bars .tv {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 9.5px;
|
||||
color: var(--fg-faint);
|
||||
width: 18px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 明德恒定层 */
|
||||
.mingde-strip {
|
||||
border: 1px dashed var(--gilt-dim);
|
||||
border-left: 3px solid var(--gilt-dim);
|
||||
border-radius: 6px;
|
||||
padding: 13px 18px;
|
||||
background: var(--gilt-faint);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.mingde-strip .seal {
|
||||
font-family: "Songti SC", serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--gilt);
|
||||
letter-spacing: 0.06em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mingde-strip .desc {
|
||||
font-size: 12.5px;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.mingde-strip .desc code {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 11.5px;
|
||||
color: var(--gilt);
|
||||
background: var(--surface);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.mingde-strip .tag {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 10px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
border: 1px dashed var(--gilt-dim);
|
||||
color: var(--gilt);
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 下方双栏 */
|
||||
.lower-region {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.lower-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.strata-rail {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.mirror-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.lower-region {
|
||||
flex-direction: column;
|
||||
}
|
||||
.strata-rail {
|
||||
width: 100%;
|
||||
}
|
||||
.mirror-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.mirror-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.seed-band {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tier-legend {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "./features/cosmos.css";
|
||||
@import "./features/chat.css";
|
||||
@import "./features/characters.css";
|
||||
@import "./features/memory.css";
|
||||
@import "./features/time.css";
|
||||
@import "./features/drive.css";
|
||||
@import "./features/world.css";
|
||||
@import "./features/evolution.css";
|
||||
@import "./features/personality.css";
|
||||
|
||||
/*
|
||||
* ============================================================
|
||||
|
||||
Reference in new issue
Block a user