Initial commit: thinking-level-selector DSH plugin with docs
This commit is contained in:
commit
15cc134310
6 files changed
+236
No files matched your search
@@ -0,0 +1,148 @@
|
||||
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));
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user