1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258// 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(/<pre><code/g, '<pre><button class="copy-btn" onclick="copyCode(this)">Copy</button><code');
return html;
}
return escapeHtml(text);
}
function copyCode(btn) {
var code = btn.nextElementSibling;
if (code) {
navigator.clipboard.writeText(code.textContent).then(function() {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500);
});
}
}
// Tool category icon SVGs โ returns inline SVG for each tool category
function toolIcon(toolName) {
if (!toolName) return '';
var n = toolName.toLowerCase();
var s = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
// File/directory operations
if (n.indexOf('file_') === 0 || n.indexOf('directory_') === 0)
return '<svg ' + s + '><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
// Web/fetch
if (n.indexOf('web_') === 0 || n.indexOf('link_') === 0)
return '<svg ' + s + '><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10 15 15 0 0 1 4-10z"/></svg>';
// Shell/exec
if (n.indexOf('shell') === 0 || n.indexOf('exec_') === 0)
return '<svg ' + s + '><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
// Agent operations
if (n.indexOf('agent_') === 0)
return '<svg ' + s + '><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
// Memory/knowledge
if (n.indexOf('memory_') === 0 || n.indexOf('knowledge_') === 0)
return '<svg ' + s + '><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>';
// Cron/schedule
if (n.indexOf('cron_') === 0 || n.indexOf('schedule_') === 0)
return '<svg ' + s + '><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
// Browser/playwright
if (n.indexOf('browser_') === 0 || n.indexOf('playwright_') === 0)
return '<svg ' + s + '><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>';
// Container/docker
if (n.indexOf('container_') === 0 || n.indexOf('docker_') === 0)
return '<svg ' + s + '><path d="M22 12H2"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>';
// Image/media
if (n.indexOf('image_') === 0 || n.indexOf('tts_') === 0)
return '<svg ' + s + '><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
// Hand tools
if (n.indexOf('hand_') === 0)
return '<svg ' + s + '><path d="M18 11V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2 2 2 0 0 0-2 2v6"/><path d="M10 10.5V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-5.7-2.4L3.4 16a2 2 0 0 1 3.2-2.4L8 15"/></svg>';
// Task/collab
if (n.indexOf('task_') === 0)
return '<svg ' + s + '><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>';
// Default โ wrench
return '<svg ' + s + '><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>';
}
// Alpine.js global store
document.addEventListener('alpine:init', function() {
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,
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');
}
});
});
// Main app component
function app() {
return {
page: 'agents',
theme: localStorage.getItem('openfang-theme') || 'light',
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;
// 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();
setInterval(function() { self.pollStatus(); }, 5000);
},
navigate(p) {
this.page = p;
window.location.hash = p;
this.mobileMenuOpen = false;
},
toggleTheme() {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem('openfang-theme', this.theme);
},
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();
}
};
}