Files
Cyrene_v2/frontend/web/src/admin/modules/personaThinkingForm.tsx
T
AskaEth 2aa3233f84
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(persona,web): 人格读侧(thinking 三态 + 角色级 mood)+ 面板表单 + mood 播种
按 design/04 §3.2(thinking 语义,showToUser 三态)+ design/01 §2.1 + 主人两条定论(心情跟角色不跟会话;mood 初值走口径 ①:由角色读端点暴露、前端加载播种)。
- 管理侧 GET /admin/personas 的 PersonaSummary 增 thinking(budgetTokens/effort/mode/showToUser)——**showToUser 保持三态**(未配置 ≠ false)
- 用户侧 GET /personas 增**角色级 mood**(取自 emotion 层该角色当前状态,非从消息推断)→ 前端加载播种 currentMood,消除「刷新后心情徽标短暂为空」
- 前端 Personas 页加 thinking 四控件(预算/effort/模式/showToUser 三态):只做输入校验,**provider 是否真支持由后端 WARN 决定,前端不假装能力**
- 先拆 useAppStore 选择器(原 494/500)到独立模块再做,避免越线
- Docs/api-client 版本号 +1(两处契约 + 三态与角色级 mood 语义)
- 验收:go build/vet/test 全绿 + linux/darwin 交叉编译 + 前端 build 绿 + personaSession/messageSync 逻辑测试通过
2026-10-03 20:40:44 +08:00

116 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ThinkingDraft } from "../../lib/personaThinking";
import {
THINKING_EFFORTS,
THINKING_MODES,
thinkingValidationError,
} from "../../lib/personaThinking";
import { Tag } from "../../components/primitives";
/** The panel's four `thinking` controls (design/01 §2.1). Split out of Personas.tsx
* (which is near its size ceiling) and kept dumb: it renders + validates the form
* draft, the request mapping lives in lib/personaThinking.ts.
*
* It deliberately does NOT claim a capability: `effort` is offered from the union
* of the values the providers behind the API recognize, but whether the *active*
* provider honors the value is decided by the backend (it validates against the
* provider whitelist and WARNs when it drops one). The hint says so instead of
* pretending the knob is on. */
const inputCls =
"w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]";
export function ThinkingFields({
value,
onChange,
}: {
value: ThinkingDraft;
onChange: (next: ThinkingDraft) => void;
}): React.ReactNode {
const set = <K extends keyof ThinkingDraft>(key: K, v: ThinkingDraft[K]) =>
onChange({ ...value, [key]: v });
const err = thinkingValidationError(value);
return (
<div className="space-y-2 rounded-[var(--mu-radius)] bg-[var(--mu-surface-3)]/40 px-3 py-3">
<div className="flex flex-wrap items-baseline gap-2">
<span className="text-sm font-semibold">思考配置 thinking</span>
<span className="text-xs text-[var(--mu-text-lo)]">
只描述呈现/资源,不构成能力边界(design/01 §2.1);留空 = 不配置
</span>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">预算 budgetTokens(非负整数,留空 = 不配置)</span>
<input
value={value.budgetTokens}
inputMode="numeric"
placeholder="4096"
onChange={(e) => set("budgetTokens", e.target.value)}
className={inputCls}
/>
</label>
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">强度 effort(可留空)</span>
<select
value={value.effort}
onChange={(e) => set("effort", e.target.value)}
className={inputCls}
>
<option value="">(不配置)</option>
{THINKING_EFFORTS.map((e) => (
<option key={e} value={e}>
{e}
</option>
))}
{/* A stored value outside the list is shown as-is rather than being
* silently displayed as "unconfigured" (Docs/api-client.md §1.1: an
* unknown value is never dropped or replaced by a placeholder). The
* validation error then asks the user to pick a recognized one. */}
{value.effort && !(THINKING_EFFORTS as readonly string[]).includes(value.effort) ? (
<option value={value.effort}>{value.effort}(未知取值,后端会 WARN 并忽略)</option>
) : null}
</select>
</label>
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">
回显 showToUser(未配置 = 保持现行为:思考帧照常推给客户端)
</span>
<select
value={value.showToUser}
onChange={(e) => set("showToUser", e.target.value as ThinkingDraft["showToUser"])}
className={inputCls}
>
<option value="unset">未配置(保持现行为)</option>
<option value="on">开(推帧)</option>
<option value="off">关(只落库不回显)</option>
</select>
</label>
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">开关 mode(硬开关,推理)</span>
<select
value={value.mode}
onChange={(e) => set("mode", e.target.value as ThinkingDraft["mode"])}
className={inputCls}
>
<option value="">(未配置)</option>
{THINKING_MODES.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
</div>
<p className="text-xs text-[var(--mu-text-lo)]">
effort 的有效取值随当前生效 provider 而定(OpenAI 兼容系
none/minimal/low/medium/high/xhigh/max;DeepSeek none/low/high/max,别名
minimal→low、medium→high、xhigh→high、ultra→max;未知 provider 不支持)。面板只做前端枚举校验;
该值在上游是否真生效,由后端按 provider 校验并记 WARN 决定,此处不代为承诺。
</p>
{err ? (
<Tag tone="err">{err}</Tag>
) : null}
</div>
);
}