tensorgrid-ui 1.4.1 → 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 +316 -35
- 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 +102 -7
- package/lib/review.js +212 -39
- package/lib/reviewers.js +178 -17
- 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
|
|
@@ -2284,12 +2424,9 @@ window.__ModuleLoader__.load({
|
|
|
2284
2424
|
setBusy(true)
|
|
2285
2425
|
try {
|
|
2286
2426
|
const next = await send(body)
|
|
2287
|
-
// Ответ без каталога — это отказ, а не новое состояние.
|
|
2288
|
-
//
|
|
2289
|
-
//
|
|
2290
|
-
// галочки убирало все карточки: ни повторить, ни увидеть выбранное
|
|
2291
|
-
// было нельзя, помогала только перезагрузка. Ту же ошибку я уже
|
|
2292
|
-
// допускал в строке входа и там же чинил.
|
|
2427
|
+
// Ответ без каталога — это отказ, а не новое состояние. Замена
|
|
2428
|
+
// состояния целиком убирала все карточки после одного неудачного
|
|
2429
|
+
// нажатия: ни повторить, ни увидеть выбранное было нельзя.
|
|
2293
2430
|
setState((previous) => {
|
|
2294
2431
|
const complete = Array.isArray(next.catalogue) && next.catalogue.length > 0
|
|
2295
2432
|
if (complete) return next
|
|
@@ -2306,11 +2443,12 @@ window.__ModuleLoader__.load({
|
|
|
2306
2443
|
React.useEffect(() => { refresh() }, [refresh])
|
|
2307
2444
|
|
|
2308
2445
|
const catalogue = state !== null && Array.isArray(state.catalogue) ? state.catalogue : []
|
|
2446
|
+
const modes = state !== null && Array.isArray(state.modes) ? state.modes : []
|
|
2309
2447
|
const settings = state !== null && state.settings ? state.settings : null
|
|
2448
|
+
const journal = state !== null && state.journal ? state.journal : null
|
|
2310
2449
|
|
|
2311
|
-
// Изменение
|
|
2312
|
-
//
|
|
2313
|
-
// забыть нажать и потом удивляться.
|
|
2450
|
+
// Изменение сохраняется сразу. Отдельная кнопка «применить» создаёт
|
|
2451
|
+
// состояние «изменено, но не сохранено», в котором легко забыть нажать.
|
|
2314
2452
|
const save = React.useCallback((next) => { refresh(next) }, [refresh])
|
|
2315
2453
|
|
|
2316
2454
|
const toggle = (kind) => {
|
|
@@ -2322,21 +2460,23 @@ window.__ModuleLoader__.load({
|
|
|
2322
2460
|
|
|
2323
2461
|
const pick = (kind, field, value) => {
|
|
2324
2462
|
if (settings === null) return
|
|
2325
|
-
const
|
|
2463
|
+
const previous = settings.choices[kind] || { provider: null, model: null, effort: null }
|
|
2464
|
+
const choices = { ...settings.choices, [kind]: { ...previous, [field]: value } }
|
|
2326
2465
|
save({ ...settings, choices })
|
|
2327
2466
|
}
|
|
2328
2467
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
})
|
|
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
|
+
}
|
|
2340
2480
|
|
|
2341
2481
|
return jsxs('div', {
|
|
2342
2482
|
className: 'dsx-subs',
|
|
@@ -2351,29 +2491,54 @@ window.__ModuleLoader__.load({
|
|
|
2351
2491
|
state !== null && state.problem
|
|
2352
2492
|
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: state.problem })
|
|
2353
2493
|
: null,
|
|
2354
|
-
|
|
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', {
|
|
2355
2522
|
className: 'dsx-rev__list',
|
|
2356
2523
|
children: catalogue.map((entry) => {
|
|
2357
2524
|
const on = settings !== null && settings.enabled.includes(entry.kind)
|
|
2358
2525
|
// Запись может отсутствовать, если списки Host и браузера
|
|
2359
|
-
//
|
|
2360
|
-
// в отрисовке, и падал ВЕСЬ раздел настроек, а не только наша
|
|
2361
|
-
// карточка.
|
|
2526
|
+
// разойдутся; раньше обращение к ней роняло весь раздел.
|
|
2362
2527
|
const stored = settings === null ? null : settings.choices[entry.kind]
|
|
2363
|
-
const choice = stored
|
|
2528
|
+
const choice = stored || { provider: null, model: null, effort: null }
|
|
2529
|
+
const models = Array.isArray(entry.models) ? entry.models : []
|
|
2530
|
+
|
|
2364
2531
|
return jsxs('div', {
|
|
2365
2532
|
className: 'dsx-rev__card' + (on ? ' dsx-rev__card--on' : ''),
|
|
2366
2533
|
children: [
|
|
2367
2534
|
jsxs('label', {
|
|
2368
2535
|
className: 'dsx-rev__head',
|
|
2369
2536
|
children: [
|
|
2370
|
-
jsx('input', {
|
|
2371
|
-
type: 'checkbox',
|
|
2372
|
-
checked: on,
|
|
2373
|
-
disabled: busy,
|
|
2374
|
-
onChange: () => toggle(entry.kind),
|
|
2375
|
-
}),
|
|
2537
|
+
jsx('input', { type: 'checkbox', checked: on, disabled: busy, onChange: () => toggle(entry.kind) }),
|
|
2376
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,
|
|
2377
2542
|
],
|
|
2378
2543
|
}),
|
|
2379
2544
|
jsxs('div', {
|
|
@@ -2383,14 +2548,34 @@ window.__ModuleLoader__.load({
|
|
|
2383
2548
|
className: 'dsx-rev__field',
|
|
2384
2549
|
children: [
|
|
2385
2550
|
jsx('span', { children: t('review.model') }),
|
|
2386
|
-
|
|
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
|
+
}),
|
|
2387
2565
|
],
|
|
2388
2566
|
}),
|
|
2389
2567
|
jsxs('label', {
|
|
2390
2568
|
className: 'dsx-rev__field',
|
|
2391
2569
|
children: [
|
|
2392
2570
|
jsx('span', { children: t('review.effort') }),
|
|
2393
|
-
|
|
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
|
+
}),
|
|
2394
2579
|
],
|
|
2395
2580
|
}),
|
|
2396
2581
|
],
|
|
@@ -2399,6 +2584,102 @@ window.__ModuleLoader__.load({
|
|
|
2399
2584
|
}, entry.kind)
|
|
2400
2585
|
}),
|
|
2401
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
|
+
}),
|
|
2402
2683
|
settings !== null && settings.enabled.length === 0
|
|
2403
2684
|
? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.none') })
|
|
2404
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
|
}
|