aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
822 lines (680 loc) • 27.2 kB
JavaScript
/**
* AIWG Docs - Terminal Enhancements
* Makes docs site work like aiwg.io with log entry pattern and console commands
*/
// Import dbbuilder's search functionality
import { MANIFEST } from './manifest.js';
import { searchContent, filterSections } from './lib/search.js';
// ═══════════════════════════════════════════════════════════════════════════
// Constants
// ═══════════════════════════════════════════════════════════════════════════
const THEME_KEY = 'aiwg-theme';
const THEMES = ['dark', 'light', 'matrix'];
// Author categories for log entry styling
const AUTHOR_MAP = {
'quickstart': 'INSTALL',
'getting-started': 'INSTALL',
'installation': 'INSTALL',
'cli': 'INSTALL',
'agents': 'FEATURES',
'commands': 'FEATURES',
'frameworks': 'FEATURES',
'sdlc': 'FEATURES',
'marketing': 'FEATURES',
'integrations': 'FEATURES',
'changelog': 'RELEASE',
'releases': 'RELEASE',
'announcements': 'RELEASE',
'troubleshooting': 'HELP',
'faq': 'HELP',
'support': 'HELP',
};
// ═══════════════════════════════════════════════════════════════════════════
// Theme Switching
// ═══════════════════════════════════════════════════════════════════════════
function getTheme() {
return document.documentElement.dataset.theme || 'dark';
}
function setTheme(theme) {
if (!THEMES.includes(theme)) theme = 'dark';
document.documentElement.dataset.theme = theme;
localStorage.setItem(THEME_KEY, theme);
updateThemeButton();
}
function cycleTheme() {
const current = getTheme();
const idx = THEMES.indexOf(current);
const next = THEMES[(idx + 1) % THEMES.length];
setTheme(next);
}
function updateThemeButton() {
const btn = document.getElementById('themeToggle');
if (btn) {
btn.textContent = `[${getTheme()}]`;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Log Entry System
// ═══════════════════════════════════════════════════════════════════════════
/**
* Get author category from section ID
*/
function getAuthorFromSection(sectionId) {
if (!sectionId) return 'SYSTEM';
const lower = sectionId.toLowerCase();
for (const [key, author] of Object.entries(AUTHOR_MAP)) {
if (lower.includes(key)) return author;
}
return 'DOCS';
}
/**
* Create a log entry element
*/
function createLogEntry(author, content, title = '') {
const entry = document.createElement('article');
entry.className = 'log-entry';
entry.dataset.author = author;
const header = document.createElement('header');
header.className = 'log-entry__header';
header.innerHTML = `
<span class="author">${author}</span>
<span class="timestamp">${title || new Date().toLocaleTimeString()}</span>
`;
const body = document.createElement('div');
body.className = 'log-entry__body';
if (typeof content === 'string') {
body.innerHTML = `<pre>${escapeHtml(content)}</pre>`;
} else if (content instanceof HTMLElement) {
body.appendChild(content);
} else {
body.innerHTML = content;
}
entry.appendChild(header);
entry.appendChild(body);
return entry;
}
/**
* Append a text log entry to the main panel
*/
function appendLogEntry(author, content, title = '') {
const app = document.getElementById('app');
if (!app) return;
const entry = createLogEntry(author, content, title);
app.appendChild(entry);
entry.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* Escape HTML for safe display
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ═══════════════════════════════════════════════════════════════════════════
// Content Wrapping
// ═══════════════════════════════════════════════════════════════════════════
let lastSectionId = null;
let contentObserver = null;
/**
* Wrap new content in log entry format
*/
function wrapContentInLogEntry(container) {
// Get current section from hash
const sectionId = location.hash.replace('#', '') || 'welcome';
// Don't re-wrap the same section
if (sectionId === lastSectionId) return;
lastSectionId = sectionId;
// Find the section element (dbbuilder creates <section> or content directly)
const section = container.querySelector('section') || container.querySelector('article');
if (!section) return;
// Check if already wrapped
if (section.closest('.log-entry')) return;
// Get title from first heading or section id
const heading = section.querySelector('h1, h2');
const title = heading ? heading.textContent : sectionId;
// Determine author category
const author = getAuthorFromSection(sectionId);
// Create wrapper
const wrapper = document.createElement('article');
wrapper.className = 'log-entry';
wrapper.dataset.author = author;
const header = document.createElement('header');
header.className = 'log-entry__header';
header.innerHTML = `
<span class="author">${author}</span>
<span class="timestamp">${title}</span>
`;
const body = document.createElement('div');
body.className = 'log-entry__body';
// Move all content into the body
while (container.firstChild) {
body.appendChild(container.firstChild);
}
wrapper.appendChild(header);
wrapper.appendChild(body);
container.appendChild(wrapper);
// Scroll to top of new content
container.scrollTop = 0;
wrapper.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* Initialize content observer to wrap loaded content
*/
function initContentObserver() {
const app = document.getElementById('app');
if (!app) return;
contentObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Debounce to let dbbuilder finish rendering
setTimeout(() => wrapContentInLogEntry(app), 50);
break;
}
}
});
contentObserver.observe(app, { childList: true, subtree: false });
// Wrap initial content if present
if (app.children.length > 0) {
setTimeout(() => wrapContentInLogEntry(app), 100);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Console Commands
// ═══════════════════════════════════════════════════════════════════════════
const COMMANDS = {
help: {
description: 'Show available commands',
handler: () => showHelp(),
},
search: {
description: 'Search documentation',
handler: (args) => searchDocs(args.join(' ')),
},
theme: {
description: 'Switch theme: dark | light | matrix',
handler: (args) => {
const theme = args[0] || 'dark';
if (THEMES.includes(theme)) {
setTheme(theme);
appendLogEntry('THEME', `Switched to "${theme}" theme`);
} else {
cycleTheme();
appendLogEntry('THEME', `Switched to "${getTheme()}" theme`);
}
},
},
clear: {
description: 'Clear the display',
handler: () => clearDisplay(),
},
top: {
description: 'Scroll to top',
handler: () => {
scrollToTop();
appendLogEntry('NAV', 'Scrolled to top');
},
},
home: {
description: 'Go to home page',
handler: () => {
location.hash = '#welcome';
},
},
version: {
description: 'Show AIWG version',
handler: () => appendLogEntry('VERSION', 'AIWG Docs v2024.12.5'),
},
github: {
description: 'Open GitHub repo',
handler: () => {
window.open('https://github.com/jmagly/aiwg', '_blank');
appendLogEntry('NAV', 'Opened GitHub repository');
},
},
npm: {
description: 'Open npm package',
handler: () => {
window.open('https://www.npmjs.com/package/aiwg', '_blank');
appendLogEntry('NAV', 'Opened npm package');
},
},
discord: {
description: 'Open Discord community',
handler: () => {
window.open('https://discord.gg/BuAusFMxdA', '_blank');
appendLogEntry('NAV', 'Opened Discord community');
},
},
};
function showHelp() {
const lines = Object.entries(COMMANDS)
.map(([name, { description }]) => ` ${name.padEnd(12)} ${description}`)
.join('\n');
appendLogEntry('HELP', `Available commands:\n\n${lines}\n\nTip: Type a search term to find docs.`);
}
// Store current search query for highlighting
let currentSearchQuery = '';
async function searchDocs(query) {
if (!query.trim()) {
appendLogEntry('SEARCH', 'Usage: search <query>\n\nExample: search agents');
return;
}
appendLogEntry('SEARCH', `Searching for "${query}"... (indexing on first search)`);
currentSearchQuery = query.trim();
try {
// Use dbbuilder's full-text search (searches content, not just titles)
const results = await searchContent(MANIFEST, query);
displaySearchResults(results, query);
} catch (err) {
console.error('Full-text search failed, falling back to title search:', err);
// Fallback to quick filter if full search fails
const results = filterSections(MANIFEST, query);
displaySearchResults(results, query, true);
}
}
function displaySearchResults(results, query, isFallback = false) {
const mode = isFallback ? ' (titles only)' : '';
if (results.length === 0) {
appendLogEntry('SEARCH', `No results for "${query}"${mode}\n\nTry a different search term or browse the navigation.`);
return;
}
// Create clickable results list
const container = document.createElement('div');
container.className = 'search-results';
const header = document.createElement('div');
header.textContent = `Found ${results.length} results for "${query}"${mode}:`;
header.style.marginBottom = 'var(--space-3)';
container.appendChild(header);
const list = document.createElement('ul');
list.className = 'search-results-list';
results.forEach((r, i) => {
const item = document.createElement('li');
const link = document.createElement('a');
link.href = `#${r.id}`;
link.className = 'search-result-link';
link.dataset.section = r.id;
link.dataset.query = query;
const num = document.createElement('span');
num.className = 'search-result-num';
num.textContent = `${(i + 1).toString().padStart(2)}.`;
const title = document.createElement('span');
title.className = 'search-result-title';
title.textContent = r.title;
link.appendChild(num);
link.appendChild(title);
if (r.group && r.group !== r.title) {
const group = document.createElement('span');
group.className = 'search-result-group';
group.textContent = `[${r.group}]`;
link.appendChild(group);
}
// Click handler for highlighting
link.addEventListener('click', (e) => {
e.preventDefault();
navigateWithHighlight(r.id, query);
});
item.appendChild(link);
list.appendChild(item);
});
container.appendChild(list);
appendLogEntry('SEARCH', container, `${results.length} results`);
// Still store for number navigation as fallback
window._searchResults = results.map(r => ({ section: r.id, title: r.title, summary: r.summary }));
window._searchQuery = query;
}
// Navigate to search result with highlighting
function navigateWithHighlight(sectionId, query) {
// Store query in localStorage for dbbuilder's highlight system
if (query) {
localStorage.setItem('docs-toolkit-command-query', query);
}
location.hash = `#${sectionId}`;
}
function clearDisplay() {
const app = document.getElementById('app');
if (!app) return;
// Keep only the current section, remove log history
const entries = app.querySelectorAll('.log-entry');
entries.forEach((entry, index) => {
if (index > 0) entry.remove();
});
appendLogEntry('SYSTEM', 'Display cleared. Current section retained.');
}
async function processCommand(input) {
if (!input) return;
const raw = input.trim();
// Check for number input (search result selection)
const num = parseInt(raw, 10);
if (!isNaN(num) && window._searchResults && window._searchResults.length > 0) {
const idx = num - 1;
if (idx >= 0 && idx < window._searchResults.length) {
const result = window._searchResults[idx];
appendLogEntry('NAV', `Navigating to: ${result.title} (with highlights)`);
navigateWithHighlight(result.section, window._searchQuery);
window._searchResults = null;
window._searchQuery = null;
return;
}
}
const [cmd, ...args] = raw.toLowerCase().split(/\s+/);
if (COMMANDS[cmd]) {
await COMMANDS[cmd].handler(args);
return;
}
// Treat as search query - always show results, don't auto-navigate
await searchDocs(raw);
}
// ═══════════════════════════════════════════════════════════════════════════
// Keyboard Shortcuts
// ═══════════════════════════════════════════════════════════════════════════
let lastKeyTime = 0;
let lastKey = '';
function initKeyboard() {
window.addEventListener('keydown', (e) => {
const target = e.target;
const isTyping = target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;
// Allow these shortcuts even when typing
if (e.key === 'Escape') {
e.preventDefault();
closeDrawer();
const palette = document.getElementById('commandPalette');
if (palette && !palette.hidden) {
return; // Let palette handle its own escape
}
}
// Skip other shortcuts when typing
if (isTyping) return;
const now = Date.now();
switch (e.key) {
case '?':
e.preventDefault();
toggleHelp();
break;
case '/':
e.preventDefault();
focusConsole();
break;
case 't':
e.preventDefault();
cycleTheme();
break;
case 'g':
// Double-tap 'g' to scroll to top
if (lastKey === 'g' && now - lastKeyTime < 300) {
e.preventDefault();
scrollToTop();
}
break;
case 'G':
e.preventDefault();
scrollToBottom();
break;
case 'j':
e.preventDefault();
scrollDown();
break;
case 'k':
e.preventDefault();
scrollUp();
break;
}
lastKey = e.key;
lastKeyTime = now;
});
}
function toggleHelp() {
const helpSection = document.querySelector('.sidebar__section:first-child');
if (helpSection) {
helpSection.classList.toggle('hidden');
}
}
function focusConsole() {
const input = document.getElementById('consoleInput');
if (input) {
input.focus();
input.select();
}
}
function scrollToTop() {
const mainPanel = document.querySelector('.main-panel');
if (mainPanel) {
mainPanel.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function scrollToBottom() {
const mainPanel = document.querySelector('.main-panel');
if (mainPanel) {
mainPanel.scrollTo({ top: mainPanel.scrollHeight, behavior: 'smooth' });
}
}
function scrollDown() {
const mainPanel = document.querySelector('.main-panel');
if (mainPanel) {
mainPanel.scrollBy({ top: 100, behavior: 'smooth' });
}
}
function scrollUp() {
const mainPanel = document.querySelector('.main-panel');
if (mainPanel) {
mainPanel.scrollBy({ top: -100, behavior: 'smooth' });
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Mobile Drawer
// ═══════════════════════════════════════════════════════════════════════════
function initDrawer() {
const toggle = document.getElementById('mobileMenuToggle');
const sidebar = document.getElementById('sidebarPanel');
const overlay = document.getElementById('drawerOverlay');
if (!toggle || !sidebar || !overlay) return;
toggle.addEventListener('click', () => {
const isOpen = sidebar.classList.contains('sidebar--open');
if (isOpen) {
closeDrawer();
} else {
openDrawer();
}
});
overlay.addEventListener('click', closeDrawer);
}
function openDrawer() {
const sidebar = document.getElementById('sidebarPanel');
const overlay = document.getElementById('drawerOverlay');
const toggle = document.getElementById('mobileMenuToggle');
if (sidebar) sidebar.classList.add('sidebar--open');
if (overlay) overlay.classList.add('drawer-overlay--visible');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
document.body.classList.add('drawer-open');
// Focus first link in sidebar
const firstLink = sidebar?.querySelector('a, button');
if (firstLink) firstLink.focus();
}
function closeDrawer() {
const sidebar = document.getElementById('sidebarPanel');
const overlay = document.getElementById('drawerOverlay');
const toggle = document.getElementById('mobileMenuToggle');
if (sidebar) sidebar.classList.remove('sidebar--open');
if (overlay) overlay.classList.remove('drawer-overlay--visible');
if (toggle) toggle.setAttribute('aria-expanded', 'false');
document.body.classList.remove('drawer-open');
}
// ═══════════════════════════════════════════════════════════════════════════
// Console Input
// ═══════════════════════════════════════════════════════════════════════════
function initConsole() {
const input = document.getElementById('consoleInput');
if (!input) return;
// Show welcome on first focus
input.addEventListener('focus', () => {
showWelcomeOnFirstInteraction();
}, { once: true });
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
processCommand(input.value.trim());
input.value = '';
}
});
}
// ═══════════════════════════════════════════════════════════════════════════
// Context Indicator
// ═══════════════════════════════════════════════════════════════════════════
function initContextIndicator() {
const indicator = document.getElementById('contextIndicator');
if (!indicator) return;
function updateContext() {
const hash = location.hash.replace('#', '') || 'welcome';
indicator.textContent = `docs:${hash}`;
}
window.addEventListener('hashchange', updateContext);
updateContext();
}
// ═══════════════════════════════════════════════════════════════════════════
// Section Counter
// ═══════════════════════════════════════════════════════════════════════════
function initSectionCounter() {
const counter = document.getElementById('sectionCount');
if (!counter) return;
function updateCount() {
const nav = document.getElementById('nav');
if (nav) {
const items = nav.querySelectorAll('button[data-section]');
counter.textContent = `${items.length} sections`;
}
}
// Update after nav is built
setTimeout(updateCount, 500);
}
// ═══════════════════════════════════════════════════════════════════════════
// Copy Buttons for Code Blocks
// ═══════════════════════════════════════════════════════════════════════════
function initCopyButtons() {
// Watch for new content and add copy buttons
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
addCopyButtons(node);
}
});
}
}
});
const app = document.getElementById('app');
if (app) {
observer.observe(app, { childList: true, subtree: true });
addCopyButtons(app);
}
}
function addCopyButtons(container) {
const preBlocks = container.querySelectorAll('pre:not(.has-copy-btn)');
preBlocks.forEach((pre) => {
pre.classList.add('has-copy-btn');
const wrapper = document.createElement('div');
wrapper.className = 'code-block';
const code = pre.querySelector('code') || pre;
const text = code.textContent;
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = '[CPY]';
btn.setAttribute('data-copy', text);
btn.addEventListener('click', () => copyToClipboard(btn, text));
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
wrapper.appendChild(btn);
});
}
async function copyToClipboard(btn, text) {
try {
await navigator.clipboard.writeText(text);
btn.textContent = '[OK!]';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = '[CPY]';
btn.classList.remove('copied');
}, 1500);
} catch (err) {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
btn.textContent = '[OK!]';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = '[CPY]';
btn.classList.remove('copied');
}, 1500);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Theme Toggle Button
// ═══════════════════════════════════════════════════════════════════════════
function initThemeToggle() {
const btn = document.getElementById('themeToggle');
if (!btn) return;
btn.addEventListener('click', cycleTheme);
updateThemeButton();
}
// ═══════════════════════════════════════════════════════════════════════════
// Welcome Message (shown on first CLI interaction)
// ═══════════════════════════════════════════════════════════════════════════
const WELCOME_SHOWN_KEY = 'aiwg-welcome-shown';
let welcomeShown = false;
function showWelcomeOnFirstInteraction() {
// Check if already shown this session or previously
if (welcomeShown || sessionStorage.getItem(WELCOME_SHOWN_KEY)) {
return;
}
welcomeShown = true;
sessionStorage.setItem(WELCOME_SHOWN_KEY, 'true');
appendLogEntry('SYSTEM', `AIWG Documentation Terminal
Welcome to the AIWG documentation.
Type "help" for available commands.
Use the navigation panel or search to explore.
Quick commands:
help Show all commands
search Search documentation
theme Switch theme (dark/light/matrix)
clear Clear display
Keyboard shortcuts:
? Toggle help panel
/ Focus search
t Cycle themes
gg Scroll to top
G Scroll to bottom`);
}
// ═══════════════════════════════════════════════════════════════════════════
// Initialize
// ═══════════════════════════════════════════════════════════════════════════
function init() {
// Let dbbuilder handle routing - it already defaults to DEFAULT_SECTION
// Don't interfere with hash navigation here
// Theme is already applied via inline script in <head>
updateThemeButton();
initKeyboard();
initDrawer();
initConsole();
initContextIndicator();
initSectionCounter();
initCopyButtons();
initThemeToggle();
initContentObserver();
// Remove loading state
document.body.classList.remove('loading');
document.body.classList.add('loaded');
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}