v1.1.1: advisor popup matches the model picker (MenuSurface/MenuGroup, rows, brand icons, tokens)
This commit is contained in:
1 parent
31319ea7ae
commit
f85f445ef6
2 files changed
+85
-60
No files matched your search
@@ -27,26 +27,35 @@ window.__ModuleLoader__.load({
|
||||
hint: '审查每一步并给出提示或阻断意见。',
|
||||
};
|
||||
|
||||
// Mirrors model-selector-plus (`.msp-*`) — same surface, rows, type scale and tokens — so the
|
||||
// advisor popup reads as a sibling of the model picker in every theme.
|
||||
const CSS = `
|
||||
.oadv-root{position:relative;display:flex;align-items:center;min-width:0}
|
||||
.oadv-trigger{border:0;background:none;color:var(--dsw-alias-label-secondary);height:28px;max-width:200px;padding:0 6px 0 6px;border-radius:var(--dsw-radius-sm);display:flex;align-items:center;gap:5px;font:inherit;font-size:13px;cursor:pointer;min-width:0}
|
||||
.oadv-trigger{border-radius:var(--dsw-radius-sm);min-width:0;max-width:min(240px,30cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:0 4px 0 8px;font-size:13px;font-weight:400;line-height:20px;display:flex}
|
||||
.oadv-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
||||
.oadv-trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}
|
||||
.oadv-trigger[data-off]{color:var(--dsw-alias-label-caption)}
|
||||
.oadv-trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline:none}
|
||||
.oadv-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.oadv-effort{color:var(--dsw-alias-label-caption);white-space:nowrap}
|
||||
.oadv-menu,.oadv-sub{position:fixed;z-index:1100;min-width:240px;max-width:min(380px,100vw - 32px);max-height:min(420px,100vh - 96px);overflow-y:auto;padding:4px;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-prominent);border-radius:var(--dsw-radius-lg,10px);background:var(--dsw-alias-bg-l1,var(--dsw-alias-surface-l1,#1e1e1e))}
|
||||
.oadv-sub{min-width:160px}
|
||||
.oadv-head{padding:6px 8px 4px;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
||||
.oadv-group{padding:8px 8px 2px;font-size:11px;letter-spacing:.02em;color:var(--dsw-alias-label-caption);text-transform:uppercase}
|
||||
.oadv-item{width:100%;display:flex;align-items:center;gap:8px;min-height:32px;padding:0 8px;border:0;background:none;color:inherit;font:inherit;font-size:13px;text-align:left;border-radius:var(--dsw-radius-md);cursor:pointer}
|
||||
.oadv-item:hover,.oadv-item[data-active]{background:var(--dsw-alias-interactive-bg-hover)}
|
||||
.oadv-item-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.oadv-item-meta{color:var(--dsw-alias-label-caption);font-size:12px;white-space:nowrap}
|
||||
.oadv-check{width:14px;flex:none;display:inline-flex;justify-content:center}
|
||||
.oadv-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.2))}
|
||||
.oadv-status{padding:8px;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
||||
.oadv-err{padding:6px 8px;font-size:12px;color:var(--dsw-alias-state-danger,#f85149)}
|
||||
.oadv-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}
|
||||
.oadv-effort{color:var(--dsw-alias-label-caption);white-space:nowrap;flex-shrink:1000}
|
||||
.oadv-chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}
|
||||
.oadv-chevron-open{transform:rotate(180deg)}
|
||||
.oadv-icon{flex:none;display:inline-block;line-height:1}
|
||||
.oadv-menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(260px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(420px,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}
|
||||
.oadv-sub{min-width:min(180px,100vw - 32px)}
|
||||
.oadv-groups{min-height:0;overflow-y:auto}
|
||||
.oadv-option{box-sizing:border-box;width:100%;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:8px;min-height:34px;padding:5px 7px;display:flex;border-radius:var(--dsw-radius-md);font:inherit}
|
||||
.oadv-option:hover,.oadv-option:focus-visible,.oadv-option[data-active]{background:var(--dsw-alias-interactive-bg-hover)}
|
||||
.oadv-option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
|
||||
.oadv-copy{flex-direction:column;flex:1;min-width:0;display:flex}
|
||||
.oadv-name{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:400;line-height:18px;overflow:hidden}
|
||||
.oadv-sub-text{color:var(--dsw-alias-label-caption);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:14px;overflow:hidden}
|
||||
.oadv-meta{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}
|
||||
.oadv-check{color:var(--dsw-alias-label-primary);flex:0 0 14px;place-items:center;display:grid}
|
||||
.oadv-check svg{width:14px;height:14px}
|
||||
.oadv-more{width:12px;height:12px;color:var(--dsw-alias-menu-icon,var(--dsw-alias-label-caption));flex:none}
|
||||
.oadv-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1)}
|
||||
.oadv-status{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:12px;line-height:18px}
|
||||
.oadv-err{border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);margin-bottom:3px;padding:6px 7px;font-size:11px;line-height:16px}
|
||||
`;
|
||||
let cssDone = false;
|
||||
function installCss() {
|
||||
@@ -63,6 +72,15 @@ window.__ModuleLoader__.load({
|
||||
const IconChevronDown = Ic('IconChevronDownOutlineRegular');
|
||||
const IconChevronRight = Ic('IconChevronRightOutlineRegular');
|
||||
const IconAdvisor = P.IconSparkleRegular || P.IconEyeOutlineRegular || (() => null);
|
||||
// Shared surfaces from DSH primitives; plain divs if a build lacks them.
|
||||
const Surface = P.MenuSurface || React.forwardRef((p, ref) => h('div', { ...p, ref }));
|
||||
const Group = P.MenuGroup || (({ label, children }) => h('div', null, h('div', { className: 'oadv-status' }, label), children));
|
||||
/** Brand icon from model-selector-plus when it is mounted; an empty 16px slot otherwise. */
|
||||
function Brand({ group, model }) {
|
||||
const b = window.__dshModelBrands;
|
||||
if (b) return h(b.BrandIcon, { brand: b.brandOf(group.id, group.name, model.id, model.name) });
|
||||
return h('span', { className: 'oadv-icon', style: { width: 16, height: 16 }, 'aria-hidden': true });
|
||||
}
|
||||
|
||||
const fill = (s, v) => String(s).replace(/\{(\w+)\}/g, (_, k) => (v && v[k] !== undefined ? v[k] : ''));
|
||||
const effortName = (e) => (e?.id === 'xhigh' ? 'Extra' : e?.name ?? e?.id);
|
||||
@@ -183,74 +201,81 @@ window.__ModuleLoader__.load({
|
||||
};
|
||||
const leaveRow = () => { clearTimeout(hoverTimer.current); };
|
||||
|
||||
const check = (on) => h('span', { className: 'oadv-check' }, on ? h(IconCheck, { size: 14 }) : null);
|
||||
const check = (on) => h('span', { className: 'oadv-check' }, on ? h(IconCheck) : null);
|
||||
const hidden = { left: 0, top: 0, visibility: 'hidden' };
|
||||
const isSel = (group, model) => enabled && own && st.provider === group.id && st.model === model.id;
|
||||
|
||||
const menu = open ? ReactDOM.createPortal(h('div', {
|
||||
/** One row in the model picker's shape: icon · name (+ caption) · meta · check · chevron. */
|
||||
const option = ({ key, checked, icon, name, caption, meta, more, active, ...rest }) => h('button', {
|
||||
key, type: 'button', role: 'menuitemradio', 'aria-checked': checked, className: 'oadv-option',
|
||||
'data-active': active ? '' : undefined, ...rest,
|
||||
},
|
||||
icon ?? null,
|
||||
h('span', { className: 'oadv-copy' },
|
||||
h('span', { className: 'oadv-name' }, name),
|
||||
caption ? h('span', { className: 'oadv-sub-text' }, caption) : null),
|
||||
meta ? h('span', { className: 'oadv-meta' }, meta) : null,
|
||||
check(checked),
|
||||
more ? h(IconChevronRight, { className: 'oadv-more' }) : null);
|
||||
|
||||
const menu = open ? ReactDOM.createPortal(h(Surface, {
|
||||
ref: menuRef, className: 'oadv-menu', role: 'menu', 'aria-label': t('title'),
|
||||
style: pos ? { left: pos.left, top: pos.top } : { left: -9999, top: -9999, visibility: 'hidden' },
|
||||
style: pos ? { left: pos.left, top: pos.top } : hidden,
|
||||
onMouseLeave: () => { clearTimeout(hoverTimer.current); },
|
||||
},
|
||||
h('div', { className: 'oadv-head' }, t('title'), ' · ', t('hint')),
|
||||
err ? h('div', { className: 'oadv-err' }, fill(t('error'), { msg: err })) : null,
|
||||
h('button', { type: 'button', role: 'menuitemradio', 'aria-checked': !enabled, className: 'oadv-item', onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: false }) },
|
||||
check(!enabled), h('span', { className: 'oadv-item-name' }, t('offItem'))),
|
||||
h('button', { type: 'button', role: 'menuitemradio', 'aria-checked': enabled && !own, className: 'oadv-item', onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: true, provider: '', model: '', reasoningEffort: null }) },
|
||||
check(enabled && !own), h('span', { className: 'oadv-item-name' }, t('sameItem'))),
|
||||
h('div', { className: 'oadv-sep' }),
|
||||
dir?.status === 'loading' && !groups.length ? h('div', { className: 'oadv-status' }, t('loading')) : null,
|
||||
...groups.flatMap((group) => [
|
||||
h('div', { key: `g:${group.id}`, className: 'oadv-group' }, group.name ?? group.id),
|
||||
...group.models.map((model) => {
|
||||
err ? h('div', { className: 'oadv-err', role: 'alert' }, fill(t('error'), { msg: err })) : null,
|
||||
h('div', { className: 'oadv-groups' },
|
||||
h(Group, { label: t('title') },
|
||||
option({ key: 'off', checked: !enabled, icon: h(IconAdvisor, { size: 16 }), name: t('offItem'), caption: t('hint'), onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: false }) }),
|
||||
option({ key: 'same', checked: enabled && !own, icon: h('span', { className: 'oadv-icon', style: { width: 16, height: 16 } }), name: t('sameItem'), onMouseEnter: () => setHover(null), onClick: () => choose({ enabled: true, provider: '', model: '', reasoningEffort: null }) })),
|
||||
dir?.status === 'loading' && !groups.length ? h('div', { className: 'oadv-status', role: 'status' }, t('loading')) : null,
|
||||
...groups.map((group) => h(Group, { key: `g:${group.id}`, label: group.id === 'deepseek-account' ? 'DeepSeek' : (group.name ?? group.id) },
|
||||
group.models.map((model) => {
|
||||
const key = `${group.id}/${model.id}`;
|
||||
const selected = enabled && own && st.provider === group.id && st.model === model.id;
|
||||
const selected = isSel(group, model);
|
||||
const hasEfforts = !!model.reasoning?.efforts?.length;
|
||||
return h('button', {
|
||||
key, type: 'button', role: 'menuitemradio', 'aria-checked': selected, 'aria-haspopup': hasEfforts ? 'menu' : undefined,
|
||||
className: 'oadv-item', 'data-active': hover?.key === key ? '' : undefined,
|
||||
return option({
|
||||
key, checked: selected, icon: h(Brand, { group, model }), name: model.name ?? model.id,
|
||||
meta: selected && st.reasoningEffort ? effortOf(model, st.reasoningEffort) : null,
|
||||
more: hasEfforts, active: hover?.key === key,
|
||||
'aria-haspopup': hasEfforts ? 'menu' : undefined, disabled: model.available === false,
|
||||
onMouseEnter: (e) => enterRow(group, model, e.currentTarget),
|
||||
onMouseLeave: leaveRow,
|
||||
onFocus: (e) => enterRow(group, model, e.currentTarget),
|
||||
onKeyDown: (e) => { if (e.key === 'ArrowRight' && hasEfforts) { setHover({ key, el: e.currentTarget, group, model }); setTimeout(() => subRef.current?.querySelector('button')?.focus(), 0); } },
|
||||
onClick: () => pickModel(group, model),
|
||||
disabled: model.available === false,
|
||||
},
|
||||
check(selected),
|
||||
h('span', { className: 'oadv-item-name' }, model.name ?? model.id),
|
||||
selected && st.reasoningEffort ? h('span', { className: 'oadv-item-meta' }, effortOf(model, st.reasoningEffort)) : null,
|
||||
hasEfforts ? h(IconChevronRight, { size: 12 }) : null);
|
||||
}),
|
||||
])), document.body) : null;
|
||||
});
|
||||
}))))), document.body) : null;
|
||||
|
||||
const sub = open && hover ? ReactDOM.createPortal(h('div', {
|
||||
ref: subRef, className: 'oadv-sub', role: 'menu', 'aria-label': t('effort'),
|
||||
style: subPos ? { left: subPos.left, top: subPos.top } : { left: -9999, top: -9999, visibility: 'hidden' },
|
||||
const sub = open && hover ? ReactDOM.createPortal(h(Surface, {
|
||||
ref: subRef, className: 'oadv-menu oadv-sub', role: 'menu', 'aria-label': t('effort'),
|
||||
style: subPos ? { left: subPos.left, top: subPos.top } : hidden,
|
||||
onMouseEnter: () => clearTimeout(hoverTimer.current),
|
||||
onKeyDown: (e) => { if (e.key === 'ArrowLeft') { hover.el?.focus(); setHover(null); } },
|
||||
},
|
||||
h('div', { className: 'oadv-head' }, hover.model.name ?? hover.model.id, ' · ', t('effort')),
|
||||
...(hover.model.reasoning.defaultEffort === undefined ? [h('button', {
|
||||
key: 'def', type: 'button', role: 'menuitemradio', className: 'oadv-item',
|
||||
'aria-checked': enabled && own && st.provider === hover.group.id && st.model === hover.model.id && !st.reasoningEffort,
|
||||
h(Group, { label: `${hover.model.name ?? hover.model.id} · ${t('effort')}` },
|
||||
...(hover.model.reasoning.defaultEffort === undefined ? [option({
|
||||
key: 'def', checked: isSel(hover.group, hover.model) && !st.reasoningEffort, name: t('providerDefault'),
|
||||
onClick: () => pickModel(hover.group, hover.model, null),
|
||||
}, check(enabled && own && st.provider === hover.group.id && st.model === hover.model.id && !st.reasoningEffort), h('span', { className: 'oadv-item-name' }, t('providerDefault')))] : []),
|
||||
...hover.model.reasoning.efforts.map((e) => {
|
||||
const on = enabled && own && st.provider === hover.group.id && st.model === hover.model.id
|
||||
&& (st.reasoningEffort ?? hover.model.reasoning.defaultEffort) === e.id;
|
||||
return h('button', { key: e.id, type: 'button', role: 'menuitemradio', 'aria-checked': on, className: 'oadv-item', onClick: () => pickModel(hover.group, hover.model, e.id) },
|
||||
check(on), h('span', { className: 'oadv-item-name' }, effortName(e)),
|
||||
e.id === hover.model.reasoning.defaultEffort ? h('span', { className: 'oadv-item-meta' }, t('providerDefault')) : null);
|
||||
})), document.body) : null;
|
||||
})] : []),
|
||||
...hover.model.reasoning.efforts.map((e) => option({
|
||||
key: e.id, name: effortName(e),
|
||||
checked: isSel(hover.group, hover.model) && (st.reasoningEffort ?? hover.model.reasoning.defaultEffort) === e.id,
|
||||
meta: e.id === hover.model.reasoning.defaultEffort ? t('providerDefault') : null,
|
||||
onClick: () => pickModel(hover.group, hover.model, e.id),
|
||||
})))), document.body) : null;
|
||||
|
||||
const triggerIcon = enabled && own && current ? h(Brand, { group: groups.find((g) => g.id === st.provider), model: current }) : h(IconAdvisor, { size: 16 });
|
||||
return h('div', { ref: rootRef, className: 'oadv-root' },
|
||||
h('button', {
|
||||
ref: trigRef, type: 'button', className: 'oadv-trigger', 'data-off': enabled ? undefined : '',
|
||||
'aria-haspopup': 'menu', 'aria-expanded': open, title: t('title'),
|
||||
onClick: () => setOpen((v) => !v),
|
||||
},
|
||||
h(IconAdvisor, { size: 14 }),
|
||||
triggerIcon,
|
||||
h('span', { className: 'oadv-label' }, label),
|
||||
effortLabel ? h('span', { className: 'oadv-effort' }, effortLabel) : null,
|
||||
h(IconChevronDown, { size: 12 })),
|
||||
h(IconChevronDown, { className: open ? 'oadv-chevron oadv-chevron-open' : 'oadv-chevron' })),
|
||||
menu, sub);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@local/omp-advisor",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "oh-my-pi style advisor for DSH: a second model reviews every agent step and injects notes, concerns or blockers.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Reference in new issue
Block a user