@dungsil/dsh-plugin-ui-model-picker 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/client.js +163 -91
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
DSH Web 입력창에서 모델과 추론 강도를 선택합니다. 하나의 선택 버튼을 누르면 추론 강도 팝오버가 먼저 열리며, 팝오버 안의 모델명을 눌러 모델 목록으로 이동합니다.
|
|
5
|
+
DSH Web 입력창에서 모델과 추론 강도를 선택합니다. 하나의 선택 버튼을 누르면 추론 강도 팝오버가 먼저 열리며, 팝오버 안의 모델명을 눌러 모델 목록으로 이동합니다. 모델을 고르면 실제 모델은 바로 바뀌지 않고 추론 강도 슬라이더가 이어서 표시되며, 적용 버튼을 누르면 모델과 강도가 함께 적용됩니다.
|
|
6
6
|
|
|
7
7
|
위 이미지는 플러그인 컴포넌트에 예시 모델 목록과 테마 색상을 제공한 검증 화면을 각각 캡처해 합성했습니다. 표시된 모델명과 추론 단계는 예시이며, 실제 목록은 연결한 제공자가 결정합니다.
|
|
8
8
|
|
|
9
9
|
## 사용 방법
|
|
10
10
|
|
|
11
11
|
1. DSH 플러그인 설치 화면에서 `@dungsil/dsh-plugin-ui-model-picker`를 설치하고 활성화합니다.
|
|
12
|
-
2. 입력창 오른쪽 선택 버튼을 눌러 추론 강도 슬라이더를 엽니다. 방향키와 Home/End 키도 사용할 수 있습니다.
|
|
13
|
-
3. 모델을 바꾸려면 팝오버 상단의 모델명 부분을 누르고 제공자별 모델을 선택합니다. 오른쪽 위 재설정 아이콘은 기본 추론 강도를 복원합니다.
|
|
12
|
+
2. 입력창 오른쪽 선택 버튼을 눌러 추론 강도 슬라이더를 엽니다. 방향키와 Home/End 키도 사용할 수 있습니다. 원하는 강도를 고른 뒤 적용 버튼을 누릅니다.
|
|
13
|
+
3. 모델을 바꾸려면 팝오버 상단의 모델명 부분을 누르고 제공자별 모델을 선택합니다. 추론 강도를 지원하는 모델을 고르면 슬라이더가 이어서 나타나며, 적용 버튼을 눌러야 모델이 적용됩니다. 오른쪽 위 재설정 아이콘은 기본 추론 강도를 복원합니다.
|
|
14
14
|
|
|
15
15
|
## 동작과 호환성
|
|
16
16
|
|
|
17
17
|
- DSH `0.2.1-rc.5` 이상 `0.3.0` 미만의 Web UI를 지원합니다. 기본 `ui-model-selection` 기능이 활성화되어 있어야 합니다.
|
|
18
18
|
- 공용 모델 디렉터리와 선택 API를 그대로 사용하므로 `/model` 명령과 선택 상태를 공유합니다. 별도 모델 설정을 저장하거나 API 키를 다루지 않습니다.
|
|
19
|
-
- 실제 제공자 목록과 모델별 추론 메타데이터만 사용합니다. 별도 모델 목록, Fast 모드, 고정 추론 단계는 추가하지 않습니다. 추론을 지원하지 않는 모델에는 슬라이더를 표시하지 않으며,
|
|
20
|
-
- 다른 모델로 바꾸면
|
|
19
|
+
- 실제 제공자 목록과 모델별 추론 메타데이터만 사용합니다. 별도 모델 목록, Fast 모드, 고정 추론 단계는 추가하지 않습니다. 추론을 지원하지 않는 모델에는 슬라이더를 표시하지 않으며, 그런 모델은 목록에서 고르는 즉시 적용됩니다.
|
|
20
|
+
- 다른 모델로 바꾸면 그 모델이 지원하는 추론 강도에서 슬라이더가 시작되고, 적용 버튼을 눌러야 모델 선택이 적용됩니다. 적용하지 않고 팝오버를 닫으면 모델은 바뀌지 않습니다. 기본값 재설정은 별도 강도 지정을 해제합니다.
|
|
21
21
|
- Escape, 바깥 클릭, 스크롤, 창 크기 변경으로 메뉴를 닫습니다. 메뉴의 방향키와 Tab 탐색을 지원합니다. 한국어·영어·중국어(간체)와 DSH 테마 색상을 따릅니다.
|
|
22
22
|
- 기본 모델 슬롯만 우선순위로 대체합니다. 제거하면 기본 선택 UI가 돌아오며 모델 선택은 유지됩니다. 같은 슬롯을 대체하는 다른 플러그인과는 함께 사용하지 마세요.
|
|
23
23
|
|
package/dist/client.js
CHANGED
|
@@ -29,7 +29,7 @@ function IconResetOutlineRegular() {
|
|
|
29
29
|
}
|
|
30
30
|
// ../../tools/ui/src/composer.tsx
|
|
31
31
|
const composerStyles = `
|
|
32
|
-
:where(.dsh-plugin-composer-trigger) { display: flex; align-items: center; gap: 4px; min-width: 0; height: 28px; padding: 0 4px 0 8px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; line-height:
|
|
32
|
+
:where(.dsh-plugin-composer-trigger) { display: flex; align-items: center; gap: 4px; min-width: 0; height: 28px; padding: 0 4px 0 8px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; line-height: 1.5385; cursor: pointer; }
|
|
33
33
|
:where(.dsh-plugin-composer-trigger):disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
34
34
|
.dsh-plugin-composer-icon, .dsh-plugin-composer-chevron { display: inline-flex; flex: none; }
|
|
35
35
|
.dsh-plugin-composer-label, .dsh-plugin-composer-detail { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -68,36 +68,39 @@ const ComposerTrigger = /* @__PURE__ */ (0, react.forwardRef)(function ComposerT
|
|
|
68
68
|
const styles = `${composerStyles}
|
|
69
69
|
.dsh-codex-picker { display: flex; align-items: center; min-width: 0; }
|
|
70
70
|
.dsh-codex-trigger { max-width: min(360px, 45cqw); font-weight: 400; outline: none; }
|
|
71
|
-
.dsh-codex-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
72
71
|
.dsh-codex-trigger:focus-visible:not([data-dsh-automatic-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
73
72
|
.dsh-codex-trigger .dsh-plugin-composer-icon { display: var(--dsh-composer-model-icon-display, none); }
|
|
74
73
|
.dsh-codex-trigger .dsh-plugin-composer-chevron { transition: transform .12s; }
|
|
75
74
|
.dsh-codex-trigger .dsh-plugin-composer-label, .dsh-codex-trigger .dsh-plugin-composer-detail { display: var(--dsh-composer-model-text-display, block); }
|
|
76
75
|
.dsh-codex-trigger .dsh-plugin-composer-detail { flex-shrink: 1000; }
|
|
77
|
-
.dsh-codex-popover { position: fixed; z-index: 1100; display: flex; flex-direction: column; inset: auto; margin: 0; box-sizing: border-box; width: 256px; max-width: calc(100vw - 32px); max-height: min(360px, calc(100dvh - 96px)); overflow: hidden; padding: 4px; border: 0; --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); background: transparent; color: var(--dsw-alias-label-primary); box-shadow: var(--dsw-elevation-prominent); font: 13px/
|
|
76
|
+
.dsh-codex-popover { position: fixed; z-index: 1100; display: flex; flex-direction: column; inset: auto; margin: 0; box-sizing: border-box; width: 256px; max-width: calc(100vw - 32px); max-height: min(360px, calc(100dvh - 96px)); overflow: hidden; padding: 4px; border: 0; --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); background: transparent; color: var(--dsw-alias-label-primary); box-shadow: var(--dsw-elevation-prominent); font: 13px/1.5385 var(--dsw-font-family, system-ui); }
|
|
78
77
|
@media (prefers-reduced-motion: reduce) {
|
|
79
78
|
.dsh-codex-trigger .dsh-plugin-composer-chevron { transition: none; }
|
|
80
79
|
}
|
|
81
|
-
.dsh-codex-popover > :not(
|
|
82
|
-
.dsh-codex-
|
|
83
|
-
.dsh-codex-header { display: grid; grid-template-columns: 24px minmax(0, 1fr)
|
|
84
|
-
.dsh-codex-model, .dsh-codex-reset, .dsh-codex-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; border-radius: var(--dsw-radius-md); }
|
|
80
|
+
.dsh-codex-popover > :not(.dsh-codex-models) { flex-shrink: 0; }
|
|
81
|
+
.dsh-codex-models { min-height: 0; overflow-y: auto; }
|
|
82
|
+
.dsh-codex-header { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: start; gap: 4px; }
|
|
83
|
+
.dsh-codex-model, .dsh-codex-reset, .dsh-codex-back, .dsh-codex-apply { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; border-radius: var(--dsw-radius-md); }
|
|
85
84
|
.dsh-codex-popover svg { flex: none; vertical-align: middle; }
|
|
86
|
-
.dsh-codex-model {
|
|
87
|
-
.dsh-codex-model strong { display: block; color: var(--dsw-alias-state-business-primary); font-size: 14px; line-height:
|
|
88
|
-
.dsh-codex-model > span { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; line-height:
|
|
89
|
-
.dsh-codex-model:hover, .dsh-codex-reset:hover, .dsh-codex-back:hover { background: var(--dsw-alias-interactive-bg-hover, #f2f2f2); }
|
|
85
|
+
.dsh-codex-model { justify-self: center; min-width: 0; max-width: 100%; padding: 2px 2px 3px; text-align: center; }
|
|
86
|
+
.dsh-codex-model strong { display: block; color: var(--dsw-alias-state-business-primary); font-size: 14px; line-height: 1.4286; font-weight: 600; }
|
|
87
|
+
.dsh-codex-model > span { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; line-height: 1.4167; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
90
88
|
.dsh-codex-reset { display: grid; place-items: center; width: 24px; height: 24px; color: var(--dsw-alias-label-tertiary, #999); }
|
|
91
89
|
.dsh-codex-reset svg { width: 16px; height: 16px; }
|
|
92
90
|
.dsh-codex-back { padding: 4px 8px; color: var(--dsw-alias-label-tertiary, #777); }
|
|
93
91
|
.dsh-codex-popover h2, .dsh-codex-provider { margin: 0; padding: 6px 10px; font-size: 12px; font-weight: 400; color: var(--dsw-alias-label-tertiary, #777); }
|
|
94
92
|
.dsh-codex-provider { font-size: 11px; padding-top: 10px; }
|
|
95
93
|
.dsh-codex-option { box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 100%; min-height: 34px; padding: 5px 7px; border: 0; border-radius: var(--dsw-radius-md); color: inherit; background: transparent; text-align: left; font: inherit; cursor: pointer; }
|
|
96
|
-
.dsh-codex-option:
|
|
94
|
+
.dsh-codex-option:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
95
|
+
@media (hover: hover) {
|
|
96
|
+
.dsh-codex-trigger:hover:not(:disabled), .dsh-codex-option:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
97
|
+
.dsh-codex-model:hover, .dsh-codex-reset:hover, .dsh-codex-back:hover, .dsh-codex-apply:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, #f2f2f2); }
|
|
98
|
+
}
|
|
97
99
|
.dsh-codex-option span:first-child { min-width: 0; overflow-wrap: anywhere; }
|
|
98
100
|
.dsh-codex-option small { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 11px; }
|
|
99
101
|
.dsh-codex-option .dsh-codex-check { display: grid; place-items: center; flex: 0 0 14px; }
|
|
100
102
|
.dsh-codex-slider { padding: 10px 2px 3px; }
|
|
103
|
+
.dsh-codex-apply { padding: 3px 8px; color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; line-height: 1.5; }
|
|
101
104
|
.dsh-codex-rail { position: relative; height: 28px; }
|
|
102
105
|
.dsh-codex-track { position: absolute; inset: 3px 0; border-radius: 20px; background: linear-gradient(to right, var(--dsw-alias-state-business-primary) var(--dsh-codex-progress), var(--dsw-alias-interactive-bg-hover) var(--dsh-codex-progress)); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; }
|
|
103
106
|
.dsh-codex-track span { width: 4px; height: 4px; border-radius: 50%; background: #b8b8b8; }
|
|
@@ -108,6 +111,7 @@ const styles = `${composerStyles}
|
|
|
108
111
|
.dsh-codex-slider input::-moz-range-track { height: 28px; background: transparent; }
|
|
109
112
|
.dsh-codex-slider input::-moz-range-thumb { width: 26px; height: 26px; border: 1px solid #0000000a; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px #0002; }
|
|
110
113
|
.dsh-codex-notice { padding: 6px 10px; margin: 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
114
|
+
.dsh-codex-status { position: absolute; width: 1px; height: 1px; padding: 0; margin: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
111
115
|
.dsh-codex-popover [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
|
|
112
116
|
.dsh-codex-popover :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: -2px; }
|
|
113
117
|
.dsh-codex-popover :disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
@@ -127,7 +131,6 @@ const copy = {
|
|
|
127
131
|
model: "모델 선택",
|
|
128
132
|
effort: "추론 강도",
|
|
129
133
|
choose: "추론 수준 선택",
|
|
130
|
-
default: "제공자 기본값",
|
|
131
134
|
reset: "기본값으로 재설정",
|
|
132
135
|
loading: "모델을 불러오는 중입니다…",
|
|
133
136
|
empty: "사용할 수 있는 모델이 없습니다.",
|
|
@@ -135,13 +138,13 @@ const copy = {
|
|
|
135
138
|
missing: "현재 모델을 사용할 수 없습니다.",
|
|
136
139
|
error: "변경하지 못했습니다. 현재 선택을 확인한 뒤 다시 시도하세요.",
|
|
137
140
|
retry: "모델 목록 새로고침",
|
|
138
|
-
busy: "변경 중…"
|
|
141
|
+
busy: "변경 중…",
|
|
142
|
+
apply: "적용"
|
|
139
143
|
},
|
|
140
144
|
en: {
|
|
141
145
|
model: "Select model",
|
|
142
146
|
effort: "Reasoning effort",
|
|
143
147
|
choose: "Select reasoning effort",
|
|
144
|
-
default: "Provider default",
|
|
145
148
|
reset: "Reset to default",
|
|
146
149
|
loading: "Loading models…",
|
|
147
150
|
empty: "No models available.",
|
|
@@ -149,13 +152,13 @@ const copy = {
|
|
|
149
152
|
missing: "The current model is unavailable.",
|
|
150
153
|
error: "Could not change the selection. Check the current selection before retrying.",
|
|
151
154
|
retry: "Reload models",
|
|
152
|
-
busy: "Changing…"
|
|
155
|
+
busy: "Changing…",
|
|
156
|
+
apply: "Apply"
|
|
153
157
|
},
|
|
154
158
|
zh: {
|
|
155
159
|
model: "选择模型",
|
|
156
160
|
effort: "推理强度",
|
|
157
161
|
choose: "选择推理级别",
|
|
158
|
-
default: "提供商默认值",
|
|
159
162
|
reset: "重置为默认值",
|
|
160
163
|
loading: "正在加载模型…",
|
|
161
164
|
empty: "没有可用的模型。",
|
|
@@ -163,7 +166,8 @@ const copy = {
|
|
|
163
166
|
missing: "当前模型不可用。",
|
|
164
167
|
error: "更改失败。请确认当前选择后重试。",
|
|
165
168
|
retry: "刷新模型列表",
|
|
166
|
-
busy: "正在更改…"
|
|
169
|
+
busy: "正在更改…",
|
|
170
|
+
apply: "应用"
|
|
167
171
|
}
|
|
168
172
|
};
|
|
169
173
|
function apply(ctx) {
|
|
@@ -174,25 +178,28 @@ function apply(ctx) {
|
|
|
174
178
|
const { active: locale } = useLocaleSnapshot(ctx.locale);
|
|
175
179
|
const text = copy[locale.startsWith("ko") ? "ko" : locale.startsWith("zh") ? "zh" : "en"];
|
|
176
180
|
const [requestedPanel, setPanel] = (0, react.useState)(null);
|
|
181
|
+
const [staged, setStaged] = (0, react.useState)(null);
|
|
177
182
|
const [draft, setDraft] = (0, react.useState)(0);
|
|
178
183
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
179
184
|
const trigger = (0, react.useRef)(null);
|
|
180
185
|
const anchor = (0, react.useRef)(null);
|
|
181
186
|
const popup = (0, react.useRef)(null);
|
|
182
187
|
const pending = (0, react.useRef)(false);
|
|
188
|
+
const sliderSelection = (0, react.useRef)(null);
|
|
183
189
|
const mounted = (0, react.useRef)(true);
|
|
184
190
|
const id = (0, react.useId)();
|
|
185
191
|
const current = state.current;
|
|
186
192
|
const model = state.groups.find((item) => item.id === current?.provider)?.models.find((item) => item.id === current?.model);
|
|
187
193
|
const reasoning = model?.reasoning;
|
|
188
|
-
const efforts = reasoning?.efforts
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const panel = requestedPanel === "effort" && !
|
|
194
|
+
const efforts = reasoning?.efforts ?? [];
|
|
195
|
+
const stagedGroup = staged ? state.groups.find((item) => item.id === staged.provider) : void 0;
|
|
196
|
+
const stagedModel = staged ? stagedGroup?.models.find((item) => item.id === staged.model) : model;
|
|
197
|
+
const stagedEfforts = (stagedModel?.reasoning)?.efforts ?? [];
|
|
198
|
+
const panel = requestedPanel === "effort" && !stagedEfforts.length ? "model" : requestedPanel;
|
|
193
199
|
const effective = current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
194
200
|
const effortIndex = efforts.findIndex((item) => item.id === effective);
|
|
195
201
|
const effortLabel = efforts.length ? efforts[effortIndex]?.name ?? state.retainedEffort : void 0;
|
|
202
|
+
const stagedName = stagedModel?.name ?? (staged ? staged.model : current?.model) ?? text.model;
|
|
196
203
|
const busy = state.status === "selecting";
|
|
197
204
|
const disabled = locked || busy;
|
|
198
205
|
(0, react.useEffect)(() => {
|
|
@@ -203,10 +210,21 @@ function apply(ctx) {
|
|
|
203
210
|
};
|
|
204
211
|
}, [directory]);
|
|
205
212
|
const close = () => {
|
|
213
|
+
sliderSelection.current = null;
|
|
206
214
|
setPanel(null);
|
|
215
|
+
setStaged(null);
|
|
207
216
|
if (trigger.current) (0, _deepseek_ai_dsh_client_ui_primitives.focusWithoutRing)(trigger.current);
|
|
208
217
|
};
|
|
209
|
-
|
|
218
|
+
const restorePopupFocus = () => {
|
|
219
|
+
if (!popup.current?.contains(document.activeElement)) return;
|
|
220
|
+
const target = trigger.current?.disabled ? anchor.current?.parentElement?.querySelector("button:not(:disabled), input:not(:disabled), textarea:not(:disabled), [tabindex=\"0\"]") : trigger.current;
|
|
221
|
+
if (target) (0, _deepseek_ai_dsh_client_ui_primitives.focusWithoutRing)(target);
|
|
222
|
+
};
|
|
223
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(anchor, panel !== null, () => {
|
|
224
|
+
sliderSelection.current = null;
|
|
225
|
+
setPanel(null);
|
|
226
|
+
setStaged(null);
|
|
227
|
+
}, popup);
|
|
210
228
|
(0, react.useLayoutEffect)(() => {
|
|
211
229
|
if (!panel || !popup.current || !anchor.current) return;
|
|
212
230
|
const element = popup.current;
|
|
@@ -243,7 +261,7 @@ function apply(ctx) {
|
|
|
243
261
|
buttons[event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowUp" ? -1 : 1) + buttons.length) % buttons.length]?.focus();
|
|
244
262
|
};
|
|
245
263
|
element.addEventListener("keydown", keydown);
|
|
246
|
-
(element.querySelector("[aria-
|
|
264
|
+
(element.querySelector("[aria-current=true]") ?? element.querySelector("input:not(:disabled)") ?? element.querySelector("button:not(:disabled)"))?.focus();
|
|
247
265
|
return () => {
|
|
248
266
|
observer.disconnect();
|
|
249
267
|
window.removeEventListener("resize", close);
|
|
@@ -252,25 +270,44 @@ function apply(ctx) {
|
|
|
252
270
|
};
|
|
253
271
|
}, [panel]);
|
|
254
272
|
(0, react.useEffect)(() => {
|
|
255
|
-
if (locked)
|
|
273
|
+
if (locked) {
|
|
274
|
+
restorePopupFocus();
|
|
275
|
+
sliderSelection.current = null;
|
|
276
|
+
setPanel(null);
|
|
277
|
+
setStaged(null);
|
|
278
|
+
}
|
|
256
279
|
}, [locked]);
|
|
257
280
|
(0, react.useEffect)(() => {
|
|
281
|
+
const selection = sliderSelection.current;
|
|
282
|
+
sliderSelection.current = null;
|
|
283
|
+
if (selection && current?.provider === selection.provider && current?.model === selection.model && current?.reasoningEffort === selection.reasoningEffort) {
|
|
284
|
+
setStaged(null);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
restorePopupFocus();
|
|
258
288
|
setPanel(null);
|
|
289
|
+
setStaged(null);
|
|
259
290
|
}, [
|
|
260
291
|
current?.provider,
|
|
261
292
|
current?.model,
|
|
262
293
|
current?.reasoningEffort
|
|
263
294
|
]);
|
|
264
|
-
const select = async (selection) => {
|
|
295
|
+
const select = async (selection, keepSlider = false) => {
|
|
265
296
|
if (disabled || pending.current) return;
|
|
266
297
|
pending.current = true;
|
|
298
|
+
sliderSelection.current = keepSlider ? selection : null;
|
|
267
299
|
setFailed(false);
|
|
268
300
|
try {
|
|
269
301
|
const result = await directory.select(selection);
|
|
270
302
|
if (!mounted.current) return;
|
|
271
|
-
if (result.ok)
|
|
272
|
-
|
|
303
|
+
if (result.ok) {
|
|
304
|
+
if (!keepSlider) close();
|
|
305
|
+
} else {
|
|
306
|
+
sliderSelection.current = null;
|
|
307
|
+
setFailed(true);
|
|
308
|
+
}
|
|
273
309
|
} catch {
|
|
310
|
+
sliderSelection.current = null;
|
|
274
311
|
if (mounted.current) setFailed(true);
|
|
275
312
|
} finally {
|
|
276
313
|
pending.current = false;
|
|
@@ -282,19 +319,50 @@ function apply(ctx) {
|
|
|
282
319
|
close();
|
|
283
320
|
return;
|
|
284
321
|
}
|
|
322
|
+
setStaged(null);
|
|
285
323
|
setDraft(Math.max(0, effortIndex));
|
|
286
324
|
setFailed(false);
|
|
287
325
|
setPanel("effort");
|
|
288
326
|
directory.load().catch(() => {});
|
|
289
327
|
};
|
|
290
328
|
const selectEffort = (index) => {
|
|
291
|
-
|
|
292
|
-
const effort = efforts[index].id;
|
|
293
|
-
select({
|
|
329
|
+
const target = staged ?? (current ? {
|
|
294
330
|
provider: current.provider,
|
|
295
|
-
model: current.model
|
|
296
|
-
|
|
331
|
+
model: current.model
|
|
332
|
+
} : null);
|
|
333
|
+
const effort = stagedEfforts[index];
|
|
334
|
+
if (!target || !effort) return;
|
|
335
|
+
select({
|
|
336
|
+
provider: target.provider,
|
|
337
|
+
model: target.model,
|
|
338
|
+
reasoningEffort: effort.id
|
|
339
|
+
}, true);
|
|
340
|
+
};
|
|
341
|
+
const stageModel = (provider, option) => {
|
|
342
|
+
const same = provider === current?.provider && option.id === current?.model;
|
|
343
|
+
const optionReasoning = option.reasoning;
|
|
344
|
+
const optionEfforts = optionReasoning?.efforts ?? [];
|
|
345
|
+
if (optionEfforts.length < 2) {
|
|
346
|
+
if (same) {
|
|
347
|
+
close();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const effort = optionReasoning?.defaultEffort;
|
|
351
|
+
select({
|
|
352
|
+
provider,
|
|
353
|
+
model: option.id,
|
|
354
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const preferredEffort = same ? current?.reasoningEffort ?? optionReasoning?.defaultEffort : optionReasoning?.defaultEffort ?? current?.reasoningEffort;
|
|
359
|
+
setStaged({
|
|
360
|
+
provider,
|
|
361
|
+
model: option.id
|
|
297
362
|
});
|
|
363
|
+
setDraft(Math.max(0, optionEfforts.findIndex((item) => item.id === preferredEffort)));
|
|
364
|
+
setFailed(false);
|
|
365
|
+
setPanel("effort");
|
|
298
366
|
};
|
|
299
367
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
300
368
|
className: "dsh-codex-picker",
|
|
@@ -305,13 +373,16 @@ function apply(ctx) {
|
|
|
305
373
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, { size: 16 }),
|
|
306
374
|
label: model?.name ?? current?.model ?? text.model,
|
|
307
375
|
detail: effortLabel,
|
|
308
|
-
disabled,
|
|
376
|
+
disabled: locked,
|
|
377
|
+
"aria-disabled": disabled,
|
|
309
378
|
"aria-label": `${efforts.length ? text.choose : text.model}: ${model?.name ?? current?.model ?? text.model}${effortLabel ? `, ${effortLabel}` : ""}`,
|
|
310
379
|
title: `${model?.name ?? current?.model ?? text.model}${effortLabel ? ` ${effortLabel}` : ""}`,
|
|
311
380
|
"aria-haspopup": "dialog",
|
|
312
381
|
"aria-expanded": panel !== null,
|
|
313
382
|
"aria-controls": panel ? id : void 0,
|
|
314
|
-
onClick: (event) =>
|
|
383
|
+
onClick: (event) => {
|
|
384
|
+
if (!disabled) show(event.currentTarget);
|
|
385
|
+
}
|
|
315
386
|
})
|
|
316
387
|
}), panel && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
317
388
|
id,
|
|
@@ -320,7 +391,7 @@ function apply(ctx) {
|
|
|
320
391
|
"aria-label": panel === "model" ? text.model : text.effort,
|
|
321
392
|
className: "dsh-codex-popover",
|
|
322
393
|
children: [
|
|
323
|
-
panel === "model" ?
|
|
394
|
+
panel === "model" ? stagedEfforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
324
395
|
type: "button",
|
|
325
396
|
className: "dsh-codex-back",
|
|
326
397
|
onClick: () => setPanel("effort"),
|
|
@@ -331,31 +402,47 @@ function apply(ctx) {
|
|
|
331
402
|
]
|
|
332
403
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
333
404
|
className: "dsh-codex-header",
|
|
334
|
-
children: [
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
405
|
+
children: [
|
|
406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
407
|
+
type: "button",
|
|
408
|
+
className: "dsh-codex-reset",
|
|
409
|
+
disabled: locked || !(staged ?? current),
|
|
410
|
+
"aria-disabled": disabled || !(staged ?? current),
|
|
411
|
+
"aria-label": text.reset,
|
|
412
|
+
title: text.reset,
|
|
413
|
+
onClick: () => {
|
|
414
|
+
const target = staged ?? (current ? {
|
|
415
|
+
provider: current.provider,
|
|
416
|
+
model: current.model
|
|
417
|
+
} : null);
|
|
418
|
+
if (target) select(target);
|
|
419
|
+
},
|
|
420
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconResetOutlineRegular, {})
|
|
421
|
+
}),
|
|
422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
423
|
+
type: "button",
|
|
424
|
+
className: "dsh-codex-model",
|
|
425
|
+
disabled: locked,
|
|
426
|
+
"aria-disabled": disabled,
|
|
427
|
+
title: stagedName,
|
|
428
|
+
onClick: () => {
|
|
429
|
+
if (!disabled) setPanel("model");
|
|
430
|
+
},
|
|
431
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: stagedEfforts[draft]?.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
432
|
+
stagedName,
|
|
433
|
+
" ",
|
|
434
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 })
|
|
435
|
+
] })]
|
|
436
|
+
}),
|
|
437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
438
|
+
type: "button",
|
|
439
|
+
className: "dsh-codex-apply",
|
|
440
|
+
disabled: locked,
|
|
441
|
+
"aria-disabled": disabled,
|
|
442
|
+
onClick: () => selectEffort(draft),
|
|
443
|
+
children: text.apply
|
|
444
|
+
})
|
|
445
|
+
]
|
|
359
446
|
}),
|
|
360
447
|
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
361
448
|
className: "dsh-codex-notice",
|
|
@@ -363,7 +450,7 @@ function apply(ctx) {
|
|
|
363
450
|
children: text.loading
|
|
364
451
|
}),
|
|
365
452
|
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
366
|
-
className: "dsh-codex-notice",
|
|
453
|
+
className: panel === "effort" ? "dsh-codex-status" : "dsh-codex-notice",
|
|
367
454
|
role: "status",
|
|
368
455
|
children: text.busy
|
|
369
456
|
}),
|
|
@@ -384,7 +471,8 @@ function apply(ctx) {
|
|
|
384
471
|
}),
|
|
385
472
|
panel === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
386
473
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
387
|
-
|
|
474
|
+
className: "dsh-codex-models",
|
|
475
|
+
role: "group",
|
|
388
476
|
"aria-label": text.model,
|
|
389
477
|
children: state.groups.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
390
478
|
"aria-label": provider.name,
|
|
@@ -393,18 +481,10 @@ function apply(ctx) {
|
|
|
393
481
|
children: provider.name
|
|
394
482
|
}), provider.models.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
395
483
|
type: "button",
|
|
396
|
-
|
|
397
|
-
"aria-checked": provider.id === current?.provider && option.id === current?.model,
|
|
484
|
+
"aria-current": provider.id === current?.provider && option.id === current?.model ? "true" : void 0,
|
|
398
485
|
className: "dsh-codex-option",
|
|
399
486
|
disabled,
|
|
400
|
-
onClick: () =>
|
|
401
|
-
const effort = provider.id === current?.provider && option.id === current?.model ? current.reasoningEffort : option.reasoning?.defaultEffort;
|
|
402
|
-
select({
|
|
403
|
-
provider: provider.id,
|
|
404
|
-
model: option.id,
|
|
405
|
-
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
406
|
-
});
|
|
407
|
-
},
|
|
487
|
+
onClick: () => stageModel(provider.id, option),
|
|
408
488
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [option.name, option.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: option.description })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
409
489
|
className: "dsh-codex-check",
|
|
410
490
|
"aria-hidden": "true",
|
|
@@ -428,36 +508,28 @@ function apply(ctx) {
|
|
|
428
508
|
},
|
|
429
509
|
children: text.retry
|
|
430
510
|
})
|
|
431
|
-
] }) :
|
|
511
|
+
] }) : stagedEfforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
432
512
|
className: "dsh-codex-slider",
|
|
433
513
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
434
514
|
className: "dsh-codex-rail",
|
|
435
|
-
style: { "--dsh-codex-progress": `${
|
|
515
|
+
style: { "--dsh-codex-progress": `${stagedEfforts.length > 1 ? draft / (stagedEfforts.length - 1) * 100 : 0}%` },
|
|
436
516
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
437
517
|
className: "dsh-codex-track",
|
|
438
518
|
"aria-hidden": "true",
|
|
439
|
-
children:
|
|
519
|
+
children: stagedEfforts.map((effort, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-filled": index <= draft }, effort.id))
|
|
440
520
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
441
521
|
id: `${id}-effort`,
|
|
442
522
|
type: "range",
|
|
443
523
|
min: 0,
|
|
444
|
-
max:
|
|
524
|
+
max: stagedEfforts.length - 1,
|
|
445
525
|
step: 1,
|
|
446
526
|
value: draft,
|
|
447
|
-
disabled:
|
|
527
|
+
disabled: locked || stagedEfforts.length < 2,
|
|
528
|
+
"aria-disabled": disabled || stagedEfforts.length < 2,
|
|
448
529
|
"aria-label": text.effort,
|
|
449
|
-
"aria-valuetext":
|
|
450
|
-
onChange: (event) =>
|
|
451
|
-
|
|
452
|
-
onKeyUp: (event) => {
|
|
453
|
-
if ([
|
|
454
|
-
"ArrowLeft",
|
|
455
|
-
"ArrowRight",
|
|
456
|
-
"ArrowUp",
|
|
457
|
-
"ArrowDown",
|
|
458
|
-
"Home",
|
|
459
|
-
"End"
|
|
460
|
-
].includes(event.key)) selectEffort(Number(event.currentTarget.value));
|
|
530
|
+
"aria-valuetext": stagedEfforts[draft]?.name,
|
|
531
|
+
onChange: (event) => {
|
|
532
|
+
if (!disabled) setDraft(Number(event.target.value));
|
|
461
533
|
}
|
|
462
534
|
})]
|
|
463
535
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dungsil/dsh-plugin-ui-model-picker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Model and reasoning effort picker for the DeepSeek Harness composer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@dungsil/dsh-ui": "0.0.0",
|
|
41
41
|
"@types/react": "18.3.31",
|
|
42
42
|
"@types/react-dom": "18.3.7",
|
|
43
|
+
"happy-dom": "21.0.4",
|
|
43
44
|
"react": "18.3.1",
|
|
44
45
|
"react-dom": "18.3.1",
|
|
45
46
|
"tsdown": "0.23.4"
|
|
@@ -88,6 +89,7 @@
|
|
|
88
89
|
},
|
|
89
90
|
"scripts": {
|
|
90
91
|
"build": "tsdown",
|
|
91
|
-
"typecheck": "tsc --noEmit"
|
|
92
|
+
"typecheck": "tsc --noEmit",
|
|
93
|
+
"test": "node --test test/*.test.mjs"
|
|
92
94
|
}
|
|
93
95
|
}
|