按 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 逻辑测试通过
116 lines
5.0 KiB
TypeScript
116 lines
5.0 KiB
TypeScript
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>
|
||
);
|
||
}
|