// OpenFang App — Alpine.js init, hash router, global store 'use strict'; // Marked.js configuration if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true, highlight: function(code, lang) { if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch(e) {} } return code; } }); } function escapeHtml(text) { var div = document.createElement('div'); div.textContent = text || ''; return div.innerHTML; } function renderMarkdown(text) { if (!text) return ''; if (typeof marked !== 'undefined') { var html = marked.parse(text); // Add copy buttons to code blocks html = html.replace(/
';
// Web/fetch
if (n.indexOf('web_') === 0 || n.indexOf('link_') === 0)
return '';
// Shell/exec
if (n.indexOf('shell') === 0 || n.indexOf('exec_') === 0)
return '';
// Agent operations
if (n.indexOf('agent_') === 0)
return '';
// Memory/knowledge
if (n.indexOf('memory_') === 0 || n.indexOf('knowledge_') === 0)
return '';
// Cron/schedule
if (n.indexOf('cron_') === 0 || n.indexOf('schedule_') === 0)
return '';
// Browser/playwright
if (n.indexOf('browser_') === 0 || n.indexOf('playwright_') === 0)
return '';
// Container/docker
if (n.indexOf('container_') === 0 || n.indexOf('docker_') === 0)
return '';
// Image/media
if (n.indexOf('image_') === 0 || n.indexOf('tts_') === 0)
return '';
// Hand tools
if (n.indexOf('hand_') === 0)
return '';
// Task/collab
if (n.indexOf('task_') === 0)
return '';
// Default — wrench
return '';
}
// Alpine.js global store
document.addEventListener('alpine:init', function() {
// Restore saved API key on load
var savedKey = localStorage.getItem('openfang-api-key');
if (savedKey) OpenFangAPI.setAuthToken(savedKey);
Alpine.store('app', {
agents: [],
connected: false,
booting: true,
wsConnected: false,
connectionState: 'connected',
lastError: '',
version: '0.1.0',
agentCount: 0,
pendingAgent: null,
focusMode: localStorage.getItem('openfang-focus') === 'true',
showOnboarding: false,
showAuthPrompt: false,
toggleFocusMode() {
this.focusMode = !this.focusMode;
localStorage.setItem('openfang-focus', this.focusMode);
},
async refreshAgents() {
try {
var agents = await OpenFangAPI.get('/api/agents');
this.agents = Array.isArray(agents) ? agents : [];
this.agentCount = this.agents.length;
} catch(e) { /* silent */ }
},
async checkStatus() {
try {
var s = await OpenFangAPI.get('/api/status');
this.connected = true;
this.booting = false;
this.lastError = '';
this.version = s.version || '0.1.0';
this.agentCount = s.agent_count || 0;
} catch(e) {
this.connected = false;
this.lastError = e.message || 'Unknown error';
console.warn('[OpenFang] Status check failed:', e.message);
}
},
async checkOnboarding() {
if (localStorage.getItem('openfang-onboarded')) return;
try {
var config = await OpenFangAPI.get('/api/config');
var apiKey = config && config.api_key;
var noKey = !apiKey || apiKey === 'not set' || apiKey === '';
if (noKey && this.agentCount === 0) {
this.showOnboarding = true;
}
} catch(e) {
// If config endpoint fails, still show onboarding if no agents
if (this.agentCount === 0) this.showOnboarding = true;
}
},
dismissOnboarding() {
this.showOnboarding = false;
localStorage.setItem('openfang-onboarded', 'true');
},
async checkAuth() {
try {
// Use a protected endpoint (not in the public allowlist) to detect
// whether the server requires an API key.
await OpenFangAPI.get('/api/tools');
this.showAuthPrompt = false;
} catch(e) {
if (e.message && (e.message.indexOf('Not authorized') >= 0 || e.message.indexOf('401') >= 0 || e.message.indexOf('Missing Authorization') >= 0 || e.message.indexOf('Unauthorized') >= 0)) {
// Only show prompt if we don't already have a saved key
var saved = localStorage.getItem('openfang-api-key');
if (saved) {
// Saved key might be stale — clear it and show prompt
OpenFangAPI.setAuthToken('');
localStorage.removeItem('openfang-api-key');
}
this.showAuthPrompt = true;
}
}
},
submitApiKey(key) {
if (!key || !key.trim()) return;
OpenFangAPI.setAuthToken(key.trim());
localStorage.setItem('openfang-api-key', key.trim());
this.showAuthPrompt = false;
this.refreshAgents();
},
clearApiKey() {
OpenFangAPI.setAuthToken('');
localStorage.removeItem('openfang-api-key');
}
});
});
// Main app component
function app() {
return {
page: 'agents',
themeMode: localStorage.getItem('openfang-theme-mode') || 'system',
theme: (() => {
var mode = localStorage.getItem('openfang-theme-mode') || 'system';
if (mode === 'system') return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
return mode;
})(),
sidebarCollapsed: localStorage.getItem('openfang-sidebar') === 'collapsed',
mobileMenuOpen: false,
connected: false,
wsConnected: false,
version: '0.1.0',
agentCount: 0,
get agents() { return Alpine.store('app').agents; },
init() {
var self = this;
// Listen for OS theme changes (only matters when mode is 'system')
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (self.themeMode === 'system') {
self.theme = e.matches ? 'dark' : 'light';
}
});
// Hash routing
var validPages = ['overview','agents','sessions','approvals','workflows','scheduler','channels','skills','hands','analytics','logs','settings','wizard'];
var pageRedirects = {
'chat': 'agents',
'templates': 'agents',
'triggers': 'workflows',
'cron': 'scheduler',
'schedules': 'scheduler',
'memory': 'sessions',
'audit': 'logs',
'security': 'settings',
'peers': 'settings',
'migration': 'settings',
'usage': 'analytics',
'approval': 'approvals'
};
function handleHash() {
var hash = window.location.hash.replace('#', '') || 'agents';
if (pageRedirects[hash]) {
hash = pageRedirects[hash];
window.location.hash = hash;
}
if (validPages.indexOf(hash) >= 0) self.page = hash;
}
window.addEventListener('hashchange', handleHash);
handleHash();
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl+K — focus agent switch / go to agents
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
self.navigate('agents');
}
// Ctrl+N — new agent
if ((e.ctrlKey || e.metaKey) && e.key === 'n' && !e.shiftKey) {
e.preventDefault();
self.navigate('agents');
}
// Ctrl+Shift+F — toggle focus mode
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'F') {
e.preventDefault();
Alpine.store('app').toggleFocusMode();
}
// Escape — close mobile menu
if (e.key === 'Escape') {
self.mobileMenuOpen = false;
}
});
// Connection state listener
OpenFangAPI.onConnectionChange(function(state) {
Alpine.store('app').connectionState = state;
});
// Initial data load
this.pollStatus();
Alpine.store('app').checkOnboarding();
Alpine.store('app').checkAuth();
setInterval(function() { self.pollStatus(); }, 5000);
},
navigate(p) {
this.page = p;
window.location.hash = p;
this.mobileMenuOpen = false;
},
setTheme(mode) {
this.themeMode = mode;
localStorage.setItem('openfang-theme-mode', mode);
if (mode === 'system') {
this.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} else {
this.theme = mode;
}
},
toggleTheme() {
var modes = ['light', 'system', 'dark'];
var next = modes[(modes.indexOf(this.themeMode) + 1) % modes.length];
this.setTheme(next);
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
localStorage.setItem('openfang-sidebar', this.sidebarCollapsed ? 'collapsed' : 'expanded');
},
async pollStatus() {
var store = Alpine.store('app');
await store.checkStatus();
await store.refreshAgents();
this.connected = store.connected;
this.version = store.version;
this.agentCount = store.agentCount;
this.wsConnected = OpenFangAPI.isWsConnected();
}
};
}