tensorgrid-ui 1.4.0 → 1.6.1
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/lib/client.js +326 -28
- package/lib/http-runtime.js +87 -4
- package/lib/index.js +17 -1
- package/lib/review-log.js +277 -0
- package/lib/review-settings.js +137 -11
- package/lib/review.js +299 -46
- package/lib/reviewers.js +242 -24
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -177,6 +177,22 @@ window.__ModuleLoader__.load({
|
|
|
177
177
|
'intensity.vivid': 'Vivid',
|
|
178
178
|
'accent.title': 'Obsidian / Ion accent',
|
|
179
179
|
'accent.hint': 'Leading tone of the interface and the glow',
|
|
180
|
+
'review.effortName': 'effort',
|
|
181
|
+
'review.other': 'another…',
|
|
182
|
+
'review.modelName': 'model name',
|
|
183
|
+
'review.custom': 'Your own set of reviewers — no mode matches it.',
|
|
184
|
+
'review.details': 'Detailed settings',
|
|
185
|
+
'review.fromList': 'back to the list',
|
|
186
|
+
'review.ours': 'ours',
|
|
187
|
+
'review.objectFalse': 'I disagree — this is real',
|
|
188
|
+
'review.accuracy': 'How often each reviewer turned out to be right',
|
|
189
|
+
'review.notChecked': 'not verified yet',
|
|
190
|
+
'review.byAgent': 'checked by the assistant',
|
|
191
|
+
'review.humanEvidence': 'Marked by the user.',
|
|
192
|
+
'review.lastReview': 'Last review',
|
|
193
|
+
'review.objectReal': 'I disagree — this is wrong',
|
|
194
|
+
'review.of': 'of',
|
|
195
|
+
'review.byHuman': 'your call',
|
|
180
196
|
'review.nav': 'Reviewers',
|
|
181
197
|
'review.title': 'Second opinion',
|
|
182
198
|
'review.hint': 'Independent reviewers examine finished work and report what is wrong. They run as separate products and do not see this conversation.',
|
|
@@ -232,6 +248,22 @@ window.__ModuleLoader__.load({
|
|
|
232
248
|
'intensity.vivid': '强烈',
|
|
233
249
|
'accent.title': 'Obsidian / Ion 主色',
|
|
234
250
|
'accent.hint': '界面与光晕的主色调',
|
|
251
|
+
'review.effortName': '强度',
|
|
252
|
+
'review.other': '其他…',
|
|
253
|
+
'review.modelName': '模型名称',
|
|
254
|
+
'review.custom': '自定义审阅者组合——没有匹配的模式。',
|
|
255
|
+
'review.details': '详细设置',
|
|
256
|
+
'review.fromList': '返回列表',
|
|
257
|
+
'review.ours': '我们的',
|
|
258
|
+
'review.objectFalse': '我不同意——这是真的',
|
|
259
|
+
'review.accuracy': '各审阅者的正确率',
|
|
260
|
+
'review.notChecked': '尚未核实',
|
|
261
|
+
'review.byAgent': '由助手核实',
|
|
262
|
+
'review.humanEvidence': '由用户标记。',
|
|
263
|
+
'review.lastReview': '上次审阅',
|
|
264
|
+
'review.objectReal': '我不同意——这是错的',
|
|
265
|
+
'review.of': '共',
|
|
266
|
+
'review.byHuman': '你的判断',
|
|
235
267
|
'review.nav': '审阅者',
|
|
236
268
|
'review.title': '第二意见',
|
|
237
269
|
'review.hint': '独立审阅者检查已完成的工作并报告问题。它们作为独立产品运行,看不到本次对话。',
|
|
@@ -1584,6 +1616,22 @@ window.__ModuleLoader__.load({
|
|
|
1584
1616
|
'intensity.vivid': 'Ярко',
|
|
1585
1617
|
'accent.title': 'Акцент Obsidian / Ion',
|
|
1586
1618
|
'accent.hint': 'Ведущий тон интерфейса и свечения',
|
|
1619
|
+
'review.effortName': 'усилие',
|
|
1620
|
+
'review.other': 'другая…',
|
|
1621
|
+
'review.modelName': 'имя модели',
|
|
1622
|
+
'review.custom': 'Свой набор ревизоров — ни один режим ему не соответствует.',
|
|
1623
|
+
'review.details': 'Подробные настройки',
|
|
1624
|
+
'review.fromList': 'вернуться к списку',
|
|
1625
|
+
'review.ours': 'наш',
|
|
1626
|
+
'review.objectFalse': 'Не согласен — это настоящая',
|
|
1627
|
+
'review.accuracy': 'Как часто каждый ревизор оказывался прав',
|
|
1628
|
+
'review.notChecked': 'ещё не проверено',
|
|
1629
|
+
'review.byAgent': 'проверил помощник',
|
|
1630
|
+
'review.humanEvidence': 'Отмечено пользователем.',
|
|
1631
|
+
'review.lastReview': 'Последний обзор',
|
|
1632
|
+
'review.objectReal': 'Не согласен — это ложная',
|
|
1633
|
+
'review.of': 'из',
|
|
1634
|
+
'review.byHuman': 'ваше решение',
|
|
1587
1635
|
'review.nav': 'Ревизоры',
|
|
1588
1636
|
'review.title': 'Второе мнение',
|
|
1589
1637
|
'review.hint': 'Независимые ревизоры смотрят готовую работу и сообщают, что в ней не так. Работают как отдельные продукты и нашей переписки не видят.',
|
|
@@ -1740,7 +1788,7 @@ window.__ModuleLoader__.load({
|
|
|
1740
1788
|
// поэтому строка остаётся согласованной при любой палитре.
|
|
1741
1789
|
'.dsx-setting{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;}',
|
|
1742
1790
|
'.dsx-setting__title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;}',
|
|
1743
|
-
'.dsx-rev__list{display:flex;flex-direction:column;gap:8px}.dsx-rev__card{padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);opacity:.6;transition:opacity .15s ease,border-color .15s ease}.dsx-rev__card--on{opacity:1;border-color:var(--dsw-alias-border-l2)}.dsx-rev__head{display:flex;align-items:center;gap:8px;cursor:pointer}.dsx-rev__name{font-weight:600;color:var(--dsw-alias-label-primary)}.dsx-rev__fields{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;padding-left:22px}.dsx-rev__field{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsx-rev__select{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsx-subs{display:flex;flex-direction:column;gap:14px;padding:4px 0}.dsx-subs__head{display:flex;flex-direction:column;gap:4px}.dsx-subs__title{font-weight:600;font-size:15px;color:var(--dsw-alias-label-primary)}.dsx-subs__note{padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.dsx-setting--stack{flex-direction:column;align-items:stretch;gap:10px}.dsx-auth__list{display:flex;flex-direction:column;gap:6px}.dsx-auth__row{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsx-auth__name{color:var(--dsw-alias-label-primary)}.dsx-auth__badge{margin-left:8px;padding:1px 6px;border-radius:999px;font-size:11px;color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-border-l2)}.dsx-auth__panel{display:flex;flex-direction:column;gap:8px;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1)}.dsx-auth__notice,.dsx-auth__prompt{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary)}.dsx-auth__link{color:var(--dsw-alias-brand-primary);word-break:break-all}.dsx-auth__code{font-family:ui-monospace,monospace;letter-spacing:.08em;color:var(--dsw-alias-label-primary)}.dsx-auth__input{flex:1;min-width:0;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dsx-setting__hint--strong{color:var(--dsw-alias-label-primary)}.dsx-setting__hint--warn{color:var(--dsw-alias-state-warn-primary)}.dsx-setting__hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;}',
|
|
1791
|
+
'.dsx-rev__panel{margin-top:14px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__panel-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.dsx-rev__count{padding:1px 7px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsx-rev__findings{display:flex;flex-direction:column;gap:8px}.dsx-rev__finding{padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}.dsx-rev__finding--false{opacity:.55}.dsx-rev__finding-head{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.dsx-rev__finding-title{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px;flex:1}.dsx-rev__sev{padding:1px 6px;border-radius:4px;font-size:10px;text-transform:uppercase;letter-spacing:.03em}.dsx-rev__sev--crit{background:#5c1f22;color:#ffb4b4}.dsx-rev__sev--high{background:#5a3f16;color:#f0cd8a}.dsx-rev__sev--unknown{background:#3a3a44;color:#c9c9d4}.dsx-rev__sev--low{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}.dsx-rev__who{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__where{margin-top:4px;font-family:ui-monospace,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__what{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dsx-rev__verdict{display:flex;align-items:flex-start;gap:6px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l1);font-size:11px;color:var(--dsw-alias-label-secondary)}.dsx-rev__verdict-mark{font-weight:700}.dsx-rev__verdict-text{flex:1;line-height:1.4}.dsx-rev__by{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dsx-rev__object{margin-top:5px}.dsx-rev__object-btn{border:0;background:none;padding:0;color:var(--dsw-alias-label-tertiary);font-size:11px;cursor:pointer;text-decoration:underline dotted}.dsx-rev__object-btn:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__stats{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__stat{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.dsx-rev__stat-name{color:var(--dsw-alias-label-secondary)}.dsx-rev__stat-rate{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}.dsx-rev__modes{display:flex;gap:8px;margin:10px 0 4px}.dsx-rev__mode{flex:1;display:flex;flex-direction:column;gap:3px;padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left;transition:border-color .15s ease,background .15s ease}.dsx-rev__mode:hover{border-color:var(--dsw-alias-border-l2)}.dsx-rev__mode--on{border-color:var(--dsw-alias-brand-default);background:var(--dsw-alias-bg-layer-1)}.dsx-rev__mode-name{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px}.dsx-rev__mode-hint{font-size:11px;line-height:1.35}.dsx-rev__toggle{align-self:flex-start;margin-top:6px;padding:2px 0;border:0;background:none;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}.dsx-rev__toggle:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__tag{margin-left:6px;padding:1px 6px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:10px}.dsx-rev__custom{display:inline-flex;align-items:center;gap:4px}.dsx-rev__input{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;width:170px}.dsx-rev__back{border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:1;padding:0 2px}.dsx-rev__list{display:flex;flex-direction:column;gap:8px}.dsx-rev__card{padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);opacity:.6;transition:opacity .15s ease,border-color .15s ease}.dsx-rev__card--on{opacity:1;border-color:var(--dsw-alias-border-l2)}.dsx-rev__head{display:flex;align-items:center;gap:8px;cursor:pointer}.dsx-rev__name{font-weight:600;color:var(--dsw-alias-label-primary)}.dsx-rev__fields{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;padding-left:22px}.dsx-rev__field{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsx-rev__select{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsx-subs{display:flex;flex-direction:column;gap:14px;padding:4px 0}.dsx-subs__head{display:flex;flex-direction:column;gap:4px}.dsx-subs__title{font-weight:600;font-size:15px;color:var(--dsw-alias-label-primary)}.dsx-subs__note{padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.dsx-setting--stack{flex-direction:column;align-items:stretch;gap:10px}.dsx-auth__list{display:flex;flex-direction:column;gap:6px}.dsx-auth__row{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsx-auth__name{color:var(--dsw-alias-label-primary)}.dsx-auth__badge{margin-left:8px;padding:1px 6px;border-radius:999px;font-size:11px;color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-border-l2)}.dsx-auth__panel{display:flex;flex-direction:column;gap:8px;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1)}.dsx-auth__notice,.dsx-auth__prompt{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary)}.dsx-auth__link{color:var(--dsw-alias-brand-primary);word-break:break-all}.dsx-auth__code{font-family:ui-monospace,monospace;letter-spacing:.08em;color:var(--dsw-alias-label-primary)}.dsx-auth__input{flex:1;min-width:0;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dsx-setting__hint--strong{color:var(--dsw-alias-label-primary)}.dsx-setting__hint--warn{color:var(--dsw-alias-state-warn-primary)}.dsx-setting__hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;}',
|
|
1744
1792
|
'.dsx-setting__control{display:inline-flex;padding:2px;gap:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);flex:none;}',
|
|
1745
1793
|
'.dsx-seg{appearance:none;border:0;cursor:pointer;padding:4px 10px;border-radius:6px;font-size:12px;line-height:18px;background:transparent;color:var(--dsw-alias-label-secondary);transition:background .16s ease,color .16s ease;}',
|
|
1746
1794
|
'.dsx-seg:hover{color:var(--dsw-alias-label-primary);}',
|
|
@@ -2241,6 +2289,21 @@ window.__ModuleLoader__.load({
|
|
|
2241
2289
|
})
|
|
2242
2290
|
}
|
|
2243
2291
|
|
|
2292
|
+
/** Класс важности: имена уровней приходят с Host по-русски. */
|
|
2293
|
+
function severityClass(severity) {
|
|
2294
|
+
if (severity === 'критично') return 'crit'
|
|
2295
|
+
if (severity === 'серьёзно') return 'high'
|
|
2296
|
+
if (severity === 'не указана') return 'unknown'
|
|
2297
|
+
return 'low'
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/** Знак приговора. Пустой кружок — проверки ещё не было. */
|
|
2301
|
+
function verdictMark(verdict) {
|
|
2302
|
+
if (verdict === 'confirmed') return '✓'
|
|
2303
|
+
if (verdict === 'false') return '✗'
|
|
2304
|
+
if (verdict === 'deferred') return '⋯'
|
|
2305
|
+
return '○'
|
|
2306
|
+
}
|
|
2244
2307
|
// ── Ревизоры ─────────────────────────────────────────────────────────
|
|
2245
2308
|
//
|
|
2246
2309
|
// Раздел настроек, где выбирают, кто смотрит работу вторым мнением,
|
|
@@ -2252,9 +2315,86 @@ window.__ModuleLoader__.load({
|
|
|
2252
2315
|
// свой Claude Code и Codex, и навязывать поверх нечего.
|
|
2253
2316
|
const REVIEW_PATH = '/api/tensorgrid.review'
|
|
2254
2317
|
|
|
2318
|
+
/**
|
|
2319
|
+
* Выбор значения: известные варианты плюс «своё».
|
|
2320
|
+
*
|
|
2321
|
+
* Список моделей — подсказка, а не ограничение. У наших ревизоров он
|
|
2322
|
+
* приходит живым из маршрута, у внешних продуктов зашит коротким: спросить
|
|
2323
|
+
* у Claude Code или Codex их собственный перечень нечем. Поэтому рядом со
|
|
2324
|
+
* списком всегда есть «другая» — иначе любой новый выпуск модели делал бы
|
|
2325
|
+
* интерфейс бесполезным до следующего обновления пакета.
|
|
2326
|
+
*/
|
|
2327
|
+
function ValuePicker({ t, value, options, disabled, onPick, placeholder }) {
|
|
2328
|
+
const known = options.some((option) => option.value === value)
|
|
2329
|
+
const [custom, setCustom] = React.useState(value !== null && !known)
|
|
2330
|
+
|
|
2331
|
+
if (custom) {
|
|
2332
|
+
return jsxs('span', {
|
|
2333
|
+
className: 'dsx-rev__custom',
|
|
2334
|
+
children: [
|
|
2335
|
+
jsx('input', {
|
|
2336
|
+
className: 'dsx-rev__input',
|
|
2337
|
+
type: 'text',
|
|
2338
|
+
value: value === null ? '' : value,
|
|
2339
|
+
placeholder: placeholder,
|
|
2340
|
+
disabled: disabled,
|
|
2341
|
+
onChange: (event) => onPick(event.target.value.trim() === '' ? null : event.target.value.trim()),
|
|
2342
|
+
}),
|
|
2343
|
+
jsx('button', {
|
|
2344
|
+
type: 'button',
|
|
2345
|
+
className: 'dsx-rev__back',
|
|
2346
|
+
disabled: disabled,
|
|
2347
|
+
title: t('review.fromList'),
|
|
2348
|
+
onClick: () => { setCustom(false); onPick(null) },
|
|
2349
|
+
children: '×',
|
|
2350
|
+
}),
|
|
2351
|
+
],
|
|
2352
|
+
})
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
return jsxs('select', {
|
|
2356
|
+
className: 'dsx-rev__select',
|
|
2357
|
+
value: value === null ? '' : value,
|
|
2358
|
+
disabled: disabled,
|
|
2359
|
+
onChange: (event) => {
|
|
2360
|
+
if (event.target.value === '\u0000custom') { setCustom(true); return }
|
|
2361
|
+
onPick(event.target.value === '' ? null : event.target.value)
|
|
2362
|
+
},
|
|
2363
|
+
children: [
|
|
2364
|
+
jsx('option', { value: '', children: t('review.asConfigured') }, ''),
|
|
2365
|
+
...options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value)),
|
|
2366
|
+
jsx('option', { value: '\u0000custom', children: t('review.other') }, 'custom'),
|
|
2367
|
+
],
|
|
2368
|
+
})
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/** Класс важности: имена уровней приходят с Host по-русски. */
|
|
2372
|
+
function severityClass(severity) {
|
|
2373
|
+
if (severity === 'критично') return 'crit'
|
|
2374
|
+
if (severity === 'серьёзно') return 'high'
|
|
2375
|
+
if (severity === 'не указана') return 'unknown'
|
|
2376
|
+
return 'low'
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/** Знак приговора. Пустой кружок — проверки ещё не было. */
|
|
2380
|
+
function verdictMark(verdict) {
|
|
2381
|
+
if (verdict === 'confirmed') return '✓'
|
|
2382
|
+
if (verdict === 'false') return '✗'
|
|
2383
|
+
if (verdict === 'deferred') return '⋯'
|
|
2384
|
+
return '○'
|
|
2385
|
+
}
|
|
2386
|
+
// ── Ревизоры ─────────────────────────────────────────────────────────
|
|
2387
|
+
//
|
|
2388
|
+
// Раздел настроек: чем и как проверять работу вторым мнением.
|
|
2389
|
+
//
|
|
2390
|
+
// Сверху режимы — один выбор для тех, кто не хочет вникать. Ниже
|
|
2391
|
+
// поштучно: кто участвует, какая модель, какое усилие. Поштучное НЕ
|
|
2392
|
+
// спрятано за «для опытных»: человек, знающий, какая модель ему нужна,
|
|
2393
|
+
// не должен её выпрашивать.
|
|
2255
2394
|
function ReviewRow({ t }) {
|
|
2256
2395
|
const [state, setState] = React.useState(null)
|
|
2257
2396
|
const [busy, setBusy] = React.useState(false)
|
|
2397
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
2258
2398
|
|
|
2259
2399
|
const translate = React.useRef(t)
|
|
2260
2400
|
translate.current = t
|
|
@@ -2283,9 +2423,18 @@ window.__ModuleLoader__.load({
|
|
|
2283
2423
|
const refresh = React.useCallback(async (body) => {
|
|
2284
2424
|
setBusy(true)
|
|
2285
2425
|
try {
|
|
2286
|
-
|
|
2426
|
+
const next = await send(body)
|
|
2427
|
+
// Ответ без каталога — это отказ, а не новое состояние. Замена
|
|
2428
|
+
// состояния целиком убирала все карточки после одного неудачного
|
|
2429
|
+
// нажатия: ни повторить, ни увидеть выбранное было нельзя.
|
|
2430
|
+
setState((previous) => {
|
|
2431
|
+
const complete = Array.isArray(next.catalogue) && next.catalogue.length > 0
|
|
2432
|
+
if (complete) return next
|
|
2433
|
+
return previous === null ? next : { ...previous, problem: next.problem ?? 'сохранить не удалось' }
|
|
2434
|
+
})
|
|
2287
2435
|
} catch (error) {
|
|
2288
|
-
|
|
2436
|
+
const text = String(error && error.message ? error.message : error)
|
|
2437
|
+
setState((previous) => (previous === null ? { catalogue: [], problem: text } : { ...previous, problem: text }))
|
|
2289
2438
|
} finally {
|
|
2290
2439
|
setBusy(false)
|
|
2291
2440
|
}
|
|
@@ -2294,11 +2443,12 @@ window.__ModuleLoader__.load({
|
|
|
2294
2443
|
React.useEffect(() => { refresh() }, [refresh])
|
|
2295
2444
|
|
|
2296
2445
|
const catalogue = state !== null && Array.isArray(state.catalogue) ? state.catalogue : []
|
|
2446
|
+
const modes = state !== null && Array.isArray(state.modes) ? state.modes : []
|
|
2297
2447
|
const settings = state !== null && state.settings ? state.settings : null
|
|
2448
|
+
const journal = state !== null && state.journal ? state.journal : null
|
|
2298
2449
|
|
|
2299
|
-
// Изменение
|
|
2300
|
-
//
|
|
2301
|
-
// забыть нажать и потом удивляться.
|
|
2450
|
+
// Изменение сохраняется сразу. Отдельная кнопка «применить» создаёт
|
|
2451
|
+
// состояние «изменено, но не сохранено», в котором легко забыть нажать.
|
|
2302
2452
|
const save = React.useCallback((next) => { refresh(next) }, [refresh])
|
|
2303
2453
|
|
|
2304
2454
|
const toggle = (kind) => {
|
|
@@ -2310,21 +2460,23 @@ window.__ModuleLoader__.load({
|
|
|
2310
2460
|
|
|
2311
2461
|
const pick = (kind, field, value) => {
|
|
2312
2462
|
if (settings === null) return
|
|
2313
|
-
const
|
|
2463
|
+
const previous = settings.choices[kind] || { provider: null, model: null, effort: null }
|
|
2464
|
+
const choices = { ...settings.choices, [kind]: { ...previous, [field]: value } }
|
|
2314
2465
|
save({ ...settings, choices })
|
|
2315
2466
|
}
|
|
2316
2467
|
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
})
|
|
2468
|
+
// Модель нашего ревизора приходит парой «провайдер и имя»: у OpenRouter
|
|
2469
|
+
// имя само содержит косую черту, и делить по ней нельзя — провайдер там
|
|
2470
|
+
// называется иначе, чем первая часть имени.
|
|
2471
|
+
const pickModel = (kind, entry) => {
|
|
2472
|
+
if (settings === null) return
|
|
2473
|
+
const previous = settings.choices[kind] || { provider: null, model: null, effort: null }
|
|
2474
|
+
const choices = {
|
|
2475
|
+
...settings.choices,
|
|
2476
|
+
[kind]: { ...previous, provider: entry === null ? null : entry.provider, model: entry === null ? null : entry.id },
|
|
2477
|
+
}
|
|
2478
|
+
save({ ...settings, choices })
|
|
2479
|
+
}
|
|
2328
2480
|
|
|
2329
2481
|
return jsxs('div', {
|
|
2330
2482
|
className: 'dsx-subs',
|
|
@@ -2339,24 +2491,54 @@ window.__ModuleLoader__.load({
|
|
|
2339
2491
|
state !== null && state.problem
|
|
2340
2492
|
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: state.problem })
|
|
2341
2493
|
: null,
|
|
2342
|
-
|
|
2494
|
+
|
|
2495
|
+
// Режимы
|
|
2496
|
+
modes.length === 0 ? null : jsx('div', {
|
|
2497
|
+
className: 'dsx-rev__modes',
|
|
2498
|
+
children: modes.map((mode) => jsxs('button', {
|
|
2499
|
+
type: 'button',
|
|
2500
|
+
className: 'dsx-rev__mode' + (settings !== null && settings.mode === mode.id ? ' dsx-rev__mode--on' : ''),
|
|
2501
|
+
disabled: busy,
|
|
2502
|
+
onClick: () => save({ mode: mode.id, applyMode: true }),
|
|
2503
|
+
children: [
|
|
2504
|
+
jsx('span', { className: 'dsx-rev__mode-name', children: mode.label }),
|
|
2505
|
+
jsx('span', { className: 'dsx-rev__mode-hint', children: mode.hint }),
|
|
2506
|
+
],
|
|
2507
|
+
}, mode.id)),
|
|
2508
|
+
}),
|
|
2509
|
+
settings !== null && settings.mode === null
|
|
2510
|
+
? jsx('div', { className: 'dsx-setting__hint', children: t('review.custom') })
|
|
2511
|
+
: null,
|
|
2512
|
+
|
|
2513
|
+
// Подробные настройки
|
|
2514
|
+
jsx('button', {
|
|
2515
|
+
type: 'button',
|
|
2516
|
+
className: 'dsx-rev__toggle',
|
|
2517
|
+
onClick: () => setExpanded(!expanded),
|
|
2518
|
+
children: (expanded ? '▾ ' : '▸ ') + t('review.details'),
|
|
2519
|
+
}),
|
|
2520
|
+
|
|
2521
|
+
!expanded ? null : jsx('div', {
|
|
2343
2522
|
className: 'dsx-rev__list',
|
|
2344
2523
|
children: catalogue.map((entry) => {
|
|
2345
2524
|
const on = settings !== null && settings.enabled.includes(entry.kind)
|
|
2346
|
-
|
|
2525
|
+
// Запись может отсутствовать, если списки Host и браузера
|
|
2526
|
+
// разойдутся; раньше обращение к ней роняло весь раздел.
|
|
2527
|
+
const stored = settings === null ? null : settings.choices[entry.kind]
|
|
2528
|
+
const choice = stored || { provider: null, model: null, effort: null }
|
|
2529
|
+
const models = Array.isArray(entry.models) ? entry.models : []
|
|
2530
|
+
|
|
2347
2531
|
return jsxs('div', {
|
|
2348
2532
|
className: 'dsx-rev__card' + (on ? ' dsx-rev__card--on' : ''),
|
|
2349
2533
|
children: [
|
|
2350
2534
|
jsxs('label', {
|
|
2351
2535
|
className: 'dsx-rev__head',
|
|
2352
2536
|
children: [
|
|
2353
|
-
jsx('input', {
|
|
2354
|
-
type: 'checkbox',
|
|
2355
|
-
checked: on,
|
|
2356
|
-
disabled: busy,
|
|
2357
|
-
onChange: () => toggle(entry.kind),
|
|
2358
|
-
}),
|
|
2537
|
+
jsx('input', { type: 'checkbox', checked: on, disabled: busy, onChange: () => toggle(entry.kind) }),
|
|
2359
2538
|
jsx('span', { className: 'dsx-rev__name', children: entry.label }),
|
|
2539
|
+
entry.internal
|
|
2540
|
+
? jsx('span', { className: 'dsx-rev__tag', children: t('review.ours') })
|
|
2541
|
+
: null,
|
|
2360
2542
|
],
|
|
2361
2543
|
}),
|
|
2362
2544
|
jsxs('div', {
|
|
@@ -2366,14 +2548,34 @@ window.__ModuleLoader__.load({
|
|
|
2366
2548
|
className: 'dsx-rev__field',
|
|
2367
2549
|
children: [
|
|
2368
2550
|
jsx('span', { children: t('review.model') }),
|
|
2369
|
-
|
|
2551
|
+
jsx(ValuePicker, {
|
|
2552
|
+
t: t,
|
|
2553
|
+
value: choice.model,
|
|
2554
|
+
disabled: busy,
|
|
2555
|
+
placeholder: t('review.modelName'),
|
|
2556
|
+
options: models.map((m) => ({
|
|
2557
|
+
value: m.id,
|
|
2558
|
+
label: m.provider === null ? m.id : m.provider + ' · ' + m.id,
|
|
2559
|
+
})),
|
|
2560
|
+
onPick: (value) => {
|
|
2561
|
+
const found = models.find((m) => m.id === value)
|
|
2562
|
+
pickModel(entry.kind, value === null ? null : (found || { provider: choice.provider, id: value }))
|
|
2563
|
+
},
|
|
2564
|
+
}),
|
|
2370
2565
|
],
|
|
2371
2566
|
}),
|
|
2372
2567
|
jsxs('label', {
|
|
2373
2568
|
className: 'dsx-rev__field',
|
|
2374
2569
|
children: [
|
|
2375
2570
|
jsx('span', { children: t('review.effort') }),
|
|
2376
|
-
|
|
2571
|
+
jsx(ValuePicker, {
|
|
2572
|
+
t: t,
|
|
2573
|
+
value: choice.effort,
|
|
2574
|
+
disabled: busy,
|
|
2575
|
+
placeholder: t('review.effortName'),
|
|
2576
|
+
options: (entry.efforts || []).map((e) => ({ value: e, label: e })),
|
|
2577
|
+
onPick: (value) => pick(entry.kind, 'effort', value),
|
|
2578
|
+
}),
|
|
2377
2579
|
],
|
|
2378
2580
|
}),
|
|
2379
2581
|
],
|
|
@@ -2382,6 +2584,102 @@ window.__ModuleLoader__.load({
|
|
|
2382
2584
|
}, entry.kind)
|
|
2383
2585
|
}),
|
|
2384
2586
|
}),
|
|
2587
|
+
|
|
2588
|
+
// ── Панель находок ──────────────────────────────────────────
|
|
2589
|
+
//
|
|
2590
|
+
// Показывает последний обзор: что нашли ревизоры и что из этого
|
|
2591
|
+
// подтвердилось проверкой.
|
|
2592
|
+
//
|
|
2593
|
+
// Судит НЕ тот, кто смотрит. Приговор выносит агент и обязан
|
|
2594
|
+
// приложить доказательство — что именно он запустил или прочитал.
|
|
2595
|
+
// Просить человека, не разбирающегося в предмете, решить
|
|
2596
|
+
// «настоящая это ошибка или нет» значит просить невозможного и
|
|
2597
|
+
// получить случайные нажатия, которые ещё и будут выглядеть как
|
|
2598
|
+
// данные.
|
|
2599
|
+
//
|
|
2600
|
+
// Возразить можно, но не нужно: возражение человека перекрывает
|
|
2601
|
+
// приговор агента и обратно уже не отменяется.
|
|
2602
|
+
journal === null || journal.latest === null ? null : jsxs('div', {
|
|
2603
|
+
className: 'dsx-rev__panel',
|
|
2604
|
+
children: [
|
|
2605
|
+
jsxs('div', {
|
|
2606
|
+
className: 'dsx-rev__panel-head',
|
|
2607
|
+
children: [
|
|
2608
|
+
jsx('span', { className: 'dsx-subs__title', children: t('review.lastReview') }),
|
|
2609
|
+
jsx('span', { className: 'dsx-rev__count', children: journal.latest.findings.length }),
|
|
2610
|
+
],
|
|
2611
|
+
}),
|
|
2612
|
+
jsx('div', {
|
|
2613
|
+
className: 'dsx-rev__findings',
|
|
2614
|
+
children: journal.latest.findings.map((finding) => jsxs('div', {
|
|
2615
|
+
className: 'dsx-rev__finding dsx-rev__finding--' + finding.verdict,
|
|
2616
|
+
children: [
|
|
2617
|
+
jsxs('div', {
|
|
2618
|
+
className: 'dsx-rev__finding-head',
|
|
2619
|
+
children: [
|
|
2620
|
+
jsx('span', { className: 'dsx-rev__sev dsx-rev__sev--' + severityClass(finding.severity), children: finding.severity }),
|
|
2621
|
+
jsx('span', { className: 'dsx-rev__finding-title', children: finding.title }),
|
|
2622
|
+
jsx('span', { className: 'dsx-rev__who', children: finding.reviewer }),
|
|
2623
|
+
],
|
|
2624
|
+
}),
|
|
2625
|
+
finding.where === null ? null : jsx('div', { className: 'dsx-rev__where', children: finding.where }),
|
|
2626
|
+
finding.what === null ? null : jsx('div', { className: 'dsx-rev__what', children: finding.what }),
|
|
2627
|
+
jsxs('div', {
|
|
2628
|
+
className: 'dsx-rev__verdict',
|
|
2629
|
+
children: [
|
|
2630
|
+
jsx('span', {
|
|
2631
|
+
className: 'dsx-rev__verdict-mark',
|
|
2632
|
+
children: verdictMark(finding.verdict),
|
|
2633
|
+
}),
|
|
2634
|
+
jsx('span', {
|
|
2635
|
+
className: 'dsx-rev__verdict-text',
|
|
2636
|
+
children: finding.evidence === ''
|
|
2637
|
+
? t('review.notChecked')
|
|
2638
|
+
: finding.evidence,
|
|
2639
|
+
}),
|
|
2640
|
+
finding.by === null ? null : jsx('span', {
|
|
2641
|
+
className: 'dsx-rev__by',
|
|
2642
|
+
children: finding.by === 'human' ? t('review.byHuman') : t('review.byAgent'),
|
|
2643
|
+
}),
|
|
2644
|
+
],
|
|
2645
|
+
}),
|
|
2646
|
+
jsx('div', {
|
|
2647
|
+
className: 'dsx-rev__object',
|
|
2648
|
+
children: jsx('button', {
|
|
2649
|
+
type: 'button',
|
|
2650
|
+
className: 'dsx-rev__object-btn',
|
|
2651
|
+
disabled: busy,
|
|
2652
|
+
onClick: () => save({
|
|
2653
|
+
findingId: finding.id,
|
|
2654
|
+
verdict: finding.verdict === 'confirmed' ? 'false' : 'confirmed',
|
|
2655
|
+
evidence: t('review.humanEvidence'),
|
|
2656
|
+
}),
|
|
2657
|
+
children: finding.verdict === 'confirmed' ? t('review.objectFalse') : t('review.objectReal'),
|
|
2658
|
+
}),
|
|
2659
|
+
}),
|
|
2660
|
+
],
|
|
2661
|
+
}, finding.id)),
|
|
2662
|
+
}),
|
|
2663
|
+
journal.accuracy.length === 0 ? null : jsxs('div', {
|
|
2664
|
+
className: 'dsx-rev__stats',
|
|
2665
|
+
children: [
|
|
2666
|
+
jsx('div', { className: 'dsx-setting__hint', children: t('review.accuracy') }),
|
|
2667
|
+
...journal.accuracy.map((stat) => jsxs('div', {
|
|
2668
|
+
className: 'dsx-rev__stat',
|
|
2669
|
+
children: [
|
|
2670
|
+
jsx('span', { className: 'dsx-rev__stat-name', children: stat.reviewer }),
|
|
2671
|
+
jsx('span', {
|
|
2672
|
+
className: 'dsx-rev__stat-rate',
|
|
2673
|
+
// Доля без числа находок ничего не значит: «100%» по
|
|
2674
|
+
// одной проверенной находке выглядит как надёжность.
|
|
2675
|
+
children: Math.round(stat.rate * 100) + '% ' + t('review.of') + ' ' + stat.judged,
|
|
2676
|
+
}),
|
|
2677
|
+
],
|
|
2678
|
+
}, stat.reviewer)),
|
|
2679
|
+
],
|
|
2680
|
+
}),
|
|
2681
|
+
],
|
|
2682
|
+
}),
|
|
2385
2683
|
settings !== null && settings.enabled.length === 0
|
|
2386
2684
|
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.none') })
|
|
2387
2685
|
: null,
|
package/lib/http-runtime.js
CHANGED
|
@@ -215,6 +215,44 @@ export async function handleUpdate(req, res, state, loadModule) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Собрать модели, доступные на маршруте.
|
|
220
|
+
*
|
|
221
|
+
* Это единственный список, который можно узнать ЖИВЫМ: наш ревизор ходит
|
|
222
|
+
* тем же маршрутом, что и основной агент. Отказ любого провайдера не должен
|
|
223
|
+
* ронять весь справочник — один не ответил, остальные покажем.
|
|
224
|
+
*
|
|
225
|
+
* @param llm - служба моделей; может отсутствовать в профиле без неё.
|
|
226
|
+
* @returns пары «провайдер и имя модели», пригодные для показа.
|
|
227
|
+
*/
|
|
228
|
+
async function listRouteModels(llm) {
|
|
229
|
+
if (llm === undefined || llm === null || typeof llm.listProviders !== 'function') return []
|
|
230
|
+
|
|
231
|
+
const providers = (() => {
|
|
232
|
+
try {
|
|
233
|
+
return llm.listProviders()
|
|
234
|
+
} catch {
|
|
235
|
+
return []
|
|
236
|
+
}
|
|
237
|
+
})()
|
|
238
|
+
|
|
239
|
+
const collected = []
|
|
240
|
+
for (const info of providers) {
|
|
241
|
+
const name = typeof info === 'string' ? info : info?.name ?? info?.provider
|
|
242
|
+
if (typeof name !== 'string' || name === '') continue
|
|
243
|
+
try {
|
|
244
|
+
const models = await llm.listModels(name)
|
|
245
|
+
for (const model of models ?? []) {
|
|
246
|
+
const id = typeof model === 'string' ? model : model?.id ?? model?.name
|
|
247
|
+
if (typeof id === 'string' && id !== '') collected.push({ provider: name, id })
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Провайдер без списка или с истёкшим ключом просто не добавит своих.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return collected
|
|
254
|
+
}
|
|
255
|
+
|
|
218
256
|
/**
|
|
219
257
|
* Обслужить `/api/tensorgrid.review` — настройки обзора.
|
|
220
258
|
*
|
|
@@ -225,20 +263,43 @@ export async function handleUpdate(req, res, state, loadModule) {
|
|
|
225
263
|
* POST сохраняет выбор. Форма проверяется на Host — файл правит и человек
|
|
226
264
|
* руками, и надеяться, что дошло только отправленное браузером, нельзя.
|
|
227
265
|
*/
|
|
228
|
-
export async function handleReview(req, res, loadModule) {
|
|
266
|
+
export async function handleReview(req, res, loadModule, services) {
|
|
229
267
|
try {
|
|
230
268
|
const settings = await loadModule('./review-settings.js')
|
|
231
269
|
const { REVIEWERS } = await loadModule('./reviewers.js')
|
|
232
270
|
|
|
271
|
+
// Модели НАШЕГО ревизора берутся живыми из маршрута, а не из зашитого
|
|
272
|
+
// списка: подключил человек OpenRouter — и в выборе появились сотни
|
|
273
|
+
// моделей, ничего не переустанавливая.
|
|
274
|
+
//
|
|
275
|
+
// Для внешних продуктов так не выйдет, и это не лень: Claude Code и
|
|
276
|
+
// Codex понимают только СВОИ имена моделей, а спросить у них список
|
|
277
|
+
// нечем. Там короткий перечень плюс возможность вписать своё.
|
|
278
|
+
const routeModels = await listRouteModels(services?.llm)
|
|
279
|
+
|
|
233
280
|
const catalogue = Object.entries(REVIEWERS).map(([kind, spec]) => ({
|
|
234
281
|
kind,
|
|
235
282
|
label: spec.label,
|
|
236
|
-
|
|
283
|
+
internal: spec.kind === 'internal',
|
|
284
|
+
models: spec.kind === 'internal' ? routeModels : spec.models.map((id) => ({ provider: null, id })),
|
|
237
285
|
efforts: spec.efforts,
|
|
238
286
|
}))
|
|
239
287
|
|
|
288
|
+
const modes = Object.entries(settings.MODES).map(([id, spec]) => ({
|
|
289
|
+
id,
|
|
290
|
+
label: spec.label,
|
|
291
|
+
hint: spec.hint,
|
|
292
|
+
enabled: spec.enabled,
|
|
293
|
+
}))
|
|
294
|
+
|
|
295
|
+
// Журнал отдаётся вместе с настройками: панель находок и настройки —
|
|
296
|
+
// один раздел, и второй запрос за тем же экраном был бы лишней задержкой.
|
|
297
|
+
const log = await loadModule('./review-log.js')
|
|
298
|
+
const journal = await log.read()
|
|
299
|
+
const digest = { latest: log.latest(journal), accuracy: log.accuracy(journal), runs: journal.runs.length }
|
|
300
|
+
|
|
240
301
|
if (req.method === 'GET') {
|
|
241
|
-
reply(res, 200, { catalogue, settings: await settings.readSettings(null) })
|
|
302
|
+
reply(res, 200, { catalogue, modes, settings: await settings.readSettings(null), journal: digest })
|
|
242
303
|
return
|
|
243
304
|
}
|
|
244
305
|
|
|
@@ -263,7 +324,29 @@ export async function handleReview(req, res, loadModule) {
|
|
|
263
324
|
return
|
|
264
325
|
}
|
|
265
326
|
|
|
266
|
-
|
|
327
|
+
// Возражение человека по находке. Оно перекрывает приговор агента и
|
|
328
|
+
// обратно уже не отменяется: следующий обзор не должен молча стереть
|
|
329
|
+
// ровно то несогласие, ради которого панель и сделана.
|
|
330
|
+
if (typeof body.findingId === 'string' && typeof body.verdict === 'string') {
|
|
331
|
+
await log.recordVerdict(body.findingId, body.verdict, typeof body.evidence === 'string' ? body.evidence : '', 'human')
|
|
332
|
+
const after = await log.read()
|
|
333
|
+
reply(res, 200, {
|
|
334
|
+
catalogue,
|
|
335
|
+
modes,
|
|
336
|
+
settings: await settings.readSettings(null),
|
|
337
|
+
journal: { latest: log.latest(after), accuracy: log.accuracy(after), runs: after.runs.length },
|
|
338
|
+
})
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Переключение режима и поштучная правка приходят одним путём: если в
|
|
343
|
+
// теле есть `mode`, состав считает Host по режиму, а не браузер. Так
|
|
344
|
+
// определение режима живёт в одном месте, а не в двух.
|
|
345
|
+
const next = typeof body.mode === 'string' && body.applyMode === true
|
|
346
|
+
? settings.applyMode(await settings.readSettings(null), body.mode)
|
|
347
|
+
: body
|
|
348
|
+
|
|
349
|
+
reply(res, 200, { catalogue, modes, settings: await settings.writeSettings(null, next), journal: digest })
|
|
267
350
|
} catch (error) {
|
|
268
351
|
reply(res, 500, { problem: String(error?.message ?? error) })
|
|
269
352
|
}
|
package/lib/index.js
CHANGED
|
@@ -220,6 +220,22 @@ export function apply(ctx) {
|
|
|
220
220
|
})
|
|
221
221
|
})
|
|
222
222
|
|
|
223
|
+
// Служба моделей нужна отдельным рядом: она отдаёт живой список
|
|
224
|
+
// подключённых моделей для выбора ревизора. Отдельным — потому что
|
|
225
|
+
// объединение с парой выше связало бы настройки обзора со стойкой
|
|
226
|
+
// входа: нет одной — пропала бы и другая, хотя они независимы.
|
|
227
|
+
const modelServices = { llm: null, generation: null }
|
|
228
|
+
web.inject(['llm'], (bound) => {
|
|
229
|
+
const generation = {}
|
|
230
|
+
modelServices.llm = bound.llm
|
|
231
|
+
modelServices.generation = generation
|
|
232
|
+
bound.effect(() => () => {
|
|
233
|
+
if (modelServices.generation !== generation) return
|
|
234
|
+
modelServices.llm = null
|
|
235
|
+
modelServices.generation = null
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
223
239
|
// Манифест собирается в горячем модуле: он ЧИТАЕТ поставочный файл и
|
|
224
240
|
// дополняет его нашими полями, а значит зависит от чужой формы, которая
|
|
225
241
|
// может поменяться. Такую логику правильнее держать там, где её можно
|
|
@@ -247,7 +263,7 @@ export function apply(ctx) {
|
|
|
247
263
|
async handler(req, res) {
|
|
248
264
|
try {
|
|
249
265
|
const runtime = await loadModule('./http-runtime.js')
|
|
250
|
-
await runtime.handleReview(req, res, loadModule)
|
|
266
|
+
await runtime.handleReview(req, res, loadModule, modelServices)
|
|
251
267
|
} catch (error) {
|
|
252
268
|
fail(res, error)
|
|
253
269
|
}
|