app.jsโข33.9 kB
// ACE MCP Server Implementation
// Agentic Context Engineering Framework
class ACEServer {
constructor() {
// In-memory storage (session only - no localStorage/sessionStorage)
this.contexts = new Map();
this.contexts.set('default', {
id: 'default',
name: 'Default Context',
bullets: [],
created_at: Date.now()
});
this.currentContextId = 'default';
this.operationLog = [];
this.sessionStart = Date.now();
// Settings
this.settings = {
dedupThreshold: 0.85,
maxBullets: 1000,
maxIterations: 5,
debugMode: false,
autoDedup: true
};
this.init();
}
init() {
this.setupEventListeners();
this.updateUI();
this.log('ACE MCP Server initialized successfully', 'info');
this.updateSessionInfo();
}
setupEventListeners() {
// Tab navigation
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
});
// Context management
document.getElementById('context-select').addEventListener('change', (e) => {
this.switchContext(e.target.value);
});
document.getElementById('new-context-btn').addEventListener('click', () => this.showNewContextModal());
document.getElementById('create-context-btn').addEventListener('click', () => this.createNewContext());
document.getElementById('cancel-context-btn').addEventListener('click', () => this.hideNewContextModal());
document.getElementById('close-modal-btn').addEventListener('click', () => this.hideNewContextModal());
// Quick actions
document.getElementById('demo-workflow-btn').addEventListener('click', () => this.runDemoWorkflow());
document.getElementById('export-playbook-btn').addEventListener('click', () => this.exportPlaybook());
document.getElementById('import-playbook-btn').addEventListener('click', () => this.importPlaybook());
document.getElementById('reset-context-btn').addEventListener('click', () => this.resetContext());
// MCP Tools
document.getElementById('run-generate-btn').addEventListener('click', () => this.runGenerate());
document.getElementById('run-reflect-btn').addEventListener('click', () => this.runReflect());
document.getElementById('run-curate-btn').addEventListener('click', () => this.runCurate());
document.getElementById('run-update-btn').addEventListener('click', () => this.runUpdate());
document.getElementById('run-get-btn').addEventListener('click', () => this.runGet());
document.getElementById('run-reset-btn').addEventListener('click', () => this.runReset());
// Playbook browser
document.getElementById('bullet-search').addEventListener('input', () => this.filterBullets());
document.getElementById('section-filter').addEventListener('change', () => this.filterBullets());
document.getElementById('sort-by').addEventListener('change', () => this.filterBullets());
// Settings
document.getElementById('save-settings-btn').addEventListener('click', () => this.saveSettings());
document.getElementById('reset-settings-btn').addEventListener('click', () => this.resetSettings());
// Logs
document.getElementById('clear-logs-btn').addEventListener('click', () => this.clearLogs());
document.getElementById('export-logs-btn').addEventListener('click', () => this.exportLogs());
}
// Tab Management
switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`${tabName}-tab`).classList.add('active');
}
// Context Management
switchContext(contextId) {
this.currentContextId = contextId;
this.updateUI();
this.log(`Switched to context: ${contextId}`, 'info');
}
showNewContextModal() {
document.getElementById('new-context-modal').classList.add('show');
}
hideNewContextModal() {
document.getElementById('new-context-modal').classList.remove('show');
document.getElementById('new-context-name').value = '';
}
createNewContext() {
const name = document.getElementById('new-context-name').value.trim();
if (!name) {
alert('Please enter a context name');
return;
}
const id = name.toLowerCase().replace(/\s+/g, '-');
if (this.contexts.has(id)) {
alert('Context with this name already exists');
return;
}
this.contexts.set(id, {
id,
name,
bullets: [],
created_at: Date.now()
});
this.updateContextSelector();
this.currentContextId = id;
document.getElementById('context-select').value = id;
this.hideNewContextModal();
this.updateUI();
this.log(`Created new context: ${name}`, 'success');
}
updateContextSelector() {
const select = document.getElementById('context-select');
select.innerHTML = '';
this.contexts.forEach(context => {
const option = document.createElement('option');
option.value = context.id;
option.textContent = context.name;
select.appendChild(option);
});
select.value = this.currentContextId;
}
getCurrentContext() {
return this.contexts.get(this.currentContextId);
}
// MCP Tools Implementation
// 1. ace_generate
aceGenerate(query, contextId = this.currentContextId) {
const context = this.contexts.get(contextId);
if (!context) {
throw new Error(`Context not found: ${contextId}`);
}
// TODO: Replace with actual LLM API call
// This is a mock implementation showing the expected structure
const relevantBullets = this.getRelevantBullets(query, context.bullets);
const trajectory = {
query,
context_id: contextId,
timestamp: Date.now(),
steps: [
{
action: 'Analyze query requirements',
helpful_bullets: relevantBullets.slice(0, 2).map(b => b.id),
harmful_bullets: [],
reasoning: 'Used playbook bullets to understand best practices'
},
{
action: 'Generate solution',
helpful_bullets: relevantBullets.slice(2, 4).map(b => b.id),
harmful_bullets: [],
reasoning: 'Applied strategies from successful patterns'
}
],
result: `Generated response for: ${query}`,
success: true,
bullets_used: relevantBullets.map(b => ({
id: b.id,
section: b.section,
content: b.content,
helpful: true
}))
};
// Update helpful counts
relevantBullets.forEach(bullet => {
bullet.helpful_count++;
});
return trajectory;
}
// 2. ace_reflect
aceReflect(trajectory, maxIterations = this.settings.maxIterations) {
if (!trajectory || !trajectory.query) {
throw new Error('Invalid trajectory object');
}
// TODO: Replace with actual LLM API call for insight extraction
// This is a mock implementation
const insights = {
trajectory_id: trajectory.timestamp,
iterations: maxIterations,
successes: [],
failures: [],
suggestions: [],
timestamp: Date.now()
};
// Analyze helpful bullets
if (trajectory.bullets_used && trajectory.bullets_used.length > 0) {
insights.successes.push(
`Successfully applied ${trajectory.bullets_used.length} relevant strategies`,
'Playbook guidance improved response accuracy'
);
} else {
insights.failures.push('No relevant bullets found in playbook');
insights.suggestions.push('Add more domain-specific strategies to playbook');
}
// Extract patterns from steps
if (trajectory.success) {
insights.successes.push('Task completed successfully');
insights.suggestions.push(
'Codify successful patterns as new bullets',
'Enhance existing bullets with additional context'
);
} else {
insights.failures.push('Task failed to complete');
insights.suggestions.push(
'Analyze failure points and add preventive strategies',
'Update harmful bullets to avoid similar issues'
);
}
// Iterative refinement simulation
for (let i = 1; i < maxIterations; i++) {
insights.suggestions.push(`Refined insight from iteration ${i + 1}`);
}
return insights;
}
// 3. ace_curate
aceCurate(insights, currentPlaybook) {
if (!insights || !insights.suggestions) {
throw new Error('Invalid insights object');
}
const context = currentPlaybook || this.getCurrentContext();
// TODO: Replace with actual LLM API call for delta generation
// This is a mock implementation
const deltaOperations = [];
// Generate ADD operations from suggestions
insights.suggestions.forEach((suggestion, index) => {
if (suggestion.includes('Add') || suggestion.includes('Codify')) {
deltaOperations.push({
operation: 'ADD',
bullet: {
section: this.inferSection(suggestion),
content: suggestion,
helpful_count: 0,
harmful_count: 0
}
});
}
});
// Generate UPDATE operations for successful bullets
if (insights.successes && insights.successes.length > 0) {
const bulletIds = this.extractBulletIds(insights);
bulletIds.forEach(bulletId => {
deltaOperations.push({
operation: 'UPDATE',
bullet_id: bulletId,
updates: {
helpful_count: '+1'
}
});
});
}
// Generate DELETE operations for harmful patterns
if (insights.failures && insights.failures.length > 0) {
const harmfulIds = this.extractHarmfulBulletIds(insights);
harmfulIds.forEach(bulletId => {
deltaOperations.push({
operation: 'DELETE',
bullet_id: bulletId,
reason: 'Harmful pattern identified'
});
});
}
return {
delta_operations: deltaOperations,
timestamp: Date.now(),
total_operations: deltaOperations.length
};
}
// 4. ace_update_playbook
aceUpdatePlaybook(deltaOperations, contextId = this.currentContextId) {
const context = this.contexts.get(contextId);
if (!context) {
throw new Error(`Context not found: ${contextId}`);
}
const stats = {
added: 0,
updated: 0,
deleted: 0,
deduplicated: 0
};
deltaOperations.forEach(op => {
switch (op.operation) {
case 'ADD':
const newBullet = {
id: this.generateBulletId(),
section: op.bullet.section,
content: op.bullet.content,
helpful_count: op.bullet.helpful_count || 0,
harmful_count: op.bullet.harmful_count || 0,
created_at: Date.now(),
updated_at: Date.now()
};
context.bullets.push(newBullet);
stats.added++;
break;
case 'UPDATE':
const bulletToUpdate = context.bullets.find(b => b.id === op.bullet_id);
if (bulletToUpdate) {
if (op.updates.helpful_count === '+1') {
bulletToUpdate.helpful_count++;
} else if (op.updates.helpful_count) {
bulletToUpdate.helpful_count = op.updates.helpful_count;
}
if (op.updates.harmful_count === '+1') {
bulletToUpdate.harmful_count++;
} else if (op.updates.harmful_count) {
bulletToUpdate.harmful_count = op.updates.harmful_count;
}
bulletToUpdate.updated_at = Date.now();
stats.updated++;
}
break;
case 'DELETE':
const indexToDelete = context.bullets.findIndex(b => b.id === op.bullet_id);
if (indexToDelete !== -1) {
context.bullets.splice(indexToDelete, 1);
stats.deleted++;
}
break;
}
});
// Perform deduplication if enabled
if (this.settings.autoDedup) {
stats.deduplicated = this.deduplicateBullets(context);
}
// Enforce max bullets limit
if (context.bullets.length > this.settings.maxBullets) {
const excess = context.bullets.length - this.settings.maxBullets;
// Remove oldest bullets
context.bullets.sort((a, b) => a.created_at - b.created_at);
context.bullets.splice(0, excess);
stats.deleted += excess;
}
return {
success: true,
context_id: contextId,
stats,
total_bullets: context.bullets.length,
timestamp: Date.now()
};
}
// 5. ace_get_playbook
aceGetPlaybook(contextId = this.currentContextId, filter = null) {
const context = this.contexts.get(contextId);
if (!context) {
throw new Error(`Context not found: ${contextId}`);
}
let bullets = context.bullets;
if (filter) {
if (filter.section) {
bullets = bullets.filter(b => b.section === filter.section);
}
if (filter.min_helpful_count) {
bullets = bullets.filter(b => b.helpful_count >= filter.min_helpful_count);
}
if (filter.search) {
const searchLower = filter.search.toLowerCase();
bullets = bullets.filter(b =>
b.content.toLowerCase().includes(searchLower) ||
b.section.toLowerCase().includes(searchLower)
);
}
}
return {
context_id: contextId,
context_name: context.name,
bullets: bullets,
total: bullets.length,
sections: [...new Set(context.bullets.map(b => b.section))],
timestamp: Date.now()
};
}
// 6. ace_reset_playbook
aceResetPlaybook(contextId = this.currentContextId) {
const context = this.contexts.get(contextId);
if (!context) {
throw new Error(`Context not found: ${contextId}`);
}
const previousCount = context.bullets.length;
context.bullets = [];
return {
success: true,
context_id: contextId,
bullets_removed: previousCount,
timestamp: Date.now()
};
}
// Helper Methods
generateBulletId() {
return 'b' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
getRelevantBullets(query, bullets) {
// Simple relevance scoring based on keyword matching
const queryWords = query.toLowerCase().split(/\s+/);
return bullets
.map(bullet => {
const contentWords = bullet.content.toLowerCase().split(/\s+/);
const matches = queryWords.filter(word => contentWords.some(cw => cw.includes(word))).length;
return { bullet, score: matches };
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(item => item.bullet);
}
inferSection(text) {
const sectionKeywords = {
'Code Generation': ['code', 'function', 'class', 'method', 'generate'],
'API Design': ['api', 'endpoint', 'rest', 'http', 'request'],
'Validation': ['validate', 'validation', 'check', 'verify'],
'Security': ['security', 'auth', 'authentication', 'authorization'],
'Testing': ['test', 'testing', 'unit', 'integration'],
'Performance': ['performance', 'optimize', 'speed', 'cache'],
'Documentation': ['document', 'documentation', 'comment', 'readme']
};
const textLower = text.toLowerCase();
for (const [section, keywords] of Object.entries(sectionKeywords)) {
if (keywords.some(keyword => textLower.includes(keyword))) {
return section;
}
}
return 'General';
}
extractBulletIds(insights) {
// Mock implementation - would extract from trajectory in real version
return [];
}
extractHarmfulBulletIds(insights) {
// Mock implementation
return [];
}
// Deduplication using simple string similarity
deduplicateBullets(context) {
const bullets = context.bullets;
const toRemove = new Set();
let deduped = 0;
for (let i = 0; i < bullets.length; i++) {
if (toRemove.has(i)) continue;
for (let j = i + 1; j < bullets.length; j++) {
if (toRemove.has(j)) continue;
const similarity = this.calculateSimilarity(bullets[i].content, bullets[j].content);
if (similarity >= this.settings.dedupThreshold) {
// Merge: keep the one with higher helpful_count
if (bullets[i].helpful_count >= bullets[j].helpful_count) {
bullets[i].helpful_count += bullets[j].helpful_count;
bullets[i].harmful_count += bullets[j].harmful_count;
toRemove.add(j);
} else {
bullets[j].helpful_count += bullets[i].helpful_count;
bullets[j].harmful_count += bullets[i].harmful_count;
toRemove.add(i);
break;
}
deduped++;
}
}
}
// Remove duplicates
context.bullets = bullets.filter((_, index) => !toRemove.has(index));
return deduped;
}
calculateSimilarity(str1, str2) {
// Simple Jaccard similarity
const words1 = new Set(str1.toLowerCase().split(/\s+/));
const words2 = new Set(str2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
// UI Methods
runGenerate() {
const query = document.getElementById('generate-query').value.trim();
if (!query) {
alert('Please enter a query');
return;
}
try {
const trajectory = this.aceGenerate(query);
this.showToolOutput('generate-output', trajectory);
this.log(`Generated trajectory for query: "${query}"`, 'success');
this.updateUI();
} catch (error) {
this.showToolOutput('generate-output', { error: error.message });
this.log(`Error in ace_generate: ${error.message}`, 'error');
}
}
runReflect() {
const trajectoryJson = document.getElementById('reflect-trajectory').value.trim();
const iterations = parseInt(document.getElementById('reflect-iterations').value);
if (!trajectoryJson) {
alert('Please enter trajectory JSON');
return;
}
try {
const trajectory = JSON.parse(trajectoryJson);
const insights = this.aceReflect(trajectory, iterations);
this.showToolOutput('reflect-output', insights);
this.log(`Reflected on trajectory with ${iterations} iterations`, 'success');
} catch (error) {
this.showToolOutput('reflect-output', { error: error.message });
this.log(`Error in ace_reflect: ${error.message}`, 'error');
}
}
runCurate() {
const insightsJson = document.getElementById('curate-insights').value.trim();
if (!insightsJson) {
alert('Please enter insights JSON');
return;
}
try {
const insights = JSON.parse(insightsJson);
const deltas = this.aceCurate(insights);
this.showToolOutput('curate-output', deltas);
this.log(`Curated ${deltas.total_operations} delta operations`, 'success');
} catch (error) {
this.showToolOutput('curate-output', { error: error.message });
this.log(`Error in ace_curate: ${error.message}`, 'error');
}
}
runUpdate() {
const deltasJson = document.getElementById('update-deltas').value.trim();
if (!deltasJson) {
alert('Please enter delta operations JSON');
return;
}
try {
const data = JSON.parse(deltasJson);
const deltas = data.delta_operations || data;
const result = this.aceUpdatePlaybook(deltas);
this.showToolOutput('update-output', result);
this.log(`Updated playbook: +${result.stats.added} -${result.stats.deleted} ~${result.stats.updated}`, 'success');
this.updateUI();
} catch (error) {
this.showToolOutput('update-output', { error: error.message });
this.log(`Error in ace_update_playbook: ${error.message}`, 'error');
}
}
runGet() {
const section = document.getElementById('get-section').value.trim();
try {
const filter = section ? { section } : null;
const result = this.aceGetPlaybook(this.currentContextId, filter);
this.showToolOutput('get-output', result);
this.log(`Retrieved playbook: ${result.total} bullets`, 'success');
} catch (error) {
this.showToolOutput('get-output', { error: error.message });
this.log(`Error in ace_get_playbook: ${error.message}`, 'error');
}
}
runReset() {
if (!confirm('Are you sure you want to reset the playbook? This will delete all bullets.')) {
return;
}
try {
const result = this.aceResetPlaybook();
this.showToolOutput('reset-output', result);
this.log(`Reset playbook: removed ${result.bullets_removed} bullets`, 'warning');
this.updateUI();
} catch (error) {
this.showToolOutput('reset-output', { error: error.message });
this.log(`Error in ace_reset_playbook: ${error.message}`, 'error');
}
}
showToolOutput(elementId, data) {
const output = document.getElementById(elementId);
output.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
output.classList.add('show');
}
runDemoWorkflow() {
this.log('Starting demo ACE workflow...', 'info');
// Step 1: Add some initial bullets
const initialBullets = [
{ section: 'Code Generation', content: 'Always add type annotations to function parameters' },
{ section: 'API Design', content: 'Use RESTful conventions for endpoint naming' },
{ section: 'Validation', content: 'Validate all user inputs before processing' }
];
const addOps = initialBullets.map(b => ({
operation: 'ADD',
bullet: { ...b, helpful_count: 0, harmful_count: 0 }
}));
this.aceUpdatePlaybook(addOps);
this.log('Added initial bullets to playbook', 'success');
// Step 2: Generate trajectory
setTimeout(() => {
const trajectory = this.aceGenerate('Create a user authentication endpoint');
document.getElementById('generate-query').value = 'Create a user authentication endpoint';
this.showToolOutput('generate-output', trajectory);
this.log('Generated trajectory for authentication endpoint', 'success');
// Step 3: Reflect
setTimeout(() => {
const insights = this.aceReflect(trajectory);
document.getElementById('reflect-trajectory').value = JSON.stringify(trajectory, null, 2);
this.showToolOutput('reflect-output', insights);
this.log('Reflected on trajectory and extracted insights', 'success');
// Step 4: Curate
setTimeout(() => {
const deltas = this.aceCurate(insights);
document.getElementById('curate-insights').value = JSON.stringify(insights, null, 2);
this.showToolOutput('curate-output', deltas);
this.log('Curated delta operations from insights', 'success');
// Step 5: Update
setTimeout(() => {
const result = this.aceUpdatePlaybook(deltas.delta_operations);
document.getElementById('update-deltas').value = JSON.stringify(deltas, null, 2);
this.showToolOutput('update-output', result);
this.log('Applied delta updates to playbook', 'success');
this.updateUI();
this.log('โ
Demo workflow completed successfully!', 'success');
this.switchTab('tools');
}, 500);
}, 500);
}, 500);
}, 500);
}
exportPlaybook() {
const playbook = this.aceGetPlaybook();
const json = JSON.stringify(playbook, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `playbook-${this.currentContextId}-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
this.log('Exported playbook to JSON', 'success');
}
importPlaybook() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result);
if (data.bullets) {
const context = this.getCurrentContext();
context.bullets = data.bullets;
this.updateUI();
this.log(`Imported ${data.bullets.length} bullets`, 'success');
}
} catch (error) {
alert('Invalid JSON file');
this.log(`Import failed: ${error.message}`, 'error');
}
};
reader.readAsText(file);
};
input.click();
}
resetContext() {
if (!confirm('Reset current context? This will delete all bullets.')) {
return;
}
this.aceResetPlaybook();
this.updateUI();
}
filterBullets() {
const search = document.getElementById('bullet-search').value.toLowerCase();
const sectionFilter = document.getElementById('section-filter').value;
const sortBy = document.getElementById('sort-by').value;
const context = this.getCurrentContext();
let bullets = [...context.bullets];
// Filter by search
if (search) {
bullets = bullets.filter(b =>
b.content.toLowerCase().includes(search) ||
b.section.toLowerCase().includes(search)
);
}
// Filter by section
if (sectionFilter !== 'all') {
bullets = bullets.filter(b => b.section === sectionFilter);
}
// Sort
switch (sortBy) {
case 'created_desc':
bullets.sort((a, b) => b.created_at - a.created_at);
break;
case 'created_asc':
bullets.sort((a, b) => a.created_at - b.created_at);
break;
case 'helpful_desc':
bullets.sort((a, b) => b.helpful_count - a.helpful_count);
break;
case 'helpful_asc':
bullets.sort((a, b) => a.helpful_count - b.helpful_count);
break;
}
this.renderBullets(bullets);
}
renderBullets(bullets = null) {
const container = document.getElementById('bullets-container');
if (!bullets) {
bullets = this.getCurrentContext().bullets;
}
if (bullets.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">๐</div>
<h3>No Bullets Yet</h3>
<p>Your playbook is empty. Use the MCP Tools tab to generate and curate bullets.</p>
</div>
`;
return;
}
container.innerHTML = bullets.map(bullet => `
<div class="bullet-card">
<div class="bullet-header">
<span class="bullet-section">${bullet.section}</span>
<span class="bullet-id">${bullet.id}</span>
</div>
<div class="bullet-content">${bullet.content}</div>
<div class="bullet-meta">
<div class="bullet-stats">
<div class="stat-item stat-helpful">
<span>โ</span>
<span>${bullet.helpful_count}</span>
</div>
<div class="stat-item stat-harmful">
<span>โ</span>
<span>${bullet.harmful_count}</span>
</div>
</div>
<div class="bullet-timestamp">${this.formatDate(bullet.created_at)}</div>
</div>
</div>
`).join('');
}
updateUI() {
const context = this.getCurrentContext();
const bullets = context.bullets;
// Update stats
document.getElementById('stat-total-bullets').textContent = bullets.length;
document.getElementById('stat-contexts').textContent = this.contexts.size;
document.getElementById('stat-helpful').textContent = bullets.reduce((sum, b) => sum + b.helpful_count, 0);
document.getElementById('stat-operations').textContent = this.operationLog.length;
// Update section filter
const sections = [...new Set(bullets.map(b => b.section))];
const sectionFilter = document.getElementById('section-filter');
const currentValue = sectionFilter.value;
sectionFilter.innerHTML = '<option value="all">All Sections</option>';
sections.forEach(section => {
const option = document.createElement('option');
option.value = section;
option.textContent = section;
sectionFilter.appendChild(option);
});
sectionFilter.value = currentValue;
// Render bullets
this.renderBullets();
// Update playbook overview chart
this.updatePlaybookChart(sections, bullets);
// Update context selector
this.updateContextSelector();
// Update settings display
document.getElementById('setting-dedup-threshold').value = this.settings.dedupThreshold;
document.getElementById('setting-max-bullets').value = this.settings.maxBullets;
document.getElementById('setting-max-iterations').value = this.settings.maxIterations;
document.getElementById('setting-debug-mode').checked = this.settings.debugMode;
document.getElementById('setting-auto-dedup').checked = this.settings.autoDedup;
document.getElementById('info-contexts').textContent = this.contexts.size;
}
updatePlaybookChart(sections, bullets) {
const chartDiv = document.getElementById('playbook-sections-chart');
if (sections.length === 0) {
chartDiv.innerHTML = '<p style="text-align: center; color: var(--color-text-secondary);">No data to display</p>';
return;
}
const sectionCounts = {};
sections.forEach(section => {
sectionCounts[section] = bullets.filter(b => b.section === section).length;
});
const maxCount = Math.max(...Object.values(sectionCounts));
chartDiv.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 12px;">
${sections.map(section => {
const count = sectionCounts[section];
const percentage = (count / maxCount) * 100;
return `
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 12px;">
<span>${section}</span>
<span style="color: var(--color-text-secondary);">${count}</span>
</div>
<div style="background: var(--color-secondary); height: 8px; border-radius: 4px; overflow: hidden;">
<div style="background: var(--color-primary); width: ${percentage}%; height: 100%; transition: width 0.3s;"></div>
</div>
</div>
`;
}).join('')}
</div>
`;
}
saveSettings() {
this.settings.dedupThreshold = parseFloat(document.getElementById('setting-dedup-threshold').value);
this.settings.maxBullets = parseInt(document.getElementById('setting-max-bullets').value);
this.settings.maxIterations = parseInt(document.getElementById('setting-max-iterations').value);
this.settings.debugMode = document.getElementById('setting-debug-mode').checked;
this.settings.autoDedup = document.getElementById('setting-auto-dedup').checked;
this.log('Settings saved successfully', 'success');
alert('Settings saved!');
}
resetSettings() {
this.settings = {
dedupThreshold: 0.85,
maxBullets: 1000,
maxIterations: 5,
debugMode: false,
autoDedup: true
};
this.updateUI();
this.log('Settings reset to defaults', 'info');
alert('Settings reset to defaults!');
}
updateSessionInfo() {
const startTime = new Date(this.sessionStart).toLocaleString();
document.getElementById('info-session-start').textContent = startTime;
}
log(message, level = 'info') {
const timestamp = new Date().toLocaleTimeString();
const entry = {
timestamp,
message,
level
};
this.operationLog.push(entry);
const logsContainer = document.getElementById('logs-container');
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${level}`;
logEntry.innerHTML = `
<span class="log-timestamp">[${timestamp}]</span>
<span class="log-message">${message}</span>
`;
logsContainer.appendChild(logEntry);
logsContainer.scrollTop = logsContainer.scrollHeight;
if (this.settings.debugMode) {
console.log(`[ACE ${level.toUpperCase()}]`, message);
}
}
clearLogs() {
this.operationLog = [];
document.getElementById('logs-container').innerHTML = '';
this.log('Logs cleared', 'info');
}
exportLogs() {
const text = this.operationLog.map(log =>
`[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`
).join('\n');
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ace-logs-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
this.log('Exported activity logs', 'success');
}
formatDate(timestamp) {
const date = new Date(timestamp);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
}
}
// Initialize ACE Server when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.aceServer = new ACEServer();
});
} else {
window.aceServer = new ACEServer();
}