pygpt-net 2.6.43__py3-none-any.whl → 2.6.44__py3-none-any.whl
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.
- pygpt_net/CHANGELOG.txt +8 -0
- pygpt_net/__init__.py +1 -1
- pygpt_net/controller/debug/debug.py +7 -19
- pygpt_net/controller/debug/fixtures.py +103 -0
- pygpt_net/core/debug/debug.py +2 -2
- pygpt_net/core/fixtures/stream/__init__.py +0 -0
- pygpt_net/{provider/api/fake → core/fixtures/stream}/generator.py +1 -1
- pygpt_net/core/render/web/body.py +5 -1
- pygpt_net/data/config/config.json +9 -5
- pygpt_net/data/config/models.json +2 -2
- pygpt_net/data/config/settings.json +59 -19
- pygpt_net/data/js/app.js +727 -361
- pygpt_net/data/locale/locale.en.ini +8 -1
- pygpt_net/js_rc.py +11506 -10502
- pygpt_net/provider/api/openai/__init__.py +4 -12
- pygpt_net/provider/core/config/patch.py +14 -1
- pygpt_net/ui/base/context_menu.py +3 -2
- pygpt_net/ui/layout/ctx/ctx_list.py +3 -3
- pygpt_net/ui/menu/debug.py +36 -23
- pygpt_net/ui/widget/lists/context.py +233 -51
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.44.dist-info}/METADATA +10 -2
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.44.dist-info}/RECORD +26 -24
- /pygpt_net/{provider/api/fake/__init__.py → core/fixtures/__init__} +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.44.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.44.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.44.dist-info}/entry_points.txt +0 -0
pygpt_net/data/js/app.js
CHANGED
|
@@ -553,8 +553,9 @@
|
|
|
553
553
|
growth: 2.6, // Ramp step quickly if adaptive is enabled
|
|
554
554
|
minInterval: 500, // Minimum time between snapshots (ms) to avoid churn
|
|
555
555
|
softLatency: 1200, // Force snapshot only after a noticeable idle (ms)
|
|
556
|
-
minLinesForHL:
|
|
557
|
-
|
|
556
|
+
minLinesForHL: Utils.g('PROFILE_CODE_HL_N_LINE', 25),
|
|
557
|
+
minCharsForHL: Utils.g('PROFILE_CODE_HL_N_CHARS', 5000),
|
|
558
|
+
promoteMinInterval: 300, promoteMaxLatency: 800, promoteMinLines: Utils.g('PROFILE_CODE_HL_N_LINE', 25),
|
|
558
559
|
adaptiveStep: Utils.g('PROFILE_CODE_ADAPTIVE_STEP', true),
|
|
559
560
|
// Hard switches to plain streaming (no incremental hljs, minimal DOM churn)
|
|
560
561
|
stopAfterLines: Utils.g('PROFILE_CODE_STOP_HL_AFTER_LINES', 300), // Stop incremental hljs after this many lines
|
|
@@ -736,175 +737,159 @@
|
|
|
736
737
|
}
|
|
737
738
|
}
|
|
738
739
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
// Check if code block is already finalized (not streaming).
|
|
759
|
-
isFinalizedCode(el) {
|
|
760
|
-
if (!el || el.tagName !== 'CODE') return false;
|
|
761
|
-
if (el.dataset && el.dataset._active_stream === '1') return false;
|
|
762
|
-
const highlighted = (el.getAttribute('data-highlighted') === 'yes') || el.classList.contains('hljs');
|
|
763
|
-
return highlighted;
|
|
764
|
-
}
|
|
765
|
-
// Is element scrolled close to the bottom by a margin?
|
|
766
|
-
isNearBottomEl(el, margin = 100) {
|
|
767
|
-
if (!el) return true;
|
|
768
|
-
const distance = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
769
|
-
return distance <= margin;
|
|
770
|
-
}
|
|
771
|
-
// Scroll code element to the bottom respecting interaction state.
|
|
772
|
-
scrollToBottom(el, live = false, force = false) {
|
|
773
|
-
if (!el || !el.isConnected) return;
|
|
774
|
-
if (!force && this.isFinalizedCode(el)) return;
|
|
775
|
-
|
|
776
|
-
const st = this.state(el);
|
|
777
|
-
const now = Utils.now();
|
|
778
|
-
if (!force && st.freezeUntil && now < st.freezeUntil) return;
|
|
779
|
-
|
|
780
|
-
const distNow = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
781
|
-
if (!force && distNow <= 1) { st.lastScrollTop = el.scrollTop; return; }
|
|
782
|
-
|
|
783
|
-
const marginPx = live ? 96 : this.cfg.CODE_SCROLL.NEAR_MARGIN_PX;
|
|
784
|
-
const behavior = 'instant';
|
|
785
|
-
|
|
786
|
-
if (!force) {
|
|
787
|
-
if (live && st.autoFollow !== true) return;
|
|
788
|
-
if (!live && !(st.autoFollow === true || this.isNearBottomEl(el, marginPx) || !st.userInteracted)) return;
|
|
740
|
+
// ==========================================================================
|
|
741
|
+
// 2) Code scroll state manager
|
|
742
|
+
// ==========================================================================
|
|
743
|
+
|
|
744
|
+
class CodeScrollState {
|
|
745
|
+
constructor(cfg, raf) {
|
|
746
|
+
this.cfg = cfg;
|
|
747
|
+
this.raf = raf;
|
|
748
|
+
this.map = new WeakMap();
|
|
749
|
+
this.rafMap = new WeakMap();
|
|
750
|
+
this.rafIds = new Set(); // legacy
|
|
751
|
+
this.rafKeyMap = new WeakMap();
|
|
752
|
+
}
|
|
753
|
+
// Get or create per-code element state.
|
|
754
|
+
state(el) {
|
|
755
|
+
let s = this.map.get(el);
|
|
756
|
+
if (!s) { s = { autoFollow: false, lastScrollTop: 0, userInteracted: false, freezeUntil: 0 }; this.map.set(el, s); }
|
|
757
|
+
return s;
|
|
789
758
|
}
|
|
759
|
+
// Check if code block is already finalized (not streaming).
|
|
760
|
+
isFinalizedCode(el) {
|
|
761
|
+
if (!el || el.tagName !== 'CODE') return false;
|
|
762
|
+
if (el.dataset && el.dataset._active_stream === '1') return false;
|
|
763
|
+
const highlighted = (el.getAttribute('data-highlighted') === 'yes') || el.classList.contains('hljs');
|
|
764
|
+
return highlighted;
|
|
765
|
+
}
|
|
766
|
+
// Is element scrolled close to the bottom by a margin?
|
|
767
|
+
isNearBottomEl(el, margin = 100) {
|
|
768
|
+
if (!el) return true;
|
|
769
|
+
const distance = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
770
|
+
return distance <= margin;
|
|
771
|
+
}
|
|
772
|
+
// Scroll code element to the bottom respecting interaction state.
|
|
773
|
+
scrollToBottom(el, live = false, force = false) {
|
|
774
|
+
if (!el || !el.isConnected) return;
|
|
775
|
+
if (!force && this.isFinalizedCode(el)) return;
|
|
790
776
|
|
|
791
|
-
|
|
792
|
-
st.lastScrollTop = el.scrollTop;
|
|
793
|
-
}
|
|
794
|
-
// Schedule bottom scroll in rAF (coalesces multiple calls).
|
|
795
|
-
scheduleScroll(el, live = false, force = false) {
|
|
796
|
-
if (!el || !el.isConnected) return;
|
|
797
|
-
if (!force && this.isFinalizedCode(el)) return;
|
|
798
|
-
if (this.rafMap.get(el)) return;
|
|
799
|
-
this.rafMap.set(el, true);
|
|
800
|
-
|
|
801
|
-
let key = this.rafKeyMap.get(el);
|
|
802
|
-
if (!key) { key = { t: 'codeScroll', el }; this.rafKeyMap.set(el, key); }
|
|
803
|
-
|
|
804
|
-
this.raf.schedule(key, () => {
|
|
805
|
-
this.rafMap.delete(el);
|
|
806
|
-
this.scrollToBottom(el, live, force);
|
|
807
|
-
}, 'CodeScroll', 0);
|
|
808
|
-
}
|
|
809
|
-
// Attach scroll/wheel/touch handlers to manage auto-follow state.
|
|
810
|
-
attachHandlers(codeEl) {
|
|
811
|
-
if (!codeEl || codeEl.dataset.csListeners === '1') return;
|
|
812
|
-
codeEl.dataset.csListeners = '1';
|
|
813
|
-
const st = this.state(codeEl);
|
|
814
|
-
|
|
815
|
-
const onScroll = (ev) => {
|
|
816
|
-
const top = codeEl.scrollTop;
|
|
817
|
-
const isUser = !!(ev && ev.isTrusted === true);
|
|
777
|
+
const st = this.state(el);
|
|
818
778
|
const now = Utils.now();
|
|
779
|
+
if (!force && st.freezeUntil && now < st.freezeUntil) return;
|
|
819
780
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
781
|
+
const distNow = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
782
|
+
if (!force && distNow <= 1) { st.lastScrollTop = el.scrollTop; return; }
|
|
783
|
+
|
|
784
|
+
const marginPx = live ? 96 : this.cfg.CODE_SCROLL.NEAR_MARGIN_PX;
|
|
785
|
+
const behavior = 'instant';
|
|
786
|
+
|
|
787
|
+
if (!force) {
|
|
788
|
+
if (live && st.autoFollow !== true) return;
|
|
789
|
+
if (!live && !(st.autoFollow === true || this.isNearBottomEl(el, marginPx) || !st.userInteracted)) return;
|
|
825
790
|
}
|
|
826
791
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
792
|
+
try { el.scrollTo({ top: el.scrollHeight, behavior }); } catch (_) { el.scrollTop = el.scrollHeight; }
|
|
793
|
+
st.lastScrollTop = el.scrollTop;
|
|
794
|
+
}
|
|
795
|
+
// Schedule bottom scroll in rAF (coalesces multiple calls).
|
|
796
|
+
scheduleScroll(el, live = false, force = false) {
|
|
797
|
+
if (!el || !el.isConnected) return;
|
|
798
|
+
if (!force && this.isFinalizedCode(el)) return;
|
|
799
|
+
if (this.rafMap.get(el)) return;
|
|
800
|
+
this.rafMap.set(el, true);
|
|
801
|
+
|
|
802
|
+
let key = this.rafKeyMap.get(el);
|
|
803
|
+
if (!key) { key = { t: 'codeScroll', el }; this.rafKeyMap.set(el, key); }
|
|
804
|
+
|
|
805
|
+
this.raf.schedule(key, () => {
|
|
806
|
+
this.rafMap.delete(el);
|
|
807
|
+
this.scrollToBottom(el, live, force);
|
|
808
|
+
}, 'CodeScroll', 0);
|
|
809
|
+
}
|
|
810
|
+
// Attach scroll/wheel/touch handlers to manage auto-follow state.
|
|
811
|
+
attachHandlers(codeEl) {
|
|
812
|
+
if (!codeEl || codeEl.dataset.csListeners === '1') return;
|
|
813
|
+
codeEl.dataset.csListeners = '1';
|
|
814
|
+
const st = this.state(codeEl);
|
|
815
|
+
|
|
816
|
+
const onScroll = (ev) => {
|
|
817
|
+
const top = codeEl.scrollTop;
|
|
818
|
+
const isUser = !!(ev && ev.isTrusted === true);
|
|
819
|
+
const now = Utils.now();
|
|
820
|
+
|
|
821
|
+
if (this.isFinalizedCode(codeEl)) {
|
|
822
|
+
if (isUser) st.userInteracted = true;
|
|
823
|
+
st.autoFollow = false;
|
|
824
|
+
st.lastScrollTop = top;
|
|
825
|
+
return;
|
|
832
826
|
}
|
|
833
|
-
} else {
|
|
834
|
-
if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) st.autoFollow = true;
|
|
835
|
-
}
|
|
836
|
-
st.lastScrollTop = top;
|
|
837
|
-
};
|
|
838
827
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
828
|
+
if (isUser) {
|
|
829
|
+
if (top + 1 < st.lastScrollTop) {
|
|
830
|
+
st.autoFollow = false; st.userInteracted = true; st.freezeUntil = now + 1000;
|
|
831
|
+
} else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) {
|
|
832
|
+
st.autoFollow = true;
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) st.autoFollow = true;
|
|
836
|
+
}
|
|
837
|
+
st.lastScrollTop = top;
|
|
838
|
+
};
|
|
842
839
|
|
|
843
|
-
|
|
840
|
+
const onWheel = (ev) => {
|
|
841
|
+
st.userInteracted = true;
|
|
842
|
+
const now = Utils.now();
|
|
844
843
|
|
|
845
|
-
|
|
846
|
-
else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) { st.autoFollow = true; }
|
|
847
|
-
};
|
|
844
|
+
if (this.isFinalizedCode(codeEl)) { st.autoFollow = false; return; }
|
|
848
845
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
// Ensure code starts scrolled to bottom once after insert.
|
|
854
|
-
initCodeBottomOnce(codeEl) {
|
|
855
|
-
if (!codeEl || !codeEl.isConnected) return;
|
|
856
|
-
if (codeEl.dataset && codeEl.dataset._active_stream === '1') return;
|
|
857
|
-
if (codeEl.dataset && codeEl.dataset.csInitBtm === '1') return;
|
|
858
|
-
const wrapper = codeEl.closest('.code-wrapper');
|
|
859
|
-
if (!wrapper) return;
|
|
846
|
+
if (ev.deltaY < 0) { st.autoFollow = false; st.freezeUntil = now + 1000; }
|
|
847
|
+
else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) { st.autoFollow = true; }
|
|
848
|
+
};
|
|
860
849
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (!codeEl.isConnected) return;
|
|
865
|
-
try {
|
|
866
|
-
this.scrollToBottom(codeEl, false, true);
|
|
867
|
-
const st = this.state(codeEl);
|
|
868
|
-
st.autoFollow = false;
|
|
869
|
-
st.lastScrollTop = codeEl.scrollTop;
|
|
870
|
-
} catch (_) {}
|
|
871
|
-
}, 'CodeScroll', 0);
|
|
872
|
-
}
|
|
873
|
-
// Attach handlers to all bot code blocks under root (or document).
|
|
874
|
-
initScrollableBlocks(root) {
|
|
875
|
-
const scope = root || document;
|
|
876
|
-
let nodes = [];
|
|
877
|
-
if (scope.nodeType === 1 && scope.closest && scope.closest('.msg-box.msg-bot')) {
|
|
878
|
-
nodes = scope.querySelectorAll('pre code');
|
|
879
|
-
} else {
|
|
880
|
-
nodes = document.querySelectorAll('.msg-box.msg-bot pre code');
|
|
850
|
+
codeEl.addEventListener('scroll', onScroll, { passive: true });
|
|
851
|
+
codeEl.addEventListener('wheel', onWheel, { passive: true });
|
|
852
|
+
codeEl.addEventListener('touchstart', function () { st.userInteracted = true; }, { passive: true });
|
|
881
853
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
854
|
+
// Attach handlers to all bot code blocks under root (or document).
|
|
855
|
+
// IMPORTANT: We intentionally do NOT auto-scroll finalized/static code blocks to the bottom.
|
|
856
|
+
// Only actively streaming code blocks (data-_active_stream="1") are auto-followed live.
|
|
857
|
+
initScrollableBlocks(root) {
|
|
858
|
+
const scope = root || document;
|
|
859
|
+
let nodes = [];
|
|
860
|
+
if (scope.nodeType === 1 && scope.closest && scope.closest('.msg-box.msg-bot')) {
|
|
861
|
+
nodes = scope.querySelectorAll('pre code');
|
|
889
862
|
} else {
|
|
890
|
-
|
|
863
|
+
nodes = document.querySelectorAll('.msg-box.msg-bot pre code');
|
|
891
864
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
865
|
+
if (!nodes.length) return;
|
|
866
|
+
|
|
867
|
+
nodes.forEach((code) => {
|
|
868
|
+
this.attachHandlers(code);
|
|
869
|
+
// Live streaming blocks: enable auto-follow and keep them glued to bottom.
|
|
870
|
+
if (code.dataset._active_stream === '1') {
|
|
871
|
+
const st = this.state(code);
|
|
872
|
+
st.autoFollow = true;
|
|
873
|
+
this.scheduleScroll(code, true, false);
|
|
874
|
+
}
|
|
875
|
+
// Finalized/static blocks: do nothing (no initial scroll-to-bottom).
|
|
876
|
+
// This avoids surprising jumps when static content is rendered.
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
// Transfer stored scroll state between elements (after replace).
|
|
880
|
+
transfer(oldEl, newEl) {
|
|
881
|
+
if (!oldEl || !newEl || oldEl === newEl) return;
|
|
882
|
+
const oldState = this.map.get(oldEl);
|
|
883
|
+
if (oldState) this.map.set(newEl, { ...oldState });
|
|
884
|
+
this.attachHandlers(newEl);
|
|
885
|
+
}
|
|
886
|
+
// Cancel any scheduled scroll tasks for code blocks.
|
|
887
|
+
cancelAllScrolls() {
|
|
888
|
+
try { this.raf.cancelGroup('CodeScroll'); } catch (_) {}
|
|
889
|
+
this.rafMap = new WeakMap();
|
|
890
|
+
this.rafIds.clear();
|
|
891
|
+
}
|
|
906
892
|
}
|
|
907
|
-
}
|
|
908
893
|
|
|
909
894
|
// ==========================================================================
|
|
910
895
|
// 3) Highlighter (hljs) + rAF viewport scan
|
|
@@ -1175,7 +1160,6 @@
|
|
|
1175
1160
|
// ==========================================================================
|
|
1176
1161
|
|
|
1177
1162
|
class CustomMarkup {
|
|
1178
|
-
// Logger-aware processor; no console usage.
|
|
1179
1163
|
constructor(cfg, logger) {
|
|
1180
1164
|
this.cfg = cfg || { CUSTOM_MARKUP_RULES: [] };
|
|
1181
1165
|
this.logger = logger || new Logger(cfg);
|
|
@@ -1183,6 +1167,17 @@
|
|
|
1183
1167
|
}
|
|
1184
1168
|
_d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
|
|
1185
1169
|
|
|
1170
|
+
// Decode HTML entities once (safe)
|
|
1171
|
+
// This addresses cases when linkify/full markdown path leaves literal """ etc. in text nodes.
|
|
1172
|
+
// We decode only for rules that explicitly opt-in (see compile()) to avoid changing semantics globally.
|
|
1173
|
+
decodeEntitiesOnce(s) {
|
|
1174
|
+
if (!s || s.indexOf('&') === -1) return String(s || '');
|
|
1175
|
+
// Using a shared <textarea> avoids DOM parsing side-effects and is fast enough for small JSON payloads.
|
|
1176
|
+
const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
|
|
1177
|
+
ta.innerHTML = s;
|
|
1178
|
+
return ta.value;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1186
1181
|
// Compile rules once; also precompile strict and whitespace-tolerant "full match" regexes.
|
|
1187
1182
|
compile(rules) {
|
|
1188
1183
|
const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
|
|
@@ -1193,11 +1188,26 @@
|
|
|
1193
1188
|
const className = (r.className || r.class || '').trim();
|
|
1194
1189
|
const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
|
|
1195
1190
|
|
|
1191
|
+
// Opt-in decoding: default to true for "cmd" to fix JSON quotes (") in static full-render path,
|
|
1192
|
+
// leave false for other tags unless explicitly requested by rule author.
|
|
1193
|
+
const decodeEntities = (typeof r.decodeEntities === 'boolean')
|
|
1194
|
+
? r.decodeEntities
|
|
1195
|
+
: ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
|
|
1196
|
+
|
|
1196
1197
|
const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
|
|
1197
1198
|
const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
|
|
1198
1199
|
const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
|
|
1199
1200
|
|
|
1200
|
-
const item = {
|
|
1201
|
+
const item = {
|
|
1202
|
+
name: r.name || tag,
|
|
1203
|
+
tag,
|
|
1204
|
+
className,
|
|
1205
|
+
innerMode,
|
|
1206
|
+
open: r.open,
|
|
1207
|
+
close: r.close,
|
|
1208
|
+
decodeEntities, // per-rule decode switch
|
|
1209
|
+
re, reFull, reFullTrim
|
|
1210
|
+
};
|
|
1201
1211
|
compiled.push(item);
|
|
1202
1212
|
this._d('COMPILE_RULE', { name: item.name, tag: item.tag, innerMode: item.innerMode, open: item.open, close: item.close });
|
|
1203
1213
|
}
|
|
@@ -1205,6 +1215,7 @@
|
|
|
1205
1215
|
const open = '[!cmd]', close = '[/!cmd]';
|
|
1206
1216
|
const item = {
|
|
1207
1217
|
name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
|
|
1218
|
+
decodeEntities: true, // Fallback rule for cmd also opts-in to decoding
|
|
1208
1219
|
re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
|
|
1209
1220
|
reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
|
|
1210
1221
|
reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$')
|
|
@@ -1279,16 +1290,22 @@
|
|
|
1279
1290
|
return null;
|
|
1280
1291
|
}
|
|
1281
1292
|
|
|
1282
|
-
// Set inner content according to the rule's mode.
|
|
1283
|
-
setInnerByMode(el, mode, text, MD) {
|
|
1293
|
+
// Set inner content according to the rule's mode, with optional entity decode.
|
|
1294
|
+
setInnerByMode(el, mode, text, MD, decodeEntities = false) {
|
|
1295
|
+
let payload = String(text || '');
|
|
1296
|
+
// Decode entities only when asked by the rule (prevents global behavior change).
|
|
1297
|
+
if (decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1298
|
+
try { payload = this.decodeEntitiesOnce(payload); } catch (_) { /* keep original on failure */ }
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1284
1301
|
if (mode === 'markdown-inline' && typeof window.markdownit !== 'undefined') {
|
|
1285
1302
|
try {
|
|
1286
|
-
if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(
|
|
1303
|
+
if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(payload); return; }
|
|
1287
1304
|
const tempMD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
|
|
1288
|
-
el.innerHTML = tempMD.renderInline(
|
|
1305
|
+
el.innerHTML = tempMD.renderInline(payload); return;
|
|
1289
1306
|
} catch (_) {}
|
|
1290
1307
|
}
|
|
1291
|
-
el.textContent =
|
|
1308
|
+
el.textContent = payload;
|
|
1292
1309
|
}
|
|
1293
1310
|
|
|
1294
1311
|
// Try to replace an entire <p> that is a full custom markup match.
|
|
@@ -1317,8 +1334,8 @@
|
|
|
1317
1334
|
out.setAttribute('data-cm', rule.name);
|
|
1318
1335
|
|
|
1319
1336
|
const innerText = m[1] || '';
|
|
1320
|
-
// Use mode-driven inner content materialization (text or markdown-inline).
|
|
1321
|
-
this.setInnerByMode(out, rule.innerMode, innerText, MD);
|
|
1337
|
+
// Use mode-driven inner content materialization (text or markdown-inline) with optional decoding.
|
|
1338
|
+
this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities);
|
|
1322
1339
|
|
|
1323
1340
|
// Replace the original <p> with the desired container (<div>, <think>, <p>, etc.).
|
|
1324
1341
|
try { el.replaceWith(out); } catch (_) {
|
|
@@ -1381,7 +1398,7 @@
|
|
|
1381
1398
|
const out = document.createElement('p');
|
|
1382
1399
|
if (fm.rule.className) out.className = fm.rule.className;
|
|
1383
1400
|
out.setAttribute('data-cm', fm.rule.name);
|
|
1384
|
-
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD);
|
|
1401
|
+
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
|
|
1385
1402
|
try { parent.replaceWith(out); } catch (_) {
|
|
1386
1403
|
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1387
1404
|
}
|
|
@@ -1407,7 +1424,7 @@
|
|
|
1407
1424
|
const el = document.createElement(tag);
|
|
1408
1425
|
if (m.rule.className) el.className = m.rule.className;
|
|
1409
1426
|
el.setAttribute('data-cm', m.rule.name);
|
|
1410
|
-
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD);
|
|
1427
|
+
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities);
|
|
1411
1428
|
|
|
1412
1429
|
frag.appendChild(el);
|
|
1413
1430
|
this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
|
|
@@ -2497,237 +2514,586 @@
|
|
|
2497
2514
|
}
|
|
2498
2515
|
}
|
|
2499
2516
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
try { el.removeAttribute('md-block-markdown'); } catch (_) {}
|
|
2517
|
+
// UserCollapseManager – collapsible user messages (msg-box.msg-user)
|
|
2518
|
+
class UserCollapseManager {
|
|
2519
|
+
constructor(cfg) {
|
|
2520
|
+
this.cfg = cfg || {};
|
|
2521
|
+
// Collapse threshold in pixels (can be overridden via window.USER_MSG_COLLAPSE_HEIGHT_PX).
|
|
2522
|
+
this.threshold = Utils.g('USER_MSG_COLLAPSE_HEIGHT_PX', 1000);
|
|
2523
|
+
// Track processed .msg elements to allow cheap remeasure on resize if needed.
|
|
2524
|
+
this._processed = new Set();
|
|
2525
|
+
|
|
2526
|
+
// Visual indicator attached while collapsed (does not modify original text).
|
|
2527
|
+
this.ellipsisText = ' [...]';
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
_icons() {
|
|
2531
|
+
const I = (this.cfg && this.cfg.ICONS) || {};
|
|
2532
|
+
return { expand: I.EXPAND || '', collapse: I.COLLAPSE || '' };
|
|
2533
|
+
}
|
|
2534
|
+
_labels() {
|
|
2535
|
+
const L = (this.cfg && this.cfg.LOCALE) || {};
|
|
2536
|
+
return { expand: L.EXPAND || 'Expand', collapse: L.COLLAPSE || 'Collapse' };
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Schedule a function for next frame (ensures layout is up-to-date before scrolling).
|
|
2540
|
+
_afterLayout(fn) {
|
|
2541
|
+
try {
|
|
2542
|
+
if (typeof runtime !== 'undefined' && runtime.raf && typeof runtime.raf.schedule === 'function') {
|
|
2543
|
+
const key = { t: 'UC:afterLayout', i: Math.random() };
|
|
2544
|
+
runtime.raf.schedule(key, () => { try { fn && fn(); } catch (_) {} }, 'UserCollapse', 0);
|
|
2545
|
+
return;
|
|
2530
2546
|
}
|
|
2531
|
-
|
|
2547
|
+
} catch (_) {}
|
|
2548
|
+
try { requestAnimationFrame(() => { try { fn && fn(); } catch (_) {} }); }
|
|
2549
|
+
catch (_) { setTimeout(() => { try { fn && fn(); } catch (__){ } }, 0); }
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// Bring toggle into view with minimal scroll (upwards if it moved above after collapse).
|
|
2553
|
+
_scrollToggleIntoView(toggleEl) {
|
|
2554
|
+
if (!toggleEl || !toggleEl.isConnected) return;
|
|
2555
|
+
try { if (runtime && runtime.scrollMgr) { runtime.scrollMgr.userInteracted = true; runtime.scrollMgr.autoFollow = false; } } catch (_) {}
|
|
2556
|
+
this._afterLayout(() => {
|
|
2557
|
+
try {
|
|
2558
|
+
if (toggleEl.scrollIntoView) {
|
|
2559
|
+
// Prefer minimal movement; keep behavior non-animated and predictable.
|
|
2560
|
+
try { toggleEl.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' }); }
|
|
2561
|
+
catch (_) { toggleEl.scrollIntoView(false); }
|
|
2562
|
+
}
|
|
2563
|
+
} catch (_) {}
|
|
2532
2564
|
});
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
// Append HTML into message input container.
|
|
2536
|
-
appendToInput(content) {
|
|
2537
|
-
// Synchronous DOM update – message input must reflect immediately with no waiting.
|
|
2538
|
-
const el = this.dom.get('_append_input_'); if (!el) return; el.insertAdjacentHTML('beforeend', content);
|
|
2539
|
-
}
|
|
2540
|
-
// Append nodes into messages list and perform post-processing (markdown, code, math).
|
|
2541
|
-
appendNode(content, scrollMgr) {
|
|
2542
|
-
// Keep scroll behavior consistent with existing logic
|
|
2543
|
-
scrollMgr.userInteracted = false; scrollMgr.prevScroll = 0;
|
|
2544
|
-
this.dom.clearStreamBefore();
|
|
2565
|
+
}
|
|
2545
2566
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2567
|
+
// Ensure wrapper and toggle exist for a given .msg element.
|
|
2568
|
+
_ensureStructure(msg) {
|
|
2569
|
+
if (!msg || !msg.isConnected) return null;
|
|
2570
|
+
|
|
2571
|
+
// Wrap all direct children into a dedicated content container to measure height accurately.
|
|
2572
|
+
let content = msg.querySelector('.uc-content');
|
|
2573
|
+
if (!content) {
|
|
2574
|
+
content = document.createElement('div');
|
|
2575
|
+
content.className = 'uc-content';
|
|
2576
|
+
const frag = document.createDocumentFragment();
|
|
2577
|
+
while (msg.firstChild) frag.appendChild(msg.firstChild);
|
|
2578
|
+
content.appendChild(frag);
|
|
2579
|
+
msg.appendChild(content);
|
|
2580
|
+
}
|
|
2548
2581
|
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2582
|
+
// Ensure a single toggle exists (click and keyboard accessible).
|
|
2583
|
+
let toggle = msg.querySelector('.uc-toggle');
|
|
2584
|
+
if (!toggle) {
|
|
2585
|
+
const icons = this._icons();
|
|
2586
|
+
const labels = this._labels();
|
|
2587
|
+
|
|
2588
|
+
toggle = document.createElement('div');
|
|
2589
|
+
toggle.className = 'uc-toggle';
|
|
2590
|
+
toggle.tabIndex = 0;
|
|
2591
|
+
toggle.setAttribute('role', 'button');
|
|
2592
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
2593
|
+
toggle.title = labels.expand;
|
|
2594
|
+
|
|
2595
|
+
const img = document.createElement('img');
|
|
2596
|
+
img.className = 'uc-toggle-icon';
|
|
2597
|
+
img.alt = labels.expand;
|
|
2598
|
+
img.src = icons.expand;
|
|
2599
|
+
toggle.appendChild(img);
|
|
2600
|
+
|
|
2601
|
+
// Attach local listeners (no global handler change; production-safe).
|
|
2602
|
+
toggle.addEventListener('click', (ev) => {
|
|
2603
|
+
ev.preventDefault();
|
|
2604
|
+
ev.stopPropagation();
|
|
2605
|
+
this.toggleFromToggle(toggle);
|
|
2606
|
+
});
|
|
2607
|
+
toggle.addEventListener('keydown', (ev) => {
|
|
2608
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
2609
|
+
ev.preventDefault();
|
|
2610
|
+
ev.stopPropagation();
|
|
2611
|
+
this.toggleFromToggle(toggle);
|
|
2612
|
+
}
|
|
2613
|
+
}, { passive: false });
|
|
2614
|
+
|
|
2615
|
+
msg.appendChild(toggle);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
this._processed.add(msg);
|
|
2619
|
+
msg.dataset.ucInit = '1';
|
|
2620
|
+
return { content, toggle };
|
|
2556
2621
|
}
|
|
2557
2622
|
|
|
2558
|
-
|
|
2623
|
+
// Create or update the ellipsis indicator inside content (absolute in the bottom-right corner).
|
|
2624
|
+
_ensureEllipsisEl(msg, contentEl) {
|
|
2625
|
+
const content = contentEl || (msg && msg.querySelector('.uc-content'));
|
|
2626
|
+
if (!content) return null;
|
|
2559
2627
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
try { this.highlighter.scheduleScanVisibleCodes(null); } catch (_) {}
|
|
2628
|
+
// Ensure the content becomes a positioning context only when needed.
|
|
2629
|
+
if (getComputedStyle(content).position === 'static') {
|
|
2630
|
+
content.style.position = 'relative';
|
|
2631
|
+
}
|
|
2565
2632
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2633
|
+
let dot = content.querySelector('.uc-ellipsis');
|
|
2634
|
+
if (!dot) {
|
|
2635
|
+
dot = document.createElement('span');
|
|
2636
|
+
dot.className = 'uc-ellipsis';
|
|
2637
|
+
dot.textContent = this.ellipsisText;
|
|
2638
|
+
// Inline, theme-agnostic styles; kept minimal and non-interactive.
|
|
2639
|
+
dot.style.position = 'absolute';
|
|
2640
|
+
dot.style.right = '0';
|
|
2641
|
+
dot.style.bottom = '0';
|
|
2642
|
+
dot.style.paddingLeft = '6px';
|
|
2643
|
+
dot.style.pointerEvents = 'none';
|
|
2644
|
+
dot.style.zIndex = '1';
|
|
2645
|
+
dot.style.fontWeight = '500';
|
|
2646
|
+
dot.style.opacity = '0.75';
|
|
2647
|
+
|
|
2648
|
+
content.appendChild(dot);
|
|
2649
|
+
}
|
|
2650
|
+
return dot;
|
|
2651
|
+
}
|
|
2570
2652
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2653
|
+
// Show ellipsis only when there is hidden overflow (collapsed).
|
|
2654
|
+
_showEllipsis(msg, contentEl) {
|
|
2655
|
+
const dot = this._ensureEllipsisEl(msg, contentEl);
|
|
2656
|
+
if (dot) dot.style.display = 'inline';
|
|
2657
|
+
}
|
|
2658
|
+
// Hide and clean ellipsis when not needed (expanded or short content).
|
|
2659
|
+
_hideEllipsis(msg) {
|
|
2660
|
+
const content = msg && msg.querySelector('.uc-content');
|
|
2661
|
+
if (!content) return;
|
|
2662
|
+
const dot = content.querySelector('.uc-ellipsis');
|
|
2663
|
+
if (dot && dot.parentNode) {
|
|
2664
|
+
// Remove the indicator to avoid accidental copy/select and keep DOM lean.
|
|
2665
|
+
dot.parentNode.removeChild(dot);
|
|
2666
|
+
}
|
|
2667
|
+
// Drop positioning context when no indicator is present (keep styles minimal).
|
|
2668
|
+
try {
|
|
2669
|
+
if (content && content.style && content.querySelector('.uc-ellipsis') == null) {
|
|
2670
|
+
content.style.position = '';
|
|
2671
|
+
}
|
|
2672
|
+
} catch (_) {}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// Apply collapse to all user messages under root.
|
|
2676
|
+
apply(root) {
|
|
2677
|
+
const scope = root || document;
|
|
2678
|
+
let list;
|
|
2679
|
+
if (scope.nodeType === 1) list = scope.querySelectorAll('.msg-box.msg-user .msg');
|
|
2680
|
+
else list = document.querySelectorAll('.msg-box.msg-user .msg');
|
|
2681
|
+
if (!list || !list.length) return;
|
|
2682
|
+
|
|
2683
|
+
for (let i = 0; i < list.length; i++) {
|
|
2684
|
+
const msg = list[i];
|
|
2685
|
+
const st = this._ensureStructure(msg);
|
|
2686
|
+
if (!st) continue;
|
|
2687
|
+
this._update(msg, st.content, st.toggle);
|
|
2575
2688
|
}
|
|
2576
|
-
}
|
|
2689
|
+
}
|
|
2577
2690
|
|
|
2578
|
-
//
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2691
|
+
// Update collapsed/expanded state depending on content height.
|
|
2692
|
+
_update(msg, contentEl, toggleEl) {
|
|
2693
|
+
const c = contentEl || (msg && msg.querySelector('.uc-content'));
|
|
2694
|
+
if (!msg || !c) return;
|
|
2695
|
+
|
|
2696
|
+
// Special-case: when threshold = 0 (or '0'), auto-collapse is globally disabled.
|
|
2697
|
+
// We avoid any measurement, force the content to be fully expanded, and ensure the toggle is hidden.
|
|
2698
|
+
// This preserves public API while providing an explicit opt-out, without impacting existing behavior.
|
|
2699
|
+
if (this.threshold === 0 || this.threshold === '0') {
|
|
2700
|
+
const t = toggleEl || msg.querySelector('.uc-toggle');
|
|
2701
|
+
const labels = this._labels();
|
|
2702
|
+
|
|
2703
|
+
// Ensure expanded state and remove any limiting classes.
|
|
2704
|
+
c.classList.remove('uc-collapsed');
|
|
2705
|
+
c.classList.remove('uc-expanded'); // No class => fully expanded by default CSS.
|
|
2706
|
+
msg.dataset.ucState = 'expanded';
|
|
2707
|
+
|
|
2708
|
+
// Hide ellipsis in disabled mode.
|
|
2709
|
+
this._hideEllipsis(msg);
|
|
2710
|
+
|
|
2711
|
+
// Hide toggle in disabled mode to avoid user interaction.
|
|
2712
|
+
if (t) {
|
|
2713
|
+
t.classList.remove('visible');
|
|
2714
|
+
t.setAttribute('aria-expanded', 'false');
|
|
2715
|
+
t.title = labels.expand;
|
|
2716
|
+
const img = t.querySelector('img');
|
|
2717
|
+
if (img) { img.alt = labels.expand; }
|
|
2718
|
+
}
|
|
2719
|
+
return; // Do not proceed with measuring or collapsing.
|
|
2720
|
+
}
|
|
2587
2721
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2722
|
+
// Temporarily remove limiting classes for precise measurement.
|
|
2723
|
+
c.classList.remove('uc-collapsed');
|
|
2724
|
+
c.classList.remove('uc-expanded');
|
|
2590
2725
|
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2726
|
+
const fullHeight = Math.ceil(c.scrollHeight);
|
|
2727
|
+
const labels = this._labels();
|
|
2728
|
+
const icons = this._icons();
|
|
2729
|
+
const t = toggleEl || msg.querySelector('.uc-toggle');
|
|
2730
|
+
|
|
2731
|
+
if (fullHeight > this.threshold) {
|
|
2732
|
+
if (t) t.classList.add('visible');
|
|
2733
|
+
const desired = msg.dataset.ucState || 'collapsed';
|
|
2734
|
+
const expand = (desired === 'expanded');
|
|
2735
|
+
|
|
2736
|
+
if (expand) {
|
|
2737
|
+
c.classList.add('uc-expanded');
|
|
2738
|
+
this._hideEllipsis(msg); // Expanded => no ellipsis
|
|
2739
|
+
} else {
|
|
2740
|
+
c.classList.add('uc-collapsed');
|
|
2741
|
+
this._showEllipsis(msg, c); // Collapsed => show ellipsis overlay
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
if (t) {
|
|
2745
|
+
const img = t.querySelector('img');
|
|
2746
|
+
if (img) {
|
|
2747
|
+
if (expand) { img.src = icons.collapse; img.alt = labels.collapse; }
|
|
2748
|
+
else { img.src = icons.expand; img.alt = labels.expand; }
|
|
2749
|
+
}
|
|
2750
|
+
t.setAttribute('aria-expanded', expand ? 'true' : 'false');
|
|
2751
|
+
t.title = expand ? labels.collapse : labels.expand;
|
|
2752
|
+
}
|
|
2753
|
+
} else {
|
|
2754
|
+
// Short content – ensure fully expanded and hide toggle + ellipsis.
|
|
2755
|
+
c.classList.remove('uc-collapsed');
|
|
2756
|
+
c.classList.remove('uc-expanded');
|
|
2757
|
+
msg.dataset.ucState = 'expanded';
|
|
2758
|
+
this._hideEllipsis(msg);
|
|
2759
|
+
if (t) {
|
|
2760
|
+
t.classList.remove('visible');
|
|
2761
|
+
t.setAttribute('aria-expanded', 'false');
|
|
2762
|
+
t.title = labels.expand;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2598
2765
|
}
|
|
2599
2766
|
|
|
2600
|
-
|
|
2767
|
+
// Toggle handler via the toggle element (div.uc-toggle).
|
|
2768
|
+
toggleFromToggle(toggleEl) {
|
|
2769
|
+
const msg = toggleEl && toggleEl.closest ? toggleEl.closest('.msg-box.msg-user .msg') : null;
|
|
2770
|
+
if (!msg) return;
|
|
2771
|
+
this.toggle(msg);
|
|
2772
|
+
}
|
|
2601
2773
|
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2774
|
+
// Core toggle logic.
|
|
2775
|
+
toggle(msg) {
|
|
2776
|
+
if (!msg || !msg.isConnected) return;
|
|
2777
|
+
const c = msg.querySelector('.uc-content'); if (!c) return;
|
|
2778
|
+
const t = msg.querySelector('.uc-toggle');
|
|
2779
|
+
const labels = this._labels();
|
|
2780
|
+
const icons = this._icons();
|
|
2609
2781
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2782
|
+
const isCollapsed = c.classList.contains('uc-collapsed');
|
|
2783
|
+
if (isCollapsed) {
|
|
2784
|
+
// Expand – leave scroll as-is; remove ellipsis.
|
|
2785
|
+
c.classList.remove('uc-collapsed');
|
|
2786
|
+
c.classList.add('uc-expanded');
|
|
2787
|
+
msg.dataset.ucState = 'expanded';
|
|
2788
|
+
this._hideEllipsis(msg);
|
|
2789
|
+
if (t) {
|
|
2790
|
+
t.setAttribute('aria-expanded', 'true');
|
|
2791
|
+
t.title = labels.collapse;
|
|
2792
|
+
const img = t.querySelector('img'); if (img) { img.src = icons.collapse; img.alt = labels.collapse; }
|
|
2793
|
+
}
|
|
2612
2794
|
} else {
|
|
2613
|
-
|
|
2795
|
+
// Collapse – apply classes, show ellipsis, then bring toggle into view (scroll up if needed).
|
|
2796
|
+
c.classList.remove('uc-expanded');
|
|
2797
|
+
c.classList.add('uc-collapsed');
|
|
2798
|
+
msg.dataset.ucState = 'collapsed';
|
|
2799
|
+
this._showEllipsis(msg, c);
|
|
2800
|
+
if (t) {
|
|
2801
|
+
t.setAttribute('aria-expanded', 'false');
|
|
2802
|
+
t.title = labels.expand;
|
|
2803
|
+
const img = t.querySelector('img'); if (img) { img.src = icons.expand; img.alt = labels.expand; }
|
|
2804
|
+
// Follow the collapsing content upward – keep the toggle visible.
|
|
2805
|
+
this._scrollToggleIntoView(t);
|
|
2806
|
+
}
|
|
2614
2807
|
}
|
|
2615
|
-
}
|
|
2808
|
+
}
|
|
2616
2809
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2810
|
+
// Optional public method to re-evaluate height after layout/resize.
|
|
2811
|
+
remeasureAll() {
|
|
2812
|
+
const arr = Array.from(this._processed || []);
|
|
2813
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2814
|
+
const msg = arr[i];
|
|
2815
|
+
if (!msg || !msg.isConnected) { this._processed.delete(msg); continue; }
|
|
2816
|
+
this._update(msg);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2619
2819
|
}
|
|
2620
|
-
// Append "extra" content into a specific bot message and post-process locally.
|
|
2621
|
-
appendExtra(id, content, scrollMgr) {
|
|
2622
|
-
const el = document.getElementById('msg-bot-' + id); if (!el) return;
|
|
2623
|
-
const extra = el.querySelector('.msg-extra'); if (!extra) return;
|
|
2624
2820
|
|
|
2625
|
-
|
|
2821
|
+
class NodesManager {
|
|
2822
|
+
constructor(dom, renderer, highlighter, math) {
|
|
2823
|
+
this.dom = dom;
|
|
2824
|
+
this.renderer = renderer;
|
|
2825
|
+
this.highlighter = highlighter;
|
|
2826
|
+
this.math = math;
|
|
2827
|
+
// User message collapse manager
|
|
2828
|
+
this._userCollapse = new UserCollapseManager(this.renderer.cfg);
|
|
2829
|
+
}
|
|
2626
2830
|
|
|
2627
|
-
|
|
2628
|
-
|
|
2831
|
+
// Check if HTML contains only user messages without any markdown or code features.
|
|
2832
|
+
_isUserOnlyContent(html) {
|
|
2833
|
+
try {
|
|
2834
|
+
const tmp = document.createElement('div');
|
|
2835
|
+
tmp.innerHTML = html;
|
|
2836
|
+
const hasBot = !!tmp.querySelector('.msg-box.msg-bot');
|
|
2837
|
+
const hasUser = !!tmp.querySelector('.msg-box.msg-user');
|
|
2838
|
+
const hasMD64 = !!tmp.querySelector('[data-md64]');
|
|
2839
|
+
const hasMDNative = !!tmp.querySelector('[md-block-markdown]');
|
|
2840
|
+
const hasCode = !!tmp.querySelector('pre code');
|
|
2841
|
+
const hasMath = !!tmp.querySelector('script[type^="math/tex"]');
|
|
2842
|
+
return hasUser && !hasBot && !hasMD64 && !hasMDNative && !hasCode && !hasMath;
|
|
2843
|
+
} catch (_) { return false; }
|
|
2844
|
+
}
|
|
2629
2845
|
|
|
2630
|
-
|
|
2631
|
-
|
|
2846
|
+
// Convert user markdown placeholders into plain text nodes.
|
|
2847
|
+
_materializeUserMdAsPlainText(scopeEl) {
|
|
2848
|
+
try {
|
|
2849
|
+
const nodes = scopeEl.querySelectorAll('.msg-box.msg-user [data-md64], .msg-box.msg-user [md-block-markdown]');
|
|
2850
|
+
nodes.forEach(el => {
|
|
2851
|
+
let txt = '';
|
|
2852
|
+
if (el.hasAttribute('data-md64')) {
|
|
2853
|
+
const b64 = el.getAttribute('data-md64') || '';
|
|
2854
|
+
el.removeAttribute('data-md64');
|
|
2855
|
+
try { txt = this.renderer.b64ToUtf8(b64); } catch (_) { txt = ''; }
|
|
2856
|
+
} else {
|
|
2857
|
+
// Native Markdown block in user message: keep as plain text (no markdown-it)
|
|
2858
|
+
try { txt = el.textContent || ''; } catch (_) { txt = ''; }
|
|
2859
|
+
try { el.removeAttribute('md-block-markdown'); } catch (_) {}
|
|
2860
|
+
}
|
|
2861
|
+
const span = document.createElement('span'); span.textContent = txt; el.replaceWith(span);
|
|
2862
|
+
});
|
|
2863
|
+
} catch (_) {}
|
|
2864
|
+
}
|
|
2632
2865
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
} catch (_) {}
|
|
2866
|
+
// Append HTML into message input container.
|
|
2867
|
+
appendToInput(content) {
|
|
2868
|
+
// Synchronous DOM update – message input must reflect immediately.
|
|
2869
|
+
const el = this.dom.get('_append_input_'); if (!el) return;
|
|
2870
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
2871
|
+
// Apply collapse to any user messages in input area BEFORE the host schedules scroll.
|
|
2872
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
2873
|
+
}
|
|
2642
2874
|
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2875
|
+
// Append nodes into messages list and perform post-processing (markdown, code, math).
|
|
2876
|
+
appendNode(content, scrollMgr) {
|
|
2877
|
+
// Keep scroll behavior consistent with existing logic
|
|
2878
|
+
scrollMgr.userInteracted = false; scrollMgr.prevScroll = 0;
|
|
2879
|
+
this.dom.clearStreamBefore();
|
|
2880
|
+
|
|
2881
|
+
const el = this.dom.get('_nodes_'); if (!el) return;
|
|
2882
|
+
el.classList.remove('empty_list');
|
|
2883
|
+
|
|
2884
|
+
const userOnly = this._isUserOnlyContent(content);
|
|
2885
|
+
if (userOnly) {
|
|
2886
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
2887
|
+
this._materializeUserMdAsPlainText(el);
|
|
2888
|
+
// Collapse before scrolling to ensure final height is used for scroll computations.
|
|
2889
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
2890
|
+
scrollMgr.scrollToBottom(false);
|
|
2891
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2651
2894
|
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
post()
|
|
2895
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
2896
|
+
|
|
2897
|
+
try {
|
|
2898
|
+
// Defer post-processing (highlight/math/collapse) and perform scroll AFTER collapse.
|
|
2899
|
+
const maybePromise = this.renderer.renderPendingMarkdown(el);
|
|
2900
|
+
const post = () => {
|
|
2901
|
+
// Viewport highlight scheduling
|
|
2902
|
+
try { this.highlighter.scheduleScanVisibleCodes(null); } catch (_) {}
|
|
2903
|
+
|
|
2904
|
+
// In finalize-only mode we must explicitly schedule KaTeX
|
|
2905
|
+
try { if (getMathMode() === 'finalize-only') this.math.schedule(el, 0, true); } catch (_) {}
|
|
2906
|
+
|
|
2907
|
+
// Collapse user messages now that DOM is materialized (ensures correct height).
|
|
2908
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
2909
|
+
|
|
2910
|
+
// Only now scroll to bottom and update FAB – uses post-collapse heights.
|
|
2911
|
+
scrollMgr.scrollToBottom(false);
|
|
2912
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
2913
|
+
};
|
|
2914
|
+
|
|
2915
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
2916
|
+
maybePromise.then(post);
|
|
2917
|
+
} else {
|
|
2918
|
+
post();
|
|
2919
|
+
}
|
|
2920
|
+
} catch (_) {
|
|
2921
|
+
// In case of error, do a conservative scroll to keep UX responsive.
|
|
2922
|
+
scrollMgr.scrollToBottom(false);
|
|
2923
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
2656
2924
|
}
|
|
2657
|
-
}
|
|
2925
|
+
}
|
|
2658
2926
|
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2927
|
+
// Replace messages list content entirely and re-run post-processing.
|
|
2928
|
+
replaceNodes(content, scrollMgr) {
|
|
2929
|
+
// Same semantics as appendNode, but using a hard clone reset
|
|
2930
|
+
scrollMgr.userInteracted = false; scrollMgr.prevScroll = 0;
|
|
2931
|
+
this.dom.clearStreamBefore();
|
|
2932
|
+
|
|
2933
|
+
const el = this.dom.hardReplaceByClone('_nodes_'); if (!el) return;
|
|
2934
|
+
el.classList.remove('empty_list');
|
|
2935
|
+
|
|
2936
|
+
const userOnly = this._isUserOnlyContent(content);
|
|
2937
|
+
if (userOnly) {
|
|
2938
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
2939
|
+
this._materializeUserMdAsPlainText(el);
|
|
2940
|
+
// Collapse before scrolling to ensure final height is used for scroll computations.
|
|
2941
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
2942
|
+
scrollMgr.scrollToBottom(false, true);
|
|
2943
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
2948
|
+
|
|
2949
|
+
try {
|
|
2950
|
+
// Defer KaTeX schedule to post-Markdown to avoid races and collapse before scroll.
|
|
2951
|
+
const maybePromise = this.renderer.renderPendingMarkdown(el);
|
|
2952
|
+
const post = () => {
|
|
2953
|
+
try { this.highlighter.scheduleScanVisibleCodes(null); } catch (_) {}
|
|
2954
|
+
try { if (getMathMode() === 'finalize-only') this.math.schedule(el, 0, true); } catch (_) {}
|
|
2955
|
+
|
|
2956
|
+
// Collapse after materialization to compute final heights correctly.
|
|
2957
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
2958
|
+
|
|
2959
|
+
// Now scroll and update FAB using the collapsed layout.
|
|
2960
|
+
scrollMgr.scrollToBottom(false, true);
|
|
2961
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
2962
|
+
};
|
|
2963
|
+
|
|
2964
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
2965
|
+
maybePromise.then(post);
|
|
2966
|
+
} else {
|
|
2967
|
+
post();
|
|
2968
|
+
}
|
|
2969
|
+
} catch (_) {
|
|
2970
|
+
scrollMgr.scrollToBottom(false, true);
|
|
2971
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// Append "extra" content into a specific bot message and post-process locally.
|
|
2976
|
+
appendExtra(id, content, scrollMgr) {
|
|
2977
|
+
const el = document.getElementById('msg-bot-' + id); if (!el) return;
|
|
2978
|
+
const extra = el.querySelector('.msg-extra'); if (!extra) return;
|
|
2979
|
+
|
|
2980
|
+
extra.insertAdjacentHTML('beforeend', content);
|
|
2981
|
+
|
|
2982
|
+
try {
|
|
2983
|
+
const maybePromise = this.renderer.renderPendingMarkdown(extra);
|
|
2984
|
+
|
|
2985
|
+
const post = () => {
|
|
2986
|
+
const activeCode = (typeof runtime !== 'undefined' && runtime.stream) ? runtime.stream.activeCode : null;
|
|
2987
|
+
|
|
2988
|
+
// Attach observers after Markdown produced the nodes
|
|
2989
|
+
try {
|
|
2990
|
+
this.highlighter.observeNewCode(extra, {
|
|
2991
|
+
deferLastIfStreaming: true,
|
|
2992
|
+
minLinesForLast: this.renderer.cfg.PROFILE_CODE.minLinesForHL,
|
|
2993
|
+
minCharsForLast: this.renderer.cfg.PROFILE_CODE.minCharsForHL
|
|
2994
|
+
}, activeCode);
|
|
2995
|
+
this.highlighter.observeMsgBoxes(extra, (box) => this._onBox(box));
|
|
2996
|
+
} catch (_) {}
|
|
2997
|
+
|
|
2998
|
+
// KaTeX: honor stream mode; in finalize-only force immediate schedule
|
|
2999
|
+
try {
|
|
3000
|
+
const mm = getMathMode();
|
|
3001
|
+
if (mm === 'finalize-only') this.math.schedule(extra, 0, true);
|
|
3002
|
+
else this.math.schedule(extra);
|
|
3003
|
+
} catch (_) {}
|
|
3004
|
+
};
|
|
3005
|
+
|
|
3006
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
3007
|
+
maybePromise.then(post);
|
|
3008
|
+
} else {
|
|
3009
|
+
post();
|
|
3010
|
+
}
|
|
3011
|
+
} catch (_) { /* swallow */ }
|
|
3012
|
+
|
|
3013
|
+
scrollMgr.scheduleScroll(true);
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// When a new message box appears, hook up code/highlight handlers.
|
|
3017
|
+
_onBox(box) {
|
|
3018
|
+
const activeCode = (typeof runtime !== 'undefined' && runtime.stream) ? runtime.stream.activeCode : null;
|
|
3019
|
+
this.highlighter.observeNewCode(box, {
|
|
3020
|
+
deferLastIfStreaming: true,
|
|
3021
|
+
minLinesForLast: this.renderer.cfg.PROFILE_CODE.minLinesForHL,
|
|
3022
|
+
minCharsForLast: this.renderer.cfg.PROFILE_CODE.minCharsForHL
|
|
3023
|
+
}, activeCode);
|
|
3024
|
+
this.renderer.hooks.codeScrollInit(box);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// Remove message by id and keep scroll consistent.
|
|
3028
|
+
removeNode(id, scrollMgr) {
|
|
3029
|
+
scrollMgr.prevScroll = 0;
|
|
3030
|
+
let el = document.getElementById('msg-user-' + id); if (el) el.remove();
|
|
3031
|
+
el = document.getElementById('msg-bot-' + id); if (el) el.remove();
|
|
3032
|
+
this.dom.resetEphemeral();
|
|
3033
|
+
try { this.renderer.renderPendingMarkdown(); } catch (_) {}
|
|
3034
|
+
scrollMgr.scheduleScroll(true);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// Remove all messages from (and including) a given message id.
|
|
3038
|
+
removeNodesFromId(id, scrollMgr) {
|
|
3039
|
+
scrollMgr.prevScroll = 0;
|
|
3040
|
+
const container = this.dom.get('_nodes_'); if (!container) return;
|
|
3041
|
+
const elements = container.querySelectorAll('.msg-box');
|
|
3042
|
+
let remove = false;
|
|
3043
|
+
elements.forEach((element) => {
|
|
3044
|
+
if (element.id && element.id.endsWith('-' + id)) remove = true;
|
|
3045
|
+
if (remove) element.remove();
|
|
3046
|
+
});
|
|
3047
|
+
this.dom.resetEphemeral();
|
|
3048
|
+
try { this.renderer.renderPendingMarkdown(container); } catch (_) {}
|
|
3049
|
+
scrollMgr.scheduleScroll(true);
|
|
3050
|
+
}
|
|
2693
3051
|
}
|
|
2694
|
-
}
|
|
2695
3052
|
|
|
2696
3053
|
// ==========================================================================
|
|
2697
3054
|
// 10) UI manager
|
|
2698
3055
|
// ==========================================================================
|
|
2699
3056
|
|
|
2700
3057
|
class UIManager {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
3058
|
+
// Replace or insert app-level CSS in a <style> tag.
|
|
3059
|
+
updateCSS(styles) {
|
|
3060
|
+
let style = document.getElementById('app-style');
|
|
3061
|
+
if (!style) { style = document.createElement('style'); style.id = 'app-style'; document.head.appendChild(style); }
|
|
3062
|
+
style.textContent = styles;
|
|
3063
|
+
}
|
|
3064
|
+
// Ensure base styles for code header sticky behavior exist.
|
|
3065
|
+
ensureStickyHeaderStyle() {
|
|
3066
|
+
let style = document.getElementById('code-sticky-style');
|
|
3067
|
+
if (style) return;
|
|
3068
|
+
style = document.createElement('style'); style.id = 'code-sticky-style';
|
|
3069
|
+
style.textContent = [
|
|
3070
|
+
'.code-wrapper { position: relative; }',
|
|
3071
|
+
'.code-wrapper .code-header-wrapper { position: sticky; top: var(--code-header-sticky-top, 0px); z-index: 2; box-shadow: 0 1px 0 rgba(0,0,0,.06); }',
|
|
3072
|
+
'.code-wrapper pre { overflow: visible; margin-top: 0; }',
|
|
3073
|
+
'.code-wrapper pre code { display: block; white-space: pre; max-height: 100dvh; overflow: auto;',
|
|
3074
|
+
' overscroll-behavior: contain; -webkit-overflow-scrolling: touch; overflow-anchor: none; scrollbar-gutter: stable both-edges; scroll-behavior: auto; }',
|
|
3075
|
+
'#_loader_.hidden { display: none !important; visibility: hidden !important; }',
|
|
3076
|
+
'#_loader_.visible { display: block; visibility: visible; }',
|
|
3077
|
+
|
|
3078
|
+
/* User message collapse (uc-*) */
|
|
3079
|
+
'.msg-box.msg-user .msg { position: relative; }',
|
|
3080
|
+
'.msg-box.msg-user .msg > .uc-content { display: block; overflow: visible; }',
|
|
3081
|
+
'.msg-box.msg-user .msg > .uc-content.uc-collapsed { max-height: 1000px; overflow: hidden; }',
|
|
3082
|
+
'.msg-box.msg-user .msg > .uc-toggle { display: none; margin-top: 8px; text-align: center; cursor: pointer; user-select: none; }',
|
|
3083
|
+
'.msg-box.msg-user .msg > .uc-toggle.visible { display: block; }',
|
|
3084
|
+
'.msg-box.msg-user .msg > .uc-toggle img { width: 20px; height: 20px; opacity: .8; }',
|
|
3085
|
+
'.msg-box.msg-user .msg > .uc-toggle:hover img { opacity: 1; }'
|
|
3086
|
+
].join('\n');
|
|
3087
|
+
document.head.appendChild(style);
|
|
3088
|
+
}
|
|
3089
|
+
// Toggle classes controlling optional UI features.
|
|
3090
|
+
enableEditIcons() { document.body && document.body.classList.add('display-edit-icons'); }
|
|
3091
|
+
disableEditIcons() { document.body && document.body.classList.remove('display-edit-icons'); }
|
|
3092
|
+
enableTimestamp() { document.body && document.body.classList.add('display-timestamp'); }
|
|
3093
|
+
disableTimestamp() { document.body && document.body.classList.remove('display-timestamp'); }
|
|
3094
|
+
enableBlocks() { document.body && document.body.classList.add('display-blocks'); }
|
|
3095
|
+
disableBlocks() { document.body && document.body.classList.remove('display-blocks'); }
|
|
3096
|
+
}
|
|
2731
3097
|
|
|
2732
3098
|
// ==========================================================================
|
|
2733
3099
|
// 11) Stream snapshot engine + incremental code streaming
|