@goodandready/dsh-image-gen 0.10.18 → 0.10.19

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 CHANGED
@@ -503,6 +503,1466 @@ window.__ModuleLoader__.load({
503
503
  )
504
504
  }
505
505
 
506
+
507
+ // -------------------------------------------------------------- Diagnostics Panel
508
+ function DiagnosticsPanel(props) {
509
+ const t = props.t || ((k) => k)
510
+ const [testing, setTesting] = react.useState(false)
511
+ const [result, setResult] = react.useState(null)
512
+
513
+ const handleTest = async () => {
514
+ setTesting(true)
515
+ setResult(null)
516
+ try {
517
+ const res = await fetch('/dsh-image-gen/diagnostics/test?provider=' + encodeURIComponent(props.provider || 'fal'))
518
+ const data = await res.json()
519
+ setResult(data)
520
+ } catch (e) {
521
+ setResult({ ok: false, message: e.message })
522
+ } finally {
523
+ setTesting(false)
524
+ }
525
+ }
526
+
527
+ return react.createElement(
528
+ 'div',
529
+ {
530
+ style: {
531
+ padding: '10px 14px',
532
+ border: '1px solid var(--dsw-alias-border-l2)',
533
+ borderRadius: '8px',
534
+ background: 'var(--dsw-alias-bg-layer-2)',
535
+ display: 'flex',
536
+ alignItems: 'center',
537
+ justifyContent: 'space-between',
538
+ marginBottom: '12px',
539
+ },
540
+ },
541
+ react.createElement(
542
+ 'div',
543
+ { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
544
+ react.createElement('span', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary)' } }, t('diagnostics.title')),
545
+ result
546
+ ? react.createElement(
547
+ 'span',
548
+ {
549
+ style: {
550
+ fontSize: '12px',
551
+ fontWeight: '500',
552
+ color: result.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)',
553
+ },
554
+ },
555
+ (result.ok ? '🟢 ' : '🔴 ') + (result.message || (result.ok ? t('diagnostics.ready') : 'Failed')) + (result.latencyMs ? ' (' + result.latencyMs + 'ms)' : '')
556
+ )
557
+ : react.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('diagnostics.desc'))
558
+ ),
559
+ react.createElement(
560
+ 'button',
561
+ {
562
+ type: 'button',
563
+ disabled: testing,
564
+ onClick: handleTest,
565
+ className: 'ig-save-btn',
566
+ style: { padding: '5px 12px', fontSize: '12px', opacity: testing ? 0.7 : 1 },
567
+ },
568
+ testing ? t('diagnostics.testing') : t('diagnostics.test')
569
+ )
570
+ )
571
+ }
572
+
573
+ // -------------------------------------------------------------- Gallery View
574
+ function GalleryView(props) {
575
+ const t = props.t || ((k) => k)
576
+ const [items, setItems] = react.useState([])
577
+ const [loading, setLoading] = react.useState(true)
578
+ const [selected, setSelected] = react.useState(null)
579
+ const [copiedPrompt, setCopiedPrompt] = react.useState(false)
580
+ const [copiedLink, setCopiedLink] = react.useState(false)
581
+
582
+ const loadHistory = () => {
583
+ setLoading(true)
584
+ fetch('/dsh-image-gen/history')
585
+ .then((r) => r.json())
586
+ .then((data) => {
587
+ setItems(Array.isArray(data) ? data : [])
588
+ setLoading(false)
589
+ })
590
+ .catch(() => {
591
+ setItems([])
592
+ setLoading(false)
593
+ })
594
+ }
595
+
596
+ react.useEffect(() => {
597
+ loadHistory()
598
+ }, [])
599
+
600
+ if (loading) {
601
+ return react.createElement(
602
+ 'div',
603
+ { style: { padding: '24px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } },
604
+ 'Loading recent generations...'
605
+ )
606
+ }
607
+
608
+ if (items.length === 0) {
609
+ return react.createElement(
610
+ 'div',
611
+ {
612
+ style: {
613
+ padding: '32px 16px',
614
+ textAlign: 'center',
615
+ color: 'var(--dsw-alias-label-secondary)',
616
+ fontSize: '13px',
617
+ display: 'flex',
618
+ flexDirection: 'column',
619
+ alignItems: 'center',
620
+ gap: '8px',
621
+ },
622
+ },
623
+ react.createElement('span', { style: { fontSize: '24px' } }, '🖼️'),
624
+ react.createElement('p', null, t('gallery.empty')),
625
+ react.createElement(
626
+ 'button',
627
+ {
628
+ type: 'button',
629
+ onClick: loadHistory,
630
+ className: 'ig-tab-btn',
631
+ style: { border: '1px solid var(--dsw-alias-border-l2)', marginTop: '6px' },
632
+ },
633
+ '🔄 Refresh'
634
+ )
635
+ )
636
+ }
637
+
638
+ return react.createElement(
639
+ 'div',
640
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px', width: '100%' } },
641
+ react.createElement(
642
+ 'div',
643
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingBottom: '6px' } },
644
+ react.createElement('span', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary)' } }, t('gallery.title') + ' (' + items.length + ')'),
645
+ react.createElement('button', { type: 'button', onClick: loadHistory, className: 'ig-tab-btn', style: { padding: '3px 8px', fontSize: '12px' } }, '🔄 Refresh')
646
+ ),
647
+ react.createElement(
648
+ 'div',
649
+ {
650
+ style: {
651
+ display: 'grid',
652
+ gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))',
653
+ gap: '10px',
654
+ maxHeight: '480px',
655
+ overflowY: 'auto',
656
+ padding: '2px',
657
+ },
658
+ },
659
+ items.map((item, idx) => {
660
+ const thumb = item.thumbnailUrl || (item.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(item.attachmentId) : '')
661
+ return react.createElement(
662
+ 'div',
663
+ {
664
+ key: item.attachmentId || item.path || idx,
665
+ onClick: () => setSelected(item),
666
+ style: {
667
+ position: 'relative',
668
+ borderRadius: '8px',
669
+ overflow: 'hidden',
670
+ border: '1px solid var(--dsw-alias-border-l2)',
671
+ background: 'var(--dsw-alias-bg-layer-2)',
672
+ cursor: 'pointer',
673
+ aspectRatio: '1',
674
+ display: 'flex',
675
+ alignItems: 'center',
676
+ justifyContent: 'center',
677
+ },
678
+ },
679
+ thumb
680
+ ? react.createElement('img', {
681
+ src: thumb,
682
+ alt: item.prompt || 'Generated image',
683
+ loading: 'lazy',
684
+ style: { width: '100%', height: '100%', objectFit: 'cover' },
685
+ })
686
+ : react.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, '🖼️'),
687
+ react.createElement(
688
+ 'div',
689
+ {
690
+ style: {
691
+ position: 'absolute',
692
+ bottom: 0,
693
+ left: 0,
694
+ right: 0,
695
+ padding: '4px 6px',
696
+ background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
697
+ fontSize: '10px',
698
+ color: '#fff',
699
+ whiteSpace: 'nowrap',
700
+ overflow: 'hidden',
701
+ textOverflow: 'ellipsis',
702
+ },
703
+ },
704
+ item.prompt || 'Untitled'
705
+ )
706
+ )
707
+ })
708
+ ),
709
+ selected &&
710
+ react.createElement(
711
+ 'div',
712
+ {
713
+ style: {
714
+ position: 'fixed',
715
+ inset: 0,
716
+ background: 'rgba(0,0,0,0.7)',
717
+ zIndex: 10000,
718
+ display: 'flex',
719
+ alignItems: 'center',
720
+ justifyContent: 'center',
721
+ padding: '20px',
722
+ },
723
+ onClick: () => setSelected(null),
724
+ },
725
+ react.createElement(
726
+ 'div',
727
+ {
728
+ style: {
729
+ background: 'var(--dsw-alias-bg-layer-3)',
730
+ border: '1px solid var(--dsw-alias-border-l2)',
731
+ borderRadius: '12px',
732
+ maxWidth: '560px',
733
+ width: '100%',
734
+ maxHeight: '90vh',
735
+ overflowY: 'auto',
736
+ padding: '18px',
737
+ display: 'flex',
738
+ flexDirection: 'column',
739
+ gap: '12px',
740
+ },
741
+ onClick: (e) => e.stopPropagation(),
742
+ },
743
+ react.createElement(
744
+ 'div',
745
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
746
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, t('gallery.inspector')),
747
+ react.createElement(
748
+ 'button',
749
+ {
750
+ type: 'button',
751
+ onClick: () => setSelected(null),
752
+ style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px', color: 'var(--dsw-alias-label-secondary)' },
753
+ },
754
+ '✕'
755
+ )
756
+ ),
757
+ react.createElement('img', {
758
+ src: selected.thumbnailUrl || (selected.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(selected.attachmentId) : ''),
759
+ style: { width: '100%', borderRadius: '8px', maxHeight: '340px', objectFit: 'contain', background: '#0a0a0c' },
760
+ }),
761
+ react.createElement(
762
+ 'div',
763
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', fontSize: '11px' } },
764
+ selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, 'Seed: ' + selected.seed),
765
+ selected.provider && react.createElement('span', { className: 'ig-badge' }, 'Provider: ' + selected.provider),
766
+ selected.width && react.createElement('span', { className: 'ig-badge' }, selected.width + '×' + selected.height),
767
+ selected.cost ? react.createElement('span', { className: 'ig-badge' }, '
768
+ const t = props.t || ((k) => k)
769
+ const [open, setOpen] = react.useState(props.defaultOpen ?? true)
770
+ const [activeTab, setActiveTab] = react.useState('general')
771
+
772
+ react.useEffect(() => {
773
+ ensureCss()
774
+ }, [])
775
+
776
+ const state = (typeof props.useFalSettingsCard === 'function'
777
+ ? props.useFalSettingsCard((s) => s)
778
+ : null) || props.state || { available: true, writable: true, provider: { text: 'fal' } }
779
+
780
+ const disabled = !state.writable
781
+ const currentProvider = (state.provider && state.provider.text) || 'fal'
782
+ const blocked = !state.dirty || state.invalid || state.saving
783
+
784
+ const fieldProps = {
785
+ overriddenLabel: t('settings.overridden'),
786
+ resetLabel: t('settings.reset'),
787
+ invalidLabel: t('settings.invalidNumber'),
788
+ disabled,
789
+ }
790
+
791
+ if (!state.available) {
792
+ return react.createElement(
793
+ 'li',
794
+ { className: 'ig-section-card', style: { listStyle: 'none' } },
795
+ react.createElement('p', { className: 'ig-field-hint' }, t('settings.unavailable'))
796
+ )
797
+ }
798
+
799
+ const tabs = [
800
+ { id: 'general', label: t('tab.general'), icon: '⚙️' },
801
+ { id: 'provider', label: t('tab.provider') + ' (' + currentProvider.toUpperCase() + ')', icon: '🔌' },
802
+ { id: 'gallery', label: t('tab.gallery'), icon: '🖼️' },
803
+ { id: 'enhancer', label: t('tab.enhancer'), icon: '✨' },
804
+ { id: 'safety', label: t('tab.safety'), icon: '🛡️' },
805
+ { id: 'cache', label: t('tab.cache'), icon: '⚡' },
806
+ ]
807
+
808
+ const currentTabFields = FIELDS.filter((entry) => {
809
+ if (entry.tab !== activeTab) return false
810
+ if (entry.when && !entry.when.includes(currentProvider)) return false
811
+ return true
812
+ })
813
+
814
+ const diskCacheOn = state.diskCache && state.diskCache.text !== 'false'
815
+ const qualityGateOn = state.qualityGate && state.qualityGate.text !== 'false'
816
+ const budgetText = (state.dailyBudgetUsd && state.dailyBudgetUsd.text) || '0'
817
+ const loopLimit = (state.loopGuardLimit && state.loopGuardLimit.text) || '3'
818
+
819
+ return react.createElement(
820
+ 'li',
821
+ { className: 'ig-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
822
+ // Header
823
+ react.createElement(
824
+ 'button',
825
+ {
826
+ type: 'button',
827
+ style: {
828
+ background: 'none',
829
+ border: 'none',
830
+ cursor: 'pointer',
831
+ display: 'flex',
832
+ alignItems: 'center',
833
+ width: '100%',
834
+ padding: 0,
835
+ textAlign: 'left',
836
+ },
837
+ 'aria-expanded': open,
838
+ onClick: () => setOpen(!open),
839
+ },
840
+ react.createElement(
841
+ 'div',
842
+ { style: { flex: 1 } },
843
+ react.createElement(
844
+ 'div',
845
+ { style: { fontWeight: 700, fontSize: '16px', display: 'flex', alignItems: 'center', gap: '8px' } },
846
+ '🎨 ' + t('settings.title')
847
+ ),
848
+ react.createElement(
849
+ 'div',
850
+ { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
851
+ t('settings.description')
852
+ )
853
+ ),
854
+ state.dirty
855
+ ? react.createElement('span', { className: 'ig-badge ig-badge-warn', style: { marginRight: '8px' } }, t('settings.unsaved'))
856
+ : null,
857
+ react.createElement(
858
+ 'span',
859
+ { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
860
+ ChevronIconNode
861
+ )
862
+ ),
863
+ open
864
+ ? react.createElement(
865
+ 'div',
866
+ { className: 'ig-page' },
867
+ // Quick Stats Row
868
+ react.createElement(
869
+ 'div',
870
+ { className: 'ig-grid-4' },
871
+ react.createElement(
872
+ 'div',
873
+ { className: 'ig-stat-box' },
874
+ react.createElement('div', { className: 'ig-stat-val' }, currentProvider.toUpperCase()),
875
+ react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.active_provider'))
876
+ ),
877
+ react.createElement(
878
+ 'div',
879
+ { className: 'ig-stat-box' },
880
+ react.createElement('div', { className: 'ig-stat-val' }, ((state.defaultSize && state.defaultSize.text) || '4:3') + ' · ' + ((state.defaultFormat && state.defaultFormat.text) || 'png')),
881
+ react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.format_size'))
882
+ ),
883
+ react.createElement(
884
+ 'div',
885
+ { className: 'ig-stat-box' },
886
+ react.createElement(
887
+ 'div',
888
+ { className: 'ig-stat-val' },
889
+ react.createElement(
890
+ 'span',
891
+ { className: qualityGateOn ? 'ig-badge ig-badge-ok' : 'ig-badge ig-badge-warn' },
892
+ qualityGateOn ? 'Gate Active' : 'Off'
893
+ ),
894
+ ' ',
895
+ react.createElement('span', { className: 'ig-badge ig-badge-ok' }, 'Guard ' + loopLimit)
896
+ ),
897
+ react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.safety_status'))
898
+ ),
899
+ react.createElement(
900
+ 'div',
901
+ { className: 'ig-stat-box' },
902
+ react.createElement(
903
+ 'div',
904
+ { className: 'ig-stat-val' },
905
+ react.createElement(
906
+ 'span',
907
+ { className: diskCacheOn ? 'ig-badge ig-badge-ok' : 'ig-badge ig-badge-warn' },
908
+ diskCacheOn ? 'Cache ON' : 'Off'
909
+ ),
910
+ ' ',
911
+ budgetText !== '0' ? '$' + budgetText : 'No limit'
912
+ ),
913
+ react.createElement('div', { className: 'ig-stat-lbl' }, t('stat.budget_cache'))
914
+ )
915
+ ),
916
+ // Tab Navigation
917
+ react.createElement(
918
+ 'div',
919
+ { className: 'ig-tabs' },
920
+ tabs.map((tab) =>
921
+ react.createElement(
922
+ 'button',
923
+ {
924
+ key: tab.id,
925
+ type: 'button',
926
+ className: activeTab === tab.id ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
927
+ onClick: () => setActiveTab(tab.id),
928
+ },
929
+ tab.icon + ' ' + tab.label
930
+ )
931
+ )
932
+ ),
933
+ // Active Tab Fields
934
+ react.createElement(
935
+ 'div',
936
+ { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
937
+ currentTabFields.map((entry) =>
938
+ react.createElement(Field, {
939
+ key: entry.field,
940
+ entry,
941
+ state: state[entry.field] ?? { text: '', overridden: false, invalid: false },
942
+ fieldProps,
943
+ t,
944
+ onEdit: (val) => props.edit && props.edit(entry.field, val),
945
+ onReset: () => props.resetField && props.resetField(entry.field),
946
+ })
947
+ )
948
+ ),
949
+ // Footer Action Bar
950
+ react.createElement(
951
+ 'div',
952
+ {
953
+ style: {
954
+ display: 'flex',
955
+ justifyContent: 'flex-end',
956
+ alignItems: 'center',
957
+ gap: '10px',
958
+ paddingTop: '12px',
959
+ borderTop: '1px solid var(--dsw-alias-border-l2)',
960
+ },
961
+ },
962
+ state.failed
963
+ ? react.createElement('span', { className: 'ig-alert-err', style: { marginRight: 'auto', padding: '4px 10px' } }, t('settings.saveFailed'))
964
+ : null,
965
+ react.createElement(
966
+ 'button',
967
+ {
968
+ type: 'button',
969
+ className: 'ig-btn',
970
+ disabled: !state.dirty || state.saving,
971
+ onClick: props.discard,
972
+ },
973
+ t('settings.discard')
974
+ ),
975
+ react.createElement(
976
+ 'button',
977
+ {
978
+ type: 'button',
979
+ className: 'ig-btn ig-btn-primary',
980
+ disabled: blocked,
981
+ onClick: props.save,
982
+ },
983
+ t(state.saving ? 'settings.saving' : 'settings.save')
984
+ )
985
+ )
986
+ )
987
+ : null
988
+ )
989
+ }
990
+
991
+ // -------------------------------------------------------------- Toolview Component
992
+ const IMAGE_URL_RE = /https?:\/\/[^\s)]+\.(png|jpg|jpeg|webp|gif)(\?[^\s)]*)?/i
993
+
994
+ function readResult(block) {
995
+ if (!block) return { text: '', url: '', attachment: null }
996
+ const text = block.output || block.text || ''
997
+ const attachment = block.attachment || (block.attachments && block.attachments[0]) || null
998
+ const linked = text.match(IMAGE_URL_RE)
999
+ return {
1000
+ attachment,
1001
+ url: linked ? linked[0] : '',
1002
+ text: linked ? text.replace(linked[0], '').trim() : text,
1003
+ }
1004
+ }
1005
+
1006
+ function FalImageCard(props) {
1007
+ const block = props.block
1008
+ const running = !('kind' in (block || {}))
1009
+ const failed = block && (block.isError || block.error !== undefined)
1010
+ const parsed = readResult(block)
1011
+ const t = props.t || ((k) => k)
1012
+
1013
+ react.useEffect(() => {
1014
+ ensureCss()
1015
+ }, [])
1016
+
1017
+ let args = {}
1018
+ let prompt = ''
1019
+ try {
1020
+ const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
1021
+ if (raw) {
1022
+ args = JSON.parse(raw)
1023
+ prompt = args.prompt || ''
1024
+ }
1025
+ } catch (_) {}
1026
+
1027
+ const [copied, setCopied] = react.useState(false)
1028
+
1029
+ const sendActionPrompt = async (text) => {
1030
+ try {
1031
+ const sessions = props.sessions
1032
+ const current = sessions && sessions.list && sessions.list.current
1033
+ const binding = current && sessions.binding(current)
1034
+ const session = binding && binding.session
1035
+ if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
1036
+ } catch (_) {}
1037
+ }
1038
+
1039
+ const onCopyPrompt = () => {
1040
+ if (prompt && typeof navigator !== 'undefined' && navigator.clipboard) {
1041
+ navigator.clipboard.writeText(prompt)
1042
+ setCopied(true)
1043
+ setTimeout(() => setCopied(false), 2000)
1044
+ }
1045
+ }
1046
+
1047
+ const onReroll = () => {
1048
+ const nextSeed = typeof args.seed === 'number' ? args.seed + 1 : Math.floor(Math.random() * 100000)
1049
+ sendActionPrompt(t('card.actionReroll') + ' (' + nextSeed + '): ' + (prompt || ''))
1050
+ }
1051
+
1052
+ const onUpscale = () => {
1053
+ const ref = parsed.attachment ? (parsed.attachment.attachmentId || parsed.attachment.id) : (parsed.url || '')
1054
+ sendActionPrompt(t('card.actionUpscale') + ': upscale_image(image: "' + ref + '")')
1055
+ }
1056
+
1057
+ const onRemoveBg = () => {
1058
+ const ref = parsed.attachment ? (parsed.attachment.attachmentId || parsed.attachment.id) : (parsed.url || '')
1059
+ sendActionPrompt(t('card.actionRemoveBg') + ': remove_background(image: "' + ref + '")')
1060
+ }
1061
+
1062
+ const head = react.createElement(
1063
+ 'div',
1064
+ { className: 'fal_head' },
1065
+ running ? '⏳ ' + t('card.generating') : failed ? '❌ ' + t('card.failed') : '🖼️ ' + t('card.image')
1066
+ )
1067
+
1068
+ const body = []
1069
+ if (prompt) {
1070
+ body.push(react.createElement('div', { className: 'fal-prompt', key: 'p' }, prompt))
1071
+ }
1072
+ if (failed) {
1073
+ body.push(react.createElement('div', { className: 'fal-err', key: 'e' }, parsed.text || t('card.unknownError')))
1074
+ } else if (!parsed.attachment && parsed.url) {
1075
+ body.push(react.createElement('img', {
1076
+ key: 'i',
1077
+ src: parsed.url,
1078
+ alt: prompt || 'generated image',
1079
+ loading: 'lazy',
1080
+ style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1081
+ }))
1082
+ } else if (parsed.attachment) {
1083
+ const a = parsed.attachment
1084
+ const query = 'id=' + encodeURIComponent(a.attachmentId ?? a.id ?? '')
1085
+ + '&mt=' + encodeURIComponent(a.mediaType || 'image/png')
1086
+ + '&b=' + encodeURIComponent(String(a.bytes ?? 0))
1087
+ + '&w=' + encodeURIComponent(String(a.width ?? 0))
1088
+ + '&h=' + encodeURIComponent(String(a.height ?? 0))
1089
+ body.push(react.createElement('img', {
1090
+ key: 'i',
1091
+ src: '/dsh-image-gen/image?' + query,
1092
+ alt: prompt || 'generated image',
1093
+ loading: 'lazy',
1094
+ style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1095
+ }))
1096
+ }
1097
+
1098
+ const actions = []
1099
+ if (!running && !failed && (parsed.url || parsed.attachment)) {
1100
+ actions.push(
1101
+ react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
1102
+ react.createElement('button', { key: 'upscale', type: 'button', className: 'ig-btn', onClick: onUpscale }, '🔍 ' + t('card.upscale')),
1103
+ react.createElement('button', { key: 'removeBg', type: 'button', className: 'ig-btn', onClick: onRemoveBg }, '✂️ ' + t('card.removeBg')),
1104
+ react.createElement('button', { key: 'copy', type: 'button', className: 'ig-btn', onClick: onCopyPrompt }, copied ? '✓ Copied' : '📋 ' + t('card.copyPrompt'))
1105
+ )
1106
+ }
1107
+
1108
+ return react.createElement(
1109
+ 'div',
1110
+ { className: 'ig-section-card', style: { margin: '8px 0' } },
1111
+ head,
1112
+ body,
1113
+ actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null
1114
+ )
1115
+ }
1116
+
1117
+ // -------------------------------------------------------------- Dictionaries
1118
+ const en = {
1119
+ 'settings.navLabel': 'Image Studio',
1120
+ 'settings.title': 'Image Studio',
1121
+ 'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
1122
+ 'settings.unavailable': 'Plugin settings are initializing. They will appear automatically in a few seconds.',
1123
+ 'settings.unsaved': 'Unsaved changes',
1124
+ 'settings.saving': 'Saving…',
1125
+ 'settings.save': 'Save settings',
1126
+ 'settings.discard': 'Discard changes',
1127
+ 'settings.saveFailed': 'Failed to save settings. Please check credentials and try again.',
1128
+ 'settings.readOnly': 'Settings are read-only in this context.',
1129
+ 'settings.overridden': 'overridden',
1130
+ 'settings.reset': 'Reset to default',
1131
+ 'settings.invalidNumber': 'Please enter a valid number',
1132
+ 'settings.collapse': 'Collapse',
1133
+ 'settings.expand': 'Expand',
1134
+ 'settings.credentialHint': 'API keys are stored in DSH Credentials (~/.dsh/.credentials.yaml) and never leaked in config files.',
1135
+
1136
+ 'tab.general': 'General',
1137
+ 'tab.provider': 'Provider',
1138
+ 'tab.enhancer': 'Prompt & Styles',
1139
+ 'tab.safety': 'Safety & Budget',
1140
+ 'tab.cache': 'Cache & Storage',
1141
+
1142
+ 'stat.active_provider': 'Active Provider',
1143
+ 'stat.format_size': 'Default Specs',
1144
+ 'stat.safety_status': 'Safety & Loop Guard',
1145
+ 'stat.budget_cache': 'Budget & Cache',
1146
+
1147
+ 'card.generating': 'Generating image…',
1148
+ 'card.failed': 'Generation failed',
1149
+ 'card.image': 'Image Studio Result',
1150
+ 'card.unknownError': 'An unknown error occurred during generation.',
1151
+ 'card.sizePrefix': 'Size',
1152
+ 'card.seedPrefix': 'Seed',
1153
+ 'card.actionReroll': 'Reroll image',
1154
+ 'card.actionUpscale': 'Upscale image',
1155
+ 'card.actionRemoveBg': 'Remove background',
1156
+ 'card.reroll': 'Reroll',
1157
+ 'card.upscale': 'Upscale 2x/4x',
1158
+ 'card.removeBg': 'Remove BG',
1159
+ 'card.copyPrompt': 'Copy Prompt',
1160
+ 'card.origPrompt': 'Original Prompt',
1161
+
1162
+ 'f.enabled': 'Master Switch',
1163
+ 'f.enabledHint': 'Enable or disable image generation tools across the workspace.',
1164
+ 'f.provider': 'Image Provider',
1165
+ 'f.providerHint': 'Backend service used for image generation.',
1166
+ 'f.inherit': 'Default / Inherit',
1167
+ 'f.needFal': 'Requires FAL API key',
1168
+ 'f.needCustom': 'Requires OpenAI-compatible endpoint',
1169
+ 'f.needCodex': 'Built-in Codex subscription',
1170
+ 'f.needGrok': 'Built-in Grok subscription',
1171
+ 'f.needLocal': 'Local ComfyUI or A1111',
1172
+ 'f.needSeedream': 'Requires SeaDream API key',
1173
+ 'f.needGemini': 'Requires Google Gemini API key',
1174
+ 'f.needReplicate': 'Requires Replicate API token',
1175
+
1176
+ 'f.defaultSize': 'Default Aspect Ratio',
1177
+ 'f.defaultSizeHint': 'Aspect ratio and resolution applied when image_size is omitted.',
1178
+ 'f.defaultFormat': 'Output Format',
1179
+ 'f.defaultFormatHint': 'Target file format (PNG, JPEG, or WEBP).',
1180
+ 'f.deliverAs': 'Delivery Mode',
1181
+ 'f.deliverAsHint': 'How images reach conversation: "link" (text LLM safe) or "image" (multimodal vision).',
1182
+ 'f.outputDir': 'Output Directory',
1183
+ 'f.outputDirHint': 'Directory path where generated images are saved on disk.',
1184
+ 'p.outputDir': 'generated/images',
1185
+ 'f.historyLimit': 'History Limit',
1186
+ 'f.historyLimitHint': 'Maximum number of recent generation records kept in memory.',
1187
+ 'p.historyLimit': '50',
1188
+
1189
+ 'f.model': 'FAL Model ID',
1190
+ 'f.modelHint': 'Target model on FAL endpoint (e.g. fal-ai/flux-2/klein/9b).',
1191
+ 'p.model': 'fal-ai/flux-2/klein/9b',
1192
+ 'f.apiKeyEnv': 'FAL Key Reference',
1193
+ 'f.apiKeyEnvHint': 'Credential reference holding the FAL API key.',
1194
+ 'p.apiKeyEnv': 'FAL_API_KEY',
1195
+ 'f.baseURL': 'FAL Base URL',
1196
+ 'f.baseURLHint': 'FAL queue service endpoint.',
1197
+ 'p.baseURL': 'https://queue.fal.run',
1198
+ 'f.pollIntervalMs': 'Poll Interval (ms)',
1199
+ 'f.pollIntervalMsHint': 'Frequency of checking task progress on asynchronous queues.',
1200
+ 'p.pollIntervalMs': '2000',
1201
+ 'f.timeoutMs': 'Timeout (ms)',
1202
+ 'f.timeoutMsHint': 'Maximum overall timeout before failing a generation request.',
1203
+ 'p.timeoutMs': '180000',
1204
+
1205
+ 'f.customBaseURL': 'Custom API Base URL',
1206
+ 'f.customBaseURLHint': 'OpenAI-compatible image endpoint root (e.g. https://api.openai.com/v1).',
1207
+ 'p.customBaseURL': 'https://api.openai.com/v1',
1208
+ 'f.customModel': 'Custom Model ID',
1209
+ 'f.customModelHint': 'Model name sent to custom images endpoint.',
1210
+ 'p.customModel': 'dall-e-3',
1211
+ 'f.customKeyEnv': 'Custom Key Reference',
1212
+ 'f.customKeyEnvHint': 'Credential reference holding the API key for custom endpoint.',
1213
+ 'p.customKeyEnv': 'OPENAI_API_KEY',
1214
+ 'f.customSize': 'Custom Image Size',
1215
+ 'f.customSizeHint': 'Fixed dimension sent to custom endpoint (e.g. 1024x1024).',
1216
+ 'p.customSize': '1024x1024',
1217
+
1218
+ 'f.replicateModel': 'Replicate Model ID',
1219
+ 'f.replicateModelHint': 'Target model identifier on Replicate.',
1220
+ 'p.replicateModel': 'black-forest-labs/flux-schnell',
1221
+ 'f.replicateKeyEnv': 'Replicate Key Reference',
1222
+ 'f.replicateKeyEnvHint': 'Credential reference holding the Replicate API token.',
1223
+ 'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
1224
+
1225
+ 'f.seedreamModel': 'SeaDream Model ID',
1226
+ 'f.seedreamModelHint': 'ByteDance SeaDream model identifier.',
1227
+ 'p.seedreamModel': 'seedream-4.0',
1228
+ 'f.seedreamKeyEnv': 'SeaDream Key Reference',
1229
+ 'f.seedreamKeyEnvHint': 'Credential reference holding the SeaDream API key.',
1230
+ 'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
1231
+ 'f.seedreamBaseURL': 'SeaDream Base URL',
1232
+ 'f.seedreamBaseURLHint': 'SeaDream API endpoint URL (ByteDance/Volcengine Ark).',
1233
+ 'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
1234
+
1235
+ 'f.geminiModel': 'Gemini Model ID',
1236
+ 'f.geminiModelHint': 'Google Gemini image generation model identifier.',
1237
+ 'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
1238
+ 'f.geminiKeyEnv': 'Gemini Key Reference',
1239
+ 'f.geminiKeyEnvHint': 'Credential reference holding Google Gemini API key.',
1240
+ 'p.geminiKeyEnv': 'GEMINI_API_KEY',
1241
+
1242
+ 'f.localKind': 'Local Engine Architecture',
1243
+ 'f.localKindHint': 'Select between ComfyUI API or Automatic1111 WebUI backend.',
1244
+ 'f.localBaseURL': 'Local Server URL',
1245
+ 'f.localBaseURLHint': 'Server address (e.g. http://127.0.0.1:8188 for ComfyUI, http://127.0.0.1:7860 for A1111).',
1246
+ 'p.localBaseURL': 'http://127.0.0.1:8188',
1247
+ 'f.localModel': 'Local Model / Checkpoint',
1248
+ 'f.localModelHint': 'Checkpoint name or ComfyUI workflow name.',
1249
+ 'p.localModel': 'v1-5-pruned-emaonly.safetensors',
1250
+ 'f.localSteps': 'Sampling Steps',
1251
+ 'f.localStepsHint': 'Number of denoising sampling steps.',
1252
+ 'p.localSteps': '20',
1253
+ 'f.localCfg': 'CFG Scale',
1254
+ 'f.localCfgHint': 'Classifier-free guidance scale.',
1255
+ 'p.localCfg': '7',
1256
+ 'f.localComfyUrl': 'Local ComfyUI URL (Legacy)',
1257
+ 'f.localComfyUrlHint': 'Direct ComfyUI endpoint URL.',
1258
+ 'p.localComfyUrl': 'http://127.0.0.1:8188',
1259
+ 'f.localA1111Url': 'Local A1111 URL (Legacy)',
1260
+ 'f.localA1111UrlHint': 'Direct Automatic1111 endpoint URL.',
1261
+ 'p.localA1111Url': 'http://127.0.0.1:7860',
1262
+
1263
+ 'f.subscriptionQuality': 'Subscription Quality',
1264
+ 'f.subscriptionQualityHint': 'Target quality profile when using Codex or Grok subscriptions.',
1265
+
1266
+ 'f.enhancePrompt': 'LLM Prompt Enhancer',
1267
+ 'f.enhancePromptHint': 'Expand concise prompts with rich artistic details before generation.',
1268
+ 'f.enableLlmEnhancer': 'Enable LLM Enhancer (Legacy)',
1269
+ 'f.enableLlmEnhancerHint': 'Legacy toggle for LLM prompt expansion.',
1270
+ 'f.enhanceModel': 'Enhancement Model',
1271
+ 'f.enhanceModelHint': 'Dedicated LLM model used to expand prompts (empty = conversation model).',
1272
+ 'p.enhanceModel': 'Leave empty for default chat model',
1273
+ 'f.enhanceBelowChars': 'Enhancement Threshold',
1274
+ 'f.enhanceBelowCharsHint': 'Prompts longer than this character count will skip expansion.',
1275
+ 'p.enhanceBelowChars': '200',
1276
+ 'f.stylePreset': 'Artistic Style Preset',
1277
+ 'f.stylePresetHint': 'Automatic visual style suffix added to all generated prompts.',
1278
+
1279
+ 'f.qualityGate': 'Silent Quality Gate',
1280
+ 'f.qualityGateHint': 'Automatically inspects generated frames and silently re-rolls blank or broken outputs.',
1281
+ 'f.dailyBudgetUsd': 'Daily Spend Budget ($ USD)',
1282
+ 'f.dailyBudgetUsdHint': 'Hard spending cap per day across all visual generation tools (0 = no limit).',
1283
+ 'p.dailyBudgetUsd': '0.00',
1284
+ 'f.loopGuardLimit': 'Session Loop Guard Limit',
1285
+ 'f.loopGuardLimitHint': 'Maximum consecutive image generations allowed before requiring user input (0 = off).',
1286
+ 'p.loopGuardLimit': '3',
1287
+
1288
+ 'f.diskCache': 'Content-Addressed Disk Cache',
1289
+ 'f.diskCacheHint': 'Instant <50ms retrieval and zero API cost for repeated identical generations.',
1290
+ 'f.cacheBySeed': 'Cache by Seed + Prompt',
1291
+ 'f.cacheBySeedHint': 'Reuse existing result if the exact same seed and prompt are requested.',
1292
+ 'f.cacheByPrompt': 'Cache by Prompt Only',
1293
+ 'f.cacheByPromptHint': 'Reuse existing result if the prompt has already been generated.',
1294
+ 'f.pruneDays': 'Retention Period (Days)',
1295
+ 'f.pruneDaysHint': 'Automatically delete image files and cache older than this many days (0 = keep forever).',
1296
+ 'p.pruneDays': '0',
1297
+ 'f.cacheTtlDays': 'Cache Retention (Legacy)',
1298
+ 'f.cacheTtlDaysHint': 'Legacy name for retention period in days.',
1299
+ 'p.cacheTtlDays': '0',
1300
+ }
1301
+
1302
+ const zh = {
1303
+ 'tab.general': '常规',
1304
+ 'tab.provider': '提供商',
1305
+ 'tab.gallery': '图库',
1306
+ 'tab.enhancer': '提示词增强',
1307
+ 'tab.safety': '安全与预算',
1308
+ 'tab.cache': '缓存与性能',
1309
+
1310
+ 'diagnostics.title': '提供商连接诊断',
1311
+ 'diagnostics.desc': '即时检查与所选生成服务 API 的连接状态和延迟。',
1312
+ 'diagnostics.test': '测试连接',
1313
+ 'diagnostics.testing': '测试中...',
1314
+ 'diagnostics.ready': '正常',
1315
+
1316
+ 'gallery.title': '最近生成记录',
1317
+ 'gallery.empty': '暂无生成记录。生成一张图片后将显示在此处。',
1318
+ 'gallery.inspector': '生成记录详情',
1319
+ 'gallery.copy_prompt': '复制提示词',
1320
+ 'gallery.copy_link': '复制 Markdown 链接',
1321
+
1322
+ 'title': 'AI 图像工坊',
1323
+ 'subtitle': '多提供商支持的图像生成、编辑、局部重绘与矢量化。',
1324
+ 'card.title': 'AI 图像工坊',
1325
+ 'card.desc': '配置多提供商图像生成、局部重绘、放大及矢量化设置。',
1326
+ 'card.dirty': '有未保存的修改',
1327
+ 'card.saving': '保存中...',
1328
+ 'card.save': '保存修改',
1329
+ 'card.discard': '放弃修改',
1330
+ 'card.failed': '保存失败,请检查填写内容。',
1331
+
1332
+ 'settings.overridden': '已自定义',
1333
+ 'settings.reset': '重置',
1334
+ 'settings.invalidNumber': '请输入有效数值',
1335
+ 'settings.unavailable': '设置命名空间当前不可用。',
1336
+
1337
+ 'f.enabled': '启用图像生成',
1338
+ 'f.enabledHint': '关闭后将禁用所有图像生成及处理工具。',
1339
+ 'f.provider': '默认图像提供商',
1340
+ 'f.providerHint': '用于图像生成的默认引擎(fal, custom, replicate, seedream, gemini, local, codex, grok)。',
1341
+ 'f.defaultSize': '默认图像尺寸',
1342
+ 'f.defaultSizeHint': '未指定尺寸时的默认生成分辨率。',
1343
+ 'f.defaultFormat': '输出格式',
1344
+ 'f.defaultFormatHint': '生成图像的默认文件格式(png 或 webp)。',
1345
+ 'f.deliverAs': '输出投递方式',
1346
+ 'f.deliverAsHint': 'link 模式仅返回链接;image 模式同时返回图像自身数据。',
1347
+ 'f.outputDir': '存储目录',
1348
+ 'f.outputDirHint': '已生成图像在会话工作空间内的保存目录。',
1349
+ 'p.outputDir': 'generated/images',
1350
+ 'f.historyLimit': '历史记录数量',
1351
+ 'f.historyLimitHint': '保存在内存历史列表中的最大条目数。',
1352
+ 'p.historyLimit': '50',
1353
+
1354
+ 'f.model': 'FAL 模型 ID',
1355
+ 'f.modelHint': '调用的 FAL 队列模型标识符。',
1356
+ 'p.model': 'fal-ai/flux-2/klein/9b',
1357
+ 'f.apiKeyEnv': 'FAL 凭证名称',
1358
+ 'f.apiKeyEnvHint': '存储 FAL API 密钥的环境变量或凭证名称。',
1359
+ 'p.apiKeyEnv': 'FAL_API_KEY',
1360
+ 'f.baseURL': 'FAL 基础地址',
1361
+ 'f.baseURLHint': 'FAL 队列服务基础 URL。',
1362
+ 'p.baseURL': 'https://queue.fal.run',
1363
+ 'f.pollIntervalMs': '轮询间隔(毫秒)',
1364
+ 'f.pollIntervalMsHint': '异步队列状态检查的时间间隔。',
1365
+ 'p.pollIntervalMs': '2000',
1366
+ 'f.timeoutMs': '超时上限(毫秒)',
1367
+ 'f.timeoutMsHint': '单次生成作业的总执行时限。',
1368
+ 'p.timeoutMs': '180000',
1369
+
1370
+ 'f.customBaseURL': 'Custom API 地址',
1371
+ 'f.customBaseURLHint': '兼容 OpenAI 图像接口的基础地址。',
1372
+ 'p.customBaseURL': 'https://api.openai.com/v1',
1373
+ 'f.customModel': 'Custom 模型 ID',
1374
+ 'f.customModelHint': '自定义接口的模型标识符。',
1375
+ 'p.customModel': 'gpt-image-1',
1376
+ 'f.customKeyEnv': 'Custom 凭证名称',
1377
+ 'f.customKeyEnvHint': '存储自定义接口 API 密钥的凭证名称。',
1378
+ 'p.customKeyEnv': 'OPENAI_API_KEY',
1379
+ 'f.customSize': 'Custom 固定尺寸',
1380
+ 'f.customSizeHint': '发送至自定义接口的指定分辨率(如 1024x1024)。',
1381
+ 'p.customSize': '1024x1024',
1382
+
1383
+ 'f.replicateModel': 'Replicate 模型 ID',
1384
+ 'f.replicateModelHint': 'Replicate 平台上的目标模型。',
1385
+ 'p.replicateModel': 'black-forest-labs/flux-schnell',
1386
+ 'f.replicateKeyEnv': 'Replicate 凭证名称',
1387
+ 'f.replicateKeyEnvHint': '存储 Replicate API Token 的凭证名称。',
1388
+ 'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
1389
+
1390
+ 'f.seedreamModel': 'SeaDream 模型 ID',
1391
+ 'f.seedreamModelHint': '字节跳动 SeaDream 图像模型标识符。',
1392
+ 'p.seedreamModel': 'seedream-4.0',
1393
+ 'f.seedreamKeyEnv': 'SeaDream 凭证名称',
1394
+ 'f.seedreamKeyEnvHint': '存储 SeaDream 接口密钥的凭证名称。',
1395
+ 'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
1396
+ 'f.seedreamBaseURL': 'SeaDream 接口地址',
1397
+ 'f.seedreamBaseURLHint': 'SeaDream API 基础地址(火山引擎 Ark)。',
1398
+ 'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
1399
+
1400
+ 'f.geminiModel': 'Gemini 模型 ID',
1401
+ 'f.geminiModelHint': 'Google Gemini 图像生成模型标识符。',
1402
+ 'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
1403
+ 'f.geminiKeyEnv': 'Gemini 凭证名称',
1404
+ 'f.geminiKeyEnvHint': '存储 Google Gemini 密钥的凭证名称。',
1405
+ 'p.geminiKeyEnv': 'GEMINI_API_KEY',
1406
+
1407
+ 'f.localKind': '本地引擎架构',
1408
+ 'f.localKindHint': '在 ComfyUI API 或 Automatic1111 WebUI 架构间选择。',
1409
+ 'f.localBaseURL': '本地服务地址',
1410
+ 'f.localBaseURLHint': '服务网络地址(如 http://127.0.0.1:8188)。',
1411
+ 'p.localBaseURL': 'http://127.0.0.1:8188',
1412
+ 'f.localModel': '本地模型 / Checkpoint',
1413
+ 'f.localModelHint': '本地模型文件名或 ComfyUI 工作流名称。',
1414
+ 'p.localModel': 'v1-5-pruned-emaonly.safetensors',
1415
+ 'f.localSteps': '采样步数',
1416
+ 'f.localStepsHint': '去噪采样的计算步数。',
1417
+ 'p.localSteps': '20',
1418
+ 'f.localCfg': 'CFG Scale',
1419
+ 'f.localCfgHint': '无分类器引导系数。',
1420
+ 'p.localCfg': '7',
1421
+ 'f.localComfyUrl': '本地 ComfyUI 地址 (Legacy)',
1422
+ 'f.localComfyUrlHint': '直接指向 ComfyUI 的服务地址。',
1423
+ 'p.localComfyUrl': 'http://127.0.0.1:8188',
1424
+ 'f.localA1111Url': '本地 A1111 地址 (Legacy)',
1425
+ 'f.localA1111UrlHint': '直接指向 Automatic1111 的服务地址。',
1426
+ 'p.localA1111Url': 'http://127.0.0.1:7860',
1427
+
1428
+ 'f.subscriptionQuality': '订阅质量',
1429
+ 'f.subscriptionQualityHint': 'Codex 或 Grok 订阅通道的质量档位。',
1430
+
1431
+ 'f.autoEnhancePrompt': '智能自动增强提示词',
1432
+ 'f.autoEnhancePromptHint': '自动为提示词补充最佳摄影、光影与构图词汇。',
1433
+ 'f.defaultStylePreset': '默认风格预设',
1434
+ 'f.defaultStylePresetHint': '生成图像时应用的默认艺术或摄影风格(如 cinematic, anime 等)。',
1435
+ 'p.defaultStylePreset': 'none',
1436
+
1437
+ 'f.enhancePrompt': 'LLM 提示词扩展',
1438
+ 'f.enhancePromptHint': '在生成前由会话模型自动丰富短提示词细节。',
1439
+ 'f.enableLlmEnhancer': '启用 LLM 提示词增强 (Legacy)',
1440
+ 'f.enableLlmEnhancerHint': '提示词扩展的历史遗留开关。',
1441
+ 'f.enhanceModel': '增强模型',
1442
+ 'f.enhanceModelHint': '用于扩展提示词的独立大语言模型(留空使用会话模型)。',
1443
+ 'p.enhanceModel': '留空表示使用当前会话模型',
1444
+ 'f.enhanceBelowChars': '扩展长度阈值',
1445
+ 'f.enhanceBelowCharsHint': '长于此字数的提示词将直接生成,不予额外扩展。',
1446
+ 'p.enhanceBelowChars': '200',
1447
+ 'f.stylePreset': '艺术风格后缀',
1448
+ 'f.stylePresetHint': '自动追加到所有生成提示词末尾的风格后缀。',
1449
+
1450
+ 'f.qualityGate': '静默质量门禁',
1451
+ 'f.qualityGateHint': '自动检测破损或空白输出,并在后台静默重新生成。',
1452
+ 'f.dailyBudgetUsd': '每日开销预算 ($ USD)',
1453
+ 'f.dailyBudgetUsdHint': '所有图像工具每 24 小时的支出预算上限(0 为不限制)。',
1454
+ 'p.dailyBudgetUsd': '0.00',
1455
+ 'f.loopGuardLimit': '防死循环守卫 (Loop Guard)',
1456
+ 'f.loopGuardLimitHint': '无需用户交互的连续生成操作上限(0 为禁用)。',
1457
+ 'p.loopGuardLimit': '3',
1458
+
1459
+ 'f.diskCache': '按内容哈希的磁盘缓存',
1460
+ 'f.diskCacheHint': '重复生成完全相同的请求时实现毫秒级瞬间返回与零支出。',
1461
+ 'f.cacheBySeed': '按 Seed 与提示词命中缓存',
1462
+ 'f.cacheBySeedHint': '当 Seed 和提示词均完全一致时直接命中缓存。',
1463
+ 'f.cacheByPrompt': '按提示词命中缓存',
1464
+ 'f.cacheByPromptHint': '当提示词文本一致时直接命中缓存。',
1465
+ 'f.pruneDays': '文件保留天数',
1466
+ 'f.pruneDaysHint': '自动清理超过该天数的已生成图像(0 表示永久保留)。',
1467
+ 'p.pruneDays': '0',
1468
+ 'f.cacheTtlDays': '缓存生命周期 (Legacy)',
1469
+ 'f.cacheTtlDaysHint': '已废弃的缓存保留时长参数。',
1470
+ 'p.cacheTtlDays': '0',
1471
+ }
1472
+
1473
+ const ru = {
1474
+ 'settings.navLabel': 'Image Studio',
1475
+ 'settings.title': 'Генерация изображений',
1476
+ 'settings.description': 'Мультипровайдерная студия: генерация, инпейнтинг, вариации, апскейл и векторизация.',
1477
+ 'settings.unavailable': 'Настройки плагина инициализируются. Они появятся автоматически через несколько секунд.',
1478
+ 'settings.unsaved': 'Несохранённые изменения',
1479
+ 'settings.saving': 'Сохранение…',
1480
+ 'settings.save': 'Сохранить настройки',
1481
+ 'settings.discard': 'Сбросить изменения',
1482
+ 'settings.saveFailed': 'Не удалось сохранить настройки. Проверьте параметры и попробуйте снова.',
1483
+ 'settings.readOnly': 'Настройки доступны только для чтения.',
1484
+ 'settings.overridden': 'переопределено',
1485
+ 'settings.reset': 'Сбросить к умолчанию',
1486
+ 'settings.invalidNumber': 'Введите корректное число',
1487
+ 'settings.collapse': 'Свернуть',
1488
+ 'settings.expand': 'Развернуть',
1489
+ 'settings.credentialHint': 'Ключи API хранятся в DSH Credentials (~/.dsh/.credentials.yaml) и не попадают в файлы конфигурации.',
1490
+
1491
+ 'tab.general': 'Основные',
1492
+ 'tab.provider': 'Провайдер',
1493
+ 'tab.enhancer': 'Промпт и стили',
1494
+ 'tab.safety': 'Безопасность и бюджет',
1495
+ 'tab.cache': 'Кэш и хранение',
1496
+
1497
+ 'stat.active_provider': 'Активный провайдер',
1498
+ 'stat.format_size': 'Параметры по умолчанию',
1499
+ 'stat.safety_status': 'Защита и Loop Guard',
1500
+ 'stat.budget_cache': 'Бюджет и кэширование',
1501
+
1502
+ 'card.generating': 'Генерация изображения…',
1503
+ 'card.failed': 'Ошибка генерации',
1504
+ 'card.image': 'Результат Image Studio',
1505
+ 'card.unknownError': 'Неизвестная ошибка во время создания изображения.',
1506
+ 'card.sizePrefix': 'Размер',
1507
+ 'card.seedPrefix': 'Сид',
1508
+ 'card.actionReroll': 'Перегенерировать картинку',
1509
+ 'card.actionUpscale': 'Увеличить разрешение',
1510
+ 'card.actionRemoveBg': 'Удалить фон',
1511
+ 'card.reroll': 'Реролл',
1512
+ 'card.upscale': 'Апскейл 2x/4x',
1513
+ 'card.removeBg': 'Без фона',
1514
+ 'card.copyPrompt': 'Копировать промпт',
1515
+ 'card.origPrompt': 'Исходный промпт',
1516
+
1517
+ 'f.enabled': 'Главный выключатель',
1518
+ 'f.enabledHint': 'Включение или отключение всех инструментов генерации картинок.',
1519
+ 'f.provider': 'Провайдер генерации',
1520
+ 'f.providerHint': 'Сервис, выполняющий непосредственное создание изображений.',
1521
+ 'f.inherit': 'По умолчанию / наследовать',
1522
+ 'f.needFal': 'Требуется ключ FAL API',
1523
+ 'f.needCustom': 'Требуется OpenAI-совместимый эндпоинт',
1524
+ 'f.needCodex': 'Встроенная подписка Codex',
1525
+ 'f.needGrok': 'Встроенная подписка Grok',
1526
+ 'f.needLocal': 'Локальный ComfyUI или Automatic1111',
1527
+ 'f.needSeedream': 'Требуется ключ SeaDream API',
1528
+ 'f.needGemini': 'Требуется ключ Google Gemini API',
1529
+ 'f.needReplicate': 'Требуется токен Replicate API',
1530
+
1531
+ 'f.defaultSize': 'Соотношение сторон по умолчанию',
1532
+ 'f.defaultSizeHint': 'Разрешение и пропорции кадра, когда параметр image_size не передан.',
1533
+ 'f.defaultFormat': 'Формат файла',
1534
+ 'f.defaultFormatHint': 'Формат сохранения результатов (PNG, JPEG или WEBP).',
1535
+ 'f.deliverAs': 'Способ доставки',
1536
+ 'f.deliverAsHint': 'Способ передачи: "link" (безопасно для текстовых моделей) или "image" (для vision-моделей).',
1537
+ 'f.outputDir': 'Каталог сохранения',
1538
+ 'f.outputDirHint': 'Путь к папке сохранения сгенерированных файлов на диске.',
1539
+ 'p.outputDir': 'generated/images',
1540
+ 'f.historyLimit': 'Лимит истории',
1541
+ 'f.historyLimitHint': 'Максимальное число последних генераций, удерживаемых в памяти.',
1542
+ 'p.historyLimit': '50',
1543
+
1544
+ 'f.model': 'Модель FAL',
1545
+ 'f.modelHint': 'Идентификатор модели на сервисе FAL (например, fal-ai/flux-2/klein/9b).',
1546
+ 'p.model': 'fal-ai/flux-2/klein/9b',
1547
+ 'f.apiKeyEnv': 'Ключ FAL (credential-ref)',
1548
+ 'f.apiKeyEnvHint': 'Ссылка на ключ доступа FAL в хранилище credentials.',
1549
+ 'p.apiKeyEnv': 'FAL_API_KEY',
1550
+ 'f.baseURL': 'Базовый URL FAL',
1551
+ 'f.baseURLHint': 'Адрес сервиса очередей FAL.',
1552
+ 'p.baseURL': 'https://queue.fal.run',
1553
+ 'f.pollIntervalMs': 'Интервал опроса (мс)',
1554
+ 'f.pollIntervalMsHint': 'Периодичность проверки статуса при асинхронной генерации.',
1555
+ 'p.pollIntervalMs': '2000',
1556
+ 'f.timeoutMs': 'Таймаут (мс)',
1557
+ 'f.timeoutMsHint': 'Предельное время ожидания генерации до возврата ошибки.',
1558
+ 'p.timeoutMs': '180000',
1559
+
1560
+ 'f.customBaseURL': 'URL стороннего API',
1561
+ 'f.customBaseURLHint': 'Адрес OpenAI-совместимого эндпоинта (например, https://api.openai.com/v1).',
1562
+ 'p.customBaseURL': 'https://api.openai.com/v1',
1563
+ 'f.customModel': 'Модель стороннего API',
1564
+ 'f.customModelHint': 'Имя модели для отправки в сторонний API.',
1565
+ 'p.customModel': 'dall-e-3',
1566
+ 'f.customKeyEnv': 'Ключ стороннего API',
1567
+ 'f.customKeyEnvHint': 'Имя ссылки на ключ для стороннего API.',
1568
+ 'p.customKeyEnv': 'OPENAI_API_KEY',
1569
+ 'f.customSize': 'Фиксированный размер',
1570
+ 'f.customSizeHint': 'Точный размер кадра для стороннего API (например, 1024x1024).',
1571
+ 'p.customSize': '1024x1024',
1572
+
1573
+ 'f.replicateModel': 'Модель Replicate',
1574
+ 'f.replicateModelHint': 'Идентификатор модели на Replicate.',
1575
+ 'p.replicateModel': 'black-forest-labs/flux-schnell',
1576
+ 'f.replicateKeyEnv': 'Ключ Replicate',
1577
+ 'f.replicateKeyEnvHint': 'Имя ссылки на API-токен Replicate.',
1578
+ 'p.replicateKeyEnv': 'REPLICATE_API_TOKEN',
1579
+
1580
+ 'f.seedreamModel': 'Модель SeaDream',
1581
+ 'f.seedreamModelHint': 'Идентификатор модели ByteDance SeaDream.',
1582
+ 'p.seedreamModel': 'seedream-4.0',
1583
+ 'f.seedreamKeyEnv': 'Ключ SeaDream',
1584
+ 'f.seedreamKeyEnvHint': 'Имя ссылки на ключ API SeaDream.',
1585
+ 'p.seedreamKeyEnv': 'SEEDREAM_API_KEY',
1586
+ 'f.seedreamBaseURL': 'Базовый URL SeaDream',
1587
+ 'f.seedreamBaseURLHint': 'Адрес API SeaDream (ByteDance/Volcengine Ark).',
1588
+ 'p.seedreamBaseURL': 'https://api.bytedanceapi.com/v1',
1589
+
1590
+ 'f.geminiModel': 'Модель Gemini',
1591
+ 'f.geminiModelHint': 'Идентификатор модели генерации Google Gemini.',
1592
+ 'p.geminiModel': 'gemini-2.0-flash-exp-image-generation',
1593
+ 'f.geminiKeyEnv': 'Ключ Gemini',
1594
+ 'f.geminiKeyEnvHint': 'Имя ссылки на ключ Google Gemini API.',
1595
+ 'p.geminiKeyEnv': 'GEMINI_API_KEY',
1596
+
1597
+ 'f.localKind': 'Тип локального движка',
1598
+ 'f.localKindHint': 'Выбор между ComfyUI API и Automatic1111 WebUI.',
1599
+ 'f.localBaseURL': 'Адрес локального сервера',
1600
+ 'f.localBaseURLHint': 'Адрес локального сервера (ComfyUI: 8188, A1111: 7860).',
1601
+ 'p.localBaseURL': 'http://127.0.0.1:8188',
1602
+ 'f.localModel': 'Модель / Чекпоинт',
1603
+ 'f.localModelHint': 'Имя чекпоинта или воркфлоу ComfyUI.',
1604
+ 'p.localModel': 'v1-5-pruned-emaonly.safetensors',
1605
+ 'f.localSteps': 'Шаги сэмплирования',
1606
+ 'f.localStepsHint': 'Количество шагов генерации.',
1607
+ 'p.localSteps': '20',
1608
+ 'f.localCfg': 'Шкала CFG',
1609
+ 'f.localCfgHint': 'Сила следования промпту.',
1610
+ 'p.localCfg': '7',
1611
+ 'f.localComfyUrl': 'Локальный ComfyUI URL (Legacy)',
1612
+ 'f.localComfyUrlHint': 'Прямой адрес ComfyUI.',
1613
+ 'p.localComfyUrl': 'http://127.0.0.1:8188',
1614
+ 'f.localA1111Url': 'Локальный A1111 URL (Legacy)',
1615
+ 'f.localA1111UrlHint': 'Прямой адрес Automatic1111.',
1616
+ 'p.localA1111Url': 'http://127.0.0.1:7860',
1617
+
1618
+ 'f.subscriptionQuality': 'Качество подписки',
1619
+ 'f.subscriptionQualityHint': 'Профиль качества для провайдеров Codex и Grok.',
1620
+
1621
+ 'f.enhancePrompt': 'LLM-улучшение промпта',
1622
+ 'f.enhancePromptHint': 'Автоматическое обогащение коротких запросов художественными деталями.',
1623
+ 'f.enableLlmEnhancer': 'Включить LLM Enhancer (Legacy)',
1624
+ 'f.enableLlmEnhancerHint': 'Устаревший переключатель улучшения промптов.',
1625
+ 'f.enhanceModel': 'Модель улучшения',
1626
+ 'f.enhanceModelHint': 'Модель для расширения промптов (пусто = основная модель чата).',
1627
+ 'p.enhanceModel': 'Пусто для основной модели чата',
1628
+ 'f.enhanceBelowChars': 'Порог длины промпта',
1629
+ 'f.enhanceBelowCharsHint': 'Промпты длиннее этого количества символов не расширяются.',
1630
+ 'p.enhanceBelowChars': '200',
1631
+ 'f.stylePreset': 'Художественный стиль',
1632
+ 'f.stylePresetHint': 'Автоматический суффикс стиля для всех создаваемых изображений.',
1633
+
1634
+ 'f.qualityGate': 'Контроль качества (Quality Gate)',
1635
+ 'f.qualityGateHint': 'Автоматическая проверка кадров и скрытый реролл при дефектах или пустом кадре.',
1636
+ 'f.dailyBudgetUsd': 'Дневной бюджет ($ USD)',
1637
+ 'f.dailyBudgetUsdHint': 'Предельный лимит затрат в сутки по всем графическим инструментам (0 = без лимита).',
1638
+ 'p.dailyBudgetUsd': '0.00',
1639
+ 'f.loopGuardLimit': 'Защита от циклов (Loop Guard)',
1640
+ 'f.loopGuardLimitHint': 'Максимум последовательных генераций подряд без участия пользователя (0 = выкл).',
1641
+ 'p.loopGuardLimit': '3',
1642
+
1643
+ 'f.diskCache': 'Дисковый кэш по хэшу',
1644
+ 'f.diskCacheHint': 'Мгновенная выдача (<50 мс) и нулевая стоимость для повторных идентичных генераций.',
1645
+ 'f.cacheBySeed': 'Кэш по сиду и промпту',
1646
+ 'f.cacheBySeedHint': 'Возвращать кэш при полном совпадении сида и текста промпта.',
1647
+ 'f.cacheByPrompt': 'Кэш по тексту промпта',
1648
+ 'f.cacheByPromptHint': 'Возвращать кэш при повторении того же текста промпта.',
1649
+ 'f.pruneDays': 'Срок хранения файлов (дни)',
1650
+ 'f.pruneDaysHint': 'Автоматическое удаление файлов и записей старше указанного числа дней (0 = бессрочно).',
1651
+ 'p.pruneDays': '0',
1652
+ 'f.cacheTtlDays': 'Срок хранения кэша (Legacy)',
1653
+ 'f.cacheTtlDaysHint': 'Устаревшее имя параметра срока хранения.',
1654
+ 'p.cacheTtlDays': '0',
1655
+ }
1656
+
1657
+ // -------------------------------------------------------------- Registration & Apply
1658
+ const inject = ['slots', 'settingsScope', 'locale', 'sessions']
1659
+
1660
+ function apply(ctx) {
1661
+ if (ctx.locale && typeof ctx.locale.define === 'function') {
1662
+ ctx.locale.define('en', NS, en)
1663
+ ctx.locale.define('zh', NS, zh)
1664
+ ctx.locale.define('ru', NS, ru)
1665
+ }
1666
+
1667
+ let card
1668
+ const cardOnce = () => {
1669
+ if (card === undefined) {
1670
+ const scope = ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).bind({ namespace: SETTINGS_NS })
1671
+ card = new FalSettingsCardController(scope)
1672
+ }
1673
+ return card
1674
+ }
1675
+
1676
+ function registerSlotWhenReady(slotName, registerFn) {
1677
+ if (!ctx.slots) return
1678
+ if (typeof ctx.slots.inject === 'function') {
1679
+ try {
1680
+ ctx.slots.inject(slotName, () => {
1681
+ try {
1682
+ return registerFn()
1683
+ } catch (err) {
1684
+ console.warn('[dsh-image-gen] Error registering slot ' + slotName + ':', err)
1685
+ }
1686
+ })
1687
+ return
1688
+ } catch (err) {
1689
+ console.warn('[dsh-image-gen] Failed to inject slot ' + slotName + ':', err)
1690
+ }
1691
+ }
1692
+ if (typeof ctx.slots.register === 'function') {
1693
+ try {
1694
+ registerFn()
1695
+ } catch (err) {
1696
+ console.warn('[dsh-image-gen] Failed direct registration for ' + slotName + ':', err)
1697
+ }
1698
+ }
1699
+ }
1700
+
1701
+ // Register toolviews for all 8 visual tools
1702
+ const toolviewTools = [
1703
+ 'generate_image',
1704
+ 'edit_image',
1705
+ 'vary_image',
1706
+ 'blend_images',
1707
+ 'generate_image_pack',
1708
+ 'remove_background',
1709
+ 'upscale_image',
1710
+ 'vectorize_image',
1711
+ 'assemble_image_grid',
1712
+ ]
1713
+
1714
+ for (const toolName of toolviewTools) {
1715
+ registerSlotWhenReady('tool.call.toolview', () =>
1716
+ ctx.slots.register(
1717
+ {
1718
+ name: 'tool.call.toolview',
1719
+ key: toolName,
1720
+ locale: NS,
1721
+ inject: () => ({ sessions: ctx.sessions }),
1722
+ },
1723
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(FalImageCard, props))
1724
+ )
1725
+ )
1726
+ }
1727
+
1728
+
1729
+ // Native Sidebar Right Pane Tab & BetterSidebar
1730
+ if (typeof ctx.inject === 'function') {
1731
+ try {
1732
+ ctx.inject(['sidebarRightTabs'], (sctx) => {
1733
+ const tabs = sctx && sctx.sidebarRightTabs
1734
+ if (!tabs || typeof tabs.register !== 'function') return
1735
+ try {
1736
+ const def = {
1737
+ id: '@goodandready/dsh-image-gen:gallery',
1738
+ kind: 'image-gallery',
1739
+ priority: 'extension',
1740
+ title: () => 'Gallery',
1741
+ guide: [
1742
+ {
1743
+ order: 45,
1744
+ title: () => 'Image Studio Gallery',
1745
+ description: () => 'Browse and inspect recent image generations',
1746
+ icon: () => react.createElement('span', null, '🖼️'),
1747
+ },
1748
+ ],
1749
+ }
1750
+ if (typeof sctx.effect === 'function') {
1751
+ sctx.effect(() => tabs.register(def))
1752
+ } else {
1753
+ tabs.register(def)
1754
+ }
1755
+
1756
+ if (ctx.slots) {
1757
+ const registerPaneTab = () => {
1758
+ try {
1759
+ return ctx.slots.register(
1760
+ {
1761
+ name: 'sidebar.right.pane.tab',
1762
+ key: '@goodandready/dsh-image-gen:gallery',
1763
+ locale: NS,
1764
+ inject: () => ({ ctx }),
1765
+ },
1766
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(GalleryView, { ...props, ctx }))
1767
+ )
1768
+ } catch (e) {
1769
+ console.warn('[dsh-image-gen] native sidebar pane tab register failed', e)
1770
+ }
1771
+ }
1772
+ if (typeof ctx.slots.inject === 'function') {
1773
+ ctx.slots.inject('sidebar.right.pane.tab', registerPaneTab)
1774
+ } else {
1775
+ registerPaneTab()
1776
+ }
1777
+ }
1778
+ } catch (e) {
1779
+ console.warn('[dsh-image-gen] native sidebar registration failed', e)
1780
+ }
1781
+ })
1782
+ } catch (e) {}
1783
+
1784
+ try {
1785
+ ctx.inject(['betterSidebar'], (sctx) => {
1786
+ const svc = sctx && sctx.betterSidebar
1787
+ if (!svc || typeof svc.registerTab !== 'function') return
1788
+ try {
1789
+ svc.registerTab({
1790
+ id: 'dsh-image-gen:gallery',
1791
+ title: () => 'Gallery',
1792
+ icon: () => react.createElement('span', null, '🖼️'),
1793
+ order: 45,
1794
+ component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(GalleryView, { ctx, scope })),
1795
+ })
1796
+ } catch (e) {
1797
+ console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
1798
+ }
1799
+ })
1800
+ } catch (e) {}
1801
+ }
1802
+
1803
+ // Conversation header utilities chip (quick-access gallery button in chat header)
1804
+ registerSlotWhenReady('conversation.session.header.utilities', () =>
1805
+ ctx.slots.register(
1806
+ {
1807
+ name: 'conversation.session.header.utilities',
1808
+ id: 'dsh-image-gen-gallery-chip',
1809
+ order: 8,
1810
+ locale: NS,
1811
+ inject: () => ({ ctx }),
1812
+ },
1813
+ (props) => react.createElement(GalleryHeaderChip, { ...props, ctx })
1814
+ )
1815
+ )
1816
+
1817
+ // Settings card item
1818
+ registerSlotWhenReady('settings.plugin.item', () =>
1819
+ ctx.slots.register(
1820
+ {
1821
+ name: 'settings.plugin.item',
1822
+ key: SETTINGS_NS,
1823
+ locale: NS,
1824
+ inject: () => cardOnce().inject(),
1825
+ },
1826
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(FalSettingsCard, props))
1827
+ )
1828
+ )
1829
+ }
1830
+
1831
+ exports.apply = apply
1832
+ exports.inject = inject
1833
+ return module.exports
1834
+ },
1835
+ })
1836
+ + Number(selected.cost).toFixed(4)) : null
1837
+ ),
1838
+ react.createElement(
1839
+ 'div',
1840
+ { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
1841
+ react.createElement('span', { style: { fontSize: '11px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Prompt:'),
1842
+ react.createElement(
1843
+ 'p',
1844
+ {
1845
+ style: {
1846
+ fontSize: '12px',
1847
+ color: 'var(--dsw-alias-label-primary)',
1848
+ lineHeight: '1.4',
1849
+ background: 'var(--dsw-alias-bg-layer-2)',
1850
+ padding: '8px',
1851
+ borderRadius: '6px',
1852
+ margin: 0,
1853
+ },
1854
+ },
1855
+ selected.prompt || 'No prompt recorded'
1856
+ )
1857
+ ),
1858
+ react.createElement(
1859
+ 'div',
1860
+ { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '4px' } },
1861
+ react.createElement(
1862
+ 'button',
1863
+ {
1864
+ type: 'button',
1865
+ className: 'ig-tab-btn',
1866
+ style: { border: '1px solid var(--dsw-alias-border-l2)' },
1867
+ onClick: () => {
1868
+ if (typeof navigator !== 'undefined' && navigator.clipboard && selected.prompt) {
1869
+ navigator.clipboard.writeText(selected.prompt)
1870
+ setCopiedPrompt(true)
1871
+ setTimeout(() => setCopiedPrompt(false), 2000)
1872
+ }
1873
+ },
1874
+ },
1875
+ copiedPrompt ? '✓ Copied!' : t('gallery.copy_prompt')
1876
+ ),
1877
+ react.createElement(
1878
+ 'button',
1879
+ {
1880
+ type: 'button',
1881
+ className: 'ig-save-btn',
1882
+ onClick: () => {
1883
+ const url = selected.thumbnailUrl || (selected.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(selected.attachmentId) : '')
1884
+ if (typeof navigator !== 'undefined' && navigator.clipboard && url) {
1885
+ navigator.clipboard.writeText('![' + (selected.prompt || 'Image') + '](' + url + ')')
1886
+ setCopiedLink(true)
1887
+ setTimeout(() => setCopiedLink(false), 2000)
1888
+ }
1889
+ },
1890
+ },
1891
+ copiedLink ? '✓ Copied!' : t('gallery.copy_link')
1892
+ )
1893
+ )
1894
+ )
1895
+ )
1896
+ )
1897
+ }
1898
+
1899
+ // -------------------------------------------------------------- Header Quick-Access Chip
1900
+ function GalleryHeaderChip(props) {
1901
+ const [open, setOpen] = react.useState(false)
1902
+ return react.createElement(
1903
+ 'div',
1904
+ { style: { display: 'inline-flex', alignItems: 'center', position: 'relative' } },
1905
+ react.createElement(
1906
+ 'button',
1907
+ {
1908
+ type: 'button',
1909
+ title: 'Image Studio Gallery',
1910
+ onClick: () => setOpen(!open),
1911
+ style: {
1912
+ appearance: 'none',
1913
+ background: 'none',
1914
+ border: '1px solid var(--dsw-alias-border-l2)',
1915
+ borderRadius: '6px',
1916
+ padding: '4px 8px',
1917
+ cursor: 'pointer',
1918
+ display: 'flex',
1919
+ alignItems: 'center',
1920
+ gap: '5px',
1921
+ fontSize: '12px',
1922
+ color: 'var(--dsw-alias-label-secondary)',
1923
+ },
1924
+ },
1925
+ react.createElement('span', null, '🖼️'),
1926
+ react.createElement('span', null, 'Gallery')
1927
+ ),
1928
+ open &&
1929
+ react.createElement(
1930
+ 'div',
1931
+ {
1932
+ style: {
1933
+ position: 'fixed',
1934
+ top: '56px',
1935
+ right: '16px',
1936
+ width: '380px',
1937
+ maxHeight: '520px',
1938
+ background: 'var(--dsw-alias-bg-layer-3)',
1939
+ border: '1px solid var(--dsw-alias-border-l2)',
1940
+ borderRadius: '12px',
1941
+ boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
1942
+ zIndex: 9999,
1943
+ padding: '16px',
1944
+ overflowY: 'auto',
1945
+ },
1946
+ },
1947
+ react.createElement(
1948
+ 'div',
1949
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' } },
1950
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Image Studio Gallery'),
1951
+ react.createElement(
1952
+ 'button',
1953
+ {
1954
+ type: 'button',
1955
+ onClick: () => setOpen(false),
1956
+ style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: 'var(--dsw-alias-label-secondary)' },
1957
+ },
1958
+ '✕'
1959
+ )
1960
+ ),
1961
+ react.createElement(GalleryView, { ctx: props.ctx })
1962
+ )
1963
+ )
1964
+ }
1965
+
506
1966
  function FalSettingsCard(props) {
507
1967
  const t = props.t || ((k) => k)
508
1968
  const [open, setOpen] = react.useState(props.defaultOpen ?? true)
@@ -1274,6 +2734,7 @@ window.__ModuleLoader__.load({
1274
2734
  'remove_background',
1275
2735
  'upscale_image',
1276
2736
  'vectorize_image',
2737
+ 'assemble_image_grid',
1277
2738
  ]
1278
2739
 
1279
2740
  for (const toolName of toolviewTools) {