twining-mcp 1.7.1 → 1.8.0
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/dist/dashboard/public/app.js +774 -68
- package/dist/dashboard/public/index.html +93 -34
- package/dist/dashboard/public/style.css +699 -67
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +1 -1
- package/dist/tools/coordination-tools.d.ts.map +1 -1
- package/dist/tools/coordination-tools.js +43 -2
- package/dist/tools/coordination-tools.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/public/app.js +774 -68
- package/src/dashboard/public/index.html +93 -34
- package/src/dashboard/public/style.css +699 -67
|
@@ -156,6 +156,11 @@ function fetchBlackboard() {
|
|
|
156
156
|
state.connected = true;
|
|
157
157
|
updateConnectionIndicator();
|
|
158
158
|
renderBlackboard();
|
|
159
|
+
// Refresh stream if visible
|
|
160
|
+
var streamView = document.getElementById('blackboard-stream-view');
|
|
161
|
+
if (streamView && streamView.style.display !== 'none') {
|
|
162
|
+
renderStream();
|
|
163
|
+
}
|
|
159
164
|
renderActivityBreakdown();
|
|
160
165
|
renderRecentActivity();
|
|
161
166
|
})
|
|
@@ -430,8 +435,13 @@ function handleSort(tabName, key) {
|
|
|
430
435
|
}
|
|
431
436
|
tabState.page = 1;
|
|
432
437
|
|
|
433
|
-
if (tabName === "blackboard")
|
|
434
|
-
|
|
438
|
+
if (tabName === "blackboard") {
|
|
439
|
+
renderBlackboard();
|
|
440
|
+
var streamView = document.getElementById('blackboard-stream-view');
|
|
441
|
+
if (streamView && streamView.style.display !== 'none') {
|
|
442
|
+
renderStream();
|
|
443
|
+
}
|
|
444
|
+
} else if (tabName === "decisions") renderDecisions();
|
|
435
445
|
else if (tabName === "graph") renderGraph();
|
|
436
446
|
else if (tabName === "search") renderSearchResults();
|
|
437
447
|
else if (tabName === "agents") renderAgents();
|
|
@@ -1688,16 +1698,14 @@ function buildSearchUrl() {
|
|
|
1688
1698
|
if (!q) return null;
|
|
1689
1699
|
var params = "q=" + encodeURIComponent(q);
|
|
1690
1700
|
|
|
1691
|
-
// Type filter
|
|
1692
|
-
var
|
|
1693
|
-
if (
|
|
1701
|
+
// Type filter (from chips)
|
|
1702
|
+
var typeChips = document.querySelectorAll(".search-chip.active[data-type]");
|
|
1703
|
+
if (typeChips.length > 0 && typeChips.length < 3) {
|
|
1694
1704
|
var selectedTypes = [];
|
|
1695
|
-
for (var i = 0; i <
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
if (selectedTypes.length > 0 && selectedTypes.length < 3) {
|
|
1699
|
-
params += "&types=" + selectedTypes.join(",");
|
|
1705
|
+
for (var i = 0; i < typeChips.length; i++) {
|
|
1706
|
+
selectedTypes.push(typeChips[i].getAttribute("data-type"));
|
|
1700
1707
|
}
|
|
1708
|
+
params += "&types=" + selectedTypes.join(",");
|
|
1701
1709
|
}
|
|
1702
1710
|
|
|
1703
1711
|
// Status filter
|
|
@@ -1932,6 +1940,11 @@ function toggleView(tab, viewName) {
|
|
|
1932
1940
|
if (viewName === 'delegations' && state.delegations.data.length === 0) fetchDelegations();
|
|
1933
1941
|
if (viewName === 'handoffs' && state.handoffs.data.length === 0) fetchHandoffs();
|
|
1934
1942
|
}
|
|
1943
|
+
if (tab === 'blackboard') {
|
|
1944
|
+
document.getElementById('blackboard-table-view').style.display = viewName === 'table' ? 'block' : 'none';
|
|
1945
|
+
document.getElementById('blackboard-stream-view').style.display = viewName === 'stream' ? 'block' : 'none';
|
|
1946
|
+
if (viewName === 'stream' && typeof renderStream === 'function') renderStream();
|
|
1947
|
+
}
|
|
1935
1948
|
}
|
|
1936
1949
|
|
|
1937
1950
|
/* ========== Decision Timeline Visualization ========== */
|
|
@@ -1939,14 +1952,48 @@ function toggleView(tab, viewName) {
|
|
|
1939
1952
|
var CONFIDENCE_CLASSES = { high: 'confidence-high', medium: 'confidence-medium', low: 'confidence-low' };
|
|
1940
1953
|
var STATUS_CLASSES = { provisional: 'status-provisional', superseded: 'status-superseded', overridden: 'status-overridden' };
|
|
1941
1954
|
|
|
1955
|
+
var DOMAIN_COLORS = {
|
|
1956
|
+
architecture: '#6366f1',
|
|
1957
|
+
implementation: '#3b82f6',
|
|
1958
|
+
testing: '#10b981',
|
|
1959
|
+
deployment: '#f59e0b',
|
|
1960
|
+
security: '#ef4444',
|
|
1961
|
+
performance: '#8b5cf6',
|
|
1962
|
+
'api-design': '#06b6d4',
|
|
1963
|
+
'data-model': '#ec4899'
|
|
1964
|
+
};
|
|
1965
|
+
var DOMAIN_COLOR_DEFAULT = '#6b7280';
|
|
1966
|
+
|
|
1967
|
+
// Track which domains are active in filters (null = all active)
|
|
1968
|
+
var timelineDomainFilter = null;
|
|
1969
|
+
|
|
1942
1970
|
function getDecisionClassName(d) {
|
|
1971
|
+
var classes = [];
|
|
1943
1972
|
if (d.status && d.status !== 'active' && STATUS_CLASSES[d.status]) {
|
|
1944
|
-
|
|
1973
|
+
classes.push(STATUS_CLASSES[d.status]);
|
|
1945
1974
|
}
|
|
1946
1975
|
if (d.confidence && CONFIDENCE_CLASSES[d.confidence]) {
|
|
1947
|
-
|
|
1976
|
+
classes.push(CONFIDENCE_CLASSES[d.confidence]);
|
|
1948
1977
|
}
|
|
1949
|
-
return '';
|
|
1978
|
+
return classes.join(' ');
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function buildTimelineGroups(decisions) {
|
|
1982
|
+
var domainSet = {};
|
|
1983
|
+
for (var i = 0; i < decisions.length; i++) {
|
|
1984
|
+
var domain = decisions[i].domain || 'other';
|
|
1985
|
+
domainSet[domain] = true;
|
|
1986
|
+
}
|
|
1987
|
+
var groups = [];
|
|
1988
|
+
var sorted = Object.keys(domainSet).sort();
|
|
1989
|
+
for (var j = 0; j < sorted.length; j++) {
|
|
1990
|
+
groups.push({
|
|
1991
|
+
id: sorted[j],
|
|
1992
|
+
content: sorted[j],
|
|
1993
|
+
style: 'border-left: 3px solid ' + (DOMAIN_COLORS[sorted[j]] || DOMAIN_COLOR_DEFAULT) + '; padding-left: 8px;'
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
return groups;
|
|
1950
1997
|
}
|
|
1951
1998
|
|
|
1952
1999
|
function buildTimelineItems(decisions) {
|
|
@@ -1954,12 +2001,22 @@ function buildTimelineItems(decisions) {
|
|
|
1954
2001
|
var items = [];
|
|
1955
2002
|
for (var i = 0; i < scoped.length; i++) {
|
|
1956
2003
|
var d = scoped[i];
|
|
2004
|
+
var domain = d.domain || 'other';
|
|
2005
|
+
// Apply domain filter
|
|
2006
|
+
if (timelineDomainFilter && !timelineDomainFilter[domain]) continue;
|
|
2007
|
+
var conf = d.confidence || 'unknown';
|
|
2008
|
+
var stat = d.status || 'active';
|
|
2009
|
+
var tooltipParts = [d.summary];
|
|
2010
|
+
tooltipParts.push(conf.charAt(0).toUpperCase() + conf.slice(1) + ' confidence');
|
|
2011
|
+
tooltipParts.push('Status: ' + stat);
|
|
2012
|
+
if (d.scope) tooltipParts.push('Scope: ' + d.scope);
|
|
1957
2013
|
items.push({
|
|
1958
2014
|
id: d.id,
|
|
1959
|
-
|
|
2015
|
+
group: domain,
|
|
2016
|
+
content: truncate(d.summary, 50),
|
|
1960
2017
|
start: d.timestamp,
|
|
1961
2018
|
className: getDecisionClassName(d),
|
|
1962
|
-
title:
|
|
2019
|
+
title: tooltipParts.join('\n')
|
|
1963
2020
|
});
|
|
1964
2021
|
}
|
|
1965
2022
|
return items;
|
|
@@ -1972,24 +2029,31 @@ function initTimeline() {
|
|
|
1972
2029
|
}
|
|
1973
2030
|
if (typeof vis === 'undefined' || !vis.Timeline) return;
|
|
1974
2031
|
|
|
2032
|
+
var scoped = applyGlobalScope(state.decisions.data, 'scope');
|
|
2033
|
+
var groups = buildTimelineGroups(scoped);
|
|
1975
2034
|
var items = buildTimelineItems(state.decisions.data);
|
|
1976
2035
|
window.timelineDataSet = new vis.DataSet(items);
|
|
2036
|
+
window.timelineGroupSet = new vis.DataSet(groups);
|
|
1977
2037
|
|
|
1978
2038
|
var container = document.getElementById('timeline-container');
|
|
1979
2039
|
if (!container) return;
|
|
1980
2040
|
|
|
1981
2041
|
var options = {
|
|
1982
|
-
zoomMin: 1000 * 60 *
|
|
1983
|
-
zoomMax: 1000 * 60 * 60 * 24 * 365,
|
|
2042
|
+
zoomMin: 1000 * 60 * 5,
|
|
2043
|
+
zoomMax: 1000 * 60 * 60 * 24 * 365 * 2,
|
|
1984
2044
|
orientation: { axis: 'top' },
|
|
1985
2045
|
selectable: true,
|
|
1986
|
-
tooltip: { followMouse: true },
|
|
1987
|
-
margin: { item: { horizontal:
|
|
2046
|
+
tooltip: { followMouse: true, delay: 150 },
|
|
2047
|
+
margin: { item: { horizontal: 8, vertical: 5 } },
|
|
1988
2048
|
stack: true,
|
|
1989
|
-
maxHeight: 600
|
|
2049
|
+
maxHeight: 600,
|
|
2050
|
+
verticalScroll: true,
|
|
2051
|
+
zoomKey: '',
|
|
2052
|
+
showCurrentTime: true,
|
|
2053
|
+
groupOrder: 'content'
|
|
1990
2054
|
};
|
|
1991
2055
|
|
|
1992
|
-
window.timelineInstance = new vis.Timeline(container, window.timelineDataSet, options);
|
|
2056
|
+
window.timelineInstance = new vis.Timeline(container, window.timelineDataSet, window.timelineGroupSet, options);
|
|
1993
2057
|
|
|
1994
2058
|
window.timelineInstance.on('select', function(properties) {
|
|
1995
2059
|
if (properties.items.length > 0) {
|
|
@@ -1999,26 +2063,51 @@ function initTimeline() {
|
|
|
1999
2063
|
}
|
|
2000
2064
|
});
|
|
2001
2065
|
|
|
2002
|
-
window.timelineInstance.fit();
|
|
2066
|
+
window.timelineInstance.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
|
|
2067
|
+
|
|
2068
|
+
// Wire up controls
|
|
2069
|
+
wireTimelineControls();
|
|
2003
2070
|
renderTimelineLegend();
|
|
2071
|
+
renderTimelineDomainFilters();
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function wireTimelineControls() {
|
|
2075
|
+
var zoomIn = document.getElementById('timeline-zoom-in');
|
|
2076
|
+
var zoomOut = document.getElementById('timeline-zoom-out');
|
|
2077
|
+
var fit = document.getElementById('timeline-fit');
|
|
2078
|
+
var today = document.getElementById('timeline-today');
|
|
2079
|
+
|
|
2080
|
+
if (zoomIn) zoomIn.addEventListener('click', function() {
|
|
2081
|
+
if (window.timelineInstance) window.timelineInstance.zoomIn(0.4, { animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
|
|
2082
|
+
});
|
|
2083
|
+
if (zoomOut) zoomOut.addEventListener('click', function() {
|
|
2084
|
+
if (window.timelineInstance) window.timelineInstance.zoomOut(0.4, { animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
|
|
2085
|
+
});
|
|
2086
|
+
if (fit) fit.addEventListener('click', function() {
|
|
2087
|
+
if (window.timelineInstance) window.timelineInstance.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
|
|
2088
|
+
});
|
|
2089
|
+
if (today) today.addEventListener('click', function() {
|
|
2090
|
+
if (window.timelineInstance) window.timelineInstance.moveTo(new Date(), { animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
|
|
2091
|
+
});
|
|
2004
2092
|
}
|
|
2005
2093
|
|
|
2006
2094
|
function renderTimelineLegend() {
|
|
2007
2095
|
var legend = document.getElementById('timeline-legend');
|
|
2008
|
-
if (!legend
|
|
2096
|
+
if (!legend) return;
|
|
2097
|
+
clearElement(legend);
|
|
2009
2098
|
var items = [
|
|
2010
|
-
{ color: '
|
|
2011
|
-
{ color: '
|
|
2012
|
-
{ color: '
|
|
2013
|
-
{ color:
|
|
2014
|
-
{ color:
|
|
2099
|
+
{ color: 'var(--success)', label: 'High confidence' },
|
|
2100
|
+
{ color: 'var(--warning)', label: 'Medium confidence' },
|
|
2101
|
+
{ color: 'var(--error)', label: 'Low confidence' },
|
|
2102
|
+
{ color: null, label: 'Provisional', dashed: true },
|
|
2103
|
+
{ color: null, label: 'Superseded', strike: true }
|
|
2015
2104
|
];
|
|
2016
2105
|
for (var i = 0; i < items.length; i++) {
|
|
2017
2106
|
var item = document.createElement('span');
|
|
2018
|
-
item.
|
|
2107
|
+
item.className = 'timeline-legend-item';
|
|
2019
2108
|
var dot = document.createElement('span');
|
|
2020
|
-
dot.
|
|
2021
|
-
if (items[i].
|
|
2109
|
+
dot.className = 'timeline-legend-dot' + (items[i].dashed ? ' dashed' : '');
|
|
2110
|
+
if (items[i].color) dot.style.background = items[i].color;
|
|
2022
2111
|
var label = document.createElement('span');
|
|
2023
2112
|
label.textContent = items[i].label;
|
|
2024
2113
|
if (items[i].strike) label.style.textDecoration = 'line-through';
|
|
@@ -2028,11 +2117,80 @@ function renderTimelineLegend() {
|
|
|
2028
2117
|
}
|
|
2029
2118
|
}
|
|
2030
2119
|
|
|
2120
|
+
function renderTimelineDomainFilters() {
|
|
2121
|
+
var container = document.getElementById('timeline-domain-filters');
|
|
2122
|
+
if (!container) return;
|
|
2123
|
+
clearElement(container);
|
|
2124
|
+
|
|
2125
|
+
var scoped = applyGlobalScope(state.decisions.data, 'scope');
|
|
2126
|
+
var domainSet = {};
|
|
2127
|
+
for (var i = 0; i < scoped.length; i++) {
|
|
2128
|
+
var domain = scoped[i].domain || 'other';
|
|
2129
|
+
domainSet[domain] = (domainSet[domain] || 0) + 1;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
var domains = Object.keys(domainSet).sort();
|
|
2133
|
+
if (domains.length <= 1) return; // No point showing filter for 1 domain
|
|
2134
|
+
|
|
2135
|
+
// "All" chip
|
|
2136
|
+
var allChip = document.createElement('button');
|
|
2137
|
+
allChip.className = 'timeline-domain-chip' + (timelineDomainFilter === null ? ' active' : '');
|
|
2138
|
+
allChip.textContent = 'All';
|
|
2139
|
+
allChip.style.setProperty('--domain-color', 'var(--accent)');
|
|
2140
|
+
allChip.addEventListener('click', function() {
|
|
2141
|
+
timelineDomainFilter = null;
|
|
2142
|
+
updateTimelineData();
|
|
2143
|
+
renderTimelineDomainFilters();
|
|
2144
|
+
});
|
|
2145
|
+
container.appendChild(allChip);
|
|
2146
|
+
|
|
2147
|
+
for (var j = 0; j < domains.length; j++) {
|
|
2148
|
+
(function(domain) {
|
|
2149
|
+
var color = DOMAIN_COLORS[domain] || DOMAIN_COLOR_DEFAULT;
|
|
2150
|
+
var isActive = timelineDomainFilter === null || !!timelineDomainFilter[domain];
|
|
2151
|
+
var chip = document.createElement('button');
|
|
2152
|
+
chip.className = 'timeline-domain-chip' + (timelineDomainFilter !== null && isActive ? ' active' : '');
|
|
2153
|
+
chip.style.setProperty('--domain-color', color);
|
|
2154
|
+
var dot = document.createElement('span');
|
|
2155
|
+
dot.className = 'chip-dot';
|
|
2156
|
+
chip.appendChild(dot);
|
|
2157
|
+
var text = document.createElement('span');
|
|
2158
|
+
text.textContent = domain + ' (' + domainSet[domain] + ')';
|
|
2159
|
+
chip.appendChild(text);
|
|
2160
|
+
chip.addEventListener('click', function() {
|
|
2161
|
+
if (timelineDomainFilter === null) {
|
|
2162
|
+
// Switch from "all" to just this domain
|
|
2163
|
+
timelineDomainFilter = {};
|
|
2164
|
+
timelineDomainFilter[domain] = true;
|
|
2165
|
+
} else if (timelineDomainFilter[domain]) {
|
|
2166
|
+
// Toggle off
|
|
2167
|
+
delete timelineDomainFilter[domain];
|
|
2168
|
+
if (Object.keys(timelineDomainFilter).length === 0) {
|
|
2169
|
+
timelineDomainFilter = null; // Back to all
|
|
2170
|
+
}
|
|
2171
|
+
} else {
|
|
2172
|
+
// Toggle on
|
|
2173
|
+
timelineDomainFilter[domain] = true;
|
|
2174
|
+
}
|
|
2175
|
+
updateTimelineData();
|
|
2176
|
+
renderTimelineDomainFilters();
|
|
2177
|
+
});
|
|
2178
|
+
container.appendChild(chip);
|
|
2179
|
+
})(domains[j]);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2031
2183
|
function updateTimelineData() {
|
|
2032
2184
|
if (!window.timelineDataSet) return;
|
|
2185
|
+
var scoped = applyGlobalScope(state.decisions.data, 'scope');
|
|
2186
|
+
var groups = buildTimelineGroups(scoped);
|
|
2033
2187
|
var items = buildTimelineItems(state.decisions.data);
|
|
2034
2188
|
window.timelineDataSet.clear();
|
|
2035
2189
|
window.timelineDataSet.add(items);
|
|
2190
|
+
if (window.timelineGroupSet) {
|
|
2191
|
+
window.timelineGroupSet.clear();
|
|
2192
|
+
window.timelineGroupSet.add(groups);
|
|
2193
|
+
}
|
|
2036
2194
|
}
|
|
2037
2195
|
|
|
2038
2196
|
function fetchTimelineDecisionDetail(id) {
|
|
@@ -2070,11 +2228,348 @@ var ENTITY_COLORS = {
|
|
|
2070
2228
|
api_endpoint: '#ec4899'
|
|
2071
2229
|
};
|
|
2072
2230
|
|
|
2231
|
+
// Track which entity types are visible (null = all)
|
|
2232
|
+
var graphTypeFilter = null;
|
|
2233
|
+
|
|
2234
|
+
/* ========== Blackboard Stream View ========== */
|
|
2235
|
+
|
|
2236
|
+
var ENTRY_TYPE_COLORS = {
|
|
2237
|
+
warning: '#ffaa00',
|
|
2238
|
+
constraint: '#ff4466',
|
|
2239
|
+
need: '#a78bfa',
|
|
2240
|
+
finding: '#6366f1',
|
|
2241
|
+
decision: '#00d4aa',
|
|
2242
|
+
question: '#22d3ee',
|
|
2243
|
+
answer: '#2dd4bf',
|
|
2244
|
+
status: '#8892a8',
|
|
2245
|
+
offer: '#10b981',
|
|
2246
|
+
artifact: '#fbbf24'
|
|
2247
|
+
};
|
|
2248
|
+
|
|
2249
|
+
var streamTypeFilter = null;
|
|
2250
|
+
|
|
2251
|
+
function getTimeGroupLabel(dateStr) {
|
|
2252
|
+
var d = new Date(dateStr);
|
|
2253
|
+
if (isNaN(d.getTime())) return 'Unknown';
|
|
2254
|
+
var now = new Date();
|
|
2255
|
+
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
2256
|
+
var yesterday = new Date(today);
|
|
2257
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
2258
|
+
var entryDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
2259
|
+
if (entryDate.getTime() === today.getTime()) return 'Today';
|
|
2260
|
+
if (entryDate.getTime() === yesterday.getTime()) return 'Yesterday';
|
|
2261
|
+
return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
function formatTimeOnly(dateStr) {
|
|
2265
|
+
var d = new Date(dateStr);
|
|
2266
|
+
if (isNaN(d.getTime())) return '--';
|
|
2267
|
+
return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function renderStream() {
|
|
2271
|
+
var container = document.getElementById('stream-container');
|
|
2272
|
+
if (!container) return;
|
|
2273
|
+
clearElement(container);
|
|
2274
|
+
|
|
2275
|
+
var entries = applyGlobalScope(state.blackboard.data || [], 'scope');
|
|
2276
|
+
|
|
2277
|
+
// Apply type filter
|
|
2278
|
+
if (streamTypeFilter) {
|
|
2279
|
+
entries = entries.filter(function(e) {
|
|
2280
|
+
return !!streamTypeFilter[e.entry_type];
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// Sort newest first
|
|
2285
|
+
entries = entries.slice().sort(function(a, b) {
|
|
2286
|
+
return new Date(b.timestamp) - new Date(a.timestamp);
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
if (entries.length === 0) {
|
|
2290
|
+
container.appendChild(el('p', 'placeholder', 'No entries to display'));
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Build ID lookup for visible entries (for threading)
|
|
2295
|
+
var visibleIds = {};
|
|
2296
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2297
|
+
visibleIds[entries[i].id] = true;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// Group by date and render cards
|
|
2301
|
+
var currentGroup = null;
|
|
2302
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2303
|
+
var entry = entries[i];
|
|
2304
|
+
var groupLabel = getTimeGroupLabel(entry.timestamp);
|
|
2305
|
+
|
|
2306
|
+
// Insert time group header if new group
|
|
2307
|
+
if (groupLabel !== currentGroup) {
|
|
2308
|
+
currentGroup = groupLabel;
|
|
2309
|
+
var header = el('div', 'stream-time-group');
|
|
2310
|
+
var headerLine = el('span', 'stream-time-line');
|
|
2311
|
+
var headerLabel = el('span', 'stream-time-label', groupLabel);
|
|
2312
|
+
header.appendChild(headerLine);
|
|
2313
|
+
header.appendChild(headerLabel);
|
|
2314
|
+
header.appendChild(headerLine.cloneNode(true));
|
|
2315
|
+
container.appendChild(header);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// Create card
|
|
2319
|
+
var typeColor = ENTRY_TYPE_COLORS[entry.entry_type] || '#6b7280';
|
|
2320
|
+
var card = el('div', 'stream-card');
|
|
2321
|
+
card.setAttribute('data-id', entry.id || '');
|
|
2322
|
+
card.style.setProperty('--card-color', typeColor);
|
|
2323
|
+
|
|
2324
|
+
if (state.blackboard.selectedId && entry.id === state.blackboard.selectedId) {
|
|
2325
|
+
card.classList.add('selected');
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Type badge
|
|
2329
|
+
var badge = el('div', 'stream-card-badge');
|
|
2330
|
+
var badgeDot = el('span', 'stream-badge-dot');
|
|
2331
|
+
badge.appendChild(badgeDot);
|
|
2332
|
+
badge.appendChild(document.createTextNode(' ' + (entry.entry_type || 'unknown')));
|
|
2333
|
+
card.appendChild(badge);
|
|
2334
|
+
|
|
2335
|
+
// Summary
|
|
2336
|
+
var summary = el('div', 'stream-card-summary', truncate(entry.summary, 120));
|
|
2337
|
+
card.appendChild(summary);
|
|
2338
|
+
|
|
2339
|
+
// Footer: scope + time
|
|
2340
|
+
var footer = el('div', 'stream-card-footer');
|
|
2341
|
+
if (entry.scope) {
|
|
2342
|
+
footer.appendChild(el('span', 'stream-card-scope', truncate(entry.scope, 40)));
|
|
2343
|
+
}
|
|
2344
|
+
footer.appendChild(el('span', 'stream-card-time', formatTimeOnly(entry.timestamp)));
|
|
2345
|
+
|
|
2346
|
+
// Linked icon for off-screen relates_to
|
|
2347
|
+
if (entry.relates_to && entry.relates_to.length) {
|
|
2348
|
+
var hasOffscreen = false;
|
|
2349
|
+
for (var r = 0; r < entry.relates_to.length; r++) {
|
|
2350
|
+
if (!visibleIds[entry.relates_to[r]]) { hasOffscreen = true; break; }
|
|
2351
|
+
}
|
|
2352
|
+
if (hasOffscreen) {
|
|
2353
|
+
var linkIcon = el('span', 'stream-card-link', '\u{1F517}');
|
|
2354
|
+
linkIcon.title = 'Has related entries not visible in current filter';
|
|
2355
|
+
footer.appendChild(linkIcon);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
card.appendChild(footer);
|
|
2360
|
+
|
|
2361
|
+
// Click handler — render detail in stream detail panel
|
|
2362
|
+
(function(e) {
|
|
2363
|
+
card.addEventListener('click', function() {
|
|
2364
|
+
state.blackboard.selectedId = e.id;
|
|
2365
|
+
var allCards = container.querySelectorAll('.stream-card');
|
|
2366
|
+
for (var c = 0; c < allCards.length; c++) {
|
|
2367
|
+
allCards[c].classList.remove('selected');
|
|
2368
|
+
}
|
|
2369
|
+
card.classList.add('selected');
|
|
2370
|
+
// Render detail directly into the stream detail panel
|
|
2371
|
+
renderStreamDetail(e);
|
|
2372
|
+
});
|
|
2373
|
+
})(entry);
|
|
2374
|
+
|
|
2375
|
+
container.appendChild(card);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// Render thread lines (stub — implemented in Task 4)
|
|
2379
|
+
if (typeof renderStreamThreads === 'function') {
|
|
2380
|
+
renderStreamThreads(container, entries, visibleIds);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// Render type filter chips (stub — implemented in Task 5)
|
|
2384
|
+
if (typeof renderStreamTypeFilters === 'function') {
|
|
2385
|
+
renderStreamTypeFilters();
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function renderStreamDetail(entry) {
|
|
2390
|
+
var panel = document.getElementById('blackboard-stream-detail');
|
|
2391
|
+
if (!panel) return;
|
|
2392
|
+
clearElement(panel);
|
|
2393
|
+
|
|
2394
|
+
panel.appendChild(el('h3', null, 'Entry Details'));
|
|
2395
|
+
|
|
2396
|
+
var fields = [
|
|
2397
|
+
{ label: 'ID', value: entry.id },
|
|
2398
|
+
{ label: 'Timestamp', value: formatTimestamp(entry.timestamp) },
|
|
2399
|
+
{ label: 'Type', value: entry.entry_type },
|
|
2400
|
+
{ label: 'Summary', value: entry.summary },
|
|
2401
|
+
{ label: 'Scope', value: entry.scope },
|
|
2402
|
+
{ label: 'Agent ID', value: entry.agent_id },
|
|
2403
|
+
{ label: 'Tags', value: (entry.tags && entry.tags.length) ? entry.tags.join(', ') : null }
|
|
2404
|
+
];
|
|
2405
|
+
|
|
2406
|
+
for (var i = 0; i < fields.length; i++) {
|
|
2407
|
+
var f = fields[i];
|
|
2408
|
+
if (f.value === undefined || f.value === null) continue;
|
|
2409
|
+
var div = el('div', 'detail-field');
|
|
2410
|
+
div.appendChild(el('div', 'detail-label', f.label));
|
|
2411
|
+
if (f.label === 'ID') {
|
|
2412
|
+
var valDiv = el('div', 'detail-value');
|
|
2413
|
+
renderIdValue(valDiv, String(f.value));
|
|
2414
|
+
div.appendChild(valDiv);
|
|
2415
|
+
} else {
|
|
2416
|
+
div.appendChild(el('div', 'detail-value', String(f.value)));
|
|
2417
|
+
}
|
|
2418
|
+
panel.appendChild(div);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// Relates To with clickable IDs
|
|
2422
|
+
if (entry.relates_to && entry.relates_to.length) {
|
|
2423
|
+
var rtDiv = el('div', 'detail-field');
|
|
2424
|
+
rtDiv.appendChild(el('div', 'detail-label', 'Relates To'));
|
|
2425
|
+
var rtVal = el('div', 'detail-value');
|
|
2426
|
+
renderIdList(rtVal, entry.relates_to);
|
|
2427
|
+
rtDiv.appendChild(rtVal);
|
|
2428
|
+
panel.appendChild(rtDiv);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// Detail field (long text)
|
|
2432
|
+
if (entry.detail) {
|
|
2433
|
+
var detDiv = el('div', 'detail-field');
|
|
2434
|
+
detDiv.appendChild(el('div', 'detail-label', 'Detail'));
|
|
2435
|
+
var detVal = el('div', 'detail-value detail-long-text', entry.detail);
|
|
2436
|
+
detDiv.appendChild(detVal);
|
|
2437
|
+
panel.appendChild(detDiv);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function renderStreamThreads(container, entries, visibleIds) {
|
|
2442
|
+
// Build map of entry ID -> DOM card element
|
|
2443
|
+
var cardElements = {};
|
|
2444
|
+
var cards = container.querySelectorAll('.stream-card');
|
|
2445
|
+
for (var i = 0; i < cards.length; i++) {
|
|
2446
|
+
var id = cards[i].getAttribute('data-id');
|
|
2447
|
+
if (id) cardElements[id] = cards[i];
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Use getBoundingClientRect for positioning (offsetTop unreliable with position:relative cards)
|
|
2451
|
+
var containerRect = container.getBoundingClientRect();
|
|
2452
|
+
var scrollTop = container.scrollTop;
|
|
2453
|
+
|
|
2454
|
+
function cardTop(card) {
|
|
2455
|
+
return card.getBoundingClientRect().top - containerRect.top + scrollTop;
|
|
2456
|
+
}
|
|
2457
|
+
function cardBottom(card) {
|
|
2458
|
+
return card.getBoundingClientRect().bottom - containerRect.top + scrollTop;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// For each entry with relates_to, draw thread to visible targets
|
|
2462
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2463
|
+
var entry = entries[i];
|
|
2464
|
+
if (!entry.relates_to || !entry.relates_to.length) continue;
|
|
2465
|
+
|
|
2466
|
+
var sourceCard = cardElements[entry.id];
|
|
2467
|
+
if (!sourceCard) continue;
|
|
2468
|
+
|
|
2469
|
+
for (var r = 0; r < entry.relates_to.length; r++) {
|
|
2470
|
+
var targetId = entry.relates_to[r];
|
|
2471
|
+
var targetCard = cardElements[targetId];
|
|
2472
|
+
if (!targetCard) continue;
|
|
2473
|
+
|
|
2474
|
+
// Determine which card is higher in the DOM
|
|
2475
|
+
var topEl, bottomEl;
|
|
2476
|
+
if (cardTop(sourceCard) < cardTop(targetCard)) {
|
|
2477
|
+
topEl = sourceCard;
|
|
2478
|
+
bottomEl = targetCard;
|
|
2479
|
+
} else {
|
|
2480
|
+
topEl = targetCard;
|
|
2481
|
+
bottomEl = sourceCard;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
var topBottom = cardBottom(topEl);
|
|
2485
|
+
var bottomTop = cardTop(bottomEl);
|
|
2486
|
+
var height = bottomTop - topBottom;
|
|
2487
|
+
|
|
2488
|
+
if (height > 0) {
|
|
2489
|
+
var thread = el('div', 'stream-thread');
|
|
2490
|
+
var typeColor = ENTRY_TYPE_COLORS[entry.entry_type] || '#6b7280';
|
|
2491
|
+
thread.style.setProperty('--thread-color', typeColor);
|
|
2492
|
+
thread.style.top = topBottom + 'px';
|
|
2493
|
+
thread.style.height = height + 'px';
|
|
2494
|
+
container.appendChild(thread);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function renderStreamTypeFilters() {
|
|
2501
|
+
var container = document.getElementById('stream-type-filters');
|
|
2502
|
+
if (!container) return;
|
|
2503
|
+
clearElement(container);
|
|
2504
|
+
|
|
2505
|
+
// Count entry types from scoped data
|
|
2506
|
+
var scoped = applyGlobalScope(state.blackboard.data || [], 'scope');
|
|
2507
|
+
var typeSet = {};
|
|
2508
|
+
for (var i = 0; i < scoped.length; i++) {
|
|
2509
|
+
var t = scoped[i].entry_type || 'unknown';
|
|
2510
|
+
typeSet[t] = (typeSet[t] || 0) + 1;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
var types = Object.keys(typeSet).sort(function(a, b) {
|
|
2514
|
+
return typeSet[b] - typeSet[a];
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
if (types.length === 0) return;
|
|
2518
|
+
|
|
2519
|
+
// "All" chip
|
|
2520
|
+
var allChip = document.createElement('button');
|
|
2521
|
+
allChip.className = 'stream-type-chip' + (streamTypeFilter === null ? ' active' : '');
|
|
2522
|
+
allChip.style.setProperty('--type-color', 'var(--accent)');
|
|
2523
|
+
var allDot = document.createElement('span');
|
|
2524
|
+
allDot.className = 'chip-dot';
|
|
2525
|
+
allChip.appendChild(allDot);
|
|
2526
|
+
var allText = document.createElement('span');
|
|
2527
|
+
allText.textContent = 'All';
|
|
2528
|
+
allChip.appendChild(allText);
|
|
2529
|
+
allChip.addEventListener('click', function() {
|
|
2530
|
+
streamTypeFilter = null;
|
|
2531
|
+
renderStream();
|
|
2532
|
+
});
|
|
2533
|
+
container.appendChild(allChip);
|
|
2534
|
+
|
|
2535
|
+
// Per-type chips
|
|
2536
|
+
for (var j = 0; j < types.length; j++) {
|
|
2537
|
+
(function(type) {
|
|
2538
|
+
var color = ENTRY_TYPE_COLORS[type] || '#6b7280';
|
|
2539
|
+
var isActive = streamTypeFilter === null || !!streamTypeFilter[type];
|
|
2540
|
+
var chip = document.createElement('button');
|
|
2541
|
+
chip.className = 'stream-type-chip' + (isActive ? ' active' : '');
|
|
2542
|
+
chip.style.setProperty('--type-color', color);
|
|
2543
|
+
var dot = document.createElement('span');
|
|
2544
|
+
dot.className = 'chip-dot';
|
|
2545
|
+
chip.appendChild(dot);
|
|
2546
|
+
var text = document.createElement('span');
|
|
2547
|
+
text.textContent = type + ' (' + typeSet[type] + ')';
|
|
2548
|
+
chip.appendChild(text);
|
|
2549
|
+
chip.addEventListener('click', function() {
|
|
2550
|
+
if (streamTypeFilter === null) {
|
|
2551
|
+
streamTypeFilter = {};
|
|
2552
|
+
streamTypeFilter[type] = true;
|
|
2553
|
+
} else if (streamTypeFilter[type]) {
|
|
2554
|
+
delete streamTypeFilter[type];
|
|
2555
|
+
if (Object.keys(streamTypeFilter).length === 0) streamTypeFilter = null;
|
|
2556
|
+
} else {
|
|
2557
|
+
streamTypeFilter[type] = true;
|
|
2558
|
+
}
|
|
2559
|
+
renderStream();
|
|
2560
|
+
});
|
|
2561
|
+
container.appendChild(chip);
|
|
2562
|
+
})(types[j]);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2073
2566
|
function buildGraphStyles() {
|
|
2074
2567
|
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
2075
|
-
var textColor = isDark ? '#
|
|
2076
|
-
var
|
|
2077
|
-
var
|
|
2568
|
+
var textColor = isDark ? '#c8d0e0' : '#1a1a2e';
|
|
2569
|
+
var textHaloColor = isDark ? 'rgba(11, 15, 26, 0.85)' : 'rgba(255, 255, 255, 0.85)';
|
|
2570
|
+
var edgeColor = isDark ? 'rgba(100, 116, 139, 0.5)' : 'rgba(148, 163, 184, 0.6)';
|
|
2571
|
+
var edgeLabelColor = isDark ? '#64748b' : '#94a3b8';
|
|
2572
|
+
var accentColor = isDark ? '#00d4aa' : '#00a88a';
|
|
2078
2573
|
|
|
2079
2574
|
var styles = [
|
|
2080
2575
|
{
|
|
@@ -2083,36 +2578,112 @@ function buildGraphStyles() {
|
|
|
2083
2578
|
'label': 'data(label)',
|
|
2084
2579
|
'text-valign': 'bottom',
|
|
2085
2580
|
'text-halign': 'center',
|
|
2086
|
-
'font-size': '
|
|
2087
|
-
'
|
|
2088
|
-
'
|
|
2581
|
+
'font-size': '10px',
|
|
2582
|
+
'font-weight': 500,
|
|
2583
|
+
'width': 32,
|
|
2584
|
+
'height': 32,
|
|
2089
2585
|
'color': textColor,
|
|
2090
2586
|
'text-margin-y': 6,
|
|
2091
2587
|
'background-color': '#6b7280',
|
|
2092
|
-
'
|
|
2093
|
-
'
|
|
2588
|
+
'background-opacity': 0.9,
|
|
2589
|
+
'border-width': 2,
|
|
2590
|
+
'border-color': 'rgba(255, 255, 255, 0.08)',
|
|
2591
|
+
'border-opacity': 1,
|
|
2592
|
+
'text-wrap': 'ellipsis',
|
|
2593
|
+
'text-max-width': '100px',
|
|
2594
|
+
'text-outline-width': 0,
|
|
2595
|
+
'text-background-opacity': 0.8,
|
|
2596
|
+
'text-background-color': textHaloColor,
|
|
2597
|
+
'text-background-padding': '2px',
|
|
2598
|
+
'text-background-shape': 'roundrectangle',
|
|
2599
|
+
'overlay-opacity': 0,
|
|
2600
|
+
'transition-property': 'border-width, border-color, width, height, background-opacity',
|
|
2601
|
+
'transition-duration': '0.15s'
|
|
2602
|
+
}
|
|
2603
|
+
},
|
|
2604
|
+
{
|
|
2605
|
+
selector: 'node:active',
|
|
2606
|
+
style: {
|
|
2607
|
+
'overlay-opacity': 0
|
|
2094
2608
|
}
|
|
2095
2609
|
},
|
|
2096
2610
|
{
|
|
2097
2611
|
selector: 'edge',
|
|
2098
2612
|
style: {
|
|
2099
|
-
'width':
|
|
2613
|
+
'width': 1.5,
|
|
2100
2614
|
'line-color': edgeColor,
|
|
2101
2615
|
'target-arrow-color': edgeColor,
|
|
2102
2616
|
'target-arrow-shape': 'triangle',
|
|
2617
|
+
'arrow-scale': 0.8,
|
|
2103
2618
|
'curve-style': 'bezier',
|
|
2104
2619
|
'label': 'data(label)',
|
|
2105
2620
|
'font-size': '8px',
|
|
2106
|
-
'color':
|
|
2621
|
+
'color': edgeLabelColor,
|
|
2107
2622
|
'text-rotation': 'autorotate',
|
|
2108
|
-
'text-margin-y': -8
|
|
2623
|
+
'text-margin-y': -8,
|
|
2624
|
+
'text-outline-width': 0,
|
|
2625
|
+
'text-background-opacity': 0.75,
|
|
2626
|
+
'text-background-color': textHaloColor,
|
|
2627
|
+
'text-background-padding': '1px',
|
|
2628
|
+
'text-background-shape': 'roundrectangle',
|
|
2629
|
+
'opacity': 0.7,
|
|
2630
|
+
'transition-property': 'opacity, width, line-color',
|
|
2631
|
+
'transition-duration': '0.15s'
|
|
2109
2632
|
}
|
|
2110
2633
|
},
|
|
2111
2634
|
{
|
|
2112
2635
|
selector: 'node:selected',
|
|
2113
2636
|
style: {
|
|
2114
2637
|
'border-width': 3,
|
|
2115
|
-
'border-color': accentColor
|
|
2638
|
+
'border-color': accentColor,
|
|
2639
|
+
'border-opacity': 1,
|
|
2640
|
+
'width': 40,
|
|
2641
|
+
'height': 40,
|
|
2642
|
+
'background-opacity': 1,
|
|
2643
|
+
'overlay-color': accentColor,
|
|
2644
|
+
'overlay-padding': 6,
|
|
2645
|
+
'overlay-opacity': 0.15,
|
|
2646
|
+
'z-index': 10
|
|
2647
|
+
}
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
selector: 'node.hover',
|
|
2651
|
+
style: {
|
|
2652
|
+
'border-width': 2.5,
|
|
2653
|
+
'border-color': accentColor,
|
|
2654
|
+
'width': 36,
|
|
2655
|
+
'height': 36,
|
|
2656
|
+
'background-opacity': 1,
|
|
2657
|
+
'z-index': 5
|
|
2658
|
+
}
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
selector: 'node.dimmed',
|
|
2662
|
+
style: {
|
|
2663
|
+
'opacity': 0.2,
|
|
2664
|
+
'text-opacity': 0.15
|
|
2665
|
+
}
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
selector: 'edge.dimmed',
|
|
2669
|
+
style: {
|
|
2670
|
+
'opacity': 0.08
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
selector: 'node.highlighted',
|
|
2675
|
+
style: {
|
|
2676
|
+
'opacity': 1,
|
|
2677
|
+
'text-opacity': 1
|
|
2678
|
+
}
|
|
2679
|
+
},
|
|
2680
|
+
{
|
|
2681
|
+
selector: 'edge.highlighted',
|
|
2682
|
+
style: {
|
|
2683
|
+
'opacity': 1,
|
|
2684
|
+
'width': 2.5,
|
|
2685
|
+
'line-color': accentColor,
|
|
2686
|
+
'target-arrow-color': accentColor
|
|
2116
2687
|
}
|
|
2117
2688
|
}
|
|
2118
2689
|
];
|
|
@@ -2139,10 +2710,9 @@ function renderGraphLegend() {
|
|
|
2139
2710
|
for (var i = 0; i < types.length; i++) {
|
|
2140
2711
|
var item = document.createElement('span');
|
|
2141
2712
|
item.className = 'graph-legend-item';
|
|
2142
|
-
item.style.cssText = 'display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:0.8rem;';
|
|
2143
2713
|
var dot = document.createElement('span');
|
|
2144
2714
|
dot.className = 'graph-legend-dot';
|
|
2145
|
-
dot.style.
|
|
2715
|
+
dot.style.background = ENTITY_COLORS[types[i]];
|
|
2146
2716
|
var label = document.createElement('span');
|
|
2147
2717
|
label.textContent = types[i];
|
|
2148
2718
|
item.appendChild(dot);
|
|
@@ -2151,6 +2721,90 @@ function renderGraphLegend() {
|
|
|
2151
2721
|
}
|
|
2152
2722
|
}
|
|
2153
2723
|
|
|
2724
|
+
function renderGraphTypeFilters() {
|
|
2725
|
+
var container = document.getElementById('graph-type-filters');
|
|
2726
|
+
if (!container) return;
|
|
2727
|
+
clearElement(container);
|
|
2728
|
+
|
|
2729
|
+
// Count entity types
|
|
2730
|
+
var typeSet = {};
|
|
2731
|
+
var scoped = applyGlobalScope(state.graph.data, 'scope');
|
|
2732
|
+
for (var i = 0; i < scoped.length; i++) {
|
|
2733
|
+
var t = scoped[i].type || 'concept';
|
|
2734
|
+
typeSet[t] = (typeSet[t] || 0) + 1;
|
|
2735
|
+
}
|
|
2736
|
+
var types = Object.keys(typeSet).sort();
|
|
2737
|
+
if (types.length <= 1) return;
|
|
2738
|
+
|
|
2739
|
+
// "All" chip
|
|
2740
|
+
var allChip = document.createElement('button');
|
|
2741
|
+
allChip.className = 'graph-type-chip' + (graphTypeFilter === null ? ' active' : '');
|
|
2742
|
+
allChip.textContent = 'All';
|
|
2743
|
+
allChip.style.setProperty('--type-color', 'var(--accent)');
|
|
2744
|
+
allChip.addEventListener('click', function() {
|
|
2745
|
+
graphTypeFilter = null;
|
|
2746
|
+
applyGraphTypeFilter();
|
|
2747
|
+
renderGraphTypeFilters();
|
|
2748
|
+
});
|
|
2749
|
+
container.appendChild(allChip);
|
|
2750
|
+
|
|
2751
|
+
for (var j = 0; j < types.length; j++) {
|
|
2752
|
+
(function(type) {
|
|
2753
|
+
var color = ENTITY_COLORS[type] || '#6b7280';
|
|
2754
|
+
var isActive = graphTypeFilter === null || !!graphTypeFilter[type];
|
|
2755
|
+
var chip = document.createElement('button');
|
|
2756
|
+
chip.className = 'graph-type-chip' + (graphTypeFilter !== null && isActive ? ' active' : '');
|
|
2757
|
+
chip.style.setProperty('--type-color', color);
|
|
2758
|
+
var dot = document.createElement('span');
|
|
2759
|
+
dot.className = 'chip-dot';
|
|
2760
|
+
chip.appendChild(dot);
|
|
2761
|
+
var text = document.createElement('span');
|
|
2762
|
+
text.textContent = type + ' (' + typeSet[type] + ')';
|
|
2763
|
+
chip.appendChild(text);
|
|
2764
|
+
chip.addEventListener('click', function() {
|
|
2765
|
+
if (graphTypeFilter === null) {
|
|
2766
|
+
graphTypeFilter = {};
|
|
2767
|
+
graphTypeFilter[type] = true;
|
|
2768
|
+
} else if (graphTypeFilter[type]) {
|
|
2769
|
+
delete graphTypeFilter[type];
|
|
2770
|
+
if (Object.keys(graphTypeFilter).length === 0) graphTypeFilter = null;
|
|
2771
|
+
} else {
|
|
2772
|
+
graphTypeFilter[type] = true;
|
|
2773
|
+
}
|
|
2774
|
+
applyGraphTypeFilter();
|
|
2775
|
+
renderGraphTypeFilters();
|
|
2776
|
+
});
|
|
2777
|
+
container.appendChild(chip);
|
|
2778
|
+
})(types[j]);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function applyGraphTypeFilter() {
|
|
2783
|
+
if (!window.cyInstance) return;
|
|
2784
|
+
if (graphTypeFilter === null) {
|
|
2785
|
+
// Show all
|
|
2786
|
+
window.cyInstance.nodes().show();
|
|
2787
|
+
window.cyInstance.edges().show();
|
|
2788
|
+
} else {
|
|
2789
|
+
window.cyInstance.nodes().forEach(function(n) {
|
|
2790
|
+
var type = n.data('type') || 'concept';
|
|
2791
|
+
if (graphTypeFilter[type]) {
|
|
2792
|
+
n.show();
|
|
2793
|
+
} else {
|
|
2794
|
+
n.hide();
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
// Only show edges where both endpoints are visible
|
|
2798
|
+
window.cyInstance.edges().forEach(function(e) {
|
|
2799
|
+
if (e.source().visible() && e.target().visible()) {
|
|
2800
|
+
e.show();
|
|
2801
|
+
} else {
|
|
2802
|
+
e.hide();
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2154
2808
|
function buildGraphElements() {
|
|
2155
2809
|
var entities = state.graph.data;
|
|
2156
2810
|
var relations = state.graph.relations;
|
|
@@ -2201,12 +2855,14 @@ function initGraphVis() {
|
|
|
2201
2855
|
canvas.appendChild(msg);
|
|
2202
2856
|
}
|
|
2203
2857
|
renderGraphLegend();
|
|
2858
|
+
renderGraphTypeFilters();
|
|
2204
2859
|
return;
|
|
2205
2860
|
}
|
|
2206
2861
|
|
|
2207
2862
|
// Create-once guard: if already initialized, just update data
|
|
2208
2863
|
if (window.cyInstance) {
|
|
2209
2864
|
updateGraphData();
|
|
2865
|
+
renderGraphTypeFilters();
|
|
2210
2866
|
return;
|
|
2211
2867
|
}
|
|
2212
2868
|
|
|
@@ -2225,16 +2881,34 @@ function initGraphVis() {
|
|
|
2225
2881
|
layout: {
|
|
2226
2882
|
name: 'cose',
|
|
2227
2883
|
animate: true,
|
|
2228
|
-
animationDuration:
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2884
|
+
animationDuration: 600,
|
|
2885
|
+
animationEasing: 'ease-out',
|
|
2886
|
+
nodeRepulsion: function() { return 14000; },
|
|
2887
|
+
idealEdgeLength: function() { return 120; },
|
|
2888
|
+
nodeOverlap: 24,
|
|
2889
|
+
padding: 50
|
|
2233
2890
|
},
|
|
2234
2891
|
style: buildGraphStyles(),
|
|
2235
|
-
minZoom: 0.
|
|
2892
|
+
minZoom: 0.15,
|
|
2236
2893
|
maxZoom: 5,
|
|
2237
|
-
wheelSensitivity: 0.
|
|
2894
|
+
wheelSensitivity: 0.3,
|
|
2895
|
+
pixelRatio: 'auto'
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
// Hover: highlight node and its neighborhood
|
|
2899
|
+
window.cyInstance.on('mouseover', 'node', function(evt) {
|
|
2900
|
+
var node = evt.target;
|
|
2901
|
+
node.addClass('hover');
|
|
2902
|
+
// Dim everything, highlight neighborhood
|
|
2903
|
+
var neighborhood = node.closedNeighborhood();
|
|
2904
|
+
window.cyInstance.elements().not(neighborhood).addClass('dimmed');
|
|
2905
|
+
neighborhood.addClass('highlighted');
|
|
2906
|
+
neighborhood.connectedEdges().addClass('highlighted');
|
|
2907
|
+
});
|
|
2908
|
+
|
|
2909
|
+
window.cyInstance.on('mouseout', 'node', function(evt) {
|
|
2910
|
+
evt.target.removeClass('hover');
|
|
2911
|
+
window.cyInstance.elements().removeClass('dimmed highlighted');
|
|
2238
2912
|
});
|
|
2239
2913
|
|
|
2240
2914
|
// Click node to show detail and expand neighbors
|
|
@@ -2261,7 +2935,15 @@ function initGraphVis() {
|
|
|
2261
2935
|
}
|
|
2262
2936
|
});
|
|
2263
2937
|
|
|
2938
|
+
// Click background to clear highlight
|
|
2939
|
+
window.cyInstance.on('tap', function(evt) {
|
|
2940
|
+
if (evt.target === window.cyInstance) {
|
|
2941
|
+
window.cyInstance.elements().removeClass('dimmed highlighted hover');
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2264
2945
|
renderGraphLegend();
|
|
2946
|
+
renderGraphTypeFilters();
|
|
2265
2947
|
setupGraphControls();
|
|
2266
2948
|
}
|
|
2267
2949
|
|
|
@@ -2273,33 +2955,42 @@ function setupGraphControls() {
|
|
|
2273
2955
|
|
|
2274
2956
|
if (zoomIn) {
|
|
2275
2957
|
zoomIn.onclick = function() {
|
|
2276
|
-
if (window.cyInstance)
|
|
2958
|
+
if (window.cyInstance) {
|
|
2959
|
+
window.cyInstance.animate({ zoom: window.cyInstance.zoom() * 1.3, center: window.cyInstance.extent() }, { duration: 250, easing: 'ease-in-out-quad' });
|
|
2960
|
+
}
|
|
2277
2961
|
};
|
|
2278
2962
|
}
|
|
2279
2963
|
|
|
2280
2964
|
if (zoomOut) {
|
|
2281
2965
|
zoomOut.onclick = function() {
|
|
2282
|
-
if (window.cyInstance)
|
|
2966
|
+
if (window.cyInstance) {
|
|
2967
|
+
window.cyInstance.animate({ zoom: window.cyInstance.zoom() * 0.7, center: window.cyInstance.extent() }, { duration: 250, easing: 'ease-in-out-quad' });
|
|
2968
|
+
}
|
|
2283
2969
|
};
|
|
2284
2970
|
}
|
|
2285
2971
|
|
|
2286
2972
|
if (fit) {
|
|
2287
2973
|
fit.onclick = function() {
|
|
2288
|
-
if (window.cyInstance) window.cyInstance.fit
|
|
2974
|
+
if (window.cyInstance) window.cyInstance.animate({ fit: { padding: 50 } }, { duration: 400, easing: 'ease-in-out-quad' });
|
|
2289
2975
|
};
|
|
2290
2976
|
}
|
|
2291
2977
|
|
|
2292
2978
|
if (reset) {
|
|
2293
2979
|
reset.onclick = function() {
|
|
2294
2980
|
if (window.cyInstance) {
|
|
2981
|
+
// Reset type filter
|
|
2982
|
+
graphTypeFilter = null;
|
|
2983
|
+
applyGraphTypeFilter();
|
|
2984
|
+
renderGraphTypeFilters();
|
|
2295
2985
|
var layout = window.cyInstance.layout({
|
|
2296
2986
|
name: 'cose',
|
|
2297
2987
|
animate: true,
|
|
2298
|
-
animationDuration:
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2988
|
+
animationDuration: 600,
|
|
2989
|
+
animationEasing: 'ease-out',
|
|
2990
|
+
nodeRepulsion: function() { return 14000; },
|
|
2991
|
+
idealEdgeLength: function() { return 120; },
|
|
2992
|
+
nodeOverlap: 24,
|
|
2993
|
+
padding: 50
|
|
2303
2994
|
});
|
|
2304
2995
|
layout.run();
|
|
2305
2996
|
}
|
|
@@ -2655,9 +3346,14 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
2655
3346
|
|
|
2656
3347
|
// Search input: debounced performSearch on "input" event
|
|
2657
3348
|
var searchInput = document.getElementById("search-input");
|
|
3349
|
+
var clearBtn = document.getElementById("search-clear-btn");
|
|
2658
3350
|
if (searchInput) {
|
|
2659
3351
|
var debouncedSearch = debounce(performSearch, 300);
|
|
2660
|
-
searchInput.addEventListener("input",
|
|
3352
|
+
searchInput.addEventListener("input", function() {
|
|
3353
|
+
// Show/hide clear icon based on input content
|
|
3354
|
+
if (clearBtn) clearBtn.style.display = searchInput.value ? "flex" : "none";
|
|
3355
|
+
debouncedSearch();
|
|
3356
|
+
});
|
|
2661
3357
|
// Enter key: immediate performSearch (bypass debounce)
|
|
2662
3358
|
searchInput.addEventListener("keydown", function(e) {
|
|
2663
3359
|
if (e.key === "Enter") {
|
|
@@ -2667,18 +3363,12 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
2667
3363
|
});
|
|
2668
3364
|
}
|
|
2669
3365
|
|
|
2670
|
-
// Search button: performSearch on click
|
|
2671
|
-
var searchBtn = document.getElementById("search-btn");
|
|
2672
|
-
if (searchBtn) {
|
|
2673
|
-
searchBtn.addEventListener("click", performSearch);
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
3366
|
// Clear button: clear search input, reset search state, switch to stats tab
|
|
2677
|
-
var clearBtn = document.getElementById("search-clear-btn");
|
|
2678
3367
|
if (clearBtn) {
|
|
2679
3368
|
clearBtn.addEventListener("click", function() {
|
|
2680
3369
|
var input = document.getElementById("search-input");
|
|
2681
3370
|
if (input) input.value = "";
|
|
3371
|
+
clearBtn.style.display = "none";
|
|
2682
3372
|
state.search.query = "";
|
|
2683
3373
|
state.search.results = [];
|
|
2684
3374
|
state.search.selectedId = null;
|
|
@@ -2688,6 +3378,21 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
2688
3378
|
});
|
|
2689
3379
|
}
|
|
2690
3380
|
|
|
3381
|
+
// Search type chips: toggle active state
|
|
3382
|
+
var typeChips = document.querySelectorAll(".search-chip[data-type]");
|
|
3383
|
+
for (var tc = 0; tc < typeChips.length; tc++) {
|
|
3384
|
+
(function(chip) {
|
|
3385
|
+
chip.addEventListener("click", function() {
|
|
3386
|
+
chip.classList.toggle("active");
|
|
3387
|
+
// Ensure at least one type remains active
|
|
3388
|
+
var active = document.querySelectorAll(".search-chip.active[data-type]");
|
|
3389
|
+
if (active.length === 0) chip.classList.add("active");
|
|
3390
|
+
// Re-run search if there's a query
|
|
3391
|
+
if (state.search.query) performSearch();
|
|
3392
|
+
});
|
|
3393
|
+
})(typeChips[tc]);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
2691
3396
|
// Global scope filter
|
|
2692
3397
|
var globalScopeInput = document.getElementById("global-scope");
|
|
2693
3398
|
if (globalScopeInput) {
|
|
@@ -2704,6 +3409,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
2704
3409
|
if (indicator2) indicator2.style.display = "none";
|
|
2705
3410
|
}
|
|
2706
3411
|
// Reset pagination, re-render active tab
|
|
3412
|
+
streamTypeFilter = null;
|
|
2707
3413
|
state.blackboard.page = 1;
|
|
2708
3414
|
state.decisions.page = 1;
|
|
2709
3415
|
state.graph.page = 1;
|