minecodex 0.1.18 → 0.1.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/package.json
CHANGED
|
@@ -298,6 +298,8 @@ export function createInjectionSource(features, {
|
|
|
298
298
|
let modelSelectorTriggerObserver = null;
|
|
299
299
|
let modelSelectorObservedTrigger = null;
|
|
300
300
|
let modelSelectorNativeFastIcons = null;
|
|
301
|
+
// Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
|
|
302
|
+
let modelSelectorNativeFastIconSources = null;
|
|
301
303
|
const modelSelectorStyle = document.createElement("style");
|
|
302
304
|
modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
|
|
303
305
|
modelSelectorStyle.textContent = `
|
|
@@ -376,6 +378,17 @@ export function createInjectionSource(features, {
|
|
|
376
378
|
if (modelSelectorFeature) {
|
|
377
379
|
document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
|
|
378
380
|
document.documentElement.append(modelSelectorStyle);
|
|
381
|
+
// 启动阶段就解析 Fast 图标:先读持久化的原生克隆,再从 app bundle 静态提取兜底,
|
|
382
|
+
// 避免首次会话必须等用户点开弹窗后 DOM 里才出现图标。
|
|
383
|
+
const persistedIcons = loadPersistedNativeFastIcons();
|
|
384
|
+
if (persistedIcons) {
|
|
385
|
+
modelSelectorNativeFastIcons = persistedIcons;
|
|
386
|
+
modelSelectorNativeFastIconSources = {};
|
|
387
|
+
for (const state of Object.keys(persistedIcons)) {
|
|
388
|
+
modelSelectorNativeFastIconSources[state] = "persisted";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
void loadNativeFastIconFromBundle();
|
|
379
392
|
}
|
|
380
393
|
|
|
381
394
|
for (const feature of pageScriptFeatures) {
|
|
@@ -649,9 +662,99 @@ export function createInjectionSource(features, {
|
|
|
649
662
|
return null;
|
|
650
663
|
}
|
|
651
664
|
|
|
665
|
+
const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
|
|
666
|
+
|
|
667
|
+
function persistNativeFastIcons() {
|
|
668
|
+
if (!modelSelectorNativeFastIcons) return;
|
|
669
|
+
try {
|
|
670
|
+
const payload = {};
|
|
671
|
+
for (const [state, template] of Object.entries(modelSelectorNativeFastIcons)) {
|
|
672
|
+
payload[state] = template.outerHTML;
|
|
673
|
+
}
|
|
674
|
+
localStorage.setItem(modelSelectorFastIconStorageKey, JSON.stringify(payload));
|
|
675
|
+
} catch {
|
|
676
|
+
// localStorage 不可用时静默降级,仅本次会话内生效。
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function loadPersistedNativeFastIcons() {
|
|
681
|
+
try {
|
|
682
|
+
const payload = JSON.parse(localStorage.getItem(modelSelectorFastIconStorageKey) ?? "null");
|
|
683
|
+
if (!payload || typeof payload !== "object") return null;
|
|
684
|
+
const icons = {};
|
|
685
|
+
for (const [state, markup] of Object.entries(payload)) {
|
|
686
|
+
if (typeof markup !== "string") continue;
|
|
687
|
+
const container = document.createElement("template");
|
|
688
|
+
container.innerHTML = markup;
|
|
689
|
+
const svg = container.content.querySelector("svg");
|
|
690
|
+
if (svg) icons[state] = svg;
|
|
691
|
+
}
|
|
692
|
+
return Object.keys(icons).length ? icons : null;
|
|
693
|
+
} catch {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function extractFastIconPath(source, prefix) {
|
|
699
|
+
const match = String(source).match(new RegExp(`d:\`(${prefix}[^\`]*)\``));
|
|
700
|
+
return match?.[1] ?? null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function loadNativeFastIconFromBundle() {
|
|
704
|
+
const sourceLink = document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
|
|
705
|
+
if (!sourceLink?.href) return;
|
|
706
|
+
try {
|
|
707
|
+
const source = await fetch(sourceLink.href).then((response) => response.text());
|
|
708
|
+
if (!modelSelectorNativeFastIcons) {
|
|
709
|
+
modelSelectorNativeFastIcons = {};
|
|
710
|
+
modelSelectorNativeFastIconSources = {};
|
|
711
|
+
}
|
|
712
|
+
let gained = false;
|
|
713
|
+
const define = (state, prefix, definition) => {
|
|
714
|
+
if (modelSelectorNativeFastIcons[state]) return;
|
|
715
|
+
const path = extractFastIconPath(source, prefix);
|
|
716
|
+
if (!path) return;
|
|
717
|
+
const svg = createModelSelectorIcon({
|
|
718
|
+
viewBox: definition.viewBox,
|
|
719
|
+
markup: definition.transform
|
|
720
|
+
? `<g transform="${definition.transform}"><path d="${path}" fill="currentColor" /></g>`
|
|
721
|
+
: `<path d="${path}" fill="currentColor" />`,
|
|
722
|
+
}, { brand: true });
|
|
723
|
+
if (!svg) return;
|
|
724
|
+
modelSelectorNativeFastIcons[state] = svg;
|
|
725
|
+
modelSelectorNativeFastIconSources[state] = "bundle";
|
|
726
|
+
gained = true;
|
|
727
|
+
};
|
|
728
|
+
define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
|
|
729
|
+
define("inactive", "M7\\.38 16\\.2207", { viewBox: "0 0 20 20", transform: "translate(2.43 1.609)" });
|
|
730
|
+
if (gained) {
|
|
731
|
+
persistNativeFastIcons();
|
|
732
|
+
applyNativeFastIconsToShells();
|
|
733
|
+
queueEnsure();
|
|
734
|
+
}
|
|
735
|
+
} catch {
|
|
736
|
+
// 私有资源移动时保持原生控件不变,等待 DOM 克隆兜底。
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function hasVisibleFastControl() {
|
|
741
|
+
return Boolean(
|
|
742
|
+
document.querySelector("[data-codex-model-slider-fast]")
|
|
743
|
+
|| document.querySelector('[class*="_FastModeToggle_"]')
|
|
744
|
+
|| document.querySelector('[class*="_ModelPickerTriggerFastIndicator_"] svg'),
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
652
748
|
function captureNativeFastIcon() {
|
|
749
|
+
// Fast 图标只在 Fast 控件渲染后才出现在 DOM(打开菜单或 GPT 模型);
|
|
750
|
+
// 启动阶段已有 bundle/持久化兜底,因此无可见 Fast 控件时不做全页扫描。
|
|
751
|
+
if (!hasVisibleFastControl()) {
|
|
752
|
+
return modelSelectorNativeFastIcons;
|
|
753
|
+
}
|
|
653
754
|
const capture = (state, pathPrefix) => {
|
|
654
|
-
|
|
755
|
+
const current = modelSelectorNativeFastIcons?.[state];
|
|
756
|
+
const currentSource = modelSelectorNativeFastIconSources?.[state];
|
|
757
|
+
if (current && currentSource === "dom") return;
|
|
655
758
|
const path = Array.from(document.querySelectorAll("svg path")).find(
|
|
656
759
|
(candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
|
|
657
760
|
);
|
|
@@ -661,14 +764,33 @@ export function createInjectionSource(features, {
|
|
|
661
764
|
template.removeAttribute("id");
|
|
662
765
|
template.removeAttribute("width");
|
|
663
766
|
template.removeAttribute("height");
|
|
664
|
-
if (!modelSelectorNativeFastIcons)
|
|
767
|
+
if (!modelSelectorNativeFastIcons) {
|
|
768
|
+
modelSelectorNativeFastIcons = {};
|
|
769
|
+
modelSelectorNativeFastIconSources = {};
|
|
770
|
+
}
|
|
665
771
|
modelSelectorNativeFastIcons[state] = template;
|
|
772
|
+
modelSelectorNativeFastIconSources[state] = "dom";
|
|
666
773
|
};
|
|
667
774
|
capture("active", "M11.9125 21.4125");
|
|
668
775
|
capture("inactive", "M7.38 16.2207");
|
|
776
|
+
const ready = Boolean(
|
|
777
|
+
modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
|
|
778
|
+
);
|
|
779
|
+
if (ready) {
|
|
780
|
+
persistNativeFastIcons();
|
|
781
|
+
applyNativeFastIconsToShells();
|
|
782
|
+
}
|
|
669
783
|
return modelSelectorNativeFastIcons;
|
|
670
784
|
}
|
|
671
785
|
|
|
786
|
+
function applyNativeFastIconsToShells() {
|
|
787
|
+
document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
|
|
788
|
+
const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
|
|
789
|
+
const content = button.querySelector("[data-codex-model-slider-fast-content]");
|
|
790
|
+
if (icon && content) content.replaceChildren(icon);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
672
794
|
function createNativeFastIcon(active) {
|
|
673
795
|
const template = captureNativeFastIcon()?.[active ? "active" : "inactive"];
|
|
674
796
|
if (!template) return null;
|