149 lines
5.9 KiB
JavaScript
149 lines
5.9 KiB
JavaScript
window.__ModuleLoader__.load({
|
|
id: '@local/thinking-level-selector',
|
|
factory(require) {
|
|
const React = require('react');
|
|
const h = React.createElement;
|
|
const NS = 'thinkingLevel';
|
|
|
|
const en = {
|
|
label: 'Thinking level',
|
|
none: 'Thinking',
|
|
default: 'Default',
|
|
loading: 'Loading…',
|
|
};
|
|
const zh = {
|
|
label: '思考强度',
|
|
none: '思考',
|
|
default: '默认',
|
|
loading: '加载中…',
|
|
};
|
|
|
|
// DSH derives names from the level key ("xhigh" -> "Xhigh"); show friendlier ones.
|
|
const LEVEL_NAMES = { xhigh: 'Extra' };
|
|
const levelName = (e) => LEVEL_NAMES[e.id] ?? e.name;
|
|
|
|
// Same shape of data the model seat renders from.
|
|
function currentReasoning(state) {
|
|
const cur = state.current;
|
|
if (!cur) return undefined;
|
|
for (const g of state.groups) {
|
|
if (g.id !== cur.provider) continue;
|
|
const m = g.models.find((x) => x.id === cur.model);
|
|
if (m) return m.reasoning;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function ThinkingLevel({ locked, available, directory, load, select, t }) {
|
|
const state = React.useSyncExternalStore(
|
|
(fn) => directory.subscribe(fn),
|
|
() => directory.getSnapshot(),
|
|
);
|
|
const [open, setOpen] = React.useState(false);
|
|
const rootRef = React.useRef(null);
|
|
|
|
React.useEffect(() => { if (available) load(); }, [available]);
|
|
React.useEffect(() => {
|
|
if (!open) return undefined;
|
|
const onDown = (e) => {
|
|
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
};
|
|
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
|
document.addEventListener('mousedown', onDown);
|
|
document.addEventListener('keydown', onKey);
|
|
return () => {
|
|
document.removeEventListener('mousedown', onDown);
|
|
document.removeEventListener('keydown', onKey);
|
|
};
|
|
}, [open]);
|
|
|
|
const reasoning = currentReasoning(state);
|
|
if (!available || !reasoning || reasoning.efforts.length === 0) return null;
|
|
|
|
const cur = state.current;
|
|
const effective = cur.reasoningEffort ?? reasoning.defaultEffort;
|
|
const active = reasoning.efforts.find((e) => e.id === effective);
|
|
const label = active ? levelName(active) : t('default');
|
|
const disabled = locked || state.status === 'selecting';
|
|
|
|
const pick = (effort) => {
|
|
setOpen(false);
|
|
if (effort === effective) return;
|
|
select({
|
|
provider: cur.provider,
|
|
model: cur.model,
|
|
...(effort === undefined ? {} : { reasoningEffort: effort }),
|
|
});
|
|
};
|
|
|
|
const itemStyle = (selected) => ({
|
|
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
|
padding: '6px 10px', border: 0, borderRadius: 6, cursor: 'pointer',
|
|
textAlign: 'left', font: 'inherit', fontSize: 13, whiteSpace: 'nowrap',
|
|
background: selected ? 'var(--dsw-alias-interactive-bg-hover)' : 'transparent',
|
|
color: 'var(--dsw-alias-label-primary, inherit)',
|
|
});
|
|
|
|
return h('div', { ref: rootRef, style: { position: 'relative', display: 'inline-flex' } },
|
|
h('button', {
|
|
type: 'button',
|
|
disabled,
|
|
title: t('label'),
|
|
'aria-label': t('label'),
|
|
'aria-haspopup': 'listbox',
|
|
'aria-expanded': open,
|
|
onClick: () => setOpen((v) => !v),
|
|
style: {
|
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
padding: '2px 8px', border: 0, borderRadius: 6, background: 'transparent',
|
|
font: 'inherit', fontSize: 13, cursor: disabled ? 'default' : 'pointer',
|
|
color: 'var(--dsw-alias-label-secondary, inherit)',
|
|
opacity: disabled ? 0.5 : 1, whiteSpace: 'nowrap',
|
|
},
|
|
}, label, h('span', { 'aria-hidden': true, style: { fontSize: 10 } }, '▾')),
|
|
open && h('div', {
|
|
role: 'listbox',
|
|
style: {
|
|
position: 'absolute', bottom: 'calc(100% + 6px)', left: 0, zIndex: 50,
|
|
minWidth: 140, padding: 4, borderRadius: 8,
|
|
background: 'var(--dsw-specific-input-major, #fff)',
|
|
boxShadow: 'var(--dsw-elevation-soft, 0 4px 16px rgba(0,0,0,.18))',
|
|
border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3))',
|
|
},
|
|
},
|
|
reasoning.defaultEffort === undefined && h('button', {
|
|
type: 'button', role: 'option', 'aria-selected': effective === undefined,
|
|
onClick: () => pick(undefined), style: itemStyle(effective === undefined),
|
|
}, t('default')),
|
|
reasoning.efforts.map((e) => h('button', {
|
|
key: e.id, type: 'button', role: 'option', title: e.description,
|
|
'aria-selected': e.id === effective,
|
|
onClick: () => pick(e.id), style: itemStyle(e.id === effective),
|
|
}, levelName(e), e.id === reasoning.defaultEffort ? h('span', { style: { opacity: 0.55, fontSize: 11 } }, '· ' + t('default')) : null)),
|
|
),
|
|
);
|
|
}
|
|
|
|
return {
|
|
inject: ['slots', 'locale', 'sessions', 'remote', 'remote.session', 'modelDirectories'],
|
|
apply(ctx) {
|
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'thinking-level: dictionaries');
|
|
ctx.slots.inject('conversation.input.activity', () => ctx.slots.register({
|
|
name: 'conversation.input.activity',
|
|
locale: NS,
|
|
inject: (sessionId) => {
|
|
const directory = ctx.modelDirectories.directoryFor(sessionId);
|
|
const available = ctx.sessions.subagentAddress(sessionId) === undefined;
|
|
return {
|
|
available,
|
|
directory: directory.store,
|
|
load: () => { if (available) directory.load().catch(() => {}); },
|
|
select: (sel) => available ? directory.select(sel) : Promise.resolve(undefined),
|
|
};
|
|
},
|
|
}, ThinkingLevel));
|
|
},
|
|
};
|
|
},
|
|
});
|