dsh-vision-router 1.2.0 → 1.2.2
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 +29 -12
- package/README.zh.md +28 -12
- package/assets/pixel-loop-zh.png +0 -0
- package/assets/pixel-loop.png +0 -0
- package/assets/vision-tools-zh.svg +3 -3
- package/assets/vision-tools.svg +3 -3
- package/index.js +652 -84
- package/lib/client.js +241 -156
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -32,10 +32,13 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
onboardingGuide: '带我设置视觉模型',
|
|
33
33
|
onboardingLater: '稍后',
|
|
34
34
|
onboardingClose: '关闭',
|
|
35
|
-
|
|
35
|
+
guideStep1Title: '第 1 步 · 会话 / 文字模型',
|
|
36
|
+
guideStep1Body: '在聊天页右下角的模型选择器里选择会话/文字模型。发图片时,请选带「+ 自动识图」的模型组;组里的模型仍负责聊天、思考和调用工具。选好后点击「下一步」。',
|
|
37
|
+
guideStepNext: '下一步',
|
|
38
|
+
guidePromptTitle: '第 2 步 · 视觉模型',
|
|
36
39
|
guidePromptBody: '请打开 DSH 的「设置 → 插件」。进入插件页后,我会自动展开 Vision Router,并定位到「视觉后端链」。',
|
|
37
40
|
guidePromptCancel: '结束引导',
|
|
38
|
-
guideChainTitle: '第
|
|
41
|
+
guideChainTitle: '第 3 步 · 这里就是视觉模型',
|
|
39
42
|
guideChainBody: '上面的每一行都是你自己的视觉模型,从上到下依次尝试;可以全部留空。内置 OVH 免费链固定在最后自动兜底。这里不会修改聊天页右下角的会话/文字模型。选好后点击页面底部「保存」。',
|
|
40
43
|
guideDone: '完成引导',
|
|
41
44
|
pending: '未保存',
|
|
@@ -200,10 +203,13 @@ window.__ModuleLoader__.load({
|
|
|
200
203
|
onboardingGuide: 'Guide me to vision settings',
|
|
201
204
|
onboardingLater: 'Later',
|
|
202
205
|
onboardingClose: 'Close',
|
|
203
|
-
|
|
206
|
+
guideStep1Title: 'Step 1 · Session / text model',
|
|
207
|
+
guideStep1Body: 'Choose it from the lower-right chat selector. For image turns, use a group marked “+ Auto Vision”; the model inside still handles chat, reasoning, and tool calls. Click “Next” when done.',
|
|
208
|
+
guideStepNext: 'Next',
|
|
209
|
+
guidePromptTitle: 'Step 2 · Vision model',
|
|
204
210
|
guidePromptBody: 'Open DSH Settings → Plugins. Once that page is open, I will expand Vision Router and take you to “Vision backend chain”.',
|
|
205
211
|
guidePromptCancel: 'End guide',
|
|
206
|
-
guideChainTitle: 'Step
|
|
212
|
+
guideChainTitle: 'Step 3 · This is the vision model',
|
|
207
213
|
guideChainBody: 'Each row above is one of your own vision models, tried top to bottom; you may leave them all empty. The built-in OVH chain remains the automatic final fallback. This does not change the session/text model in the lower-right chat selector. Click “Save” after choosing.',
|
|
208
214
|
guideDone: 'Finish guide',
|
|
209
215
|
pending: 'Unsaved',
|
|
@@ -471,7 +477,7 @@ window.__ModuleLoader__.load({
|
|
|
471
477
|
'.vr-quickstart-body,.vr-quickstart-live{margin:0;font-size:12px;line-height:1.6}' +
|
|
472
478
|
'.vr-quickstart-body{color:var(--dsw-alias-label-secondary)}' +
|
|
473
479
|
'.vr-quickstart-live{color:var(--dsw-alias-brand-primary)}' +
|
|
474
|
-
'.vr-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}' +
|
|
480
|
+
'.vr-field{content-visibility:auto;contain-intrinsic-size:auto 96px;flex-direction:column;gap:6px;padding:12px 0;display:flex}' +
|
|
475
481
|
'.vr-field + .vr-field{border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
476
482
|
'.vr-field-head{align-items:center;gap:8px;display:flex}' +
|
|
477
483
|
'.vr-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}' +
|
|
@@ -490,7 +496,7 @@ window.__ModuleLoader__.load({
|
|
|
490
496
|
'.vr-stealth-notice{color:var(--dsw-alias-label-warning,var(--dsw-alias-label-secondary))}' +
|
|
491
497
|
'.vr-catalog-error{display:flex;align-items:center;gap:10px;flex-wrap:wrap}' +
|
|
492
498
|
'.vr-subheader{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:8px 0;border:none;background:none;cursor:pointer;font:inherit;color:var(--dsw-alias-label-primary);text-align:left}' +
|
|
493
|
-
'.vr-group{border-top:1px solid var(--dsw-alias-border-l2);padding:10px 0 2px;display:flex;flex-direction:column;gap:8px}' +
|
|
499
|
+
'.vr-group{content-visibility:auto;contain-intrinsic-size:auto 96px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 0 2px;display:flex;flex-direction:column;gap:8px}' +
|
|
494
500
|
'.vr-group-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);margin:0}' +
|
|
495
501
|
'.vr-select{flex:1;min-width:0;font:inherit}' +
|
|
496
502
|
'.vr-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}' +
|
|
@@ -524,6 +530,8 @@ window.__ModuleLoader__.load({
|
|
|
524
530
|
'.vr-guide-prompt{position:fixed;right:20px;bottom:20px;z-index:9999;width:min(360px,calc(100vw - 32px));box-sizing:border-box;border:1px solid var(--dsw-alias-brand-primary);border-radius:12px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 12px 40px #0004;padding:14px;display:flex;flex-direction:column;gap:7px;color:var(--dsw-alias-label-primary)}' +
|
|
525
531
|
'.vr-guide-prompt-title{font-size:13px;font-weight:700;line-height:1.5}' +
|
|
526
532
|
'.vr-guide-prompt-body{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6}' +
|
|
533
|
+
'.vr-guide-prompt-actions{display:flex;justify-content:flex-end;align-items:center;gap:8px;flex-wrap:wrap}' +
|
|
534
|
+
'.vr-guide-prompt-left{right:auto;left:20px}' +
|
|
527
535
|
'.vr-guide-target{border:2px solid var(--dsw-alias-brand-primary)!important;border-radius:12px;padding:12px!important;margin:8px -12px!important;background:var(--dsw-alias-bg-module-platform);box-shadow:0 0 0 4px color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}' +
|
|
528
536
|
'.vr-guide-callout{border-radius:9px;background:var(--dsw-alias-bg-layer-3);padding:10px 12px;display:flex;flex-direction:column;align-items:flex-start;gap:5px;margin-bottom:4px}' +
|
|
529
537
|
'.vr-guide-callout-title{font-size:13px;font-weight:700;color:var(--dsw-alias-brand-primary)}' +
|
|
@@ -546,29 +554,38 @@ window.__ModuleLoader__.load({
|
|
|
546
554
|
}
|
|
547
555
|
|
|
548
556
|
const ONBOARDING_STORAGE_KEY = 'dsh-vision-router:onboarding:model-guide-v2'
|
|
549
|
-
|
|
557
|
+
// The walkthrough has three steps: step1 (the session/text model selector
|
|
558
|
+
// on the chat page), step2 (open Settings → Plugins → Vision Router), and
|
|
559
|
+
// step3 (the highlighted vision chain, rendered by the settings card as a
|
|
560
|
+
// callout). Only step1/step2 need persistence; step3 re-derives from the
|
|
561
|
+
// card being on screen.
|
|
562
|
+
const VISION_GUIDE_STORAGE_KEY = 'dsh-vision-router:guide:vision-backend-v2'
|
|
550
563
|
const VISION_GUIDE_EVENT = 'dsh-vision-router:vision-settings-guide'
|
|
551
564
|
let visionGuidePrompt
|
|
552
|
-
let
|
|
565
|
+
let visionGuideStepMemory
|
|
553
566
|
|
|
554
|
-
function
|
|
567
|
+
function readVisionGuideStep() {
|
|
555
568
|
try {
|
|
556
|
-
|
|
569
|
+
const value = window.localStorage && window.localStorage.getItem(VISION_GUIDE_STORAGE_KEY)
|
|
570
|
+
if (value === 'step1' || value === 'step2') return value
|
|
557
571
|
} catch {
|
|
558
572
|
// Fall through to page-memory state when storage access is blocked.
|
|
559
573
|
}
|
|
560
|
-
return
|
|
574
|
+
return visionGuideStepMemory
|
|
561
575
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
576
|
+
function writeVisionGuideStep(step) {
|
|
577
|
+
visionGuideStepMemory = step === 'step1' || step === 'step2' ? step : undefined
|
|
564
578
|
try {
|
|
565
579
|
if (!window.localStorage) return
|
|
566
|
-
if (
|
|
580
|
+
if (visionGuideStepMemory) window.localStorage.setItem(VISION_GUIDE_STORAGE_KEY, visionGuideStepMemory)
|
|
567
581
|
else window.localStorage.removeItem(VISION_GUIDE_STORAGE_KEY)
|
|
568
582
|
} catch {
|
|
569
583
|
// Page-memory state still keeps the guide functional for this load.
|
|
570
584
|
}
|
|
571
585
|
}
|
|
586
|
+
function readVisionGuideActive() {
|
|
587
|
+
return readVisionGuideStep() !== undefined
|
|
588
|
+
}
|
|
572
589
|
function removeVisionGuidePrompt() {
|
|
573
590
|
if (visionGuidePrompt) visionGuidePrompt.remove()
|
|
574
591
|
visionGuidePrompt = undefined
|
|
@@ -578,46 +595,79 @@ window.__ModuleLoader__.load({
|
|
|
578
595
|
const EventCtor = window.Event
|
|
579
596
|
if (typeof EventCtor === 'function') window.dispatchEvent(new EventCtor(VISION_GUIDE_EVENT))
|
|
580
597
|
}
|
|
598
|
+
// The settings modal is an app-owned overlay; plugins cannot close it via
|
|
599
|
+
// a public API. Its panel closes on the Escape key through a native
|
|
600
|
+
// document listener, so replaying the guide "leaves the settings" by
|
|
601
|
+
// dispatching the same standard keydown the user would press.
|
|
602
|
+
function closeSettingsShell() {
|
|
603
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
604
|
+
try {
|
|
605
|
+
document.dispatchEvent(
|
|
606
|
+
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }),
|
|
607
|
+
)
|
|
608
|
+
} catch {
|
|
609
|
+
// KeyboardEvent constructor unavailable: degrade to staying put.
|
|
610
|
+
}
|
|
611
|
+
}
|
|
581
612
|
function syncVisionGuidePrompt(t) {
|
|
582
613
|
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
583
|
-
|
|
614
|
+
const step = readVisionGuideStep()
|
|
615
|
+
if (step === undefined) {
|
|
584
616
|
removeVisionGuidePrompt()
|
|
585
617
|
return
|
|
586
618
|
}
|
|
587
|
-
if (document.querySelector('[data-vr-guide-target="vision-backend"]')) {
|
|
619
|
+
if (step === 'step2' && document.querySelector('[data-vr-guide-target="vision-backend"]')) {
|
|
620
|
+
// Step 2 is done once the card is on screen; the in-card callout
|
|
621
|
+
// (step 3) takes over from here.
|
|
588
622
|
removeVisionGuidePrompt()
|
|
589
623
|
return
|
|
590
624
|
}
|
|
591
625
|
if (visionGuidePrompt || !document.body) return
|
|
592
626
|
const prompt = document.createElement('div')
|
|
593
|
-
|
|
627
|
+
// Step 1 talks about the chat page's lower-right model selector, so
|
|
628
|
+
// park the prompt on the LEFT side and keep the selector usable.
|
|
629
|
+
prompt.className = 'vr-guide-prompt' + (step === 'step1' ? ' vr-guide-prompt-left' : '')
|
|
594
630
|
prompt.setAttribute('role', 'status')
|
|
595
631
|
const title = document.createElement('div')
|
|
596
632
|
title.className = 'vr-guide-prompt-title'
|
|
597
|
-
title.textContent = t('guidePromptTitle')
|
|
633
|
+
title.textContent = step === 'step1' ? t('guideStep1Title') : t('guidePromptTitle')
|
|
598
634
|
const body = document.createElement('p')
|
|
599
635
|
body.className = 'vr-guide-prompt-body'
|
|
600
|
-
body.textContent = t('guidePromptBody')
|
|
636
|
+
body.textContent = step === 'step1' ? t('guideStep1Body') : t('guidePromptBody')
|
|
637
|
+
const actions = document.createElement('div')
|
|
638
|
+
actions.className = 'vr-guide-prompt-actions'
|
|
601
639
|
const cancel = document.createElement('button')
|
|
602
640
|
cancel.type = 'button'
|
|
603
641
|
cancel.className = 'vr-btn'
|
|
604
642
|
cancel.textContent = t('guidePromptCancel')
|
|
605
643
|
cancel.addEventListener('click', () => {
|
|
606
|
-
|
|
607
|
-
removeVisionGuidePrompt()
|
|
608
|
-
notifyVisionGuideChanged()
|
|
644
|
+
finishVisionSettingsGuide()
|
|
609
645
|
})
|
|
610
|
-
|
|
646
|
+
actions.append(cancel)
|
|
647
|
+
if (step === 'step1') {
|
|
648
|
+
const next = document.createElement('button')
|
|
649
|
+
next.type = 'button'
|
|
650
|
+
next.className = 'vr-btn vr-btn-save'
|
|
651
|
+
next.textContent = t('guideStepNext')
|
|
652
|
+
next.addEventListener('click', () => {
|
|
653
|
+
writeVisionGuideStep('step2')
|
|
654
|
+
removeVisionGuidePrompt()
|
|
655
|
+
syncVisionGuidePrompt(t)
|
|
656
|
+
notifyVisionGuideChanged()
|
|
657
|
+
})
|
|
658
|
+
actions.append(next)
|
|
659
|
+
}
|
|
660
|
+
prompt.append(title, body, actions)
|
|
611
661
|
document.body.appendChild(prompt)
|
|
612
662
|
visionGuidePrompt = prompt
|
|
613
663
|
}
|
|
614
664
|
function startVisionSettingsGuide(t) {
|
|
615
|
-
|
|
665
|
+
writeVisionGuideStep('step1')
|
|
616
666
|
syncVisionGuidePrompt(t)
|
|
617
667
|
notifyVisionGuideChanged()
|
|
618
668
|
}
|
|
619
669
|
function finishVisionSettingsGuide() {
|
|
620
|
-
|
|
670
|
+
writeVisionGuideStep(undefined)
|
|
621
671
|
removeVisionGuidePrompt()
|
|
622
672
|
notifyVisionGuideChanged()
|
|
623
673
|
}
|
|
@@ -634,106 +684,114 @@ window.__ModuleLoader__.load({
|
|
|
634
684
|
}
|
|
635
685
|
}
|
|
636
686
|
|
|
637
|
-
|
|
638
|
-
|
|
687
|
+
let onboardingOverlay
|
|
688
|
+
let onboardingKeyDown
|
|
689
|
+
|
|
690
|
+
function rememberOnboardingSeen() {
|
|
639
691
|
try {
|
|
640
|
-
if (window.localStorage
|
|
692
|
+
if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
|
|
641
693
|
} catch {
|
|
642
|
-
//
|
|
694
|
+
// Best effort: the dialog can still be dismissed for this page load.
|
|
643
695
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
|
|
650
|
-
} catch {
|
|
651
|
-
// Best effort: the dialog can still be dismissed for this page load.
|
|
652
|
-
}
|
|
696
|
+
}
|
|
697
|
+
function dismissOnboarding(remember = true) {
|
|
698
|
+
if (remember) rememberOnboardingSeen()
|
|
699
|
+
if (typeof document !== 'undefined' && onboardingKeyDown) {
|
|
700
|
+
document.removeEventListener('keydown', onboardingKeyDown)
|
|
653
701
|
}
|
|
702
|
+
onboardingKeyDown = undefined
|
|
703
|
+
if (onboardingOverlay) onboardingOverlay.remove()
|
|
704
|
+
onboardingOverlay = undefined
|
|
705
|
+
}
|
|
706
|
+
function showOnboarding(t) {
|
|
707
|
+
if (typeof document === 'undefined' || !document.body) return
|
|
708
|
+
if (onboardingOverlay) return // Already open: never stack a second dialog.
|
|
709
|
+
|
|
654
710
|
const onKeyDown = (event) => {
|
|
655
|
-
if (event && event.key === 'Escape')
|
|
656
|
-
}
|
|
657
|
-
const dismiss = () => {
|
|
658
|
-
remember()
|
|
659
|
-
document.removeEventListener('keydown', onKeyDown)
|
|
660
|
-
if (overlay) overlay.remove()
|
|
661
|
-
overlay = undefined
|
|
711
|
+
if (event && event.key === 'Escape') dismissOnboarding()
|
|
662
712
|
}
|
|
713
|
+
onboardingKeyDown = onKeyDown
|
|
663
714
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
715
|
+
const overlay = document.createElement('div')
|
|
716
|
+
overlay.className = 'vr-onboarding-backdrop'
|
|
717
|
+
overlay.setAttribute('role', 'presentation')
|
|
667
718
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
719
|
+
const dialog = document.createElement('div')
|
|
720
|
+
dialog.className = 'vr-onboarding-dialog'
|
|
721
|
+
dialog.setAttribute('role', 'dialog')
|
|
722
|
+
dialog.setAttribute('aria-modal', 'true')
|
|
723
|
+
dialog.setAttribute('aria-labelledby', 'vr-onboarding-title')
|
|
671
724
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
dialog.setAttribute('aria-labelledby', 'vr-onboarding-title')
|
|
725
|
+
const title = document.createElement('h2')
|
|
726
|
+
title.id = 'vr-onboarding-title'
|
|
727
|
+
title.className = 'vr-onboarding-title'
|
|
728
|
+
title.textContent = t('onboardingTitle')
|
|
677
729
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
title.textContent = t('onboardingTitle')
|
|
730
|
+
const body = document.createElement('p')
|
|
731
|
+
body.className = 'vr-onboarding-text'
|
|
732
|
+
body.textContent = t('onboardingBody')
|
|
682
733
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
734
|
+
const steps = document.createElement('div')
|
|
735
|
+
steps.className = 'vr-onboarding-steps'
|
|
736
|
+
for (const index of [1, 2, 3]) {
|
|
737
|
+
const step = document.createElement('div')
|
|
738
|
+
step.className = 'vr-onboarding-step'
|
|
739
|
+
const stepTitle = document.createElement('div')
|
|
740
|
+
stepTitle.className = 'vr-onboarding-step-title'
|
|
741
|
+
stepTitle.textContent = t('onboardingStep' + index + 'Title')
|
|
742
|
+
const stepBody = document.createElement('p')
|
|
743
|
+
stepBody.className = 'vr-onboarding-step-body'
|
|
744
|
+
stepBody.textContent = t('onboardingStep' + index + 'Body')
|
|
745
|
+
step.append(stepTitle, stepBody)
|
|
746
|
+
steps.appendChild(step)
|
|
747
|
+
}
|
|
686
748
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
stepTitle.className = 'vr-onboarding-step-title'
|
|
694
|
-
stepTitle.textContent = t('onboardingStep' + index + 'Title')
|
|
695
|
-
const stepBody = document.createElement('p')
|
|
696
|
-
stepBody.className = 'vr-onboarding-step-body'
|
|
697
|
-
stepBody.textContent = t('onboardingStep' + index + 'Body')
|
|
698
|
-
step.append(stepTitle, stepBody)
|
|
699
|
-
steps.appendChild(step)
|
|
700
|
-
}
|
|
749
|
+
const close = document.createElement('button')
|
|
750
|
+
close.type = 'button'
|
|
751
|
+
close.className = 'vr-onboarding-close'
|
|
752
|
+
close.setAttribute('aria-label', t('onboardingClose'))
|
|
753
|
+
close.textContent = '×'
|
|
754
|
+
close.addEventListener('click', dismissOnboarding)
|
|
701
755
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
756
|
+
const actions = document.createElement('div')
|
|
757
|
+
actions.className = 'vr-onboarding-actions'
|
|
758
|
+
const secondary = document.createElement('button')
|
|
759
|
+
secondary.type = 'button'
|
|
760
|
+
secondary.className = 'vr-onboarding-secondary'
|
|
761
|
+
secondary.textContent = t('onboardingLater')
|
|
762
|
+
secondary.addEventListener('click', dismissOnboarding)
|
|
763
|
+
const primary = document.createElement('button')
|
|
764
|
+
primary.type = 'button'
|
|
765
|
+
primary.className = 'vr-onboarding-primary'
|
|
766
|
+
primary.textContent = t('onboardingGuide')
|
|
767
|
+
primary.addEventListener('click', () => {
|
|
768
|
+
startVisionSettingsGuide(t)
|
|
769
|
+
dismissOnboarding()
|
|
770
|
+
})
|
|
771
|
+
actions.append(secondary, primary)
|
|
708
772
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
})
|
|
724
|
-
actions.append(secondary, primary)
|
|
773
|
+
dialog.append(title, body, steps, close, actions)
|
|
774
|
+
overlay.appendChild(dialog)
|
|
775
|
+
document.body.appendChild(overlay)
|
|
776
|
+
document.addEventListener('keydown', onKeyDown)
|
|
777
|
+
onboardingOverlay = overlay
|
|
778
|
+
primary.focus()
|
|
779
|
+
}
|
|
780
|
+
function installOnboarding(t) {
|
|
781
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
782
|
+
try {
|
|
783
|
+
if (window.localStorage && window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === 'seen') return
|
|
784
|
+
} catch {
|
|
785
|
+
// Privacy/storage restrictions should not prevent the guidance itself.
|
|
786
|
+
}
|
|
725
787
|
|
|
726
|
-
|
|
727
|
-
overlay.appendChild(dialog)
|
|
728
|
-
document.body.appendChild(overlay)
|
|
729
|
-
document.addEventListener('keydown', onKeyDown)
|
|
730
|
-
primary.focus()
|
|
731
|
-
}, 650)
|
|
788
|
+
const timer = window.setTimeout(() => showOnboarding(t), 650)
|
|
732
789
|
|
|
733
790
|
return () => {
|
|
734
791
|
if (timer !== undefined) window.clearTimeout(timer)
|
|
735
|
-
|
|
736
|
-
|
|
792
|
+
// Unmounting is not a user choice: drop the dialog without marking
|
|
793
|
+
// the guide seen, so the first-run overview can still auto-open.
|
|
794
|
+
if (onboardingOverlay) dismissOnboarding(false)
|
|
737
795
|
}
|
|
738
796
|
}
|
|
739
797
|
|
|
@@ -922,6 +980,58 @@ window.__ModuleLoader__.load({
|
|
|
922
980
|
} catch (error) {
|
|
923
981
|
renderError = error
|
|
924
982
|
}
|
|
983
|
+
const h = React.createElement
|
|
984
|
+
// The model catalog can carry hundreds of models per provider (e.g.
|
|
985
|
+
// openrouter), so the option vnode lists are built once per catalog /
|
|
986
|
+
// selection instead of on every render, and the per-provider model
|
|
987
|
+
// lists are cached by the models-array identity.
|
|
988
|
+
const modelOptionCache = React.useRef(new Map())
|
|
989
|
+
const modelOptionsOf = (models) => {
|
|
990
|
+
if (!Array.isArray(models) || models.length === 0) return []
|
|
991
|
+
let list = modelOptionCache.current.get(models)
|
|
992
|
+
if (list === undefined) {
|
|
993
|
+
list = models.map((model) =>
|
|
994
|
+
h('option', { value: model.id, key: model.id },
|
|
995
|
+
(model.name && model.name !== model.id ? model.name + ' (' + model.id + ')' : model.id)),
|
|
996
|
+
)
|
|
997
|
+
modelOptionCache.current.set(models, list)
|
|
998
|
+
}
|
|
999
|
+
return list
|
|
1000
|
+
}
|
|
1001
|
+
const groupOptions = useMemo(
|
|
1002
|
+
() => catalog.groups
|
|
1003
|
+
.filter((group) => group.id !== 'vision-http')
|
|
1004
|
+
.map((group) => h('option', { value: group.id, key: group.id },
|
|
1005
|
+
(group.name && group.name !== group.id ? group.name + ' (' + group.id + ')' : group.id))),
|
|
1006
|
+
[catalog.groups],
|
|
1007
|
+
)
|
|
1008
|
+
const visionGroupOptions = useMemo(
|
|
1009
|
+
() => filterVisionBackendGroups(catalog.groups, visionCaps.capabilities)
|
|
1010
|
+
.map((group) => h('option', { value: group.id, key: group.id },
|
|
1011
|
+
(group.name && group.name !== group.id ? group.name + ' (' + group.id + ')' : group.id))),
|
|
1012
|
+
[catalog.groups, visionCaps.capabilities],
|
|
1013
|
+
)
|
|
1014
|
+
const wrapGroupOptions = useMemo(
|
|
1015
|
+
() => {
|
|
1016
|
+
const chainValue = readValue(snapshot, 'chainRoute')
|
|
1017
|
+
const wrapperValue = readValue(snapshot, 'wrapperRoute')
|
|
1018
|
+
const excluded = new Set([
|
|
1019
|
+
'vision-http',
|
|
1020
|
+
typeof chainValue === 'string' && chainValue !== '' ? chainValue : 'vision-chain',
|
|
1021
|
+
typeof wrapperValue === 'string' && wrapperValue !== '' ? wrapperValue : 'deepseek-vision',
|
|
1022
|
+
])
|
|
1023
|
+
return catalog.groups
|
|
1024
|
+
.filter((group) => {
|
|
1025
|
+
if (group.id === 'deepseek-official') return true
|
|
1026
|
+
if (excluded.has(group.id)) return false
|
|
1027
|
+
if (group.id.endsWith('-vision')) return false
|
|
1028
|
+
return true
|
|
1029
|
+
})
|
|
1030
|
+
.map((group) => h('option', { value: group.id, key: group.id },
|
|
1031
|
+
(group.name && group.name !== group.id ? group.name + ' (' + group.id + ')' : group.id)))
|
|
1032
|
+
},
|
|
1033
|
+
[catalog.groups, snapshot],
|
|
1034
|
+
)
|
|
925
1035
|
if (renderError !== undefined) {
|
|
926
1036
|
return React.createElement(
|
|
927
1037
|
'li',
|
|
@@ -1172,7 +1282,6 @@ window.__ModuleLoader__.load({
|
|
|
1172
1282
|
}
|
|
1173
1283
|
}
|
|
1174
1284
|
|
|
1175
|
-
const h = React.createElement
|
|
1176
1285
|
const overriddenBadge = (key) =>
|
|
1177
1286
|
userHas(snapshot, key)
|
|
1178
1287
|
? h('span', { className: 'vr-badges' },
|
|
@@ -1227,43 +1336,10 @@ window.__ModuleLoader__.load({
|
|
|
1227
1336
|
)
|
|
1228
1337
|
}
|
|
1229
1338
|
|
|
1230
|
-
const groupOptions = catalog.groups.filter((group) => group.id !== 'vision-http').map((group) =>
|
|
1231
|
-
h('option', { value: group.id, key: group.id },
|
|
1232
|
-
(group.name && group.name !== group.id ? group.name + ' (' + group.id + ')' : group.id)),
|
|
1233
|
-
)
|
|
1234
|
-
const visionGroupOptions = visionGroups.map((group) =>
|
|
1235
|
-
h('option', { value: group.id, key: group.id },
|
|
1236
|
-
(group.name && group.name !== group.id ? group.name + ' (' + group.id + ')' : group.id)),
|
|
1237
|
-
)
|
|
1238
1339
|
const modelsOf = (providerId) => {
|
|
1239
1340
|
const group = catalog.groups.find((entry) => entry.id === providerId)
|
|
1240
1341
|
return group && Array.isArray(group.models) ? group.models : []
|
|
1241
1342
|
}
|
|
1242
|
-
// The wire catalog carries no per-model modality fields, so the wrappers
|
|
1243
|
-
// editor filters by route id: it offers only TEXT routes to wrap and
|
|
1244
|
-
// drops the plugin's own image-capable routes (wrapper, twins, the
|
|
1245
|
-
// vision-http backend, the vision chain). deepseek-official stays: it is
|
|
1246
|
-
// the pre-filled default row, served by the built-in wrapper.
|
|
1247
|
-
const wrapExcludedRoutes = () => {
|
|
1248
|
-
const chainValue = readValue(snapshot, 'chainRoute')
|
|
1249
|
-
const wrapperValue = readValue(snapshot, 'wrapperRoute')
|
|
1250
|
-
return new Set([
|
|
1251
|
-
'vision-http',
|
|
1252
|
-
typeof chainValue === 'string' && chainValue !== '' ? chainValue : 'vision-chain',
|
|
1253
|
-
typeof wrapperValue === 'string' && wrapperValue !== '' ? wrapperValue : 'deepseek-vision',
|
|
1254
|
-
])
|
|
1255
|
-
}
|
|
1256
|
-
const wrapGroupOptions = catalog.groups
|
|
1257
|
-
.filter((group) => {
|
|
1258
|
-
if (group.id === 'deepseek-official') return true
|
|
1259
|
-
if (wrapExcludedRoutes().has(group.id)) return false
|
|
1260
|
-
if (group.id.endsWith('-vision')) return false
|
|
1261
|
-
return true
|
|
1262
|
-
})
|
|
1263
|
-
.map((group) =>
|
|
1264
|
-
h('option', { value: group.id, key: group.id },
|
|
1265
|
-
(group.name && group.name !== group.id ? group.name + ' (' + group.id + ')' : group.id)),
|
|
1266
|
-
)
|
|
1267
1343
|
const finishGuide = () => {
|
|
1268
1344
|
finishVisionSettingsGuide()
|
|
1269
1345
|
setGuideActive(false)
|
|
@@ -1318,10 +1394,7 @@ window.__ModuleLoader__.load({
|
|
|
1318
1394
|
onChange: (event) => updateChain(index, { provider: row.provider, model: event.target.value }),
|
|
1319
1395
|
},
|
|
1320
1396
|
h('option', { value: '' }, visionProviderVisible(row.provider) ? t('selectModel') : t('pickProviderFirst')),
|
|
1321
|
-
visionModelsFor(row.provider)
|
|
1322
|
-
h('option', { value: model.id, key: model.id },
|
|
1323
|
-
(model.name && model.name !== model.id ? model.name + ' (' + model.id + ')' : model.id)),
|
|
1324
|
-
),
|
|
1397
|
+
modelOptionsOf(visionModelsFor(row.provider)),
|
|
1325
1398
|
),
|
|
1326
1399
|
h('button', {
|
|
1327
1400
|
type: 'button', className: 'vr-reset', disabled: !writable, title: t('removeTitle'),
|
|
@@ -1376,10 +1449,7 @@ window.__ModuleLoader__.load({
|
|
|
1376
1449
|
onChange: (event) => setPair({ provider: pair.provider, model: event.target.value }),
|
|
1377
1450
|
},
|
|
1378
1451
|
h('option', { value: '' }, pair.provider ? t('selectModel') : t('pickProviderFirst')),
|
|
1379
|
-
modelsOf(pair.provider)
|
|
1380
|
-
h('option', { value: model.id, key: model.id },
|
|
1381
|
-
(model.name && model.name !== model.id ? model.name + ' (' + model.id + ')' : model.id)),
|
|
1382
|
-
),
|
|
1452
|
+
modelOptionsOf(modelsOf(pair.provider)),
|
|
1383
1453
|
),
|
|
1384
1454
|
),
|
|
1385
1455
|
h('p', { className: 'vr-hint' }, t('textModelHint')),
|
|
@@ -1417,10 +1487,7 @@ window.__ModuleLoader__.load({
|
|
|
1417
1487
|
onChange: (event) => updateWrap(index, { provider: row.provider, model: event.target.value }),
|
|
1418
1488
|
},
|
|
1419
1489
|
h('option', { value: '' }, row.provider ? t('wrapAllModels') : t('pickProviderFirst')),
|
|
1420
|
-
modelsOf(row.provider)
|
|
1421
|
-
h('option', { value: model.id, key: model.id },
|
|
1422
|
-
(model.name && model.name !== model.id ? model.name + ' (' + model.id + ')' : model.id)),
|
|
1423
|
-
),
|
|
1490
|
+
modelOptionsOf(modelsOf(row.provider)),
|
|
1424
1491
|
),
|
|
1425
1492
|
h('button', {
|
|
1426
1493
|
type: 'button', className: 'vr-reset', disabled: !writable, title: t('removeTitle'),
|
|
@@ -1612,7 +1679,15 @@ window.__ModuleLoader__.load({
|
|
|
1612
1679
|
h('div', { className: 'vr-quickstart-actions' },
|
|
1613
1680
|
h('button', {
|
|
1614
1681
|
type: 'button', className: 'vr-btn',
|
|
1615
|
-
|
|
1682
|
+
// Re-viewing the guide replays it from the beginning:
|
|
1683
|
+
// leave the settings modal first (its panel closes on
|
|
1684
|
+
// Escape), show the overview (steps 1-3), and then walk
|
|
1685
|
+
// through step 1 on the chat page — starting in place
|
|
1686
|
+
// would skip the session/text-model step entirely.
|
|
1687
|
+
onClick: () => {
|
|
1688
|
+
closeSettingsShell()
|
|
1689
|
+
showOnboarding(t)
|
|
1690
|
+
},
|
|
1616
1691
|
}, t('quickStartGuide')),
|
|
1617
1692
|
),
|
|
1618
1693
|
),
|
|
@@ -1725,6 +1800,13 @@ window.__ModuleLoader__.load({
|
|
|
1725
1800
|
)
|
|
1726
1801
|
}
|
|
1727
1802
|
|
|
1803
|
+
// Skip re-renders when the app re-renders the settings panel for unrelated
|
|
1804
|
+
// reasons (tab switches, other cards' stores): the card's props come from
|
|
1805
|
+
// a stable inject object, so a shallow memo keeps the heavy field/select
|
|
1806
|
+
// DOM untouched until its own state or the settings scope changes.
|
|
1807
|
+
const VisionRouterCardMemoized =
|
|
1808
|
+
typeof React.memo === 'function' ? React.memo(VisionRouterCard) : VisionRouterCard
|
|
1809
|
+
|
|
1728
1810
|
const ARTIFACT_TOOL_KEYS = [
|
|
1729
1811
|
'vision_crop',
|
|
1730
1812
|
'vision_pixel_diff',
|
|
@@ -1914,6 +1996,9 @@ window.__ModuleLoader__.load({
|
|
|
1914
1996
|
ctx.effect(installStyles, 'vision-router: card styles')
|
|
1915
1997
|
ctx.effect(() => installVisionSettingsGuide(t), 'vision-router: model selection guide')
|
|
1916
1998
|
ctx.effect(() => installOnboarding(t), 'vision-router: first-run onboarding')
|
|
1999
|
+
// A stable props object: the memoized card skips re-renders only when
|
|
2000
|
+
// this identity stays fixed across slot renders.
|
|
2001
|
+
const cardInject = { scope, getConnection, t, locale: ctx.locale }
|
|
1917
2002
|
ctx.effect(
|
|
1918
2003
|
() =>
|
|
1919
2004
|
ctx.slots.inject('settings.plugin.item', function* () {
|
|
@@ -1923,9 +2008,9 @@ window.__ModuleLoader__.load({
|
|
|
1923
2008
|
id: 'vision-router',
|
|
1924
2009
|
order: 30,
|
|
1925
2010
|
label: () => t('nav'),
|
|
1926
|
-
inject: () =>
|
|
2011
|
+
inject: () => cardInject,
|
|
1927
2012
|
},
|
|
1928
|
-
|
|
2013
|
+
VisionRouterCardMemoized,
|
|
1929
2014
|
)
|
|
1930
2015
|
}),
|
|
1931
2016
|
'vision-router: settings card',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|