1327 lines
51 KiB
JavaScript
1327 lines
51 KiB
JavaScript
/* DEPRECATED: Legacy wash helpers retained for one release cycle after Stage 4 engine HTML migration. Do not extend; use packages/graph-engine instead. */
|
||
(function (root) {
|
||
"use strict";
|
||
|
||
var LABEL_CJK_WIDTH = 15;
|
||
var LABEL_LATIN_WIDTH = 8.5;
|
||
var LABEL_PADDING = 22;
|
||
var LABEL_MIN_WIDTH = 72;
|
||
var LABEL_MAX_WIDTH = 180;
|
||
var LABEL_ELLIPSIS = "…";
|
||
var LABEL_ELLIPSIS_WIDTH = 8;
|
||
var ATLAS_WORLD_WIDTH = 1000;
|
||
var ATLAS_WORLD_HEIGHT = 680;
|
||
var ATLAS_MIN_SCALE = 0.62;
|
||
var ATLAS_MAX_SCALE = 3.2;
|
||
var MINIMAP_VIEWBOX = { x: 5, y: 3, width: 150, height: 48 };
|
||
|
||
var labelSegmenter =
|
||
typeof Intl !== "undefined" && Intl.Segmenter
|
||
? new Intl.Segmenter("zh", { granularity: "grapheme" })
|
||
: null;
|
||
|
||
function isVariationSelector(grapheme) {
|
||
var code = grapheme.codePointAt(0);
|
||
return code >= 0xFE00 && code <= 0xFE0F;
|
||
}
|
||
|
||
function isCombiningMark(grapheme) {
|
||
var code = grapheme.codePointAt(0);
|
||
return (code >= 0x0300 && code <= 0x036F)
|
||
|| (code >= 0x1AB0 && code <= 0x1AFF)
|
||
|| (code >= 0x1DC0 && code <= 0x1DFF)
|
||
|| (code >= 0x20D0 && code <= 0x20FF)
|
||
|| (code >= 0xFE20 && code <= 0xFE2F);
|
||
}
|
||
|
||
function isEmojiModifier(grapheme) {
|
||
var code = grapheme.codePointAt(0);
|
||
return code >= 0x1F3FB && code <= 0x1F3FF;
|
||
}
|
||
|
||
function splitLabelGraphemes(label) {
|
||
if (labelSegmenter) {
|
||
return Array.from(labelSegmenter.segment(label), function (s) {
|
||
return s.segment;
|
||
});
|
||
}
|
||
|
||
var parts = Array.from(label);
|
||
if (!parts.length) return [];
|
||
|
||
var graphemes = [parts[0]];
|
||
for (var i = 1; i < parts.length; i++) {
|
||
var current = parts[i];
|
||
var previous = parts[i - 1];
|
||
if (
|
||
current === ""
|
||
|| previous === ""
|
||
|| isVariationSelector(current)
|
||
|| isCombiningMark(current)
|
||
|| isEmojiModifier(current)
|
||
) {
|
||
graphemes[graphemes.length - 1] += current;
|
||
} else {
|
||
graphemes.push(current);
|
||
}
|
||
}
|
||
|
||
return graphemes;
|
||
}
|
||
|
||
function labelCharWidth(grapheme) {
|
||
return /[一-鿿]/.test(grapheme) ? LABEL_CJK_WIDTH : LABEL_LATIN_WIDTH;
|
||
}
|
||
|
||
function measureLabelWidth(graphemes) {
|
||
var width = 0;
|
||
for (var i = 0; i < graphemes.length; i++) {
|
||
width += labelCharWidth(graphemes[i]);
|
||
}
|
||
return width;
|
||
}
|
||
|
||
function truncateLabel(label, maxWidth) {
|
||
if (!label || typeof label !== "string") {
|
||
return { text: "", truncated: false };
|
||
}
|
||
|
||
var graphemes = splitLabelGraphemes(label);
|
||
var totalWidth = measureLabelWidth(graphemes);
|
||
if (totalWidth + LABEL_PADDING <= maxWidth) {
|
||
return { text: label, truncated: false };
|
||
}
|
||
|
||
var out = "";
|
||
var width = 0;
|
||
for (var i = 0; i < graphemes.length; i++) {
|
||
var gw = labelCharWidth(graphemes[i]);
|
||
if (width + gw + LABEL_ELLIPSIS_WIDTH + LABEL_PADDING > maxWidth) break;
|
||
out += graphemes[i];
|
||
width += gw;
|
||
}
|
||
return { text: out + LABEL_ELLIPSIS, truncated: true };
|
||
}
|
||
|
||
function cardDims(n) {
|
||
var label = n.label || n.id;
|
||
var widthByLabel = measureLabelWidth(splitLabelGraphemes(label));
|
||
var width = Math.max(LABEL_MIN_WIDTH, Math.min(LABEL_MAX_WIDTH, widthByLabel + LABEL_PADDING));
|
||
var height = 36;
|
||
if (n.type === "topic") { height = 40; width += 6; }
|
||
if (n.type === "source") { height = 32; }
|
||
return { w: width, h: height };
|
||
}
|
||
|
||
function createSafeStorage(storage, logger) {
|
||
return {
|
||
get: function (key) {
|
||
try { return storage.getItem(key); }
|
||
catch (err) { if (logger) logger("[wiki] storage.get failed:", key, err); return null; }
|
||
},
|
||
set: function (key, value) {
|
||
try { storage.setItem(key, value); }
|
||
catch (err) { if (logger) logger("[wiki] storage.set failed:", key, err); }
|
||
}
|
||
};
|
||
}
|
||
|
||
function normalizeStorageSegment(value) {
|
||
return String(value == null ? "" : value)
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9一-鿿]+/g, "-")
|
||
.replace(/^-+|-+$/g, "")
|
||
.slice(0, 48);
|
||
}
|
||
|
||
function hashString(value) {
|
||
var input = String(value == null ? "" : value);
|
||
var hash = 0;
|
||
for (var i = 0; i < input.length; i++) {
|
||
hash = ((hash << 5) - hash + input.charCodeAt(i)) >>> 0;
|
||
}
|
||
return hash.toString(36);
|
||
}
|
||
|
||
function getWikiStorageNamespace(meta, pathname) {
|
||
var title = normalizeStorageSegment(meta && meta.wiki_title ? meta.wiki_title : "");
|
||
var basis = typeof pathname === "string" && pathname
|
||
? pathname
|
||
: (meta && meta.wiki_title) || title || "default";
|
||
return "llm-wiki:" + (title || "default") + ":" + hashString(basis);
|
||
}
|
||
|
||
function defaultQueue() {
|
||
return {
|
||
version: 1,
|
||
favorites: [],
|
||
notes: [],
|
||
recentNoteIds: []
|
||
};
|
||
}
|
||
|
||
function normalizeQueue(raw) {
|
||
if (!raw || typeof raw !== "object") return defaultQueue();
|
||
var d = defaultQueue();
|
||
var favorites = Array.isArray(raw.favorites) ? raw.favorites : d.favorites;
|
||
var notes = Array.isArray(raw.notes) ? raw.notes : d.notes;
|
||
var recentNoteIds = Array.isArray(raw.recentNoteIds) ? raw.recentNoteIds : d.recentNoteIds;
|
||
var seenFavorites = {};
|
||
var normalizedFavorites = favorites
|
||
.map(function (nodeId) {
|
||
return nodeId == null ? null : String(nodeId);
|
||
})
|
||
.filter(function (nodeId) {
|
||
if (!nodeId || seenFavorites[nodeId]) return false;
|
||
seenFavorites[nodeId] = true;
|
||
return true;
|
||
});
|
||
var normalizedNotes = notes
|
||
.map(function (note) {
|
||
if (!note || typeof note !== "object" || note.id == null || note.node_id == null) return null;
|
||
return {
|
||
id: String(note.id),
|
||
node_id: String(note.node_id),
|
||
label: note.label == null ? String(note.node_id) : String(note.label),
|
||
text: note.text == null ? "" : String(note.text),
|
||
created_at: note.created_at == null ? null : String(note.created_at)
|
||
};
|
||
})
|
||
.filter(function (note) {
|
||
return !!note;
|
||
});
|
||
var noteIdSet = {};
|
||
for (var i = 0; i < normalizedNotes.length; i++) {
|
||
noteIdSet[normalizedNotes[i].id] = true;
|
||
}
|
||
var seenRecent = {};
|
||
var normalizedRecentNoteIds = recentNoteIds
|
||
.map(function (noteId) {
|
||
return noteId == null ? null : String(noteId);
|
||
})
|
||
.filter(function (noteId) {
|
||
if (!noteId || !noteIdSet[noteId] || seenRecent[noteId]) return false;
|
||
seenRecent[noteId] = true;
|
||
return true;
|
||
});
|
||
if (!normalizedRecentNoteIds.length && normalizedNotes.length) {
|
||
normalizedRecentNoteIds = normalizedNotes.map(function (note) {
|
||
return note.id;
|
||
});
|
||
}
|
||
return {
|
||
version: d.version,
|
||
favorites: normalizedFavorites,
|
||
notes: normalizedNotes,
|
||
recentNoteIds: normalizedRecentNoteIds
|
||
};
|
||
}
|
||
|
||
function toggleQueueFavorite(queue, nodeId) {
|
||
var safe = normalizeQueue(queue);
|
||
var favoriteNodeId = nodeId == null ? "" : String(nodeId);
|
||
if (!favoriteNodeId) return safe;
|
||
var favorites = safe.favorites.slice();
|
||
var existingIndex = favorites.indexOf(favoriteNodeId);
|
||
if (existingIndex === -1) {
|
||
favorites.unshift(favoriteNodeId);
|
||
} else {
|
||
favorites.splice(existingIndex, 1);
|
||
}
|
||
return {
|
||
version: safe.version,
|
||
favorites: favorites,
|
||
notes: safe.notes.slice(),
|
||
recentNoteIds: safe.recentNoteIds.slice()
|
||
};
|
||
}
|
||
|
||
function appendQueueNote(queue, note, limit) {
|
||
var safe = normalizeQueue(queue);
|
||
if (!note || typeof note !== "object" || note.id == null || note.node_id == null) return safe;
|
||
var noteLimit = Number.isFinite(Number(limit)) ? Math.max(1, Math.round(Number(limit))) : 50;
|
||
var normalizedNote = {
|
||
id: String(note.id),
|
||
node_id: String(note.node_id),
|
||
label: note.label == null ? String(note.node_id) : String(note.label),
|
||
text: note.text == null ? "" : String(note.text),
|
||
created_at: note.created_at == null ? null : String(note.created_at)
|
||
};
|
||
var notes = [normalizedNote].concat(safe.notes.filter(function (item) {
|
||
return item.id !== normalizedNote.id;
|
||
})).slice(0, noteLimit);
|
||
var recentNoteIds = [normalizedNote.id].concat(safe.recentNoteIds.filter(function (noteId) {
|
||
return noteId !== normalizedNote.id;
|
||
})).slice(0, Math.min(noteLimit, 12));
|
||
return {
|
||
version: safe.version,
|
||
favorites: safe.favorites.slice(),
|
||
notes: notes,
|
||
recentNoteIds: recentNoteIds
|
||
};
|
||
}
|
||
|
||
function summarizeQueue(queue, nodesById, limit) {
|
||
var safe = normalizeQueue(queue);
|
||
var maxItems = Number.isFinite(Number(limit)) ? Math.max(1, Math.round(Number(limit))) : 4;
|
||
var byId = nodesById && typeof nodesById === "object" ? nodesById : {};
|
||
var notesById = {};
|
||
var recentItems = [];
|
||
var i;
|
||
|
||
for (i = 0; i < safe.notes.length; i++) {
|
||
notesById[safe.notes[i].id] = safe.notes[i];
|
||
}
|
||
|
||
for (i = 0; i < safe.recentNoteIds.length && recentItems.length < maxItems; i++) {
|
||
var note = notesById[safe.recentNoteIds[i]];
|
||
if (!note) continue;
|
||
var noteNode = byId[note.node_id];
|
||
recentItems.push({
|
||
kind: "note",
|
||
node_id: note.node_id,
|
||
label: note.label || (noteNode && (noteNode.label || noteNode.id)) || note.node_id,
|
||
text: note.text || ""
|
||
});
|
||
}
|
||
|
||
for (i = 0; i < safe.favorites.length && recentItems.length < maxItems; i++) {
|
||
var favoriteNodeId = safe.favorites[i];
|
||
var favoriteNode = byId[favoriteNodeId];
|
||
recentItems.push({
|
||
kind: "favorite",
|
||
node_id: favoriteNodeId,
|
||
label: favoriteNode && (favoriteNode.label || favoriteNode.id) ? (favoriteNode.label || favoriteNode.id) : favoriteNodeId,
|
||
text: ""
|
||
});
|
||
}
|
||
|
||
return {
|
||
favorite_count: safe.favorites.length,
|
||
note_count: safe.notes.length,
|
||
recent_items: recentItems
|
||
};
|
||
}
|
||
|
||
function defaultLearning() {
|
||
return {
|
||
version: 1,
|
||
entry: { recommended_start_node_id: null, recommended_start_reason: null, default_mode: "global" },
|
||
views: {
|
||
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
|
||
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
|
||
global: { enabled: true, node_ids: [], degraded: false }
|
||
},
|
||
communities: [],
|
||
degraded: { path_to_community: true, community_to_global: true }
|
||
};
|
||
}
|
||
|
||
function normalizeLearning(raw) {
|
||
if (!raw || typeof raw !== "object") return defaultLearning();
|
||
var d = defaultLearning();
|
||
function pick(obj, key, fallback) {
|
||
return obj && obj[key] != null ? obj[key] : fallback;
|
||
}
|
||
return {
|
||
version: pick(raw, "version", d.version),
|
||
entry: {
|
||
recommended_start_node_id: pick(raw.entry, "recommended_start_node_id", d.entry.recommended_start_node_id),
|
||
recommended_start_reason: pick(raw.entry, "recommended_start_reason", d.entry.recommended_start_reason),
|
||
default_mode: pick(raw.entry, "default_mode", d.entry.default_mode)
|
||
},
|
||
views: {
|
||
path: {
|
||
enabled: pick(raw.views && raw.views.path, "enabled", d.views.path.enabled),
|
||
start_node_id: pick(raw.views && raw.views.path, "start_node_id", d.views.path.start_node_id),
|
||
node_ids: Array.isArray(raw.views && raw.views.path && raw.views.path.node_ids) ? raw.views.path.node_ids : d.views.path.node_ids,
|
||
degraded: pick(raw.views && raw.views.path, "degraded", d.views.path.degraded)
|
||
},
|
||
community: {
|
||
enabled: pick(raw.views && raw.views.community, "enabled", d.views.community.enabled),
|
||
community_id: pick(raw.views && raw.views.community, "community_id", d.views.community.community_id),
|
||
label: pick(raw.views && raw.views.community, "label", d.views.community.label),
|
||
node_ids: Array.isArray(raw.views && raw.views.community && raw.views.community.node_ids) ? raw.views.community.node_ids : d.views.community.node_ids,
|
||
is_weak: pick(raw.views && raw.views.community, "is_weak", d.views.community.is_weak),
|
||
degraded: pick(raw.views && raw.views.community, "degraded", d.views.community.degraded)
|
||
},
|
||
global: {
|
||
enabled: pick(raw.views && raw.views.global, "enabled", d.views.global.enabled),
|
||
node_ids: Array.isArray(raw.views && raw.views.global && raw.views.global.node_ids) ? raw.views.global.node_ids : d.views.global.node_ids,
|
||
degraded: pick(raw.views && raw.views.global, "degraded", d.views.global.degraded)
|
||
}
|
||
},
|
||
communities: Array.isArray(raw.communities) ? raw.communities : d.communities,
|
||
degraded: {
|
||
path_to_community: pick(raw.degraded, "path_to_community", d.degraded.path_to_community),
|
||
community_to_global: pick(raw.degraded, "community_to_global", d.degraded.community_to_global)
|
||
}
|
||
};
|
||
}
|
||
|
||
function resolveInitialMode(learning) {
|
||
return "global";
|
||
}
|
||
|
||
function getCommunityNodeIds(nodes, communityId) {
|
||
if (!Array.isArray(nodes) || !communityId) return [];
|
||
return nodes
|
||
.filter(function (node) {
|
||
return node && node.community != null && String(node.community) === String(communityId);
|
||
})
|
||
.map(function (node) {
|
||
return node.id;
|
||
})
|
||
.sort();
|
||
}
|
||
|
||
function getVisibleNodeIds(learning, mode) {
|
||
if (!learning || !learning.views) return [];
|
||
var view = learning.views[mode];
|
||
if (!view || !view.enabled) return [];
|
||
return Array.isArray(view.node_ids) ? view.node_ids : [];
|
||
}
|
||
|
||
function getVisibleLinks(allLinks, visibleIds) {
|
||
if (!visibleIds || !visibleIds.length) return allLinks;
|
||
var idSet = {};
|
||
for (var i = 0; i < visibleIds.length; i++) idSet[visibleIds[i]] = true;
|
||
return allLinks.filter(function (l) {
|
||
var s = l.source.id || l.source;
|
||
var t = l.target.id || l.target;
|
||
return idSet[s] && idSet[t];
|
||
});
|
||
}
|
||
|
||
function buildSearchHaystack(node) {
|
||
return ((node && (node.label || node.id || "")) + "\n" + (((node && node.content) || "").slice(0, 500))).toLowerCase();
|
||
}
|
||
|
||
function buildSearchIndex(nodes) {
|
||
if (!Array.isArray(nodes)) return [];
|
||
return nodes.map(function (node) {
|
||
return { node: node, haystack: buildSearchHaystack(node) };
|
||
});
|
||
}
|
||
|
||
function filterLinksByTypes(allLinks, filters) {
|
||
if (!Array.isArray(allLinks)) return [];
|
||
if (!filters || typeof filters !== "object") return allLinks.slice();
|
||
return allLinks.filter(function (link) {
|
||
var type = link && link.type ? link.type : "EXTRACTED";
|
||
return filters[type] !== false;
|
||
});
|
||
}
|
||
|
||
function applySearchToNodeIds(searchIndex, query) {
|
||
if (!Array.isArray(searchIndex)) return [];
|
||
var normalizedQuery = typeof query === "string" ? query.trim().toLowerCase() : "";
|
||
var matches = !normalizedQuery
|
||
? searchIndex
|
||
: searchIndex.filter(function (entry) {
|
||
return entry && typeof entry.haystack === "string" && entry.haystack.indexOf(normalizedQuery) !== -1;
|
||
});
|
||
return matches
|
||
.map(function (entry) {
|
||
return entry && entry.node ? entry.node.id : null;
|
||
})
|
||
.filter(function (id) {
|
||
return id != null;
|
||
});
|
||
}
|
||
|
||
function getLinkEndpointIds(link) {
|
||
return {
|
||
sourceId: link && link.source && link.source.id ? link.source.id : link && link.source,
|
||
targetId: link && link.target && link.target.id ? link.target.id : link && link.target
|
||
};
|
||
}
|
||
|
||
function sortNodeIdsByScore(nodeIds, scores, nodesById) {
|
||
return nodeIds.slice().sort(function (left, right) {
|
||
var scoreDiff = (scores[right] || 0) - (scores[left] || 0);
|
||
if (scoreDiff) return scoreDiff;
|
||
var leftDegree = nodesById[left] && Number.isFinite(Number(nodesById[left].degree)) ? Number(nodesById[left].degree) : 0;
|
||
var rightDegree = nodesById[right] && Number.isFinite(Number(nodesById[right].degree)) ? Number(nodesById[right].degree) : 0;
|
||
if (rightDegree !== leftDegree) return rightDegree - leftDegree;
|
||
return String(left).localeCompare(String(right));
|
||
});
|
||
}
|
||
|
||
function applyFocusMode(options) {
|
||
var safe = options && typeof options === "object" ? options : {};
|
||
var nodes = Array.isArray(safe.nodes) ? safe.nodes : [];
|
||
var links = Array.isArray(safe.links) ? safe.links : [];
|
||
var nodeIds = Array.isArray(safe.nodeIds) ? safe.nodeIds.slice() : [];
|
||
var mode = safe.mode || "all";
|
||
var anchorNodeId = safe.anchorNodeId != null ? String(safe.anchorNodeId) : null;
|
||
var highConfidenceThreshold = Number.isFinite(Number(safe.highConfidenceThreshold)) ? Number(safe.highConfidenceThreshold) : 0.75;
|
||
var nodesById = {};
|
||
var idSet = {};
|
||
var i;
|
||
|
||
for (i = 0; i < nodes.length; i++) {
|
||
if (nodes[i] && nodes[i].id != null) nodesById[nodes[i].id] = nodes[i];
|
||
}
|
||
for (i = 0; i < nodeIds.length; i++) idSet[nodeIds[i]] = true;
|
||
|
||
if (!nodeIds.length) return { node_ids: [], links: [] };
|
||
|
||
var scopedLinks = getVisibleLinks(links, nodeIds);
|
||
if (mode === "all") return { node_ids: nodeIds.slice(), links: scopedLinks };
|
||
|
||
if (mode === "high_confidence") {
|
||
var strongLinks = scopedLinks.filter(function (link) {
|
||
var weight = Number(link && link.weight);
|
||
return Number.isFinite(weight) && weight >= highConfidenceThreshold;
|
||
});
|
||
var strongIdSet = {};
|
||
for (i = 0; i < strongLinks.length; i++) {
|
||
var strongEdge = getLinkEndpointIds(strongLinks[i]);
|
||
if (idSet[strongEdge.sourceId]) strongIdSet[strongEdge.sourceId] = true;
|
||
if (idSet[strongEdge.targetId]) strongIdSet[strongEdge.targetId] = true;
|
||
}
|
||
if (anchorNodeId && idSet[anchorNodeId]) strongIdSet[anchorNodeId] = true;
|
||
var strongNodeIds = nodeIds.filter(function (id) {
|
||
return !!strongIdSet[id];
|
||
});
|
||
return { node_ids: strongNodeIds, links: getVisibleLinks(strongLinks, strongNodeIds) };
|
||
}
|
||
|
||
if (mode === "one_hop") {
|
||
var hopAnchorNodeId = anchorNodeId && idSet[anchorNodeId] ? anchorNodeId : nodeIds[0] || null;
|
||
if (!hopAnchorNodeId) return { node_ids: [], links: [] };
|
||
var hopIdSet = {};
|
||
hopIdSet[hopAnchorNodeId] = true;
|
||
for (i = 0; i < scopedLinks.length; i++) {
|
||
var hopEdge = getLinkEndpointIds(scopedLinks[i]);
|
||
if (hopEdge.sourceId === hopAnchorNodeId && idSet[hopEdge.targetId]) hopIdSet[hopEdge.targetId] = true;
|
||
if (hopEdge.targetId === hopAnchorNodeId && idSet[hopEdge.sourceId]) hopIdSet[hopEdge.sourceId] = true;
|
||
}
|
||
var hopNodeIds = nodeIds.filter(function (id) {
|
||
return !!hopIdSet[id];
|
||
});
|
||
return { node_ids: hopNodeIds, links: getVisibleLinks(scopedLinks, hopNodeIds) };
|
||
}
|
||
|
||
if (mode === "core") {
|
||
if (nodeIds.length <= 3) return { node_ids: nodeIds.slice(), links: scopedLinks };
|
||
var scores = {};
|
||
for (i = 0; i < nodeIds.length; i++) scores[nodeIds[i]] = 0;
|
||
for (i = 0; i < scopedLinks.length; i++) {
|
||
var coreEdge = getLinkEndpointIds(scopedLinks[i]);
|
||
var weight = Number(scopedLinks[i] && scopedLinks[i].weight);
|
||
var score = Number.isFinite(weight) ? 1 + weight : 1.5;
|
||
if (scores[coreEdge.sourceId] != null) scores[coreEdge.sourceId] += score;
|
||
if (scores[coreEdge.targetId] != null) scores[coreEdge.targetId] += score;
|
||
}
|
||
var coreLimit = Number.isFinite(Number(safe.coreLimit)) ? Number(safe.coreLimit) : Math.max(3, Math.min(8, Math.round(nodeIds.length * 0.5)));
|
||
coreLimit = Math.max(1, Math.min(nodeIds.length, Math.round(coreLimit)));
|
||
var coreNodeIds = sortNodeIdsByScore(nodeIds, scores, nodesById).slice(0, coreLimit);
|
||
return { node_ids: coreNodeIds, links: getVisibleLinks(scopedLinks, coreNodeIds) };
|
||
}
|
||
|
||
return { node_ids: nodeIds.slice(), links: scopedLinks };
|
||
}
|
||
|
||
function resolveVisibleSnapshot(options) {
|
||
var safe = options && typeof options === "object" ? options : {};
|
||
var nodes = Array.isArray(safe.nodes) ? safe.nodes : [];
|
||
var links = Array.isArray(safe.links) ? safe.links : [];
|
||
var baseNodeIds = Array.isArray(safe.baseNodeIds)
|
||
? safe.baseNodeIds.slice()
|
||
: nodes.map(function (node) { return node.id; });
|
||
var filteredLinks = filterLinksByTypes(links, safe.filters);
|
||
var scopedLinks = getVisibleLinks(filteredLinks, baseNodeIds);
|
||
var focusResult = applyFocusMode({
|
||
mode: safe.focusMode,
|
||
nodes: nodes,
|
||
links: scopedLinks,
|
||
nodeIds: baseNodeIds,
|
||
anchorNodeId: safe.anchorNodeId,
|
||
highConfidenceThreshold: safe.highConfidenceThreshold,
|
||
coreLimit: safe.coreLimit
|
||
});
|
||
var focusNodeIds = focusResult.node_ids || [];
|
||
if (!focusNodeIds.length && safe.focusMode && safe.focusMode !== "all") {
|
||
return { node_ids: [], nodes: [], links: [], searchIndex: [] };
|
||
}
|
||
if (!focusNodeIds.length) focusNodeIds = baseNodeIds;
|
||
var focusNodes = nodes.filter(function (node) {
|
||
return focusNodeIds.indexOf(node.id) !== -1;
|
||
});
|
||
var searchIndex = buildSearchIndex(focusNodes);
|
||
var query = typeof safe.searchQuery === "string" ? safe.searchQuery.trim() : "";
|
||
var finalNodeIds = query ? applySearchToNodeIds(searchIndex, query) : focusNodeIds;
|
||
var idSet = {};
|
||
for (var i = 0; i < finalNodeIds.length; i++) idSet[finalNodeIds[i]] = true;
|
||
return {
|
||
node_ids: finalNodeIds,
|
||
nodes: nodes.filter(function (node) {
|
||
return !!idSet[node.id];
|
||
}),
|
||
links: finalNodeIds.length
|
||
? getVisibleLinks(focusResult.links && focusResult.links.length ? focusResult.links : scopedLinks, finalNodeIds)
|
||
: [],
|
||
searchIndex: searchIndex
|
||
};
|
||
}
|
||
|
||
function shouldAutoOpenDrawer(mode) {
|
||
return mode === "path";
|
||
}
|
||
|
||
var ATLAS_CONFIDENCE_LABELS = {
|
||
EXTRACTED: "直接提取",
|
||
INFERRED: "推断关联",
|
||
AMBIGUOUS: "存在歧义",
|
||
UNVERIFIED: "未核实"
|
||
};
|
||
|
||
var ATLAS_TYPE_LABELS = {
|
||
topic: "主题",
|
||
entity: "实体",
|
||
source: "来源"
|
||
};
|
||
|
||
var ATLAS_TYPE_KINDS = {
|
||
topic: "TOPIC",
|
||
entity: "ENTITY",
|
||
source: "SOURCE"
|
||
};
|
||
|
||
function normalizeAtlasType(type) {
|
||
var normalized = String(type || "entity").toLowerCase();
|
||
return ATLAS_TYPE_LABELS[normalized] ? normalized : "entity";
|
||
}
|
||
|
||
function normalizeAtlasConfidence(confidence) {
|
||
var normalized = String(confidence || "EXTRACTED").toUpperCase();
|
||
return ATLAS_CONFIDENCE_LABELS[normalized] ? normalized : "EXTRACTED";
|
||
}
|
||
|
||
function atlasConfidenceLabel(confidence) {
|
||
var normalized = normalizeAtlasConfidence(confidence);
|
||
return ATLAS_CONFIDENCE_LABELS[normalized];
|
||
}
|
||
|
||
function atlasTypeLabel(type) {
|
||
var normalized = normalizeAtlasType(type);
|
||
return ATLAS_TYPE_LABELS[normalized];
|
||
}
|
||
|
||
function atlasNodeKind(type) {
|
||
var normalized = normalizeAtlasType(type);
|
||
return ATLAS_TYPE_KINDS[normalized];
|
||
}
|
||
|
||
function clampAtlasNumber(value, fallback, min, max) {
|
||
var numeric = Number(value);
|
||
if (!Number.isFinite(numeric)) numeric = fallback;
|
||
if (Number.isFinite(Number(min))) numeric = Math.max(Number(min), numeric);
|
||
if (Number.isFinite(Number(max))) numeric = Math.min(Number(max), numeric);
|
||
return numeric;
|
||
}
|
||
|
||
function normalizeAtlasViewportSize(size) {
|
||
var safe = size && typeof size === "object" ? size : {};
|
||
return {
|
||
width: clampAtlasNumber(safe.width, ATLAS_WORLD_WIDTH, 1, 100000),
|
||
height: clampAtlasNumber(safe.height, ATLAS_WORLD_HEIGHT, 1, 100000)
|
||
};
|
||
}
|
||
|
||
function normalizeAtlasViewport(viewport) {
|
||
var safe = viewport && typeof viewport === "object" ? viewport : {};
|
||
return {
|
||
x: clampAtlasNumber(safe.x, 0, -1000000, 1000000),
|
||
y: clampAtlasNumber(safe.y, 0, -1000000, 1000000),
|
||
scale: clampAtlasNumber(safe.scale, 1, ATLAS_MIN_SCALE, ATLAS_MAX_SCALE)
|
||
};
|
||
}
|
||
|
||
function atlasNodePoint(node) {
|
||
var safe = node && typeof node === "object" ? node : {};
|
||
return {
|
||
x: clampAtlasNumber(safe.x, 50, 0, 100) / 100 * ATLAS_WORLD_WIDTH,
|
||
y: clampAtlasNumber(safe.y, 50, 0, 100) / 100 * ATLAS_WORLD_HEIGHT
|
||
};
|
||
}
|
||
|
||
function getAtlasModelBounds(nodes, padding) {
|
||
var list = Array.isArray(nodes) ? nodes : [];
|
||
var pad = Number.isFinite(Number(padding)) ? Math.max(0, Number(padding)) : 48;
|
||
if (!list.length) {
|
||
return {
|
||
x: 0,
|
||
y: 0,
|
||
width: ATLAS_WORLD_WIDTH,
|
||
height: ATLAS_WORLD_HEIGHT,
|
||
minX: 0,
|
||
minY: 0,
|
||
maxX: ATLAS_WORLD_WIDTH,
|
||
maxY: ATLAS_WORLD_HEIGHT
|
||
};
|
||
}
|
||
|
||
var minX = ATLAS_WORLD_WIDTH;
|
||
var minY = ATLAS_WORLD_HEIGHT;
|
||
var maxX = 0;
|
||
var maxY = 0;
|
||
list.forEach(function (node) {
|
||
var point = atlasNodePoint(node);
|
||
minX = Math.min(minX, point.x);
|
||
minY = Math.min(minY, point.y);
|
||
maxX = Math.max(maxX, point.x);
|
||
maxY = Math.max(maxY, point.y);
|
||
});
|
||
|
||
minX = clampAtlasNumber(minX - pad, 0, 0, ATLAS_WORLD_WIDTH);
|
||
minY = clampAtlasNumber(minY - pad, 0, 0, ATLAS_WORLD_HEIGHT);
|
||
maxX = clampAtlasNumber(maxX + pad, ATLAS_WORLD_WIDTH, 0, ATLAS_WORLD_WIDTH);
|
||
maxY = clampAtlasNumber(maxY + pad, ATLAS_WORLD_HEIGHT, 0, ATLAS_WORLD_HEIGHT);
|
||
|
||
return {
|
||
x: minX,
|
||
y: minY,
|
||
width: Math.max(1, maxX - minX),
|
||
height: Math.max(1, maxY - minY),
|
||
minX: minX,
|
||
minY: minY,
|
||
maxX: maxX,
|
||
maxY: maxY
|
||
};
|
||
}
|
||
|
||
function clampAtlasViewport(viewport, viewportSize, options) {
|
||
var size = normalizeAtlasViewportSize(viewportSize);
|
||
var safe = normalizeAtlasViewport(viewport);
|
||
var opts = options && typeof options === "object" ? options : {};
|
||
var minScale = clampAtlasNumber(opts.minScale, ATLAS_MIN_SCALE, 0.1, ATLAS_MAX_SCALE);
|
||
var maxScale = clampAtlasNumber(opts.maxScale, ATLAS_MAX_SCALE, minScale, 10);
|
||
var marginX = clampAtlasNumber(opts.marginX, size.width * 0.38, 0, size.width);
|
||
var marginY = clampAtlasNumber(opts.marginY, size.height * 0.38, 0, size.height);
|
||
var scale = clampAtlasNumber(safe.scale, 1, minScale, maxScale);
|
||
var scaledWidth = size.width * scale;
|
||
var scaledHeight = size.height * scale;
|
||
var minX = size.width - scaledWidth - marginX;
|
||
var maxX = marginX;
|
||
var minY = size.height - scaledHeight - marginY;
|
||
var maxY = marginY;
|
||
|
||
if (scaledWidth <= size.width) {
|
||
var centerX = (size.width - scaledWidth) / 2;
|
||
minX = centerX - marginX;
|
||
maxX = centerX + marginX;
|
||
}
|
||
if (scaledHeight <= size.height) {
|
||
var centerY = (size.height - scaledHeight) / 2;
|
||
minY = centerY - marginY;
|
||
maxY = centerY + marginY;
|
||
}
|
||
|
||
return {
|
||
x: clampAtlasNumber(safe.x, 0, minX, maxX),
|
||
y: clampAtlasNumber(safe.y, 0, minY, maxY),
|
||
scale: scale
|
||
};
|
||
}
|
||
|
||
function fitAtlasViewport(bounds, viewportSize, options) {
|
||
var safeBounds = bounds && typeof bounds === "object" ? bounds : getAtlasModelBounds([]);
|
||
var size = normalizeAtlasViewportSize(viewportSize);
|
||
var opts = options && typeof options === "object" ? options : {};
|
||
var padding = clampAtlasNumber(opts.padding, 0.84, 0.2, 1);
|
||
var minScale = clampAtlasNumber(opts.minScale, ATLAS_MIN_SCALE, 0.1, ATLAS_MAX_SCALE);
|
||
var maxScale = clampAtlasNumber(opts.maxScale, 2.15, minScale, ATLAS_MAX_SCALE);
|
||
var widthScale = ATLAS_WORLD_WIDTH * padding / Math.max(1, safeBounds.width || 1);
|
||
var heightScale = ATLAS_WORLD_HEIGHT * padding / Math.max(1, safeBounds.height || 1);
|
||
var scale = clampAtlasNumber(Math.min(widthScale, heightScale), 1, minScale, maxScale);
|
||
var centerX = (safeBounds.minX != null && safeBounds.maxX != null)
|
||
? (safeBounds.minX + safeBounds.maxX) / 2
|
||
: (safeBounds.x || 0) + (safeBounds.width || ATLAS_WORLD_WIDTH) / 2;
|
||
var centerY = (safeBounds.minY != null && safeBounds.maxY != null)
|
||
? (safeBounds.minY + safeBounds.maxY) / 2
|
||
: (safeBounds.y || 0) + (safeBounds.height || ATLAS_WORLD_HEIGHT) / 2;
|
||
|
||
return clampAtlasViewport({
|
||
x: size.width / 2 - scale * (centerX / ATLAS_WORLD_WIDTH * size.width),
|
||
y: size.height / 2 - scale * (centerY / ATLAS_WORLD_HEIGHT * size.height),
|
||
scale: scale
|
||
}, size, opts);
|
||
}
|
||
|
||
function centerAtlasViewportOnPoint(point, viewportSize, scale, options) {
|
||
var safePoint = point && typeof point === "object" ? point : { x: ATLAS_WORLD_WIDTH / 2, y: ATLAS_WORLD_HEIGHT / 2 };
|
||
var size = normalizeAtlasViewportSize(viewportSize);
|
||
var viewportScale = clampAtlasNumber(scale, 1, ATLAS_MIN_SCALE, ATLAS_MAX_SCALE);
|
||
return clampAtlasViewport({
|
||
x: size.width / 2 - viewportScale * (safePoint.x / ATLAS_WORLD_WIDTH * size.width),
|
||
y: size.height / 2 - viewportScale * (safePoint.y / ATLAS_WORLD_HEIGHT * size.height),
|
||
scale: viewportScale
|
||
}, size, options);
|
||
}
|
||
|
||
function zoomAtlasViewport(viewport, factor, screenPoint, viewportSize, options) {
|
||
var size = normalizeAtlasViewportSize(viewportSize);
|
||
var safe = normalizeAtlasViewport(viewport);
|
||
var point = screenPoint && typeof screenPoint === "object"
|
||
? { x: clampAtlasNumber(screenPoint.x, size.width / 2, 0, size.width), y: clampAtlasNumber(screenPoint.y, size.height / 2, 0, size.height) }
|
||
: { x: size.width / 2, y: size.height / 2 };
|
||
var zoomFactor = clampAtlasNumber(factor, 1, 0.2, 5);
|
||
var opts = options && typeof options === "object" ? options : {};
|
||
var minScale = clampAtlasNumber(opts.minScale, ATLAS_MIN_SCALE, 0.1, ATLAS_MAX_SCALE);
|
||
var maxScale = clampAtlasNumber(opts.maxScale, ATLAS_MAX_SCALE, minScale, 10);
|
||
var nextScale = clampAtlasNumber(safe.scale * zoomFactor, safe.scale, minScale, maxScale);
|
||
var ratio = nextScale / safe.scale;
|
||
return clampAtlasViewport({
|
||
x: point.x - (point.x - safe.x) * ratio,
|
||
y: point.y - (point.y - safe.y) * ratio,
|
||
scale: nextScale
|
||
}, size, opts);
|
||
}
|
||
|
||
function atlasViewportRect(viewport, viewportSize) {
|
||
var size = normalizeAtlasViewportSize(viewportSize);
|
||
var safe = normalizeAtlasViewport(viewport);
|
||
var x = (0 - safe.x) / safe.scale / size.width * ATLAS_WORLD_WIDTH;
|
||
var y = (0 - safe.y) / safe.scale / size.height * ATLAS_WORLD_HEIGHT;
|
||
var width = size.width / safe.scale / size.width * ATLAS_WORLD_WIDTH;
|
||
var height = size.height / safe.scale / size.height * ATLAS_WORLD_HEIGHT;
|
||
var minX = clampAtlasNumber(x, 0, 0, ATLAS_WORLD_WIDTH);
|
||
var minY = clampAtlasNumber(y, 0, 0, ATLAS_WORLD_HEIGHT);
|
||
var maxX = clampAtlasNumber(x + width, ATLAS_WORLD_WIDTH, 0, ATLAS_WORLD_WIDTH);
|
||
var maxY = clampAtlasNumber(y + height, ATLAS_WORLD_HEIGHT, 0, ATLAS_WORLD_HEIGHT);
|
||
return {
|
||
x: minX,
|
||
y: minY,
|
||
width: Math.max(1, maxX - minX),
|
||
height: Math.max(1, maxY - minY),
|
||
minX: minX,
|
||
minY: minY,
|
||
maxX: maxX,
|
||
maxY: maxY
|
||
};
|
||
}
|
||
|
||
function atlasPointToMinimap(point) {
|
||
var safePoint = point && typeof point === "object" ? point : { x: 0, y: 0 };
|
||
return {
|
||
x: MINIMAP_VIEWBOX.x + clampAtlasNumber(safePoint.x, 0, 0, ATLAS_WORLD_WIDTH) / ATLAS_WORLD_WIDTH * MINIMAP_VIEWBOX.width,
|
||
y: MINIMAP_VIEWBOX.y + clampAtlasNumber(safePoint.y, 0, 0, ATLAS_WORLD_HEIGHT) / ATLAS_WORLD_HEIGHT * MINIMAP_VIEWBOX.height
|
||
};
|
||
}
|
||
|
||
function minimapPointToAtlasPoint(point) {
|
||
var safePoint = point && typeof point === "object" ? point : { x: MINIMAP_VIEWBOX.x, y: MINIMAP_VIEWBOX.y };
|
||
return {
|
||
x: clampAtlasNumber((safePoint.x - MINIMAP_VIEWBOX.x) / MINIMAP_VIEWBOX.width * ATLAS_WORLD_WIDTH, 0, 0, ATLAS_WORLD_WIDTH),
|
||
y: clampAtlasNumber((safePoint.y - MINIMAP_VIEWBOX.y) / MINIMAP_VIEWBOX.height * ATLAS_WORLD_HEIGHT, 0, 0, ATLAS_WORLD_HEIGHT)
|
||
};
|
||
}
|
||
|
||
function atlasViewportToMinimapRect(viewport, viewportSize) {
|
||
var rect = atlasViewportRect(viewport, viewportSize);
|
||
var topLeft = atlasPointToMinimap({ x: rect.x, y: rect.y });
|
||
var bottomRight = atlasPointToMinimap({ x: rect.x + rect.width, y: rect.y + rect.height });
|
||
return {
|
||
x: topLeft.x,
|
||
y: topLeft.y,
|
||
width: Math.max(2, bottomRight.x - topLeft.x),
|
||
height: Math.max(2, bottomRight.y - topLeft.y)
|
||
};
|
||
}
|
||
|
||
function atlasEndpointId(value) {
|
||
if (value && typeof value === "object" && value.id != null) return String(value.id);
|
||
return value == null ? "" : String(value);
|
||
}
|
||
|
||
function stripAtlasMarkdown(raw) {
|
||
return String(raw || "")
|
||
.replace(/^---[\s\S]*?---\s*/m, "")
|
||
.replace(/```[\s\S]*?```/g, " ")
|
||
.replace(/!\[[^\]]*\]\([^)]+\)/g, " ")
|
||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||
.replace(/\[\[([^\]|]+)\|?([^\]]*)\]\]/g, function (_, target, label) {
|
||
return label || target;
|
||
})
|
||
.replace(/^#{1,6}\s+/gm, "")
|
||
.replace(/^[-*+]\s+/gm, "")
|
||
.replace(/^\d+\.\s+/gm, "")
|
||
.replace(/[*_`>#]/g, "")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
function deriveAtlasSummary(node, content) {
|
||
var explicitSummary = node && node.summary != null ? String(node.summary).trim() : "";
|
||
if (explicitSummary) return explicitSummary.length > 170 ? explicitSummary.slice(0, 170).trim() + "…" : explicitSummary;
|
||
var summarySource = String(content || (node && node.content) || "").replace(/^#\s+.*(?:\r?\n)+/, "");
|
||
var stripped = stripAtlasMarkdown(summarySource);
|
||
if (!stripped) return "";
|
||
return stripped.length > 170 ? stripped.slice(0, 170).trim() + "…" : stripped;
|
||
}
|
||
|
||
function normalizeAtlasNode(rawNode, index) {
|
||
var raw = rawNode && typeof rawNode === "object" ? rawNode : {};
|
||
var id = raw.id == null ? "node-" + index : String(raw.id);
|
||
var label = raw.label == null || String(raw.label).trim() === "" ? id : String(raw.label).trim();
|
||
var content = raw.content == null ? "" : String(raw.content);
|
||
var type = normalizeAtlasType(raw.type);
|
||
var community = raw.community == null || raw.community === "" ? "_none" : String(raw.community);
|
||
var x = Number(raw.x);
|
||
var y = Number(raw.y);
|
||
var hasX = (typeof raw.x === "number" || (typeof raw.x === "string" && raw.x.trim() !== "")) && Number.isFinite(x);
|
||
var hasY = (typeof raw.y === "number" || (typeof raw.y === "string" && raw.y.trim() !== "")) && Number.isFinite(y);
|
||
return {
|
||
id: id,
|
||
label: label,
|
||
type: type,
|
||
type_label: atlasTypeLabel(type),
|
||
kind: atlasNodeKind(type),
|
||
community: community,
|
||
source_path: raw.source_path || raw.source || raw.path || "",
|
||
confidence: normalizeAtlasConfidence(raw.confidence || raw.type_confidence),
|
||
confidence_label: atlasConfidenceLabel(raw.confidence || raw.type_confidence),
|
||
content: content,
|
||
summary: deriveAtlasSummary(raw, content),
|
||
unavailable: raw.unavailable === true || raw.available === false,
|
||
degree: 0,
|
||
weight: clampAtlasNumber(raw.weight != null ? raw.weight : raw.score, 50, 0, 100),
|
||
priority: 0,
|
||
idx: index,
|
||
x: hasX ? x : null,
|
||
y: hasY ? y : null
|
||
};
|
||
}
|
||
|
||
function normalizeAtlasEdge(rawEdge, index) {
|
||
var raw = rawEdge && typeof rawEdge === "object" ? rawEdge : {};
|
||
var sourceId = atlasEndpointId(raw.from != null ? raw.from : raw.source);
|
||
var targetId = atlasEndpointId(raw.to != null ? raw.to : raw.target);
|
||
return {
|
||
id: raw.id == null ? "edge-" + index : String(raw.id),
|
||
source: sourceId,
|
||
target: targetId,
|
||
from: sourceId,
|
||
to: targetId,
|
||
type: normalizeAtlasConfidence(raw.type || raw.confidence),
|
||
confidence_label: atlasConfidenceLabel(raw.type || raw.confidence),
|
||
weight: clampAtlasNumber(raw.weight, 0.6, 0, 1),
|
||
signals: raw.signals && typeof raw.signals === "object" ? raw.signals : {},
|
||
source_signal_available: raw.source_signal_available === true
|
||
};
|
||
}
|
||
|
||
function buildAtlasSearchHaystack(node) {
|
||
return [
|
||
node && node.label,
|
||
node && node.id,
|
||
node && node.type_label,
|
||
node && node.source_path,
|
||
node && node.summary,
|
||
node && stripAtlasMarkdown(node.content)
|
||
].join("\n").toLowerCase();
|
||
}
|
||
|
||
function buildAtlasSearchIndex(nodes) {
|
||
if (!Array.isArray(nodes)) return [];
|
||
return nodes.map(function (node) {
|
||
return { node: node, haystack: buildAtlasSearchHaystack(node) };
|
||
});
|
||
}
|
||
|
||
function deriveAtlasCommunities(rawGraph, nodes, communityById) {
|
||
var learning = normalizeLearning(rawGraph && rawGraph.learning);
|
||
var fromLearning = Array.isArray(learning.communities) ? learning.communities : [];
|
||
var communities = [];
|
||
var seen = {};
|
||
|
||
fromLearning.forEach(function (community) {
|
||
if (!community || community.id == null) return;
|
||
var id = String(community.id);
|
||
var derived = communityById[id] || { nodes: [] };
|
||
seen[id] = true;
|
||
communities.push({
|
||
id: id,
|
||
label: community.label || id,
|
||
node_count: Number.isFinite(Number(community.node_count)) ? Number(community.node_count) : derived.nodes.length,
|
||
source_count: Number.isFinite(Number(community.source_count)) ? Number(community.source_count) : 0,
|
||
is_primary: community.is_primary === true,
|
||
recommended_start_node_id: community.recommended_start_node_id || null,
|
||
color_index: communities.length
|
||
});
|
||
});
|
||
|
||
Object.keys(communityById).sort().forEach(function (id) {
|
||
if (seen[id]) return;
|
||
var group = communityById[id];
|
||
var topic = group.nodes.find(function (node) { return node.type === "topic"; });
|
||
communities.push({
|
||
id: id,
|
||
label: id === "_none" ? "未分组" : (topic && topic.label) || id,
|
||
node_count: group.nodes.length,
|
||
source_count: group.nodes.filter(function (node) { return node.type === "source"; }).length,
|
||
is_primary: communities.length === 0,
|
||
recommended_start_node_id: null,
|
||
color_index: communities.length
|
||
});
|
||
});
|
||
|
||
communities.sort(function (left, right) {
|
||
if (!!right.is_primary !== !!left.is_primary) return right.is_primary ? 1 : -1;
|
||
if ((right.node_count || 0) !== (left.node_count || 0)) return (right.node_count || 0) - (left.node_count || 0);
|
||
return String(left.label || left.id).localeCompare(String(right.label || right.id));
|
||
});
|
||
communities.forEach(function (community, index) {
|
||
community.color_index = index;
|
||
});
|
||
return communities;
|
||
}
|
||
|
||
function buildAtlasStarts(rawGraph, nodes, byId, communities) {
|
||
var starts = [];
|
||
var seen = {};
|
||
function add(id, reason) {
|
||
if (id == null) return;
|
||
var nodeId = String(id);
|
||
if (!byId[nodeId] || seen[nodeId]) return;
|
||
seen[nodeId] = true;
|
||
starts.push({ node: byId[nodeId], reason: reason || "" });
|
||
}
|
||
var learning = normalizeLearning(rawGraph && rawGraph.learning);
|
||
add(learning.entry && learning.entry.recommended_start_node_id, "全局推荐起点");
|
||
communities.forEach(function (community) {
|
||
add(community.recommended_start_node_id, community.label + " · 推荐起点");
|
||
});
|
||
nodes.slice().sort(function (left, right) {
|
||
return (right.priority || 0) - (left.priority || 0);
|
||
}).forEach(function (node) {
|
||
if (starts.length < 6) add(node.id, atlasTypeLabel(node.type) + " · " + atlasConfidenceLabel(node.confidence));
|
||
});
|
||
return starts.slice(0, 6);
|
||
}
|
||
|
||
function normalizeAtlasInsights(insights) {
|
||
var safe = insights && typeof insights === "object" ? insights : {};
|
||
return {
|
||
surprising_connections: Array.isArray(safe.surprising_connections) ? safe.surprising_connections : [],
|
||
isolated_nodes: Array.isArray(safe.isolated_nodes) ? safe.isolated_nodes : [],
|
||
bridge_nodes: Array.isArray(safe.bridge_nodes) ? safe.bridge_nodes : [],
|
||
sparse_communities: Array.isArray(safe.sparse_communities) ? safe.sparse_communities : [],
|
||
meta: safe.meta && typeof safe.meta === "object" ? safe.meta : { degraded: false }
|
||
};
|
||
}
|
||
|
||
function buildAtlasModel(rawGraph) {
|
||
var raw = rawGraph && typeof rawGraph === "object" ? rawGraph : {};
|
||
var nodes = Array.isArray(raw.nodes) ? raw.nodes.map(normalizeAtlasNode) : [];
|
||
var byId = {};
|
||
var communityById = {};
|
||
nodes.forEach(function (node) {
|
||
byId[node.id] = node;
|
||
if (!communityById[node.community]) communityById[node.community] = { id: node.community, nodes: [] };
|
||
communityById[node.community].nodes.push(node);
|
||
});
|
||
|
||
var edges = (Array.isArray(raw.edges) ? raw.edges : [])
|
||
.map(normalizeAtlasEdge)
|
||
.filter(function (edge) {
|
||
return !!(byId[edge.source] && byId[edge.target]);
|
||
});
|
||
|
||
edges.forEach(function (edge) {
|
||
byId[edge.source].degree += 1;
|
||
byId[edge.target].degree += 1;
|
||
});
|
||
nodes.forEach(function (node) {
|
||
node.priority = node.degree * 12 + node.weight + (node.type === "topic" ? 12 : node.type === "source" ? 6 : 0);
|
||
});
|
||
|
||
var communities = deriveAtlasCommunities(raw, nodes, communityById);
|
||
var communityMap = {};
|
||
communities.forEach(function (community) {
|
||
communityMap[community.id] = community;
|
||
});
|
||
|
||
return {
|
||
meta: {
|
||
wiki_title: raw.meta && raw.meta.wiki_title ? String(raw.meta.wiki_title) : "知识库",
|
||
total_nodes: nodes.length,
|
||
total_edges: edges.length,
|
||
build_date: raw.meta && raw.meta.build_date ? String(raw.meta.build_date) : ""
|
||
},
|
||
nodes: nodes,
|
||
edges: edges,
|
||
byId: byId,
|
||
communities: communities,
|
||
communityById: communityMap,
|
||
starts: buildAtlasStarts(raw, nodes, byId, communities),
|
||
searchIndex: buildAtlasSearchIndex(nodes),
|
||
insights: normalizeAtlasInsights(raw.insights)
|
||
};
|
||
}
|
||
|
||
function deriveAtlasLayout(model) {
|
||
var safe = model && typeof model === "object" ? model : { nodes: [], communities: [] };
|
||
var centers = [
|
||
{ x: 50, y: 48 },
|
||
{ x: 30, y: 34 },
|
||
{ x: 70, y: 36 },
|
||
{ x: 30, y: 72 },
|
||
{ x: 72, y: 70 },
|
||
{ x: 18, y: 52 },
|
||
{ x: 84, y: 52 },
|
||
{ x: 50, y: 78 }
|
||
];
|
||
var communityIndex = {};
|
||
(safe.communities || []).forEach(function (community, index) {
|
||
communityIndex[community.id] = index;
|
||
});
|
||
var grouped = {};
|
||
(safe.nodes || []).forEach(function (node) {
|
||
if (!grouped[node.community]) grouped[node.community] = [];
|
||
grouped[node.community].push(node);
|
||
});
|
||
Object.keys(grouped).forEach(function (communityId) {
|
||
grouped[communityId].sort(function (left, right) {
|
||
return (right.priority || 0) - (left.priority || 0);
|
||
});
|
||
var center = centers[(communityIndex[communityId] || 0) % centers.length];
|
||
var count = grouped[communityId].length;
|
||
grouped[communityId].forEach(function (node, index) {
|
||
if (node.x != null && node.y != null && Number.isFinite(Number(node.x)) && Number.isFinite(Number(node.y))) {
|
||
node.x = clampAtlasNumber(node.x, center.x, 5, 95);
|
||
node.y = clampAtlasNumber(node.y, center.y, 8, 92);
|
||
return;
|
||
}
|
||
var ring = Math.floor(index / 8);
|
||
var ringIndex = index % 8;
|
||
var angle = ((ringIndex / Math.min(8, Math.max(1, count))) * Math.PI * 2) + ring * 0.42;
|
||
var radiusX = 7 + ring * 5 + Math.min(5, count * 0.16);
|
||
var radiusY = 5 + ring * 4 + Math.min(4, count * 0.12);
|
||
node.x = clampAtlasNumber(center.x + Math.cos(angle) * radiusX, center.x, 5, 95);
|
||
node.y = clampAtlasNumber(center.y + Math.sin(angle) * radiusY, center.y, 8, 92);
|
||
});
|
||
});
|
||
return {
|
||
nodes: (safe.nodes || []).slice(),
|
||
edges: (safe.edges || []).slice(),
|
||
nodePositions: (safe.nodes || []).reduce(function (out, node) {
|
||
out[node.id] = { x: node.x, y: node.y };
|
||
return out;
|
||
}, {})
|
||
};
|
||
}
|
||
|
||
function getAtlasDensityMode(count) {
|
||
var nodeCount = Number.isFinite(Number(count)) ? Number(count) : 0;
|
||
if (nodeCount > 500) return "overview";
|
||
if (nodeCount > 200) return "point-plus-focus";
|
||
if (nodeCount > 80) return "compact-card";
|
||
return "card";
|
||
}
|
||
|
||
function atlasLabelBudget(mode, count) {
|
||
if (mode === "overview") return 40;
|
||
if (mode === "point-plus-focus") return 60;
|
||
if (mode === "compact-card") return Math.min(120, count);
|
||
return count;
|
||
}
|
||
|
||
function atlasEdgeBudget(mode, count) {
|
||
if (mode === "overview") return 1000;
|
||
if (mode === "point-plus-focus") return 800;
|
||
return count;
|
||
}
|
||
|
||
function resolveAtlasVisibleSnapshot(model, layout, uiState) {
|
||
var safeModel = model && typeof model === "object" ? model : buildAtlasModel({});
|
||
var safeUI = uiState && typeof uiState === "object" ? uiState : {};
|
||
var activeCommunityId = safeUI.activeCommunityId == null ? "all" : String(safeUI.activeCommunityId);
|
||
var query = typeof safeUI.query === "string" ? safeUI.query.trim().toLowerCase() : "";
|
||
var focusMode = safeUI.focusMode || "all";
|
||
var selectedNodeId = safeUI.selectedNodeId == null ? null : String(safeUI.selectedNodeId);
|
||
var filters = safeUI.filters && typeof safeUI.filters === "object" ? safeUI.filters : {};
|
||
|
||
var baseNodes = safeModel.nodes.filter(function (node) {
|
||
if (activeCommunityId !== "all" && node.community !== activeCommunityId) return false;
|
||
if (focusMode === "source" && node.type !== "source") return false;
|
||
return true;
|
||
});
|
||
|
||
if (focusMode === "core" && baseNodes.length > 8) {
|
||
var keepCount = Math.max(8, Math.ceil(baseNodes.length * 0.45));
|
||
var keep = {};
|
||
baseNodes.slice().sort(function (left, right) {
|
||
return (right.priority || 0) - (left.priority || 0);
|
||
}).slice(0, keepCount).forEach(function (node) {
|
||
keep[node.id] = true;
|
||
});
|
||
if (selectedNodeId && safeModel.byId[selectedNodeId]) keep[selectedNodeId] = true;
|
||
baseNodes = baseNodes.filter(function (node) { return !!keep[node.id]; });
|
||
}
|
||
|
||
var baseIdSet = {};
|
||
baseNodes.forEach(function (node) { baseIdSet[node.id] = true; });
|
||
var searchIndex = buildAtlasSearchIndex(baseNodes);
|
||
var matchedIds = {};
|
||
var visibleNodes = !query
|
||
? baseNodes
|
||
: searchIndex.filter(function (entry) {
|
||
return entry.haystack.indexOf(query) !== -1;
|
||
}).map(function (entry) {
|
||
matchedIds[entry.node.id] = true;
|
||
return entry.node;
|
||
});
|
||
var visibleIdSet = {};
|
||
visibleNodes.forEach(function (node) { visibleIdSet[node.id] = true; });
|
||
|
||
var visibleEdges = safeModel.edges.filter(function (edge) {
|
||
var edgeType = edge.type || "EXTRACTED";
|
||
if (filters[edgeType] === false) return false;
|
||
return !!(visibleIdSet[edge.source] && visibleIdSet[edge.target]);
|
||
});
|
||
|
||
var densityMode = getAtlasDensityMode(visibleNodes.length);
|
||
var labelBudget = atlasLabelBudget(densityMode, visibleNodes.length);
|
||
var labelNodeIds = {};
|
||
var startNodeIds = {};
|
||
var importantNodeIds = {};
|
||
var visibleStartEntries = safeModel.starts.filter(function (entry) {
|
||
return !!(entry && entry.node && visibleIdSet[entry.node.id]) &&
|
||
(activeCommunityId === "all" || entry.node.community === activeCommunityId);
|
||
});
|
||
|
||
visibleStartEntries.forEach(function (entry) {
|
||
startNodeIds[entry.node.id] = true;
|
||
importantNodeIds[entry.node.id] = true;
|
||
});
|
||
|
||
visibleNodes.slice().sort(function (left, right) {
|
||
return (right.priority || 0) - (left.priority || 0);
|
||
}).slice(0, Math.max(0, Math.min(8, Math.ceil(visibleNodes.length * 0.08)))).forEach(function (node) {
|
||
importantNodeIds[node.id] = true;
|
||
});
|
||
|
||
visibleNodes.slice().sort(function (left, right) {
|
||
var leftForced = (selectedNodeId === left.id || matchedIds[left.id] || importantNodeIds[left.id]) ? 1 : 0;
|
||
var rightForced = (selectedNodeId === right.id || matchedIds[right.id] || importantNodeIds[right.id]) ? 1 : 0;
|
||
if (rightForced !== leftForced) return rightForced - leftForced;
|
||
return (right.priority || 0) - (left.priority || 0);
|
||
}).slice(0, labelBudget).forEach(function (node) {
|
||
labelNodeIds[node.id] = true;
|
||
});
|
||
if (selectedNodeId && visibleIdSet[selectedNodeId]) labelNodeIds[selectedNodeId] = true;
|
||
Object.keys(matchedIds).forEach(function (id) {
|
||
if (visibleIdSet[id]) {
|
||
labelNodeIds[id] = true;
|
||
importantNodeIds[id] = true;
|
||
}
|
||
});
|
||
Object.keys(startNodeIds).forEach(function (id) {
|
||
if (visibleIdSet[id]) labelNodeIds[id] = true;
|
||
});
|
||
if (selectedNodeId && visibleIdSet[selectedNodeId]) importantNodeIds[selectedNodeId] = true;
|
||
|
||
var edgeBudget = atlasEdgeBudget(densityMode, visibleEdges.length);
|
||
visibleEdges = visibleEdges.slice().sort(function (left, right) {
|
||
var leftSelected = selectedNodeId && (left.source === selectedNodeId || left.target === selectedNodeId) ? 1 : 0;
|
||
var rightSelected = selectedNodeId && (right.source === selectedNodeId || right.target === selectedNodeId) ? 1 : 0;
|
||
if (rightSelected !== leftSelected) return rightSelected - leftSelected;
|
||
return (right.weight || 0) - (left.weight || 0);
|
||
}).slice(0, edgeBudget);
|
||
|
||
return {
|
||
node_ids: visibleNodes.map(function (node) { return node.id; }),
|
||
nodes: visibleNodes,
|
||
edges: visibleEdges,
|
||
links: visibleEdges,
|
||
searchIndex: searchIndex,
|
||
densityMode: densityMode,
|
||
labelNodeIds: labelNodeIds,
|
||
matchedNodeIds: matchedIds,
|
||
importantNodeIds: importantNodeIds,
|
||
startNodeIds: startNodeIds,
|
||
starts: visibleStartEntries,
|
||
counts: {
|
||
visible_nodes: visibleNodes.length,
|
||
visible_edges: visibleEdges.length,
|
||
total_nodes: safeModel.nodes.length,
|
||
total_edges: safeModel.edges.length,
|
||
total_communities: safeModel.communities.length
|
||
}
|
||
};
|
||
}
|
||
|
||
function resolveAtlasSelectedNodeId(model, visibleSnapshot, selectedNodeId) {
|
||
var safeModel = model && typeof model === "object" ? model : buildAtlasModel({});
|
||
var selected = selectedNodeId == null ? null : String(selectedNodeId);
|
||
var visible = visibleSnapshot && typeof visibleSnapshot === "object" ? visibleSnapshot : null;
|
||
var visibleIds = {};
|
||
if (visible && Array.isArray(visible.node_ids)) {
|
||
visible.node_ids.forEach(function (id) { visibleIds[String(id)] = true; });
|
||
}
|
||
|
||
if (selected && safeModel.byId && safeModel.byId[selected] && (!visible || visibleIds[selected])) {
|
||
return selected;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
var helpers = {
|
||
splitLabelGraphemes: splitLabelGraphemes,
|
||
labelCharWidth: labelCharWidth,
|
||
measureLabelWidth: measureLabelWidth,
|
||
truncateLabel: truncateLabel,
|
||
cardDims: cardDims,
|
||
createSafeStorage: createSafeStorage,
|
||
getWikiStorageNamespace: getWikiStorageNamespace,
|
||
defaultQueue: defaultQueue,
|
||
normalizeQueue: normalizeQueue,
|
||
toggleQueueFavorite: toggleQueueFavorite,
|
||
appendQueueNote: appendQueueNote,
|
||
summarizeQueue: summarizeQueue,
|
||
defaultLearning: defaultLearning,
|
||
normalizeLearning: normalizeLearning,
|
||
resolveInitialMode: resolveInitialMode,
|
||
getCommunityNodeIds: getCommunityNodeIds,
|
||
getVisibleNodeIds: getVisibleNodeIds,
|
||
getVisibleLinks: getVisibleLinks,
|
||
buildSearchHaystack: buildSearchHaystack,
|
||
buildSearchIndex: buildSearchIndex,
|
||
filterLinksByTypes: filterLinksByTypes,
|
||
applySearchToNodeIds: applySearchToNodeIds,
|
||
applyFocusMode: applyFocusMode,
|
||
resolveVisibleSnapshot: resolveVisibleSnapshot,
|
||
shouldAutoOpenDrawer: shouldAutoOpenDrawer,
|
||
buildAtlasModel: buildAtlasModel,
|
||
deriveAtlasLayout: deriveAtlasLayout,
|
||
resolveAtlasVisibleSnapshot: resolveAtlasVisibleSnapshot,
|
||
resolveAtlasSelectedNodeId: resolveAtlasSelectedNodeId,
|
||
getAtlasDensityMode: getAtlasDensityMode,
|
||
normalizeAtlasViewport: normalizeAtlasViewport,
|
||
atlasNodePoint: atlasNodePoint,
|
||
getAtlasModelBounds: getAtlasModelBounds,
|
||
clampAtlasViewport: clampAtlasViewport,
|
||
fitAtlasViewport: fitAtlasViewport,
|
||
centerAtlasViewportOnPoint: centerAtlasViewportOnPoint,
|
||
zoomAtlasViewport: zoomAtlasViewport,
|
||
atlasViewportRect: atlasViewportRect,
|
||
atlasPointToMinimap: atlasPointToMinimap,
|
||
minimapPointToAtlasPoint: minimapPointToAtlasPoint,
|
||
atlasViewportToMinimapRect: atlasViewportToMinimapRect,
|
||
atlasConfidenceLabel: atlasConfidenceLabel,
|
||
atlasTypeLabel: atlasTypeLabel,
|
||
atlasNodeKind: atlasNodeKind,
|
||
stripAtlasMarkdown: stripAtlasMarkdown
|
||
};
|
||
|
||
root.WikiGraphWashHelpers = helpers;
|
||
if (typeof module !== "undefined" && module.exports) {
|
||
module.exports = helpers;
|
||
}
|
||
})(typeof window !== "undefined" ? window : this);
|