@dungsil/dsh-plugin-ui-model-picker 0.0.0-stage → 0.1.0
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 +23 -2
- package/assets/light-dark-preview.webp +0 -0
- package/cordis.patch.yml +3 -0
- package/dist/client.js +430 -0
- package/dist/index.js +3 -0
- package/locale/en.json +6 -0
- package/locale/ko.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +86 -3
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Model Picker · 모델 피커
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
DSH Web 입력창에서 모델과 추론 강도를 선택합니다. 하나의 선택 버튼을 누르면 추론 강도 팝오버가 먼저 열리며, 팝오버 안의 모델명을 눌러 모델 목록으로 이동합니다.
|
|
6
|
+
|
|
7
|
+
위 이미지는 플러그인 컴포넌트에 예시 모델 목록과 테마 색상을 제공한 검증 화면을 각각 캡처해 합성했습니다. 표시된 모델명과 추론 단계는 예시이며, 실제 목록은 연결한 제공자가 결정합니다.
|
|
8
|
+
|
|
9
|
+
## 사용 방법
|
|
10
|
+
|
|
11
|
+
1. DSH 플러그인 설치 화면에서 `@dungsil/dsh-plugin-ui-model-picker`를 설치하고 활성화합니다.
|
|
12
|
+
2. 입력창 오른쪽 선택 버튼을 눌러 추론 강도 슬라이더를 엽니다. 방향키와 Home/End 키도 사용할 수 있습니다.
|
|
13
|
+
3. 모델을 바꾸려면 팝오버 상단의 모델명 부분을 누르고 제공자별 모델을 선택합니다. 오른쪽 위 재설정 아이콘은 기본 추론 강도를 복원합니다.
|
|
14
|
+
|
|
15
|
+
## 동작과 호환성
|
|
16
|
+
|
|
17
|
+
- DSH `0.2.1-rc.5` 이상 `0.3.0` 미만의 Web UI를 지원합니다. 기본 `ui-model-selection` 기능이 활성화되어 있어야 합니다.
|
|
18
|
+
- 공용 모델 디렉터리와 선택 API를 그대로 사용하므로 `/model` 명령과 선택 상태를 공유합니다. 별도 모델 설정을 저장하거나 API 키를 다루지 않습니다.
|
|
19
|
+
- 실제 제공자 목록과 모델별 추론 메타데이터만 사용합니다. 별도 모델 목록, Fast 모드, 고정 추론 단계는 추가하지 않습니다. 추론을 지원하지 않는 모델에는 슬라이더를 표시하지 않으며, 팝오버 안에서 모델은 계속 바꿀 수 있습니다.
|
|
20
|
+
- 다른 모델로 바꾸면 새 모델의 기본 추론 강도를 사용합니다. 기본값 재설정은 별도 강도 지정을 해제합니다.
|
|
21
|
+
- Escape, 바깥 클릭, 스크롤, 창 크기 변경으로 메뉴를 닫습니다. 메뉴의 방향키와 Tab 탐색을 지원합니다. 한국어·영어·중국어(간체)와 DSH 테마 색상을 따릅니다.
|
|
22
|
+
- 기본 모델 슬롯만 우선순위로 대체합니다. 제거하면 기본 선택 UI가 돌아오며 모델 선택은 유지됩니다. 같은 슬롯을 대체하는 다른 플러그인과는 함께 사용하지 마세요.
|
|
23
|
+
|
|
24
|
+
DSH가 제공하는 옵션과 플랫폼 글꼴에 따라 세부 모양은 다를 수 있습니다.
|
|
Binary file
|
package/cordis.patch.yml
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-model-picker", factory(require) { const module = { exports: {} }; const exports = module.exports;
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
let react = require("react");
|
|
4
|
+
let react_dom = require("react-dom");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
// src/styles.ts
|
|
7
|
+
const styles = `
|
|
8
|
+
.dsh-codex-picker { display: flex; align-items: center; min-width: 0; border-radius: 16px; color: var(--dsw-alias-label-primary, #252525); font: 13px/20px system-ui, sans-serif; }
|
|
9
|
+
.dsh-codex-picker button { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; height: 28px; padding: 0 6px; border-radius: 14px; min-width: 0; }
|
|
10
|
+
.dsh-codex-picker:hover { background: var(--dsw-alias-interactive-bg-hover, #8881); }
|
|
11
|
+
.dsh-codex-picker .dsh-codex-trigger { display: flex; gap: 5px; align-items: center; }
|
|
12
|
+
.dsh-codex-name { display: var(--dsh-composer-model-text-display, block); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: min(220px, 32vw); }
|
|
13
|
+
.dsh-codex-effort { color: var(--dsw-alias-label-tertiary, #777); }
|
|
14
|
+
.dsh-codex-popover { position: fixed; inset: auto; margin: 0; box-sizing: border-box; width: 256px; max-width: calc(100vw - 16px); max-height: calc(100dvh - 16px); overflow-y: auto; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #e8e8e8); border-radius: 16px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #252525); box-shadow: 0 5px 12px #0000000d, 0 2px 4px #00000005; font: 13px/20px system-ui, sans-serif; }
|
|
15
|
+
.dsh-codex-popover::backdrop { background: transparent; }
|
|
16
|
+
.dsh-codex-header { display: grid; grid-template-columns: 24px minmax(0, 1fr) 24px; align-items: start; gap: 4px; }
|
|
17
|
+
.dsh-codex-model, .dsh-codex-reset, .dsh-codex-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; border-radius: 6px; }
|
|
18
|
+
.dsh-codex-model { grid-column: 2; justify-self: center; min-width: 0; max-width: 100%; padding: 2px 10px 3px; text-align: center; }
|
|
19
|
+
.dsh-codex-model strong { display: block; color: #3983ff; font-size: 14px; line-height: 20px; font-weight: 600; }
|
|
20
|
+
.dsh-codex-model > span { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
21
|
+
.dsh-codex-model:hover, .dsh-codex-reset:hover, .dsh-codex-back:hover { background: var(--dsw-alias-interactive-bg-hover, #f2f2f2); }
|
|
22
|
+
.dsh-codex-reset { display: grid; place-items: center; width: 24px; height: 24px; color: var(--dsw-alias-label-tertiary, #999); }
|
|
23
|
+
.dsh-codex-back { padding: 4px 8px; color: var(--dsw-alias-label-tertiary, #777); }
|
|
24
|
+
.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); }
|
|
25
|
+
.dsh-codex-provider { font-size: 11px; padding-top: 10px; }
|
|
26
|
+
.dsh-codex-option { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-height: 30px; padding: 5px 10px; border: 0; border-radius: 9px; color: inherit; background: transparent; text-align: left; font: inherit; cursor: pointer; }
|
|
27
|
+
.dsh-codex-option:hover, .dsh-codex-option[aria-checked=true] { background: var(--dsw-alias-interactive-bg-hover, #f4f4f4); }
|
|
28
|
+
.dsh-codex-option span:first-child { min-width: 0; overflow-wrap: anywhere; }
|
|
29
|
+
.dsh-codex-option small { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 11px; }
|
|
30
|
+
.dsh-codex-option .dsh-codex-check { flex: 0 0 14px; }
|
|
31
|
+
.dsh-codex-slider { padding: 10px 2px 3px; }
|
|
32
|
+
.dsh-codex-rail { position: relative; height: 28px; }
|
|
33
|
+
.dsh-codex-track { position: absolute; inset: 3px 0; border-radius: 20px; background: linear-gradient(to right, #3983ff var(--dsh-codex-progress), var(--dsw-alias-interactive-bg-hover, #e8e8e8) var(--dsh-codex-progress)); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; }
|
|
34
|
+
.dsh-codex-track span { width: 4px; height: 4px; border-radius: 50%; background: #b8b8b8; }
|
|
35
|
+
.dsh-codex-track span[data-filled=true] { background: #ffffff80; }
|
|
36
|
+
.dsh-codex-slider input { appearance: none; -webkit-appearance: none; position: relative; display: block; width: 100%; height: 28px; padding: 0; margin: 0; background: transparent; cursor: pointer; }
|
|
37
|
+
.dsh-codex-slider input::-webkit-slider-runnable-track { height: 28px; background: transparent; }
|
|
38
|
+
.dsh-codex-slider input::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 28px; height: 28px; border: 1px solid #0000000a; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px #0002; }
|
|
39
|
+
.dsh-codex-slider input::-moz-range-track { height: 28px; background: transparent; }
|
|
40
|
+
.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; }
|
|
41
|
+
.dsh-codex-notice { padding: 6px 10px; margin: 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
42
|
+
.dsh-codex-popover [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
|
|
43
|
+
.dsh-codex-picker button:focus-visible, .dsh-codex-popover :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, #3186ce); outline-offset: 2px; }
|
|
44
|
+
.dsh-codex-picker button:disabled, .dsh-codex-popover :disabled { opacity: .5; cursor: default; }
|
|
45
|
+
`;
|
|
46
|
+
// src/client.tsx
|
|
47
|
+
const name = "dungsil-ui-model-picker";
|
|
48
|
+
const inject = [
|
|
49
|
+
"slots",
|
|
50
|
+
"locale",
|
|
51
|
+
"sessions",
|
|
52
|
+
"modelDirectories"
|
|
53
|
+
];
|
|
54
|
+
const copy = {
|
|
55
|
+
ko: {
|
|
56
|
+
model: "모델 선택",
|
|
57
|
+
effort: "추론 강도",
|
|
58
|
+
choose: "추론 수준 선택",
|
|
59
|
+
default: "제공자 기본값",
|
|
60
|
+
reset: "기본값으로 재설정",
|
|
61
|
+
loading: "모델을 불러오는 중입니다…",
|
|
62
|
+
empty: "사용할 수 있는 모델이 없습니다.",
|
|
63
|
+
partial: "일부 제공자의 모델을 불러오지 못했습니다.",
|
|
64
|
+
missing: "현재 모델을 사용할 수 없습니다.",
|
|
65
|
+
error: "변경하지 못했습니다. 현재 선택을 확인한 뒤 다시 시도하세요.",
|
|
66
|
+
retry: "모델 목록 새로고침",
|
|
67
|
+
busy: "변경 중…"
|
|
68
|
+
},
|
|
69
|
+
en: {
|
|
70
|
+
model: "Select model",
|
|
71
|
+
effort: "Reasoning effort",
|
|
72
|
+
choose: "Select reasoning effort",
|
|
73
|
+
default: "Provider default",
|
|
74
|
+
reset: "Reset to default",
|
|
75
|
+
loading: "Loading models…",
|
|
76
|
+
empty: "No models available.",
|
|
77
|
+
partial: "Some providers could not be loaded.",
|
|
78
|
+
missing: "The current model is unavailable.",
|
|
79
|
+
error: "Could not change the selection. Check the current selection before retrying.",
|
|
80
|
+
retry: "Reload models",
|
|
81
|
+
busy: "Changing…"
|
|
82
|
+
},
|
|
83
|
+
zh: {
|
|
84
|
+
model: "选择模型",
|
|
85
|
+
effort: "推理强度",
|
|
86
|
+
choose: "选择推理级别",
|
|
87
|
+
default: "提供商默认值",
|
|
88
|
+
reset: "重置为默认值",
|
|
89
|
+
loading: "正在加载模型…",
|
|
90
|
+
empty: "没有可用的模型。",
|
|
91
|
+
partial: "部分提供商的模型加载失败。",
|
|
92
|
+
missing: "当前模型不可用。",
|
|
93
|
+
error: "更改失败。请确认当前选择后重试。",
|
|
94
|
+
retry: "刷新模型列表",
|
|
95
|
+
busy: "正在更改…"
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function apply(ctx) {
|
|
99
|
+
function Picker({ locked, directory }) {
|
|
100
|
+
const state = (0, react.useSyncExternalStore)(directory.store.subscribe, directory.store.getSnapshot);
|
|
101
|
+
const locale = (0, react.useSyncExternalStore)((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot().active);
|
|
102
|
+
const text = copy[locale.startsWith("ko") ? "ko" : locale.startsWith("zh") ? "zh" : "en"];
|
|
103
|
+
const [panel, setPanel] = (0, react.useState)(null);
|
|
104
|
+
const [draft, setDraft] = (0, react.useState)(0);
|
|
105
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
106
|
+
const trigger = (0, react.useRef)(null);
|
|
107
|
+
const anchor = (0, react.useRef)(null);
|
|
108
|
+
const popup = (0, react.useRef)(null);
|
|
109
|
+
const pending = (0, react.useRef)(false);
|
|
110
|
+
const mounted = (0, react.useRef)(true);
|
|
111
|
+
const id = (0, react.useId)();
|
|
112
|
+
const current = state.current;
|
|
113
|
+
const model = state.groups.find((item) => item.id === current?.provider)?.models.find((item) => item.id === current?.model);
|
|
114
|
+
const reasoning = model?.reasoning;
|
|
115
|
+
const efforts = reasoning?.efforts.length ? [...reasoning.defaultEffort === void 0 ? [{
|
|
116
|
+
id: void 0,
|
|
117
|
+
name: text.default
|
|
118
|
+
}] : [], ...reasoning.efforts] : [];
|
|
119
|
+
const effective = current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
120
|
+
const effortIndex = efforts.findIndex((item) => item.id === effective);
|
|
121
|
+
const effortLabel = efforts[effortIndex]?.name ?? state.retainedEffort;
|
|
122
|
+
const busy = state.status === "selecting";
|
|
123
|
+
const disabled = locked || busy;
|
|
124
|
+
(0, react.useEffect)(() => {
|
|
125
|
+
mounted.current = true;
|
|
126
|
+
directory.load().catch(() => {});
|
|
127
|
+
return () => {
|
|
128
|
+
mounted.current = false;
|
|
129
|
+
};
|
|
130
|
+
}, [directory]);
|
|
131
|
+
const close = () => {
|
|
132
|
+
setPanel(null);
|
|
133
|
+
trigger.current?.focus();
|
|
134
|
+
};
|
|
135
|
+
(0, react.useEffect)(() => {
|
|
136
|
+
if (!panel || !popup.current || !anchor.current) return;
|
|
137
|
+
const element = popup.current;
|
|
138
|
+
element.setAttribute("popover", "auto");
|
|
139
|
+
element.showPopover();
|
|
140
|
+
const position = () => {
|
|
141
|
+
const box = anchor.current.getBoundingClientRect();
|
|
142
|
+
element.style.left = `${Math.max(8, Math.min(box.right - element.offsetWidth, window.innerWidth - element.offsetWidth - 8))}px`;
|
|
143
|
+
const above = box.top - element.offsetHeight - 8;
|
|
144
|
+
element.style.top = `${Math.max(8, above >= 8 ? above : Math.min(box.bottom + 8, window.innerHeight - element.offsetHeight - 8))}px`;
|
|
145
|
+
};
|
|
146
|
+
position();
|
|
147
|
+
const observer = new ResizeObserver(position);
|
|
148
|
+
observer.observe(element);
|
|
149
|
+
const toggle = (event) => {
|
|
150
|
+
if (event.newState === "closed") setPanel(null);
|
|
151
|
+
};
|
|
152
|
+
const scroll = (event) => {
|
|
153
|
+
if (!element.contains(event.target)) close();
|
|
154
|
+
};
|
|
155
|
+
element.addEventListener("toggle", toggle);
|
|
156
|
+
window.addEventListener("resize", close);
|
|
157
|
+
window.addEventListener("scroll", scroll, true);
|
|
158
|
+
const keydown = (event) => {
|
|
159
|
+
if (event.key === "Escape") {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
close();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (![
|
|
165
|
+
"ArrowDown",
|
|
166
|
+
"ArrowUp",
|
|
167
|
+
"Home",
|
|
168
|
+
"End"
|
|
169
|
+
].includes(event.key) || event.target instanceof HTMLInputElement) return;
|
|
170
|
+
const buttons = [...element.querySelectorAll("button:not(:disabled)")];
|
|
171
|
+
if (!buttons.length) return;
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
const index = buttons.indexOf(document.activeElement);
|
|
174
|
+
buttons[event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowUp" ? -1 : 1) + buttons.length) % buttons.length]?.focus();
|
|
175
|
+
};
|
|
176
|
+
element.addEventListener("keydown", keydown);
|
|
177
|
+
(element.querySelector("[aria-checked=true]") ?? element.querySelector("input:not(:disabled)") ?? element.querySelector("button:not(:disabled)"))?.focus();
|
|
178
|
+
return () => {
|
|
179
|
+
observer.disconnect();
|
|
180
|
+
window.removeEventListener("resize", close);
|
|
181
|
+
window.removeEventListener("scroll", scroll, true);
|
|
182
|
+
element.removeEventListener("keydown", keydown);
|
|
183
|
+
element.removeEventListener("toggle", toggle);
|
|
184
|
+
element.hidePopover();
|
|
185
|
+
};
|
|
186
|
+
}, [panel]);
|
|
187
|
+
(0, react.useEffect)(() => {
|
|
188
|
+
if (locked) setPanel(null);
|
|
189
|
+
}, [locked]);
|
|
190
|
+
(0, react.useEffect)(() => {
|
|
191
|
+
setPanel(null);
|
|
192
|
+
}, [
|
|
193
|
+
current?.provider,
|
|
194
|
+
current?.model,
|
|
195
|
+
current?.reasoningEffort
|
|
196
|
+
]);
|
|
197
|
+
const select = async (selection) => {
|
|
198
|
+
if (disabled || pending.current) return;
|
|
199
|
+
pending.current = true;
|
|
200
|
+
setFailed(false);
|
|
201
|
+
try {
|
|
202
|
+
const result = await directory.select(selection);
|
|
203
|
+
if (!mounted.current) return;
|
|
204
|
+
if (result.ok) close();
|
|
205
|
+
else setFailed(true);
|
|
206
|
+
} catch {
|
|
207
|
+
if (mounted.current) setFailed(true);
|
|
208
|
+
} finally {
|
|
209
|
+
pending.current = false;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const show = (button) => {
|
|
213
|
+
trigger.current = button;
|
|
214
|
+
setDraft(Math.max(0, effortIndex));
|
|
215
|
+
setFailed(false);
|
|
216
|
+
setPanel(panel ? null : "effort");
|
|
217
|
+
directory.load().catch(() => {});
|
|
218
|
+
};
|
|
219
|
+
const selectEffort = (index) => {
|
|
220
|
+
if (!current || !efforts[index]) return;
|
|
221
|
+
const effort = efforts[index].id;
|
|
222
|
+
select({
|
|
223
|
+
provider: current.provider,
|
|
224
|
+
model: current.model,
|
|
225
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
229
|
+
className: "dsh-codex-picker",
|
|
230
|
+
ref: anchor,
|
|
231
|
+
"aria-busy": busy,
|
|
232
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
233
|
+
type: "button",
|
|
234
|
+
className: "dsh-codex-trigger",
|
|
235
|
+
disabled,
|
|
236
|
+
"aria-label": `${text.choose}: ${model?.name ?? current?.model ?? text.model}${effortLabel ? `, ${effortLabel}` : ""}`,
|
|
237
|
+
"aria-haspopup": "dialog",
|
|
238
|
+
"aria-expanded": panel !== null,
|
|
239
|
+
"aria-controls": panel ? id : void 0,
|
|
240
|
+
onClick: (event) => show(event.currentTarget),
|
|
241
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
242
|
+
className: "dsh-codex-name",
|
|
243
|
+
children: panel ? text.choose : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [model?.name ?? current?.model ?? text.model, effortLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
244
|
+
className: "dsh-codex-effort",
|
|
245
|
+
children: effortLabel
|
|
246
|
+
})] })] })
|
|
247
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
248
|
+
"aria-hidden": "true",
|
|
249
|
+
children: "⌄"
|
|
250
|
+
})]
|
|
251
|
+
})
|
|
252
|
+
}), panel && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
253
|
+
id,
|
|
254
|
+
ref: popup,
|
|
255
|
+
role: "dialog",
|
|
256
|
+
"aria-label": panel === "model" ? text.model : text.effort,
|
|
257
|
+
className: "dsh-codex-popover",
|
|
258
|
+
children: [
|
|
259
|
+
panel === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
260
|
+
type: "button",
|
|
261
|
+
className: "dsh-codex-back",
|
|
262
|
+
onClick: () => setPanel("effort"),
|
|
263
|
+
children: ["‹ ", text.effort]
|
|
264
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
265
|
+
className: "dsh-codex-header",
|
|
266
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
267
|
+
type: "button",
|
|
268
|
+
className: "dsh-codex-model",
|
|
269
|
+
disabled,
|
|
270
|
+
"aria-label": `${text.model}: ${model?.name ?? current?.model ?? text.model}`,
|
|
271
|
+
onClick: () => setPanel("model"),
|
|
272
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: efforts[draft]?.name ?? effortLabel ?? text.default }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
273
|
+
model?.name ?? current?.model ?? text.model,
|
|
274
|
+
" ",
|
|
275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
276
|
+
"aria-hidden": "true",
|
|
277
|
+
children: "›"
|
|
278
|
+
})
|
|
279
|
+
] })]
|
|
280
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
281
|
+
type: "button",
|
|
282
|
+
className: "dsh-codex-reset",
|
|
283
|
+
disabled: disabled || !current,
|
|
284
|
+
"aria-label": text.reset,
|
|
285
|
+
title: text.reset,
|
|
286
|
+
onClick: () => {
|
|
287
|
+
if (current) select({
|
|
288
|
+
provider: current.provider,
|
|
289
|
+
model: current.model
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
293
|
+
width: "16",
|
|
294
|
+
height: "16",
|
|
295
|
+
viewBox: "0 0 16 16",
|
|
296
|
+
fill: "none",
|
|
297
|
+
stroke: "currentColor",
|
|
298
|
+
strokeWidth: "1.2",
|
|
299
|
+
"aria-hidden": "true",
|
|
300
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 4a5.5 5.5 0 1 1-1 6M3 1v3h3" })
|
|
301
|
+
})
|
|
302
|
+
})]
|
|
303
|
+
}),
|
|
304
|
+
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
305
|
+
className: "dsh-codex-notice",
|
|
306
|
+
role: "status",
|
|
307
|
+
children: text.loading
|
|
308
|
+
}),
|
|
309
|
+
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
310
|
+
className: "dsh-codex-notice",
|
|
311
|
+
role: "status",
|
|
312
|
+
children: text.busy
|
|
313
|
+
}),
|
|
314
|
+
(failed || state.error) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
315
|
+
className: "dsh-codex-notice",
|
|
316
|
+
role: "alert",
|
|
317
|
+
children: text.error
|
|
318
|
+
}),
|
|
319
|
+
state.failures.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
320
|
+
className: "dsh-codex-notice",
|
|
321
|
+
role: "status",
|
|
322
|
+
children: text.partial
|
|
323
|
+
}),
|
|
324
|
+
state.routable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
325
|
+
className: "dsh-codex-notice",
|
|
326
|
+
role: "status",
|
|
327
|
+
children: text.missing
|
|
328
|
+
}),
|
|
329
|
+
panel === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
330
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
331
|
+
role: "radiogroup",
|
|
332
|
+
"aria-label": text.model,
|
|
333
|
+
children: state.groups.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
334
|
+
"aria-label": provider.name,
|
|
335
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
336
|
+
className: "dsh-codex-provider",
|
|
337
|
+
children: provider.name
|
|
338
|
+
}), provider.models.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
339
|
+
type: "button",
|
|
340
|
+
role: "radio",
|
|
341
|
+
"aria-checked": provider.id === current?.provider && option.id === current?.model,
|
|
342
|
+
className: "dsh-codex-option",
|
|
343
|
+
disabled,
|
|
344
|
+
onClick: () => {
|
|
345
|
+
const effort = provider.id === current?.provider && option.id === current?.model ? current.reasoningEffort : option.reasoning?.defaultEffort;
|
|
346
|
+
select({
|
|
347
|
+
provider: provider.id,
|
|
348
|
+
model: option.id,
|
|
349
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
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", {
|
|
353
|
+
className: "dsh-codex-check",
|
|
354
|
+
"aria-hidden": "true",
|
|
355
|
+
children: provider.id === current?.provider && option.id === current?.model ? "✓" : ""
|
|
356
|
+
})]
|
|
357
|
+
}, option.id))]
|
|
358
|
+
}, provider.id))
|
|
359
|
+
}),
|
|
360
|
+
state.status !== "loading" && !state.groups.some((provider) => provider.models.length) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
361
|
+
className: "dsh-codex-notice",
|
|
362
|
+
role: "status",
|
|
363
|
+
children: text.empty
|
|
364
|
+
}),
|
|
365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
366
|
+
type: "button",
|
|
367
|
+
className: "dsh-codex-option",
|
|
368
|
+
disabled: busy,
|
|
369
|
+
onClick: () => {
|
|
370
|
+
setFailed(false);
|
|
371
|
+
directory.load().catch(() => {});
|
|
372
|
+
},
|
|
373
|
+
children: text.retry
|
|
374
|
+
})
|
|
375
|
+
] }) : efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
376
|
+
className: "dsh-codex-slider",
|
|
377
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
378
|
+
className: "dsh-codex-rail",
|
|
379
|
+
style: { "--dsh-codex-progress": `${efforts.length > 1 ? draft / (efforts.length - 1) * 100 : 0}%` },
|
|
380
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
381
|
+
className: "dsh-codex-track",
|
|
382
|
+
"aria-hidden": "true",
|
|
383
|
+
children: efforts.map((effort, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-filled": index <= draft }, effort.id ?? "default"))
|
|
384
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
385
|
+
id: `${id}-effort`,
|
|
386
|
+
type: "range",
|
|
387
|
+
min: 0,
|
|
388
|
+
max: efforts.length - 1,
|
|
389
|
+
step: 1,
|
|
390
|
+
value: draft,
|
|
391
|
+
disabled: disabled || efforts.length < 2,
|
|
392
|
+
"aria-label": text.effort,
|
|
393
|
+
"aria-valuetext": efforts[draft]?.name,
|
|
394
|
+
onChange: (event) => setDraft(Number(event.target.value)),
|
|
395
|
+
onPointerUp: (event) => selectEffort(Number(event.currentTarget.value)),
|
|
396
|
+
onKeyUp: (event) => {
|
|
397
|
+
if ([
|
|
398
|
+
"ArrowLeft",
|
|
399
|
+
"ArrowRight",
|
|
400
|
+
"ArrowUp",
|
|
401
|
+
"ArrowDown",
|
|
402
|
+
"Home",
|
|
403
|
+
"End"
|
|
404
|
+
].includes(event.key)) selectEffort(Number(event.currentTarget.value));
|
|
405
|
+
}
|
|
406
|
+
})]
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
]
|
|
410
|
+
}), document.body)] });
|
|
411
|
+
}
|
|
412
|
+
ctx.slots.inject("conversation.input.model", () => ctx.slots.register({
|
|
413
|
+
name: "conversation.input.model",
|
|
414
|
+
priority: -100
|
|
415
|
+
}, (props) => ctx.sessions.subagentAddress(props.sessionId) === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
|
|
416
|
+
locked: props.locked,
|
|
417
|
+
directory: ctx.modelDirectories.directoryFor(props.sessionId)
|
|
418
|
+
}, props.sessionId) : null));
|
|
419
|
+
ctx.effect(() => {
|
|
420
|
+
const style = document.createElement("style");
|
|
421
|
+
style.textContent = styles;
|
|
422
|
+
document.head.append(style);
|
|
423
|
+
return () => style.remove();
|
|
424
|
+
}, "ui-model-picker: styles");
|
|
425
|
+
}
|
|
426
|
+
exports.apply = apply;
|
|
427
|
+
exports.inject = inject;
|
|
428
|
+
exports.name = name;
|
|
429
|
+
|
|
430
|
+
return module.exports; } });
|
package/dist/index.js
ADDED
package/locale/en.json
ADDED
package/locale/ko.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dungsil/dsh-plugin-ui-model-picker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model and reasoning effort picker for the DeepSeek Harness composer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/dungsil/dsh-plugins.git",
|
|
9
|
+
"directory": "packages/ui-model-picker"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"locale",
|
|
14
|
+
"assets",
|
|
15
|
+
"cordis.patch.yml"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.js",
|
|
20
|
+
"./client": "./dist/client.js",
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./locale/*.json": "./locale/*.json"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
29
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.5",
|
|
30
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
|
|
31
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.5",
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-rc.5",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.5",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
|
|
37
|
+
"@dungsil/dsh-tsconfig": "0.0.0",
|
|
38
|
+
"@types/react": "18.3.31",
|
|
39
|
+
"@types/react-dom": "18.3.7",
|
|
40
|
+
"happy-dom": "21.0.4",
|
|
41
|
+
"react": "18.3.1",
|
|
42
|
+
"react-dom": "18.3.1",
|
|
43
|
+
"tsdown": "0.23.4"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"dsh": "^0.2.1-rc.5",
|
|
47
|
+
"node": ">=24.9.0"
|
|
48
|
+
},
|
|
49
|
+
"dsh": {
|
|
50
|
+
"bundle": {
|
|
51
|
+
"patch": "./cordis.patch.yml"
|
|
52
|
+
},
|
|
53
|
+
"client": {
|
|
54
|
+
"platform": "web",
|
|
55
|
+
"immediately": true,
|
|
56
|
+
"inject": [
|
|
57
|
+
"@deepseek-ai/dsh-client-locale",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-model-selection"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"market": {
|
|
65
|
+
"order": 9,
|
|
66
|
+
"title": {
|
|
67
|
+
"en": "Model Picker",
|
|
68
|
+
"ko": "모델 피커",
|
|
69
|
+
"zh": "模型选择器"
|
|
70
|
+
},
|
|
71
|
+
"description": {
|
|
72
|
+
"en": "A compact composer picker with model menus and a reasoning effort slider.",
|
|
73
|
+
"ko": "입력창에서 모델 메뉴와 추론 강도 슬라이더를 사용합니다.",
|
|
74
|
+
"zh": "在输入框中使用模型菜单和推理强度滑块。"
|
|
75
|
+
},
|
|
76
|
+
"category": "ui",
|
|
77
|
+
"labels": [],
|
|
78
|
+
"keywords": [
|
|
79
|
+
"models",
|
|
80
|
+
"reasoning",
|
|
81
|
+
"composer"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsdown",
|
|
86
|
+
"typecheck": "tsc --noEmit",
|
|
87
|
+
"test": "node --test test/*.test.mjs"
|
|
88
|
+
}
|
|
6
89
|
}
|