c9ai
Version:
Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration
6,178 lines โข 260 kB
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>c9ai - AI-Powered Development Assistant</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
--bg: #000000;
--panel: #121212;
--muted: #1a1a1a;
--text: #ffffff;
--sub: #b3b3b3;
--primary: #7c83ff;
--primary-ink: #ffffff;
--bubble: #1a1a1a;
--user: #7c83ff;
--danger: #ff4d6d;
--ok: #2ecc71;
--border: #333333;
--sidebar: #000000;
}
/* Light mode - clean and minimal like your test page */
[data-theme="light"] {
--bg: #ffffff;
--panel: #ffffff;
--muted: #f5f5f5;
--text: #000000;
--sub: #666666;
--primary: #7c83ff;
--primary-ink: #ffffff;
--bubble: #f9f9f9;
--user: #7c83ff;
--danger: #ff4d6d;
--ok: #2ecc71;
--border: #dddddd;
--sidebar: #fafafa;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; padding: 0; }
body {
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--text);
background: var(--bg);
display: flex;
min-height: 100vh;
max-height: 100vh;
overflow: hidden;
}
/* Sidebar Navigation */
.sidebar {
width: 280px;
background: var(--sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
.sidebar.collapsed {
transform: translateX(-100%);
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
}
.logo {
font-size: 18px;
font-weight: 700;
background: linear-gradient(45deg, var(--primary), #b794f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-section {
padding: 16px 12px 8px 12px;
}
.nav-section.conversations {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.nav-section-title {
font-size: 12px;
font-weight: 600;
color: var(--sub);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 4px;
font-size: 13px;
}
.nav-item:hover {
background: var(--muted);
}
.nav-item.active {
background: var(--primary);
color: var(--primary-ink);
}
.conversations-list {
flex: 1;
min-height: 0; /* critical for flex scrolling */
overflow-y: auto;
padding: 0 12px;
}
.conversation-item {
display: flex;
flex-direction: column;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 4px;
border: 1px solid transparent;
}
.conversation-item:hover {
background: var(--muted);
border-color: var(--border);
}
.conversation-item.active {
background: var(--bubble);
border-color: var(--primary);
}
.conversation-title {
font-size: 13px;
font-weight: 500;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-preview {
font-size: 11px;
color: var(--sub);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-time {
font-size: 10px;
color: var(--sub);
margin-top: 4px;
}
/* Main Content Area */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
/* allow inner flex children to size/scroll properly */
min-height: 0;
}
.main-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--panel);
}
.view-content {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
.provider-switcher {
display: flex;
background: var(--muted);
border-radius: 8px;
padding: 2px;
gap: 2px;
}
.pill {
padding: 6px 12px;
border: none;
background: var(--muted);
color: var(--sub);
border-radius: 20px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid var(--border);
}
.pill.active {
background: #ff6b35;
color: white;
border-color: #ff6b35;
box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.3), 0 2px 8px rgba(255, 107, 53, 0.2);
font-weight: 600;
transform: translateY(-1px);
}
.pill:hover:not(.active) {
background: var(--panel);
border-color: var(--primary);
color: var(--text);
transform: translateY(-0.5px);
}
/* Chat Area */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
height: 100%;
}
.chat-messages {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 85%;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.message.user .message-avatar {
background: var(--user);
color: var(--primary-ink);
}
.message.assistant .message-avatar {
background: var(--bubble);
color: var(--text);
}
.message-content {
background: var(--bubble);
padding: 12px 16px;
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
white-space: pre-wrap;
word-break: break-word;
}
.message-content li { list-style: inside disc; margin-left: 1rem; margin-bottom: 6px; }
/* RSS card + thumbnail styles */
.message-content .rss-item { background: var(--bubble); border: 1px solid var(--border); border-radius: 12px; padding: 12px; margin: 12px 0; }
.message-content .rss-thumb { width: 100%; max-width: 100%; height: auto; border-radius: 10px; display: block; margin: 6px 0 10px 0; object-fit: cover; }
.message-content .rss-title { font-weight: 700; margin: 6px 0 4px 0; color: var(--text); }
.message-content .rss-desc { color: var(--text); opacity: 0.9; line-height: 1.5; }
.message-content .rss-meta-line { margin: 2px 0 8px 0; font-size: 13px; color: var(--sub); }
.message-content .rss-meta { margin-top: 8px; font-size: 13px; color: var(--sub); display: flex; gap: 10px; align-items: center; }
.message-content .rss-meta a.pill-read { text-decoration: none; border: 1px solid var(--border); padding: 4px 10px; border-radius: 999px; background: var(--muted); color: var(--text); }
.message.user .message-content {
background: var(--user);
color: var(--primary-ink);
}
.message-time {
font-size: 10px;
color: var(--sub);
margin-top: 4px;
}
/* Input Area */
.chat-input-container {
position: sticky;
bottom: 0;
padding: 12px 20px;
border-top: 1px solid var(--border);
background: var(--panel);
flex-shrink: 0;
z-index: 10;
margin-top: auto;
}
/* Small footer bar pinned at the very bottom */
.app-footer {
background: var(--bg);
color: var(--sub);
font-size: 11px;
border-top: 1px solid var(--border);
padding: 6px 12px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
/* Debug Panel Styles */
.debug-panel {
position: fixed;
top: 20px;
right: 20px;
width: 400px;
max-height: 600px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
z-index: 1000;
display: none;
flex-direction: column;
}
.debug-header {
padding: 12px 16px;
background: var(--muted);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 8px 8px 0 0;
}
.debug-title {
font-weight: 600;
font-size: 14px;
color: var(--text);
}
.debug-close {
background: none;
border: none;
color: var(--sub);
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 16px;
}
.debug-close:hover {
background: var(--border);
color: var(--text);
}
.debug-content {
flex: 1;
overflow-y: auto;
padding: 12px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.debug-entry {
margin-bottom: 8px;
padding: 6px 8px;
border-radius: 4px;
border-left: 3px solid var(--border);
}
.debug-entry.info {
background: rgba(124, 131, 255, 0.1);
border-left-color: var(--primary);
}
.debug-entry.success {
background: rgba(46, 204, 113, 0.1);
border-left-color: var(--ok);
}
.debug-entry.warning {
background: rgba(255, 193, 7, 0.1);
border-left-color: #ffc107;
}
.debug-entry.error {
background: rgba(255, 77, 109, 0.1);
border-left-color: var(--danger);
}
.debug-entry.tool {
background: rgba(156, 39, 176, 0.1);
border-left-color: #9c27b0;
}
.debug-timestamp {
color: var(--sub);
font-size: 10px;
margin-right: 8px;
}
.debug-icon {
margin-right: 6px;
}
.debug-message {
color: var(--text);
}
.debug-toggle {
position: fixed;
bottom: 20px;
left: 20px;
right: auto;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--primary);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 999;
transition: all 0.3s ease;
}
.debug-toggle:hover {
transform: scale(1.1);
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
.debug-toggle.active {
background: var(--ok);
}
/* Progress indicator for executive requests */
.progress-bar {
height: 3px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
margin: 8px 0;
}
.progress-fill {
height: 100%;
background: var(--primary);
transition: width 0.3s ease;
border-radius: 2px;
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.debug-panel {
width: 300px;
right: 10px;
top: 10px;
max-height: 400px;
}
.debug-toggle {
width: 45px;
height: 45px;
bottom: 15px;
right: 15px;
}
.sidebar.show {
transform: translateX(0);
}
}
.chat-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
max-width: 1000px;
margin: 0 auto;
}
.chat-input {
flex: 1;
background: var(--muted);
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px 14px;
color: var(--text);
font-family: inherit;
font-size: 14px;
resize: none;
min-height: 36px;
max-height: 80px;
line-height: 1.4;
}
.chat-input:focus {
outline: none;
border-color: var(--primary);
}
.chat-input::placeholder {
color: var(--sub);
}
.input-actions {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.btn {
background: var(--primary);
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
transition: all 0.2s;
white-space: nowrap;
}
.btn:hover {
background: #6c7ce0;
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn.secondary {
background: var(--muted);
color: var(--text);
border: 1px solid var(--border);
}
.btn.secondary:hover {
background: var(--border);
}
.btn.ghost {
background: transparent;
border: 1px solid var(--border);
}
/* Mobile Toggle */
.mobile-toggle {
display: none;
background: none;
border: none;
color: var(--text);
cursor: pointer;
font-size: 18px;
}
/* Welcome Screen */
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
}
.welcome-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 12px;
background: linear-gradient(45deg, var(--primary), #b794f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.welcome-subtitle {
font-size: 16px;
color: var(--sub);
margin-bottom: 32px;
max-width: 500px;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
max-width: 600px;
width: 100%;
}
.quick-action {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.quick-action:hover {
border-color: var(--primary);
transform: translateY(-2px);
}
.quick-action-icon {
font-size: 24px;
margin-bottom: 8px;
}
.quick-action-title {
font-weight: 600;
margin-bottom: 4px;
}
.quick-action-desc {
font-size: 12px;
color: var(--sub);
}
/* Modal Styles */
.modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-content {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
width: 100%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-body {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text);
margin-bottom: 6px;
}
.form-input, .form-select {
width: 100%;
background: var(--muted);
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-family: inherit;
font-size: 14px;
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: var(--primary);
}
.form-hint {
font-size: 12px;
color: var(--sub);
margin-top: 4px;
}
/* Status indicator */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--sub);
margin-left: 8px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ok);
}
.status-dot.error { background: var(--danger); }
.status-dot.loading {
background: var(--primary);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
position: absolute;
left: 0;
top: 0;
height: 100%;
z-index: 100;
}
.mobile-toggle {
display: block;
}
.provider-switcher {
display: none;
}
}
/* Code action buttons styling */
.code-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.save-code-btn, .run-code-btn, .copy-code-btn {
background: var(--muted);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.save-code-btn:hover {
background: var(--primary);
color: var(--primary-ink);
border-color: var(--primary);
transform: translateY(-1px);
}
.run-code-btn:hover {
background: var(--ok);
color: white;
border-color: var(--ok);
transform: translateY(-1px);
}
.copy-code-btn:hover {
background: var(--border);
transform: translateY(-1px);
}
.save-code-btn:active, .run-code-btn:active, .copy-code-btn:active {
transform: translateY(0);
}
/* Theme toggle button */
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
width: 50px;
height: 28px;
background: var(--muted);
border: 1px solid var(--border);
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
padding: 2px;
z-index: 1000;
transition: all 0.3s ease;
}
.theme-toggle:hover {
background: var(--border);
}
.theme-toggle-slider {
width: 24px;
height: 24px;
background: var(--primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: all 0.3s ease;
color: white;
}
[data-theme="light"] .theme-toggle-slider {
transform: translateX(22px);
}
/* Make header links readable in both themes */
.message .message-content h1 a,
.message .message-content h2 a,
.message .message-content h3 a {
color: var(--text);
text-decoration: none;
}
.message .message-content h1 a:hover,
.message .message-content h2 a:hover,
.message .message-content h3 a:hover {
text-decoration: underline;
}
/* Smooth transitions for theme switching */
body, .sidebar, .main-content, .chat-messages, .chat-input, .btn, .nav-item, .debug-panel {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
</style>
<style>
/* Context menu */
.context-menu { position: fixed; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,0.4); display: none; z-index: 1200; min-width: 160px; }
.context-menu .item { padding: 8px 12px; cursor: pointer; font-size: 13px; border-bottom: 1px solid var(--border); }
.context-menu .item:last-child { border-bottom: none; }
.context-menu .item:hover { background: var(--muted); }
</style>
</head>
<body>
<style>
/* Floating chat panel */
.float-chat-btn { position: fixed; right: 20px; bottom: 20px; z-index: 1100; }
.float-chat-panel { position: fixed; right: 20px; bottom: 70px; width: 360px; max-height: 60vh; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); display: none; flex-direction: column; overflow: hidden; z-index: 1100; }
.float-chat-panel.expanded { left: 20px; right: 20px; top: 70px; bottom: 70px; width: auto; max-height: none; }
.float-chat-header { padding: 10px 12px; border-bottom: 1px solid var(--border); display:flex; justify-content: space-between; align-items:center; font-weight: 600; gap: 8px; }
.float-chat-header .actions { display:flex; gap:8px; }
.float-chat-messages { padding: 10px 12px; overflow-y: auto; flex: 1; font-size: 13px; }
.float-chat-input { display:flex; gap:6px; padding: 8px; border-top:1px solid var(--border); }
.float-chat-input input { flex:1; }
.bubble { margin: 6px 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bubble); }
.bubble.user { background: var(--muted); }
</style>
<!-- Theme Toggle Button -->
<div class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode">
<div class="theme-toggle-slider">๐</div>
</div>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">๐ค c9ai</div>
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Ready</span>
</div>
</div>
<div class="nav-section">
<div class="nav-section-title">Navigation</div>
<div class="nav-item active" data-view="chat">
<span>๐ฌ</span> Smart Chat
</div>
<div class="nav-item" data-view="help">
<span>โ</span> Help
</div>
<div class="nav-item" data-view="settings">
<span>โ๏ธ</span> Settings
</div>
<div class="nav-item" data-view="system">
<span>๐ ๏ธ</span> System
</div>
<div class="nav-item" data-view="calculators">
<span>๐งฎ</span> Calculators
</div>
<div class="nav-item" data-view="workflows">
<span>๐</span> Workflows
</div>
<div class="nav-item" data-view="terminal">
<span>๐ป</span> Terminal
</div>
</div>
<div class="nav-section conversations">
<div class="nav-section-title">Recent Conversations</div>
<div class="conversations-list" id="conversationsList">
<!-- Conversations will be populated here -->
</div>
</div>
<div class="nav-section">
<button class="btn secondary" id="newConversationBtn" style="width: 100%;" onclick="startNewConversation()">
โ New Conversation
</button>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="main-header">
<div class="header-left">
<button class="mobile-toggle" onclick="toggleSidebar()">โฐ</button>
<h2 id="viewTitle">AI Chat</h2>
</div>
<div class="header-right"></div>
</div>
<!-- Smart Chat View -->
<div class="view-content" id="chatView">
<div class="chat-container">
<div class="chat-messages" id="chatMessages">
<div class="welcome-screen" id="welcomeScreen">
<h1 class="welcome-title">Welcome to Smart Chat</h1>
<p class="welcome-subtitle">Enhanced execution environment with XML-Lisp transpiler. Create functions, run calculations, and build your personal function library.</p>
<div class="quick-actions">
<div class="quick-action" onclick="startNewConversation()" style="border: 2px solid var(--primary); background: var(--primary); color: var(--primary-ink);">
<div class="quick-action-icon">๐ฌ</div>
<div class="quick-action-title">Start Chatting</div>
<div class="quick-action-desc">Begin a new conversation</div>
</div>
<div class="quick-action" onclick="showView('calculators')">
<div class="quick-action-icon">๐งฎ</div>
<div class="quick-action-title">Open Calculators</div>
<div class="quick-action-desc">Deterministic business calculators</div>
</div>
<div class="quick-action" onclick="showView('workflows')">
<div class="quick-action-icon">๐</div>
<div class="quick-action-title">Open Workflows</div>
<div class="quick-action-desc">Run or build task pipelines</div>
</div>
<div class="quick-action" onclick="loadQuickAction('Help me write a professional email')">
<div class="quick-action-icon">โ๏ธ</div>
<div class="quick-action-title">Write Email</div>
<div class="quick-action-desc">Craft professional messages</div>
</div>
<div class="quick-action" onclick="loadQuickAction('Create a social media post')">
<div class="quick-action-icon">๐ฑ</div>
<div class="quick-action-title">Social Content</div>
<div class="quick-action-desc">Generate engaging posts</div>
</div>
<div class="quick-action" onclick="loadQuickAction('Plan my project or task')">
<div class="quick-action-icon">๐</div>
<div class="quick-action-title">Project Planning</div>
<div class="quick-action-desc">Organize and break down tasks</div>
</div>
<div class="quick-action" onclick="loadQuickAction('Help me learn something new')">
<div class="quick-action-icon">๐</div>
<div class="quick-action-title">Learn & Explore</div>
<div class="quick-action-desc">Get explanations and tutorials</div>
</div>
</div>
</div>
</div>
<div class="chat-input-container">
<div class="chat-input-wrapper">
<textarea
class="chat-input"
id="chatInput"
placeholder="Chat naturally or use @calc, @todo, @email, @search for tasks... (Ctrl+Enter to send)"
rows="1"
></textarea>
<div class="input-actions">
<button class="btn" id="sendBtn" onclick="sendMessage()">Send</button>
<button class="btn secondary" onclick="switchToAgent()">Agent</button>
<label style="display:flex;gap:4px;align-items:center;font-size:11px;color:var(--sub)">
<input id="toAgent" type="checkbox" />
<span>Tools</span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Agent View -->
<div class="view-content" id="agentView" style="display: none;">
<div style="padding: 20px; max-width: 1000px; margin: 0 auto; min-height: 100%; box-sizing: border-box;">
<h2 style="margin-bottom: 20px;">๐ Agentic Tools</h2>
<div style="background: var(--bubble); padding: 20px; border-radius: 12px; margin-bottom: 24px; border: 1px solid var(--border);">
<h3 style="margin-top: 0; color: var(--primary);">๐ฏ Task-Focused AI Interface</h3>
<p style="margin-bottom: 16px; line-height: 1.6;">
The Agentic Tools interface is designed for complex task execution with local AI models.
It provides a dedicated environment for tool-based operations, file management, and automation.
</p>
<div style="background: var(--muted); padding: 12px; border-radius: 6px; font-size: 13px; color: var(--sub);">
<strong>โจ Key Features:</strong> Local AI execution โข Tool-based commands โข Task persistence โข File operations โข API integrations
</div>
</div>
<div class="form-group">
<h3>๐ Launch Agentic Interface</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px;">
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
<h4 style="margin-top: 0; margin-bottom: 8px;">๐ ๏ธ Main Agentic Tools</h4>
<p style="font-size: 12px; color: var(--sub); margin-bottom: 12px;">
Dedicated interface for task execution and tool operations
</p>
<button class="btn primary" onclick="window.open('http://localhost:8787', '_blank')" style="width: 100%;">
Launch Agentic UI (Port 8787)
</button>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
<h4 style="margin-top: 0; margin-bottom: 8px;">โก Quick Test Interface</h4>
<p style="font-size: 12px; color: var(--sub); margin-bottom: 12px;">
Current embedded agent interface for testing
</p>
<button class="btn secondary" onclick="showEmbeddedAgent()" style="width: 100%;">
Show Embedded Agent
</button>
</div>
</div>
</div>
<div class="form-group">
<h3>๐ง Configuration</h3>
<div style="background: var(--muted); padding: 15px; border-radius: 8px;">
<p style="margin: 0 0 8px 0; font-size: 13px;">
<strong>Provider Settings:</strong> AI provider selection is managed in the main chat interface.
</p>
<p style="margin: 0; font-size: 13px; color: var(--sub);">
<strong>Communication:</strong> The agentic tools interface will communicate with this hub via shared task files.
</p>
</div>
</div>
<div id="embeddedAgentContainer" style="display: none; margin-top: 24px;">
<h3>Embedded Agent Interface</h3>
<div style="border: 1px solid var(--border); border-radius: 8px; height: 500px;">
<iframe id="embeddedAgentFrame" src="" style="width: 100%; height: 100%; border: none; border-radius: 8px;"></iframe>
</div>
<button class="btn secondary" onclick="hideEmbeddedAgent()" style="margin-top: 8px;">
Hide Embedded Agent
</button>
</div>
</div>
</div>
<!-- CLI Access View -->
<div class="view-content" id="cliView" style="display: none;">
<div style="padding: 20px; max-width: 1000px; margin: 0 auto; min-height: 100%; box-sizing: border-box;">
<h2 style="margin-bottom: 20px;">โจ๏ธ CLI Access</h2>
<div class="form-group">
<h3>Available CLI Commands</h3>
<div style="background: var(--muted); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div style="font-family: monospace; font-size: 14px; line-height: 1.6;">
<div style="margin-bottom: 8px;"><strong># Interactive Mode</strong></div>
<div style="color: var(--primary);">c9ai</div>
<div style="margin: 8px 0; opacity: 0.7;"># Launches interactive shell with your current provider settings</div>
<div style="margin: 16px 0 8px 0;"><strong># Direct Agent Commands</strong></div>
<div style="color: var(--primary);">c9ai agent "list files in current directory"</div>
<div style="color: var(--primary);">c9ai-agent "create a Python script for fibonacci"</div>
<div style="margin: 8px 0; opacity: 0.7;"># Execute specific tasks with full tool access</div>
<div style="margin: 16px 0 8px 0;"><strong># Provider Management</strong></div>
<div style="color: var(--primary);">c9ai models list</div>
<div style="color: var(--primary);">c9ai switch claude-hybrid</div>
<div style="margin: 8px 0; opacity: 0.7;"># List and switch between providers</div>
<div style="margin: 16px 0 8px 0;"><strong># Local Stack</strong></div>
<div style="color: var(--primary);">c9ai stack</div>
<div style="color: var(--primary);">c9ai stack --model ./my-model.gguf --port 8081</div>
<div style="margin: 8px 0; opacity: 0.7;"># Start local llama.cpp server + Agent API</div>
</div>
</div>
</div>
<div class="form-group">
<h3>Quick CLI Launcher</h3>
<p style="margin-bottom: 15px; color: var(--sub);">Launch CLI with your current UI provider settings</p>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
<button class="btn" onclick="launchCLI('interactive')" style="display: flex; align-items: center; gap: 8px;">
<span>๐ฅ๏ธ</span> Interactive Mode
</button>
<button class="btn" onclick="launchCLI('agent')" style="display: flex; align-items: center; gap: 8px;">
<span>๐ค</span> Agent Mode
</button>
<button class="btn" onclick="launchCLI('models')" style="display: flex; align-items: center; gap: 8px;">
<span>๐</span> List Models
</button>
<button class="btn" onclick="launchCLI('stack')" style="display: flex; align-items: center; gap: 8px;">
<span>๐</span> Start Stack
</button>
</div>
</div>
<div class="form-group">
<h3>CLI Installation</h3>
<p style="margin-bottom: 15px; color: var(--sub);">Install C9AI CLI globally for system-wide access</p>
<div style="background: var(--muted); padding: 15px; border-radius: 8px;">
<div style="font-family: monospace; font-size: 14px; line-height: 1.6;">
<div style="margin-bottom: 8px;"><strong># Install globally</strong></div>
<div style="color: var(--primary);">npm install -g .</div>
<div style="margin: 8px 0; opacity: 0.7;"># Run from project directory</div>
<div style="margin: 16px 0 8px 0;"><strong># Or use scripts</strong></div>
<div style="color: var(--primary);">./install.sh</div>
<div style="color: var(--primary);">install-windows.bat</div>
<div style="margin: 8px 0; opacity: 0.7;"># Platform-specific installers</div>
</div>
</div>
</div>
<div class="form-group">
<h3>CLI Sessions</h3>
<p style="margin-bottom: 15px; color: var(--sub);">Track and view CLI command history</p>
<div id="cliSessions" style="background: var(--muted); padding: 15px; border-radius: 8px; min-height: 200px;">
<div style="text-align: center; color: var(--sub); padding: 40px;">
No CLI sessions tracked yet. CLI sessions will appear here once you start using the CLI.
</div>
</div>
<div style="margin-top: 15px; display: flex; gap: 12px;">
<button class="btn secondary" onclick="refreshCLISessions()">๐ Refresh Sessions</button>
<button class="btn secondary" onclick="clearCLISessions()">๐๏ธ Clear History</button>
</div>
</div>
</div>
</div>
<!-- Tools View -->
<div class="view-content" id="toolsView" style="display: none;">
<div style="padding: 20px; max-width: 1200px; margin: 0 auto; min-height: 100%; box-sizing: border-box;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">๐ง Tool Package Manager</h2>
<div style="display: flex; gap: 12px;">
<button class="btn secondary" onclick="refreshToolData()">๐ Refresh</button>
<button class="btn secondary" onclick="showBatchInstall()">๐ฆ Batch Install</button>
</div>
</div>
<!-- Tool Source Tabs -->
<div style="display: flex; gap: 8px; margin-bottom: 20px;">
<button class="btn pill active" data-source="curated" onclick="switchToolSource('curated')">๐ Curated Tools</button>
<button class="btn pill" data-source="packages" onclick="switchToolSource('packages')">๐ฆ System Packages</button>
<button class="btn pill" data-source="installed" onclick="switchToolSource('installed')">โ
Installed</button>
</div>
<!-- Tool Statistics -->
<div id="toolStats" style="display: flex; gap: 15px; margin-bottom: 25px;">
<div style="background: var(--panel); padding: 15px; border-radius: 8px; flex: 1; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: bold; color: var(--primary);" id="totalTools">-</div>
<div style="font-size: 12px; color: var(--sub);">Total Tools</div>
</div>
<div style="background: var(--panel); padding: 15px; border-radius: 8px; flex: 1; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: bold; color: var(--ok);" id="installedTools">-</div>
<div style="font-size: 12px; color: var(--sub);">Installed</div>
</div>
<div style="background: var(--panel); padding: 15px; border-radius: 8px; flex: 1; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: bold; color: var(--primary);" id="availableTools">-</div>
<div style="font-size: 12px; color: var(--sub);">Available</div>
</div>
<div style="background: var(--panel); padding: 15px; border-radius: 8px; flex: 1; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: bold; color: var(--sub);" id="builtinTools">-</div>
<div style="font-size: 12px; color: var(--sub);">Built-in</div>
</div>
</div>
<!-- Category Filter -->
<div style="margin-bottom: 20px;">
<div style="display: flex; gap: 8px; flex-wrap: wrap;" id="categoryFilters">
<button class="btn pill active" data-category="all">All Tools</button>
<button class="btn pill" data-category="available">Available</button>
<button class="btn pill" data-category="installed">Installed</button>
</div>
</div>
<!-- Package Search (visible when packages tab is active) -->
<div id="packageSearch" style="display: none; margin-bottom: 25px;">
<div style="display: flex; gap: 12px; margin-bottom: 15px;">
<div style="flex: 1; position: relative;">
<input
type="text"
id="packageSearchInput"
placeholder="Search for packages (e.g., pandoc, ffmpeg, python packages...)"
style="width: 100%; padding: 12px 16px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); color: var(--text);"
onkeydown="if(event.key === 'Enter') searchPackages()"
/>
</div>
<button class="btn primary" onclick="searchPackages()">๐ Search</button>
</div>
<!-- Package Manager Status -->
<div id="packageManagerStatus" style="display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap;">
Loading package managers...
</div>
<div id="searchResults" style="display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0;">Search Results</h3>
<div id="searchResultsCount" style="color: var(--sub); font-size: 14px;"></div>
</div>
<div id="packageResults" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 15px;">
<!-- Search results will be populated here -->
</div>
</div>
</div>
<!-- Tool Grid -->
<div id="toolGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px;">
<!-- Tools will be populated here -->
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
Loading tools...
</div>
</div>
</div>
</div>
<!-- Settings View -->
<div class="view-content" id="settingsView" style="display: none;">
<div style="padding: 20px; max-width: 600px; margin: 0 auto; min-height: 100%; box-sizing: border-box;">
<h2 style="margin-bottom: 20px;">Settings</h2>
<div class="form-group">
<label class="form-label">Default Provider</label>
<select class="form-select" id="settingsProvider">
<optgroup label="๐ Local Models">
<option value="llamacpp">Llama.cpp (Local)</option>
<option value="ollama">Ollama (Local)</option>
</optgroup>
<optgroup label="โ๏ธ Cloud Models (No Tools)">
<option value="claude">Claude AI</option>
<option value="gemini">Gemini AI</option>
<option value="openai">OpenAI GPT</option>
<option value="deepseek">DeepSeek AI</option>
</optgroup>
<optgroup label="๐ Hybrid (Cloud + Local Tools)">
<option value="claude-hybrid">Claude AI + Local Tools</option>
<option value="gemini-hybrid">Gemini AI + Local Tools</option>
<option value="openai-hybrid">OpenAI GPT + Local Tools</option>
<option value="deepseek-hybrid">DeepSeek AI + Local Tools</option>
</optgroup>
</select>
<div class="form-hint">Choose your preferred AI model provider</div>
</div>
<div class="form-group">
<label class="form-label">Mode</label>
<select class="form-select" id="settingsMode">
<option value="local">Local Only</option>
<option value="hybrid" selected>Hybrid (default)</option>
<option value="cloud">Cloud Only</option>
</select>
<div class="form-hint">Route tasks between local llama.cpp and cloud</div>
</div>
<div class="form-group">
<label class="form-label">Privacy Profile</label>
<select class="form-select" id="settingsPrivacy">
<option value="strict_local">Strict Local (never use cloud)</option>
<option value="ask_before_cloud" selected>Ask Before Cloud</option>
<option value="cloud_preferred">Cloud Preferred</option>
</select>
<div class="form-hint">Controls cloud escalation behavior</div>
</div>
<div class="form-group">
<label class="form-label">Coding Provider</label>
<select class="form-select" id="settingsCodingProvider">
<option value="llamacpp">Local (llama.cpp)</option>
<option value="claude">Claude</option>
<option value="openai">OpenAI</option>
<option value="gemini">Gemini</option>
<option value="deepseek">DeepSeek</option>
</select>
<div class="form-hint">Preferred model for code tasks</div>
</div>
<div class="form-group">
<label class="form-label">Confirmation Threshold</label>
<input type="number" class="form-input" id="settingsThreshold" min="0" max="1" step="0.1" value="0.6">
<div class="form-hint">Threshold for automatic task execution (0-1)</div>
</div>
<div class="form-group">
<label class="form-label">Allowed Tools</label>
<input type="text" class="form-input" id="settingsTools" placeholder="shell.run,script.run,fs.read,fs.write">
<div class="form-hint">Comma-separated list of allowed tools</div>
</div>
<div class="form-group">
<label class="form-label">API Keys</label>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">Claude (Anthropic)</label>
<input type="password" class="form-input" id="settingsClaudeApi" placeholder="ANTHROPIC_API_KEY">
<div class="form-hint">Required for Claude AI provider fallback</div>
</div>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">Gemini (Google)</label>
<input type="password" class="form-input" id="settingsGeminiApi" placeholder="GEMINI_API_KEY">
<div class="form-hint">For Google's Gemini AI models</div>
</div>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">OpenAI</label>
<input type="password" class="form-input" id="settingsOpenAIApi" placeholder="OPENAI_API_KEY">
<div class="form-hint">For GPT models</div>
</div>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">DeepSeek</label>
<input type="password" class="form-input" id="settingsDeepSeekApi" placeholder="DEEPSEEK_API_KEY">
<div class="form-hint">Cost-effective AI alternative</div>
</div>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">KnoblyCream API</label>
<input type="password" class="form-input" id="settingsCreamApi" placeholder="CREAM_API_KEY">
<div class="form-hint">For RSS feeds, posts, and email functionality</div>
</div>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">YouTube API</label>
<input type="password" class="form-input" id="settingsYouTubeApi" placeholder="YOUTUBE_API_KEY">
<div class="form-hint">For YouTube video search and trending videos</div>
</div>
<div style="margin-bottom: 15px;">
<label class="form-label" style="font-size: 14px; margin-bottom: 5px;">SerpAPI (Web Search)</label>
<input type="password" class="form-input" id="settingsSerpApi" placeholder="SERPAPI_KEY">
<div class="form-hint">For web search functionality</div>
</div>
</div>
<button class="btn" id="saveSettingsBtn" onclick="saveSettings()">Save Settings</button>
<div style="height: 40px;"></div> <!-- Spacer for scroll -->
</div>
</div>
<!-- Workflows View -->
<div class="view-content" id="workflowsView" style="display:none;">
<div style="padding: 20px; max-width: 1100px; margin: 0 auto;">
<h2 style="margin-bottom:16px;">Workflow Builder</h2>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap:16px;">
<!-- Analyze Document -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Analyze Document/Spreadsheet</h3>
<div class="form-group"><label class="form-label">Path</label><input id="wfAnalyzePath" class="form-input" type="text" placeholder="/path/to/file.csv"/></div>
<button class="btn" onclick="runWorkflowAnalyze(document.getElementById('wfAnalyzePath').value)">Run</button>
<div id="workflowAnalyzeResult" style="margin-top:10px; font-size:13px; max-height:220px; overflow:auto;"></div>
</div>
<!-- Build Document -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Build New Document</h3>
<div class="form-group"><label class="form-label">Title</label><input id="wfBuildTitle" class="form-input" type="text" placeholder="Quarterly Summary"/></div>
<div class="form-group"><label class="form-label">Sections (bullets)</label><textarea id="wfBuildSections" class="form-input" style="min-height:100px;" placeholder="โข Revenue trends\nโข Key risks\nโข Next steps"></textarea></div>
<div class="form-group"><label class="form-label">Format</label><select id="wfBuildFormat" class="form-select"><option>markdown</option><option>latex</option></select></div>
<button class="btn" onclick="runWorkflowBuild(document.getElementById('wfBuildTitle').value, document.getElementById('wfBuildSections').value, document.getElementById('wfBuildFormat').value)">Run</button>
<div id="workflowBuildResult" style="margin-top:10px; font-size:13px; max-height:220px; overflow:auto;"></div>
</div>
<!-- Convert Document -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Convert Document</h3>
<div class="form-group"><label class="form-label">Source Path</label><input id="wfConvSource" class="form-input" type="text" placeholder="/path/to/main.tex"/></div>
<div class="form-group"><label class="form-label">Target Format</label><input id="wfConvTarget" class="form-input" type="text" placeholder="pdf"/></div>
<button class="btn" onclick="runWorkflowConvert(document.getElementById('wfConvSource').value, document.getElementById('wfConvTarget').value)">Run</button>
<div id="workflowConvertResult" style="margin-top:10px; font-size:13px; max-height:220px; overflow:auto;"></div>
</div>
<!-- Custom Workflow Builder -->
<div style="grid-column: 1 / -1; border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Custom Workflow</h3>
<div class="form-group"><label class="form-label">Name</label><input id="wfName" class="form-input" type="text" placeholder="My Workflow"/></div>
<div style="display:flex; gap:8px; align-items:flex-start; flex-wrap:wrap;">
<div style="min-width:220px; flex:1;">
<label class="form-label">Step Type</label>
<select id="wfStepType" class="form-select">
<option>shell.run</option>
<option>fs.read</option>
<option>fs.write</option>
<option>web.search</option>
<option>jit</option>
<option>tex.compile</option>
</select>
</div>
<div style="min-width:220px; flex:1;">
<label class="form-label">OS (optional)</label>
<select id="wfStepOs" class="form-select">
<option value="any">Any</option>
<option value="linux">Linux</option>
<option value="darwin">macOS</option>
<option value="win32">Windows</option>
</select>
</div>
<div style="min-width:320px; flex:2;">
<label class="form-label">Args (JSON)</label>
<textarea id="wfStepArgs" class="form-input" style="min-height:80px;" placeholder='{"cmd":"ls -la"}'></textarea>
<div class="form-hint">Use "$prev" or "$prev.stdout" to pass output from previous step.</div>
</div>
<div style="min-width:220px; flex:1;">
<label class="form-label">Shell (shell.run)</label>
<select id="wfStepShell" class="form-select">
<option value="">Default</option>
<option value="bash">bash</option>
<option value="zsh">zsh</option>
<option value="sh">sh</option>
<option value="powershell">powershell</option>
<option value="cmd">cmd</option>
</select>
</div>
<div style="align-self:flex-end;">
<button class="btn" onclick="wfAddStep()">+ Add Step</button>
</div>
</div>
<div id="wfSteps" style="margin-top:12px; display:grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap:10px;"></div>
<div style="display:flex; gap:8px; margin-top:12px;">
<button class="btn" onclick="wfRun(false)">Run</button>
<button class="btn secondary" onclick="wfRun(true)">Dry Run</button>
<button class="btn" onclick="wfSave()">Save</button>
</div>
<pre id="wfRunOut" style="margin-top:12px; background:var(--muted); border:1px solid var(--border); padding:10px; border-radius:8px; max-height:300px; overflow:auto;"></pre>
<div style="margin-top:14px;">
<h4>Saved Workflows</h4>
<div id="wfSaved" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap:10px;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- System View -->
<div class="view-content" id="systemView" style="display:none;">
<div style="padding: 20px; max-width: 700px; margin: 0 auto;">
<h2>System</h2>
<p style="color:var(--sub);">Model connectivity and environment checks.</p>
<button class="btn" onclick="loadSystemStatus()">Refresh Status</button>
<div id="systemStatus" style="margin-top:12px; font-size:13px;"></div>
<div style="margin-top:24px; border-top:1px solid var(--border); padding-top:16px;">
<h3 style="margin:0 0 10px 0;">Cream API Quick Actions</h3>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap:12px;">
<!-- Fetch Posts -->
<div style="border:1px solid var(--border); border-radius:10px; padding:12px; background:var(--panel);">
<h4 style="margin:0 0 8px 0;">Fetch Recent Posts</h4>
<div class="form-group"><label class="form-label">Limit</label><input id="creamFetchLimit" class="form-input" type="number" value="5"/></div>
<div class="form-group"><label class="form-label"><input type="checkbox" id="creamFetchRaw"/> Show raw JSON</label></div>
<button class="btn" onclick="systemCreamFetch()">Fetch</button>
</div>
<!-- Send Test Email -->
<div style="border:1px solid var(--border); border-radius:10px; padding:12px; background:var(--panel);">
<h4 style="margin:0 0 8px 0;">Send Test Email</h4>
<div class="form-group"><label class="form-label">From Email</label><input id="creamMailFrom" class="form-input" type="email" placeholder="me@example.com"/></div>
<div class="form-group"><label class="form-label">To Email</label><input id="creamMailTo" class="form-input" type="email" placeholder="you@example.com"/></div>
<div class="form-group"><label class="form-label">Subject</label><input id="creamMailSubject" class="form-input" type="text" placeholder="Hello"/></div>
<div class="form-group"><label class="form-label">Body</label><textarea id="creamMailBody" class="form-input" style="min-height:70px;" placeholder="Hi there"></textarea></div>
<button class="btn" onclick="systemCreamMail()">Send</button>
</div>
<!-- Create Post -->
<div style="border:1px solid var(--border); border-radius:10px; padding:12px; background:var(--panel);">
<h4 style="margin:0 0 8px 0;">Create Post</h4>
<div class="form-group"><label class="form-label">Content</label><textarea id="creamPostContent" class="form-input" style="min-height:70px;" placeholder="Hello from C9AI"></textarea></div>
<div class="form-group"><label class="form-label">Visibility</label><select id="creamPostVisibility" class="form-select"><option>public</option><option>private</option></select></div>
<button class="btn" onclick="systemCreamPost()">Post</button>
</div>
</div>
<pre id="creamOut" style="margin-top:12px; background:var(--muted); border:1px solid var(--border); padding:10px; border-radius:8px; max-height:300px; overflow:auto;"></pre>
<div style="margin-top:12px;">
<button class="btn secondary" onclick="systemCreamDiagnose()">Diagnose Cream APIs</button>
<button class="btn secondary" onclick="systemCreamDiagMail()">Diagnose Mail</button>
<button class="btn secondary" onclick="systemCreamDiagPost()">Diagnose Post</button>
</div>
</div>
</div>
</div>
<!-- Terminal View -->
<div class="view-content" id="terminalView" style="display:none;">
<div style="padding: 20px; max-width: 900px; margin: 0 auto;">
<h2>System Terminal (Quick Runner)</h2>
<div class="form-group"><label class="form-label">Command</label><input id="termCmd" class="form-input" type="text" placeholder="claude --help or gemini --help"/></div>
<button class="btn" onclick="runTerminalCmd()">Run</button>
<pre id="termOut" style="margin-top:12px; background:var(--muted); border:1px solid var(--border); padding:10px; border-radius:8px; max-height:400px; overflow:auto;"></pre>
</div>
</div>
<!-- Calculators View -->
<div class="view-content" id="calculatorsView" style="display: none;">
<div style="padding: 20px; max-width: 1000px; margin: 0 auto;">
<h2 style="margin-bottom:8px;">Executive Calculators</h2>
<div style="margin-bottom:16px; font-size:13px;">
<a href="#savedCalcsAnchor">Jump to Saved Calculators โ</a>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap:16px;">
<!-- Investment -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Investment</h3>
<div class="form-group"><label class="form-label">Amount</label><input id="calcInvAmount" class="form-input" type="text" placeholder="250000"/></div>
<div class="form-group"><label class="form-label">Upside %</label><input id="calcInvUpside" class="form-input" type="text" placeholder="12%"/></div>
<div class="form-group"><label class="form-label">Downside % (optional)</label><input id="calcInvDown" class="form-input" type="text" placeholder="0%"/></div>
<div class="form-group"><label class="form-label">Years</label><input id="calcInvYears" class="form-input" type="number" placeholder="2"/></div>
<div class="form-group"><label class="form-label">Probability % (optional)</label><input id="calcInvProb" class="form-input" type="text" placeholder="50%"/></div>
<button class="btn" onclick="runInvestmentCalc()">Calculate</button>
<div id="calcInvResult" style="margin-top:10px; font-size:13px;"></div>
</div>
<!-- Vendor Discount -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Vendor Discount</h3>
<div class="form-group"><label class="form-label">Amount</label><input id="calcVDAmount" class="form-input" type="text" placeholder="50000"/></div>
<div class="form-group"><label class="form-label">Discount %</label><input id="calcVDDisc" class="form-input" type="text" placeholder="2%"/></div>
<div class="form-group"><label class="form-label">Days Early (optional)</label><input id="calcVDDays" class="form-input" type="number" placeholder="20"/></div>
<button class="btn" onclick="runVendorDiscountCalc()">Calculate</button>
<div id="calcVDResult" style="margin-top:10px; font-size:13px;"></div>
</div>
<!-- Depreciation -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Depreciation</h3>
<div class="form-group"><label class="form-label">Cost</label><input id="calcDepCost" class="form-input" type="text" placeholder="100000"/></div>
<div class="form-group"><label class="form-label">Method</label><select id="calcDepMethod" class="form-select"><option value="SLM">SLM</option><option value="WDV">WDV</option></select></div>
<div class="form-group"><label class="form-label">Useful Life (years, optional for WDV)</label><input id="calcDepLife" class="form-input" type="number" placeholder="15"/></div>
<div class="form-group"><label class="form-label">Residual Value % (optional)</label><input id="calcDepResidual" class="form-input" type="text" placeholder="5%"/></div>
<div class="form-group"><label class="form-label">Rate % (WDV optional)</label><input id="calcDepRate" class="form-input" type="text" placeholder="(derive)"/></div>
<div class="form-group"><label class="form-label">Date Ready (YYYY-MM-DD)</label><input id="calcDepReady" class="form-input" type="text" placeholder="2025-06-01"/></div>
<div class="form-group"><label class="form-label">Year End (YYYY-MM-DD)</label><input id="calcDepYE" class="form-input" type="text" placeholder="2026-03-31"/></div>
<button class="btn" onclick="runDepreciationCalc()">Calculate</button>
<div id="calcDepResult" style="margin-top:10px; font-size:13px;"></div>
</div>
<!-- Custom (Expression) -->
<div style="border:1px solid var(--border); border-radius:10px; padding:14px; background:var(--panel);">
<h3 style="margin:0 0 10px 0;">Custom (Expression)</h3>
<div class="form-group"><label class="form-label">Expression</label><input id="calcExpr" class="form-input" type="text" placeholder="amount * (1 + rate)^years"/></div>
<div class="form-group"><label class="form-label">Variables (key=value, comma separated)</label><input id="calcVars" class="form-input" type="text" placeholder="amount=100000, rate=0.12, years=2"/></div>
<div style="display:flex; gap:10px;">
<button class="btn" onclick="runCustomCalc()">Calculate</button>
<button class="btn secondary" onclick="saveCustomCalcDialog()">Saveโฆ</button>
</div>
<div id="calcExprResult" style="margin-top:10px; font-size:13px;"></div>
</div>
<!-- Saved Calculators -->
<div style="grid-column: 1 / -1;">
<a id="savedCalcsAnchor"></a>
<h3>Saved Calculators</h3>
<div id="savedCalcs" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap:16px;"></div>
</div>
</div>
</div>
</div>
<!-- Vibe Sessions View -->
<div class="view-content" id="vibeView" style="display: none;">
<div style="padding: 20px; max-width: 1000px; margin: 0 auto; min-height: 100%; box-sizing: border-box;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;">
<h2 style="margin: 0;">๐ญ Universal Vibe Task Manager</h2>
<div style="font-size: 13px; color: var(--sub);">Match your energy with perfect workflows</div>
</div>
<!-- Vibe Detection Card -->
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 25px; margin-bottom: 25px;">
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: var(--primary);">๐ฏ What's Your Current Vibe?</h3>
<button id="detectVibeBtn" class="btn primary" onclick="detectCurrentVibe()">
<span>๐</span> Detect My Vibe
</button>
</div>
<div id="vibeDetectionForm" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Energy Level</label>
<select id="energyLevel" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--muted); color: var(--text);">
<option value="">Select energy...</option>
<option value="high">๐ High Energy</option>
<option value="focused">๐ฏ Focused</option>
<option value="medium">โก Medium</option>
<option value="low">๐ Low Energy</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Current Mood</label>
<select id="currentMood" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--muted); color: var(--text);">
<option value="">Select mood...</option>
<option value="creative">๐จ Creative</option>
<option value="analytical">๐ฌ Analytical</option>
<option value="productive">โก Productive</option>
<option value="exploratory">๐บ๏ธ Exploratory</option>
<option value="collaborative">๐ฅ Collaborative</option>
<option value="steady">๐ Steady</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Available Time</label>
<select id="availableTime" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--muted); color: var(--text);">
<option value="">Select time...</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="90">90 minutes</option>
<option value="120">2 hours</option>
<option value="180">3+ hours</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Work Context</label>
<select id="workContext" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--muted); color: var(--text);">
<option value="">Select context...</option>
<option value="home-office">๐ Home Office</option>
<option value="quiet-space">๐คซ Quiet Space</option>
<option value="collaborative">๐ฅ Team Environment</option>
<option value="dual-monitor">๐ป Dual Monitors</option>
<option value="mobile">๐ฑ On the Go</option>
</select>
</div>
</div>
<div id="vibeResult" style="display: none; background: var(--muted); border-radius: 8px; padding: 15px; margin-top: 15px;">
<div id="detectedVibe" style="font-weight: 600; margin-bottom: 10px;"></div>
<div id="vibeDescription" style="color: var(--sub); margin-bottom: 10px;"></div>
<div id="vibeConfidence" style="font-size: 12px; color: var(--sub);"></div>
</div>
</div>
<!-- Quick Vibe Actions -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 25px;">
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<span style="font-size: 24px; margin-right: 12px;">๐
</span>
<div>
<h4 style="margin: 0; color: var(--primary);">Morning Creative</h4>
<div style="font-size: 12px; color: var(--sub);">Fresh energy, content creation</div>
</div>
</div>
<button class="btn secondary" onclick="startVibeSession('morning-content-creator')" style="width: 100%;">
Start Creative Session
</button>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<span style="font-size: 24px; margin-right: 12px;">๐ฌ</span>
<div>
<h4 style="margin: 0; color: var(--primary);">Deep Analysis</h4>
<div style="font-size: 12px; color: var(--sub);">Data diving, systematic thinking</div>
</div>
</div>
<button class="btn secondary" onclick="startVibeSession('data-detective')" style="width: 100%;">
Start Analysis Session
</button>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<span style="font-size: 24px; margin-right: 12px;">โก</span>
<div>
<h4 style="margin: 0; color: var(--primary);">Rapid Prototype</h4>
<div style="font-size: 12px; color: var(--sub);">Fast building, experimentation</div>
</div>
</div>
<button class="btn secondary" onclick="startVibeSession('rapid-prototype')" style="width: 100%;">
Start Prototype Sprint
</button>
</div>
</div>
<!-- Workflow Templates -->
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 25px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 8px;">
<h3 style="margin: 0;">๐จ Available Workflow Templates</h3>
<div style="display:flex; gap:8px;">
<button class="btn" onclick="showGenerateWorkflowModal()" title="Use a cloud model to draft a new workflow from a prompt">
<span>โจ</span> Generate Workflow
</button>
<button class="btn primary" onclick="refreshTemplates()">
<span>๐</span> Refresh Templates
</button>
</div>
</div>
<div id="workflowTemplates" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
<!-- Templates will be loaded here -->
<div style="text-align: center; color: var(--sub); padding: 40px;">
<div style="font-size: 48px; margin-bottom: 16px;">๐ญ</div>
<div>Click "Refresh Templates" to load available workflows</div>
</div>
</div>
</div>
<!-- Slides Library -->
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 25px; margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 8px;">
<h3 style="margin: 0;">๐ฅ๏ธ Slides Library</h3>
<div style="display:flex; gap:8px;">
<button class="btn secondary" onclick="refreshSlides()">
<span>๐</span> Refresh Slides
</button>
</div>
</div>
<div id="slidesGrid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px;">
<div style="text-align: center; color: var(--sub); padding: 40px; grid-column: 1 / -1;">
<div style="font-size: 42px; margin-bottom: 16px;">๐๏ธ</div>
<div>No slides found yet. Generate one from Markdown, then refresh.</div>
</div>
</div>
</div>
<!-- Interactive Workflow Session -->
<div id="interactiveSessionCard" style="display: none; background: var(--panel); border: 1px solid var(--primary); border-radius: 12px; padding: 25px; margin-top: 25px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<div>
<h3 style="margin: 0; color: var(--primary);" id="sessionTitle">๐ฏ Interactive Workflow Session</h3>
<div style="font-size: 13px; color: var(--sub); margin-top: 4px;" id="sessionSubtitle">Step-by-step tool execution with your inputs</div>
</div>
<div style="display: flex; gap: 12px;">
<button class="btn secondary" onclick="saveWorkflowProgress()" style="font-size: 13px;">
<span>๐พ</span> Save Progress
</button>
<button class="btn danger" onclick="endInteractiveSession()">
<span>โน๏ธ</span> End Session
</button>
</div>
</div>
<!-- Workflow Progress Overview -->
<div style="background: var(--muted); border-radius: 8px; padding: 15px; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span style="font-weight: 500;">Workflow Progress</span>
<span id="workflowProgressText" style="font-size: 13px; color: var(--sub);">0 of 0 steps completed</span>
</div>
<div style="background: var(--panel); border-radius: 4px; height: 8px; overflow: hidden;">
<div id="workflowProgressBar" style="background: var(--primary); height: 100%; width: 0%; transition: width 0.3s;"></div>
</div>
</div>
<!-- Interactive Workflow Steps -->
<div id="workflowStepsContainer" style="display: flex; flex-direction: column; gap: 20px;">
<!-- Steps will be populated here -->
</div>
<!-- Workflow Results -->
<div id="workflowResults" style="display: none; background: var(--muted); border-radius: 8px; padding: 20px; margin-top: 20px;">
<h4 style="margin: 0 0 15px 0; color: var(--primary);">๐ Workflow Results</h4>
<div id="workflowResultsContent"></div>
</div>
</div>
</div>
</div>
<!-- Help View -->
<div class="view-content" id="helpView" style="display: none;">
<div style="padding: 40px; max-width: 900px; margin: 0 auto;">
<h2 style="margin-bottom: 20px;">Help & Documentation</h2>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px;">
<h3 style="margin-bottom: 12px;">๐ Getting Started</h3>
<ul style="color: var(--sub); line-height: 1.6; padding-left: 18px;">
<li>Chat: ask anything; use sigils (e.g., <code>@email</code>, <code>@post</code>, <code>@calc</code>) for actions.</li>
<li>AskAI (floating): quick Q&A with provider dropdown and expand.</li>
<li>Calculators: deterministic business calculators + custom expressions.</li>
<li>Workflows: chain steps (shell.run, fs.read/write, jit, tex.compile, cream.*).</li>
<li>System: connectivity + Cream API quick actions and diagnostics.</li>
<li>Terminal: run one-off system commands (non-interactive).</li>
</ul>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(280px,1fr)); gap:16px;">
<div style="background: var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px;">
<h3>๐ฌ Chat & Sigils</h3>
<ul style="color: var(--sub); line-height:1.6; padding-left:18px;">
<li>Send: Ctrl/Cmd + Enter</li>
<li><code>@email</code> <em>"to@example.com"</em> subject: <em>Text</em> content: <em>Body</em></li>
<li><code>@post</code> <em>"Hello world"</em> visibility: public</li>
<li><code>@calc</code> <em>22/7</em> or saved function calls</li>
<li><code>@executive</code> domains (Investment, Vendor Discount, Depreciation...)</li>
</ul>
</div>
<div style="background: var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px;">
<h3>๐งฎ Calculators</h3>
<ul style="color: var(--sub); line-height:1.6; padding-left:18px;">
<li>Built-ins: Investment, Vendor Discount, Depreciation, SaaS Breakeven, Finance.</li>
<li>Custom (Expression): enter formula + variables, Calculate; Save to reuse.</li>
<li>Saved Calculators: collapsible list; Run/Edit/Rename/Delete.</li>
</ul>
</div>
<div style="background: var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px;">
<h3>๐ Workflows</h3>
<ul style="color: var(--sub); line-height:1.6; padding-left:18px;">
<li>Steps: shell.run, fs.read/write, web.search, jit, tex.compile, cream.*</li>
<li>Data flow: use <code>"$prev"</code> or <code>"$prev.stdout"</code> in step args.</li>
<li>Portability: set OS filter (Any/Linux/macOS/Windows), choose shell (bash/zsh/sh/powershell/cmd).</li>
<li>Save/Load/Run/Dry Run from the Workflows tab.</li>
</ul>
</div>
<div style="background: var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px;">
<h3>๐ ๏ธ System & Cream API</h3>
<ul style="color: var(--sub); line-height:1.6; padding-left:18px;">
<li>Set API keys in Settings (Cream token/key, OpenAI, Gemini, etc.).</li>
<li>Quick Actions: fetch posts, send mail, create post.</li>
<li>Diagnostics: <em>Diagnose Cream APIs/Mail/Post</em> to see status/elapsed/sample.</li>
<li>RSS/Stream handle slow endpoints with long timeouts + retries.</li>
</ul>
</div>
<div style="background: var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px;">
<h3>๐พ Conversations</h3>
<ul style="color: var(--sub); line-height:1.6; padding-left:18px;">
<li>Rightโclick a conversation โ Rename / Save / Delete.</li>
<li>Local autosave to browser; optional server save for persistence.</li>
</ul>
</div>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; margin-top:16px;">
<h3>โจ๏ธ Shortcuts</h3>
<div style="display:grid; grid-template-columns: 1fr 2fr; gap:10px; color: var(--sub);">
<div><kbd style="background: var(--muted); padding: 4px 8px; border-radius: 4px;">Ctrl/Cmd + Enter</kbd></div>
<div>Send message</div>
<div><kbd style="background: var(--muted); padding: 4px 8px; border-radius: 4px;">Ctrl/Cmd + N</kbd></div>
<div>New conversation</div>
</div>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; margin-top:16px;">
<h3>๐งฐ Available Tools</h3>
<div id="toolsList" style="color: var(--sub); line-height: 1.6;">Loading tools...</div>
</div>
</div>
</div>
<!-- Functions View -->
<div class="view-content" id="functionsView" style="display: none;">
<div style="padding: 40px; max-width: 1000px; margin: 0 auto;">
<h2 style="margin-bottom: 20px;">๐ Function Library</h2>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 24px;">
<h3 style="margin-bottom: 12px;">๐ Quick Actions</h3>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button class="btn" onclick="showCreateFunction()">โจ Create Function</button>
<button class="btn secondary" onclick="refreshFunctions()">๐ Refresh</button>
<button class="btn secondary" onclick="openFunctionsDirectory()">๐ Open Directory</button>
</div>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 24px;">
<h3 style="margin-bottom: 12px;">๐ Function Statistics</h3>
<div id="functionStats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: var(--primary);" id="totalFunctions">-</div>
<div style="color: var(--sub); font-size: 14px;">Total Functions</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: var(--ok);" id="activeFunctions">-</div>
<div style="color: var(--sub); font-size: 14px;">Active</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: var(--sub);" id="storageUsed">-</div>
<div style="color: var(--sub); font-size: 14px;">Storage Used</div>
</div>
</div>
</div>
<div style="background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px;">
<h3 style="margin-bottom: 12px;">๐ Function List</h3>
<div id="functionsList" style="min-height: 200px;">
<div style="text-align: center; color: var(--sub); padding: 40px;">
Loading functions...
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Floating Chat -->
<button class="btn float-chat-btn" onclick="toggleFloatChat()">๐ก Ask AI</button>
<div class="float-chat-panel" id="floatChat">
<div class="float-chat-header">
<div style="display:flex; align-items:center; gap:8px;">
<div>AskAI</div>
<select id="floatChatProvider" class="form-select" style="height:28px; padding:2px 6px; font-size:12px;">
<option value="llamacpp">llamacpp</option>
<option value="claude">claude</option>
<option value="gemini">gemini</option>
<option value="openai">openai</option>
<option value="deepseek">deepseek</option>
</select>
</div>
<div class="actions">
<button class="btn secondary" onclick="toggleFloatChatExpand()" title="Expand/Collapse">โคข</button>
<button class="btn secondary" onclick="toggleFloatChat()" title="Close">โ</button>
</div>
</div>
<div class="float-chat-messages" id="floatChatMessages"></div>
<div class="float-chat-input">
<input id="floatChatInput" class="form-input" type="text" placeholder="Ask for a formulaโฆ" onkeydown="if(event.key==='Enter'){sendFloatChat()}" />
<button class="btn" onclick="sendFloatChat()">Send</button>
</div>
</div>
<!-- Conversation Context Menu -->
<div class="context-menu" id="convMenu">
<div class="item" onclick="convMenuRename()">โ๏ธ Rename</div>
<div class="item" onclick="convMenuSave()">๐พ Save</div>
<div class="item" onclick="convMenuDelete()">๐๏ธ Delete</div>
</div>
<script>
// Global state
let currentView = 'chat';
let currentProvider = 'llamacpp';
let conversations = JSON.parse(localStorage.getItem('c9ai-conversations') || '[]');
let currentConversationId = null;
let settings = {};
// DOM elements
const sidebar = document.getElementById('sidebar');
const chatInput = document.getElementById('chatInput');
const chatMessages = document.getElementById('chatMessages');
const welcomeScreen = document.getElementById('welcomeScreen');
const sendBtn = document.getElementById('sendBtn');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const conversationsList = document.getElementById('conversationsList');
// Initialize app
document.addEventListener('DOMContentLoaded', async function() {
await loadSettings();
loadConversations();
setupEventListeners();
updateProviderButtons();
loadSavedTheme(); // Load the saved theme preference
});
// Settings management
async function loadSettings() {
try {
const response = await fetch('/api/settings');
settings = await response.json();
currentProvider = settings.provider || 'llamacpp';
// Update UI elements
const providerSelect = document.getElementById('settingsProvider');
const thresholdInput = document.getElementById('settingsThreshold');
const toolsInput = document.getElementById('settingsTools');
const modeSelect = document.getElementById('settingsMode');
const privacySelect = document.getElementById('settingsPrivacy');
const codingProviderSelect = document.getElementById('settingsCodingProvider');
const claudeApiInput = document.getElementById('settingsClaudeApi');
const geminiApiInput = document.getElementById('settingsGeminiApi');
const openAIApiInput = document.getElementById('settingsOpenAIApi');
const deepSeekApiInput = document.getElementById('settingsDeepSeekApi');
const creamApiInput = document.getElementById('settingsCreamApi');
const youTubeApiInput = document.getElementById('settingsYouTubeApi');
const serpApiInput = document.getElementById('settingsSerpApi');
if (providerSelect) providerSelect.value = settings.provider || 'llamacpp';
if (thresholdInput) thresholdInput.value = settings.confirmThreshold || 0.6;
if (toolsInput) toolsInput.value = (settings.allowedTools || []).join(',');
if (modeSelect) modeSelect.value = settings.defaultMode || 'hybrid';
if (privacySelect) privacySelect.value = settings.privacyProfile || 'ask_before_cloud';
if (codingProviderSelect) codingProviderSelect.value = settings.codingProvider || 'claude';
if (claudeApiInput) claudeApiInput.value = settings.apiKeys?.ANTHROPIC_API_KEY || '';
if (geminiApiInput) geminiApiInput.value = settings.apiKeys?.GEMINI_API_KEY || '';
if (openAIApiInput) openAIApiInput.value = settings.apiKeys?.OPENAI_API_KEY || '';
if (deepSeekApiInput) deepSeekApiInput.value = settings.apiKeys?.DEEPSEEK_API_KEY || '';
if (creamApiInput) creamApiInput.value = settings.apiKeys?.CREAM_API_KEY || '';
if (youTubeApiInput) youTubeApiInput.value = settings.apiKeys?.YOUTUBE_API_KEY || '';
if (serpApiInput) serpApiInput.value = settings.apiKeys?.SERPAPI_KEY || '';
setStatus('ready', 'Ready');
} catch (error) {
console.warn('Failed to load settings:', error);
setStatus('error', 'Settings Error');
}
}
async function saveSettings() {
const saveBtn = document.getElementById('saveSettingsBtn');
const originalText = saveBtn.textContent;
// Visual feedback: disable button and show loading
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
saveBtn.style.backgroundColor = '#4a5568';
const providerSelect = document.getElementById('settingsProvider');
const thresholdInput = document.getElementById('settingsThreshold');
const toolsInput = document.getElementById('settingsTools');
const modeSelect = document.getElementById('settingsMode');
const privacySelect = document.getElementById('settingsPrivacy');
const codingProviderSelect = document.getElementById('settingsCodingProvider');
const claudeApiInput = document.getElementById('settingsClaudeApi');
const geminiApiInput = document.getElementById('settingsGeminiApi');
const openAIApiInput = document.getElementById('settingsOpenAIApi');
const deepSeekApiInput = document.getElementById('settingsDeepSeekApi');
const creamApiInput = document.getElementById('settingsCreamApi');
const youTubeApiInput = document.getElementById('settingsYouTubeApi');
const serpApiInput = document.getElementById('settingsSerpApi');
const newSettings = {
provider: providerSelect.value,
confirmThreshold: parseFloat(thresholdInput.value),
allowedTools: toolsInput.value.split(',').map(s => s.trim()).filter(Boolean),
defaultMode: modeSelect.value,
privacyProfile: privacySelect.value,
codingProvider: codingProviderSelect.value,
apiKeys: {
ANTHROPIC_API_KEY: document.getElementById('settingsClaudeApi').value || '',
GEMINI_API_KEY: document.getElementById('settingsGeminiApi').value || '',
OPENAI_API_KEY: document.getElementById('settingsOpenAIApi').value || '',
DEEPSEEK_API_KEY: document.getElementById('settingsDeepSeekApi').value || '',
CREAM_API_KEY: document.getElementById('settingsCreamApi').value || '',
YOUTUBE_API_KEY: document.getElementById('settingsYouTubeApi').value || '',
SERPAPI_KEY: document.getElementById('settingsSerpApi').value || ''
}
};
try {
await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newSettings)
});
settings = newSettings;
currentProvider = settings.provider;
updateProviderButtons();
// Success feedback
saveBtn.textContent = 'โ
Saved!';
saveBtn.style.backgroundColor = '#38a169';
setStatus('ready', 'Settings Saved');
// Reset button after delay
setTimeout(() => {
saveBtn.disabled = false;
saveBtn.textContent = originalText;
saveBtn.style.backgroundColor = '';
setStatus('ready', 'Ready');
}, 2000);
} catch (error) {
console.error('Failed to save settings:', error);
// Error feedback
saveBtn.textContent = 'โ Failed';
saveBtn.style.backgroundColor = '#e53e3e';
setStatus('error', 'Save Failed');
// Reset button after delay
setTimeout(() => {
saveBtn.disabled = false;
saveBtn.textContent = originalText;
saveBtn.style.backgroundColor = '';
}, 2000);
}
}
// Status management
function renderMarkdown(text) {
if (!text) return '';
// Extract and preserve code action buttons before escaping
const codeActionRegex = /<div class="code-actions"[^>]*>.*?<\/div>/gs;
const codeActions = [];
let textWithPlaceholders = text.replace(codeActionRegex, (match) => {
const placeholder = `__CODE_ACTIONS_${codeActions.length}__`;
codeActions.push(match);
return placeholder;
});
// Escape HTML to prevent XSS (but preserve our placeholders)
const escaped = textWithPlaceholders
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
// Normalize common bullet styles before HTML escaping
// Convert lines starting with "- " to use the bullet dot so we render them as list items
textWithPlaceholders = textWithPlaceholders.replace(/^\-\s+/gm, 'โข ');
// Simple markdown rendering
let rendered = escaped
// Images: 
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (m, alt, url) => {
// RSS thumbnails should span the container; others stay compact
if (String(alt).toLowerCase() === 'thumbnail') {
return `<img src="${url}" alt="${alt}" class="rss-thumb"/>`;
}
return `<img src="${url}" alt="${alt}" style="max-width:140px; width:140px; height:auto; border-radius:8px; margin:8px 12px 8px 0; display:inline-block; vertical-align:top;"/>`;
})
// Links: [text](url)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1<\/a>')
// Bold text: **text** -> <strong>text</strong>
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// Italic text: *text* -> <em>text</em>
.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>')
// Code blocks: ```code``` -> <pre><code>code</code></pre>
.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>')
// Inline code: `code` -> <code>code</code>
.replace(/`([^`]+?)`/g, '<code>$1</code>')
// Headers: ## text -> <h2>text</h2>
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
// Bullet points: โข text -> <li>text</li>
.replace(/^โข (.+)$/gm, '<li>$1</li>')
// Line breaks
.replace(/\n/g, '<br>')
// collapse excessive breaks that can cause large gaps
.replace(/(?:<br>\s*){3,}/g, '<br><br>')
// Style 'Read more' as a pill
.replace(/<a href=\"([^\"]+)\">Read more<\/a>/g, '<a class="pill-read" href="$1" target="_blank" rel="noopener noreferrer">Read more<\/a>');
// Restore code action buttons
codeActions.forEach((codeAction, index) => {
rendered = rendered.replace(`__CODE_ACTIONS_${index}__`, codeAction);
});
return rendered;
}
function setStatus(type, message) {
// Don't update status pane for progress messages - those go to chat now
const progressMessages = [
'analyzing request', 'planning action', 'executing', 'generating response',
'thinking', 'planning', 'executing', 'synthesizing', 'using',
'๐', '๐', 'โก', 'โจ', '๐ค using'
];
if (progressMessages.some(pm => message.toLowerCase().includes(pm.toLowerCase()))) {
return; // Skip status pane updates for progress
}
statusDot.className = 'status-dot ' + (type === 'ready' ? '' : type);
// Show current provider name in status when ready
if (type === 'ready' && currentProvider) {
const providerName = getProviderDisplayName(currentProvider);
statusText.textContent = `${providerName} - Ready`;
} else {
statusText.textContent = message;
}
}
// Navigation
function showView(viewName) {
document.querySelectorAll('.view-content').forEach(view => {
view.style.display = 'none';
});
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
document.getElementById(viewName + 'View').style.display = 'block';
document.querySelector(`[data-view="${viewName}"]`).classList.add('active');
currentView = viewName;
updateViewTitle();
if (viewName === 'help') {
loadToolsList();
} else if (viewName === 'calculators') {
loadSavedCalculators && loadSavedCalculators();
} else if (viewName === 'system') {
loadSystemStatus && loadSystemStatus();
} else if (viewName === 'workflows') {
if (typeof wfLoadSaved === 'function') wfLoadSaved();
// Auto-refresh templates and slides when entering Workflows view
try { refreshTemplates(); } catch(e){}
try { refreshSlides(); } catch(e){}
}
}
function updateViewTitle() {
const titles = {
chat: 'Smart Chat - Enhanced Execution',
functions: 'Function Library Manager',
settings: 'Settings',
help: 'Help & Documentation'
};
document.getElementById('viewTitle').textContent = titles[currentView] || 'c9ai';
}
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
}
// Theme Toggle Management
function toggleTheme() {
const body = document.body;
const slider = document.querySelector('.theme-toggle-slider');
const currentTheme = body.getAttribute('data-theme');
if (currentTheme === 'light') {
// Switch to dark mode
body.removeAttribute('data-theme');
slider.textContent = '๐';
localStorage.setItem('theme', 'dark');
} else {
// Switch to light mode
body.setAttribute('data-theme', 'light');
slider.textContent = 'โ๏ธ';
localStorage.setItem('theme', 'light');
}
}
// Load saved theme on page load
function loadSavedTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.body.setAttribute('data-theme', 'light');
document.querySelector('.theme-toggle-slider').textContent = 'โ๏ธ';
}
}
// Debug Panel Management
let debugPanelVisible = false;
function toggleDebugPanel() {
const debugPanel = document.getElementById('debugPanel');
const debugToggle = document.getElementById('debugToggle');
debugPanelVisible = !debugPanelVisible;
if (debugPanelVisible) {
debugPanel.style.display = 'flex';
debugToggle.classList.add('active');
debugToggle.innerHTML = '๐ฌ';
addDebugEntry('info', '๐๏ธ', 'Debug panel opened - monitoring executive requests');
} else {
debugPanel.style.display = 'none';
debugToggle.classList.remove('active');
debugToggle.innerHTML = '๐ฌ';
}
}
function addDebugEntry(type, icon, message, data = null) {
const debugContent = document.getElementById('debugContent');
const entry = document.createElement('div');
entry.className = `debug-entry ${type}`;
const timestamp = new Date().toLocaleTimeString();
let displayMessage = message;
// Format data if provided
if (data && typeof data === 'object') {
displayMessage += '\n' + JSON.stringify(data, null, 2);
}
entry.innerHTML = `
<span class="debug-timestamp">${timestamp}</span>
<span class="debug-icon">${icon}</span>
<span class="debug-message">${displayMessage}</span>
`;
debugContent.appendChild(entry);
// Auto-scroll to bottom
debugContent.scrollTop = debugContent.scrollHeight;
// Keep only last 50 entries for performance
const entries = debugContent.querySelectorAll('.debug-entry');
if (entries.length > 50) {
entries[0].remove();
}
}
function clearDebugLog() {
const debugContent = document.getElementById('debugContent');
debugContent.innerHTML = `
<div class="debug-entry info">
<span class="debug-timestamp">Cleared</span>
<span class="debug-icon">๐งน</span>
<span class="debug-message">Debug log cleared</span>
</div>
`;
}
// Progress tracking for multi-step executive requests
let currentWorkflowSteps = 0;
let completedWorkflowSteps = 0;
// Interpret calculation results for executive clarity
function interpretCalculationResult(expression, result) {
if (!expression) return '';
// Vendor discount patterns
if (expression.includes('*') && expression.includes('0.01')) {
return `(Discount Amount: โน${result} savings if paid immediately)`;
}
if (expression.includes('10000') && expression.includes('-')) {
return `(Final Amount to Pay: โน${result} after discount)`;
}
// Investment patterns
if (expression.includes('365') && expression.includes('500')) {
return `(Total Maturity Value: โน${result} after 1 year)`;
}
// Compound interest patterns
if (expression.includes('^') && expression.includes('365')) {
return `(Compound Interest Calculation: โน${result})`;
}
// ROI patterns
if (expression.includes('/') && result < 100 && result > -100) {
return `(ROI: ${result}%)`;
}
// Percentage patterns
if (result < 1 && result > 0) {
return `(${(result * 100).toFixed(2)}%)`;
}
// Large amounts (likely currency)
if (result > 1000) {
return `(โน${result.toLocaleString('en-IN')})`;
}
return '';
}
function startWorkflowProgress(totalSteps, description) {
currentWorkflowSteps = totalSteps;
completedWorkflowSteps = 0;
addDebugEntry('info', '๐ฏ', `Started workflow: ${description} (${totalSteps} steps)`);
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
progressBar.id = 'workflowProgress';
progressBar.innerHTML = '<div class="progress-fill" style="width: 0%"></div>';
document.getElementById('debugContent').appendChild(progressBar);
}
function updateWorkflowProgress(step, description) {
completedWorkflowSteps = step;
const percentage = (step / currentWorkflowSteps) * 100;
const progressFill = document.querySelector('#workflowProgress .progress-fill');
if (progressFill) {
progressFill.style.width = `${percentage}%`;
}
addDebugEntry('success', 'โ
', `Step ${step}/${currentWorkflowSteps}: ${description}`);
if (step === currentWorkflowSteps) {
setTimeout(() => {
const progressBar = document.getElementById('workflowProgress');
if (progressBar) progressBar.remove();
}, 2000);
}
}
function showSettings() {
showView('settings');
}
// Provider management
function updateProviderButtons() {
document.querySelectorAll('[data-provider]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.provider === currentProvider);
});
}
// Calculators - client helpers
async function execCalc(domain, params) {
const r = await fetch('/api/calculators/execute', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain, params })
});
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Calculation failed');
return j;
}
function fmtNum(x) { if (x == null) return ''; return typeof x === 'number' ? x.toLocaleString() : String(x); }
async function runInvestmentCalc() {
const params = {
amount: document.getElementById('calcInvAmount').value,
upside: document.getElementById('calcInvUpside').value,
downside: document.getElementById('calcInvDown').value,
years: document.getElementById('calcInvYears').value,
probability: document.getElementById('calcInvProb').value,
};
const el = document.getElementById('calcInvResult');
el.textContent = 'Calculatingโฆ';
try {
const out = await execCalc('INVESTMENT', params);
const r = out.results || {};
el.innerHTML = `
<div>Expected Value: <strong>${fmtNum(r.expected)}</strong></div>
<div>ROI: <strong>${fmtNum(r.roiPercent)}%</strong></div>
<div style="margin-top:6px; color:var(--sub);">${out.explanation || ''}</div>
`;
} catch (e) { el.textContent = 'Error: ' + e.message; }
}
async function runVendorDiscountCalc() {
const params = {
amount: document.getElementById('calcVDAmount').value,
discount: document.getElementById('calcVDDisc').value,
days: document.getElementById('calcVDDays').value,
};
const el = document.getElementById('calcVDResult');
el.textContent = 'Calculatingโฆ';
try {
const out = await execCalc('VENDOR_DISCOUNT', params);
const r = out.results || {};
el.innerHTML = `
<div>Savings: <strong>${fmtNum(r.savings)}</strong></div>
<div>Pay Amount: <strong>${fmtNum(r.discountedPrice)}</strong></div>
${r.effectiveAnnualRatePercent != null ? `<div>Effective Annualized Rate: <strong>${fmtNum(Number(r.effectiveAnnualRatePercent).toFixed(2))}%</strong></div>` : ''}
<div style="margin-top:6px; color:var(--sub);">${out.explanation || ''}</div>
`;
} catch (e) { el.textContent = 'Error: ' + e.message; }
}
async function runDepreciationCalc() {
const params = {
cost: document.getElementById('calcDepCost').value,
method: document.getElementById('calcDepMethod').value,
useful_life_years: document.getElementById('calcDepLife').value,
residual_percent: document.getElementById('calcDepResidual').value,
rate: document.getElementById('calcDepRate').value,
date_ready: document.getElementById('calcDepReady').value,
year_end: document.getElementById('calcDepYE').value,
};
const el = document.getElementById('calcDepResult');
el.textContent = 'Calculatingโฆ';
try {
const out = await execCalc('DEPRECIATION', params);
const r = out.results || {};
el.innerHTML = `
${r.annualDepreciation != null ? `<div>Annual Depreciation: <strong>${fmtNum(r.annualDepreciation)}</strong></div>` : ''}
<div>First Period Depreciation: <strong>${fmtNum(r.firstPeriodDepreciation)}</strong></div>
<div>Closing Carrying Amount: <strong>${fmtNum(r.closingCarryingAmount)}</strong></div>
${r.ratePercent != null ? `<div>Rate: <strong>${fmtNum(Number(r.ratePercent).toFixed(2))}%</strong></div>` : ''}
<div style="margin-top:6px; color:var(--sub);">${out.explanation || ''}</div>
`;
} catch (e) { el.textContent = 'Error: ' + e.message; }
}
async function runCustomCalc() {
const expr = document.getElementById('calcExpr').value;
const varsText = document.getElementById('calcVars').value || '';
const el = document.getElementById('calcExprResult');
el.textContent = 'Calculatingโฆ';
try {
const vars = {};
// Parse key=value pairs separated by comma
varsText.split(',').forEach(pair => {
const p = pair.trim();
if (!p) return;
const [k, v] = p.split('=');
if (k && v != null) vars[k.trim()] = v.trim();
});
const r = await fetch('/api/calculators/custom/execute', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expression: expr, vars })
});
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Calculation failed');
el.innerHTML = `<div>Result: <strong>${fmtNum(j.result)}</strong></div>`;
} catch (e) { el.textContent = 'Error: ' + e.message; }
}
function saveCustomCalcDialog() {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
const dialog = document.createElement('div');
dialog.style.cssText = 'background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:12px;width:420px;max-width:92vw;box-shadow:0 10px 30px rgba(0,0,0,0.4);';
dialog.innerHTML = `
<div style="padding:14px 16px;border-bottom:1px solid var(--border);font-weight:600;">Save Calculator</div>
<div style="padding:14px 16px;">
<div class="form-group"><label class="form-label">Name</label><input id="calcSaveName" class="form-input" type="text" placeholder="My ROI Calculator"/></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;padding:12px 16px;border-top:1px solid var(--border);">
<button id="dlgCancel" class="btn secondary">Cancel</button>
<button id="dlgSave" class="btn">Save</button>
</div>`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
overlay.querySelector('#dlgCancel').addEventListener('click', () => document.body.removeChild(overlay));
overlay.querySelector('#dlgSave').addEventListener('click', async () => {
const name = document.getElementById('calcSaveName').value.trim();
const expression = document.getElementById('calcExpr').value.trim();
if (!name || !expression) { alert('Please provide a name and expression'); return; }
try {
const r = await fetch('/api/calculators/custom/save', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, expression })
});
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Save failed');
await loadSavedCalculators();
document.body.removeChild(overlay);
} catch (e) { alert('Error: ' + e.message); }
});
}
async function loadSavedCalculators() {
try {
const r = await fetch('/api/calculators/custom/list');
const j = await r.json();
const cont = document.getElementById('savedCalcs');
if (!r.ok || j.success === false) { cont.textContent = 'Failed to load saved calculators'; return; }
const items = j.items || [];
if (!items.length) { cont.innerHTML = '<div style="color:var(--sub);">No saved calculators yet</div>'; return; }
cont.innerHTML = items.map((it, idx) => {
const exprEsc = (it.expression || '').replace(/</g, '<');
const nameEsc = (it.name || '').replace(/</g, '<');
return `
<div style="border:1px solid var(--border); border-radius:10px; background:var(--panel);">
<button class="btn" style="width:100%; text-align:left; border:none; border-bottom:1px solid var(--border); border-radius:10px 10px 0 0; background:transparent; padding:12px;" onclick="toggleSavedCalc(${idx})">
โธ ${nameEsc}
</button>
<div id="savedBody_${idx}" style="display:none; padding:12px;">
<div style="font-size:12px;color:var(--sub);margin-bottom:8px;">${exprEsc}</div>
<div class="form-group"><label class="form-label">Variables</label><input id="savedVars_${idx}" class="form-input" type="text" placeholder="a=1, b=2"/></div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn" onclick="runSavedCalc(${idx}, ${JSON.stringify(it).replace(/"/g,'"')})">Run</button>
<button class="btn secondary" onclick="editSavedCalc(${idx}, '${it.id}', '${it.name.replace(/'/g, "'")}', ${JSON.stringify(it.expression).replace(/"/g,'"')})">Edit</button>
<button class="btn secondary" onclick="renameSavedCalc('${it.id}', '${it.name.replace(/'/g, "'")}')">Rename</button>
<button class="btn secondary" onclick="deleteSavedCalc('${it.id}')">Delete</button>
</div>
<div id="savedOut_${idx}" style="margin-top:10px; font-size:13px;"></div>
</div>
</div>`;
}).join('');
} catch (e) {
const cont = document.getElementById('savedCalcs');
cont.textContent = 'Error loading saved calculators';
}
}
async function runSavedCalc(idx, it) {
const el = document.getElementById('savedOut_' + idx);
const vst = document.getElementById('savedVars_' + idx).value || '';
el.textContent = 'Calculatingโฆ';
try {
const vars = {};
vst.split(',').forEach(pair => { const p = pair.trim(); if (!p) return; const [k,v] = p.split('='); if (k && v!=null) vars[k.trim()] = v.trim(); });
const r = await fetch('/api/calculators/custom/execute', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ expression: it.expression, vars }) });
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Calculation failed');
el.innerHTML = `<div>Result: <strong>${fmtNum(j.result)}</strong></div>`;
} catch (e) { el.textContent = 'Error: ' + e.message; }
}
function toggleSavedCalc(idx) {
const body = document.getElementById('savedBody_' + idx);
if (!body) return;
const isHidden = (body.style.display === 'none' || !body.style.display);
body.style.display = isHidden ? 'block' : 'none';
// Update arrow in button
const parent = body.parentElement;
if (parent) {
const btn = parent.querySelector('button.btn');
if (btn) btn.innerHTML = (isHidden ? 'โพ ' : 'โธ ') + btn.textContent.replace(/^โธ\s|^โพ\s/, '');
}
}
async function editSavedCalc(idx, id, name, expression) {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
const dialog = document.createElement('div');
dialog.style.cssText = 'background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:12px;width:520px;max-width:92vw;box-shadow:0 10px 30px rgba(0,0,0,0.4);';
dialog.innerHTML = `
<div style=\"padding:14px 16px;border-bottom:1px solid var(--border);font-weight:600;\">Edit Calculator</div>
<div style=\"padding:14px 16px;\">
<div class=\"form-group\"><label class=\"form-label\">Name</label><input id=\"editCalcName\" class=\"form-input\" type=\"text\" value=\"${name}\"/></div>
<div class=\"form-group\"><label class=\"form-label\">Expression</label><textarea id=\"editCalcExpr\" class=\"form-input\" style=\"min-height:120px;\">${expression.replace(/</g,'<')}</textarea></div>
</div>
<div style=\"display:flex;gap:10px;justify-content:flex-end;padding:12px 16px;border-top:1px solid var(--border);\">
<button id=\"dlgCancel\" class=\"btn secondary\">Cancel</button>
<button id=\"dlgSave\" class=\"btn\">Save</button>
</div>`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
overlay.querySelector('#dlgCancel').addEventListener('click', () => document.body.removeChild(overlay));
overlay.querySelector('#dlgSave').addEventListener('click', async () => {
const newName = document.getElementById('editCalcName').value.trim();
const newExpr = document.getElementById('editCalcExpr').value.trim();
if (!newName || !newExpr) { alert('Please provide name and expression'); return; }
try {
const r = await fetch(`/api/calculators/custom/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ name: newName, expression: newExpr }) });
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Update failed');
await loadSavedCalculators();
document.body.removeChild(overlay);
} catch (e) { alert('Error: ' + e.message); }
});
}
async function renameSavedCalc(id, currentName) {
const name = prompt('New name for calculator', currentName || '');
if (!name) return;
try {
const r = await fetch(`/api/calculators/custom/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ name }) });
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Rename failed');
await loadSavedCalculators();
} catch (e) { alert('Error: ' + e.message); }
}
async function deleteSavedCalc(id) {
if (!confirm('Delete this calculator?')) return;
try {
const r = await fetch(`/api/calculators/custom/${id}`, { method:'DELETE' });
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Delete failed');
await loadSavedCalculators();
} catch (e) { alert('Error: ' + e.message); }
}
function toggleFloatChat() {
const pane = document.getElementById('floatChat');
const show = (pane.style.display === 'none' || !pane.style.display);
pane.style.display = show ? 'flex' : 'none';
if (show) {
// Default provider to currentProvider
const sel = document.getElementById('floatChatProvider');
if (sel) sel.value = currentProvider;
}
}
function toggleFloatChatExpand() {
const pane = document.getElementById('floatChat');
pane.classList.toggle('expanded');
}
async function sendFloatChat() {
const input = document.getElementById('floatChatInput');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
const box = document.getElementById('floatChatMessages');
const add = (role, text) => {
const div = document.createElement('div');
div.className = 'bubble ' + (role === 'user' ? 'user' : 'assistant');
if (role === 'assistant') {
// Use existing markdown renderer for formatting
div.innerHTML = renderMarkdown(text);
} else {
div.textContent = text;
}
box.appendChild(div);
box.scrollTop = box.scrollHeight;
};
add('user', msg);
try {
const providerOverride = (document.getElementById('floatChatProvider') && document.getElementById('floatChatProvider').value) || currentProvider;
const r = await fetch('/api/chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ provider: providerOverride, messages: [{ role:'user', content: msg }], max_tokens: 512 }) });
const j = await r.json();
if (!r.ok || j.error) throw new Error(j.error || 'Chat failed');
add('assistant', j.text || '(no response)');
} catch (e) {
add('assistant', 'Error: ' + e.message);
}
}
// Conversation management
function loadConversations() {
conversationsList.innerHTML = '';
if (conversations.length === 0) {
conversationsList.innerHTML = '<div style="color: var(--sub); font-size: 12px; text-align: center; padding: 20px;">No conversations yet</div>';
return;
}
conversations.forEach(conv => {
const item = document.createElement('div');
item.className = 'conversation-item';
if (conv.id === currentConversationId) item.classList.add('active');
const preview = conv.messages.length > 0
? conv.messages[conv.messages.length - 1].content.substring(0, 50) + '...'
: 'New conversation';
const sourceIcon = conv.source === 'cli' ? 'โจ๏ธ' : '๐ฌ';
const providerInfo = conv.provider ? ` (${getProviderDisplayName(conv.provider)})` : '';
// Build inner content; actions via context menu (right-click)
item.innerHTML = `
<div class="conversation-title">${sourceIcon} ${conv.title}</div>
<div class="conversation-preview">${preview}</div>
<div class="conversation-time">${new Date(conv.updated).toLocaleDateString()}${providerInfo}</div>
`;
item.onclick = () => loadConversation(conv.id);
item.oncontextmenu = (ev) => { ev.preventDefault(); openConversationMenu(ev, '${conv.id}'); };
conversationsList.appendChild(item);
});
}
function startNewConversation() {
const btn = document.getElementById('newConversationBtn');
const originalText = btn.textContent;
// Visual feedback
btn.textContent = 'โจ Creating...';
btn.disabled = true;
const newConv = {
id: Date.now().toString(),
title: 'New Conversation',
messages: [],
created: new Date().toISOString(),
updated: new Date().toISOString()
};
conversations.unshift(newConv);
currentConversationId = newConv.id;
saveConversations();
loadConversations();
clearChat();
showView('chat');
// Reset button with success feedback
setTimeout(() => {
btn.textContent = 'โ
Created!';
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
}, 1000);
}, 200);
}
function loadConversation(id) {
const conv = conversations.find(c => c.id === id);
if (!conv) return;
currentConversationId = id;
clearChat();
conv.messages.forEach(msg => {
addMessage(msg.role, msg.content, false);
});
loadConversations();
showView('chat');
}
function saveConversations() {
localStorage.setItem('c9ai-conversations', JSON.stringify(conversations));
}
async function saveConversationServer(id) {
try {
const conv = conversations.find(c => c.id === id);
if (!conv) return;
const r = await fetch('/api/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(conv) });
const j = await r.json();
if (!r.ok) throw new Error(j.error || 'Save failed');
// store server id for future updates
if (j.conversation && j.conversation.id) {
conv.serverId = j.conversation.id;
saveConversations();
}
// Optional: show a small toast
alert('Conversation saved');
} catch (e) {
alert('Save failed: ' + e.message);
}
}
function renameConversation(id) {
const conv = conversations.find(c => c.id === id);
if (!conv) return;
const name = prompt('Rename conversation', conv.title || '');
if (!name) return;
conv.title = name.trim();
conv.updated = new Date().toISOString();
// Update on server if known there
if (conv.serverId) {
fetch('/api/conversations/' + encodeURIComponent(conv.serverId), {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: conv.title })
}).catch(() => {});
}
saveConversations();
loadConversations();
alert('Renamed');
}
function deleteConversation(id) {
if (!confirm('Delete this conversation?')) return;
const idx = conversations.findIndex(c => c.id === id);
if (idx === -1) return;
conversations.splice(idx, 1);
if (currentConversationId === id) {
currentConversationId = conversations[0]?.id || null;
clearChat();
if (currentConversationId) loadConversation(currentConversationId);
}
saveConversations();
loadConversations();
}
// Context menu logic
let convMenuTargetId = null;
function openConversationMenu(ev, id) {
convMenuTargetId = id;
const menu = document.getElementById('convMenu');
if (!menu) return;
// Position near cursor with viewport bounds check
const pad = 6;
const rect = { w: menu.offsetWidth || 200, h: menu.offsetHeight || 120 };
let x = ev.clientX, y = ev.clientY;
if (x + rect.w + pad > window.innerWidth) x = window.innerWidth - rect.w - pad;
if (y + rect.h + pad > window.innerHeight) y = window.innerHeight - rect.h - pad;
menu.style.left = x + 'px';
menu.style.top = y + 'px';
menu.style.display = 'block';
// Close on outside click
const close = (e) => { if (!menu.contains(e.target)) { menu.style.display = 'none'; document.removeEventListener('mousedown', close); } };
document.addEventListener('mousedown', close);
}
function convMenuRename() { if (convMenuTargetId) { renameConversation(convMenuTargetId); hideConvMenu(); } }
function convMenuSave() { if (convMenuTargetId) { saveConversationServer(convMenuTargetId); hideConvMenu(); } }
function convMenuDelete() { if (convMenuTargetId) { deleteConversation(convMenuTargetId); hideConvMenu(); } }
function hideConvMenu() { const menu = document.getElementById('convMenu'); if (menu) menu.style.display = 'none'; }
function updateCurrentConversation(role, content) {
if (!currentConversationId) {
startNewConversation();
}
const conv = conversations.find(c => c.id === currentConversationId);
if (!conv) return;
conv.messages.push({ role, content, timestamp: new Date().toISOString() });
conv.updated = new Date().toISOString();
// Update title based on first user message
if (conv.messages.length === 1 && role === 'user') {
conv.title = content.substring(0, 30) + (content.length > 30 ? '...' : '');
}
saveConversations();
loadConversations();
}
// Chat functionality
function clearChat() {
chatMessages.innerHTML = '';
welcomeScreen.style.display = 'flex';
}
function addMessage(role, content, save = true) {
if (welcomeScreen.style.display !== 'none') {
welcomeScreen.style.display = 'none';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : '๐ค';
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
messageContent.innerHTML = renderMarkdown(content);
const messageTime = document.createElement('div');
messageTime.className = 'message-time';
messageTime.textContent = new Date().toLocaleTimeString();
messageDiv.appendChild(avatar);
const contentWrapper = document.createElement('div');
contentWrapper.appendChild(messageContent);
contentWrapper.appendChild(messageTime);
messageDiv.appendChild(contentWrapper);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
if (save) {
updateCurrentConversation(role, content);
}
}
function addProgressMessage(statusText) {
console.log('Adding progress message:', statusText); // Debug log
const messageId = 'progress-' + Date.now();
const messageDiv = document.createElement('div');
messageDiv.id = messageId;
messageDiv.className = 'message assistant progress';
messageDiv.style.cssText = 'opacity: 0.7; font-style: italic; background: var(--muted);';
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = 'โก'; // Progress indicator
avatar.style.cssText = 'animation: pulse 1.5s infinite;';
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
messageContent.innerHTML = `<span style="color: var(--primary);">${statusText}</span>`;
const messageTime = document.createElement('div');
messageTime.className = 'message-time';
messageTime.textContent = new Date().toLocaleTimeString();
messageDiv.appendChild(avatar);
const contentWrapper = document.createElement('div');
contentWrapper.appendChild(messageContent);
contentWrapper.appendChild(messageTime);
messageDiv.appendChild(contentWrapper);
if (welcomeScreen.style.display !== 'none') {
welcomeScreen.style.display = 'none';
}
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageId;
}
// Cloud escalation confirmation helpers
function detectIntentLocal(text) {
const t = String(text || '').toLowerCase();
if (t.startsWith('@executive') || t.startsWith('@business')) return 'exec';
if (t.includes('```') || /(refactor|compile|build|test|unit test|patch|pr|typescript|python|javascript|java|golang|rust|function\s|class\s|import\s)/i.test(t)) return 'code';
if (/(calc|calculate|sum|average|percent|roi|breakeven|interest)/i.test(t)) return 'math';
return 'simple';
}
function predictCloudUse(message) {
try {
const mode = (settings && settings.defaultMode) ? settings.defaultMode : 'hybrid';
const privacy = (settings && settings.privacyProfile) ? settings.privacyProfile : 'ask_before_cloud';
const coding = (settings && settings.codingProvider) ? settings.codingProvider : 'claude';
const defaultProv = (settings && settings.provider) ? settings.provider : 'llamacpp';
const userOverrode = currentProvider !== defaultProv;
const intent = detectIntentLocal(message);
// Strict local blocks any cloud
if (privacy === 'strict_local' || mode === 'local') {
return { willUse: false, provider: 'llamacpp', reason: 'Strict local mode' };
}
// If user manually chose a cloud/hybrid provider, ask
if (userOverrode) {
const p = currentProvider;
const isCloudy = ['claude','openai','gemini','deepseek'].some(x => p === x || p.startsWith(x));
const isHybrid = /-hybrid$/.test(p);
if (isCloudy || isHybrid) {
return { willUse: true, provider: p, reason: isHybrid ? 'Hybrid planning via cloud' : 'User-selected cloud provider' };
}
}
// Auto routing: Hybrid + code intent โ cloud-hybrid unless codingProvider is local
if (mode === 'cloud') {
return { willUse: true, provider: coding, reason: 'Cloud-only mode' };
}
if (mode === 'hybrid' && intent === 'code' && coding !== 'llamacpp') {
return { willUse: true, provider: coding + '-hybrid', reason: 'Code task in Hybrid mode' };
}
return { willUse: false, provider: 'llamacpp', reason: 'Local-first routing' };
} catch (e) {
return { willUse: false, provider: 'llamacpp', reason: 'Routing error' };
}
}
function showCloudConfirm(provider, reason, message) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
const dialog = document.createElement('div');
dialog.style.cssText = 'background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:12px;width:520px;max-width:92vw;box-shadow:0 10px 30px rgba(0,0,0,0.4);';
dialog.innerHTML = `
<div style="padding:16px 18px;border-bottom:1px solid var(--border);font-weight:600;">Confirm Cloud Escalation</div>
<div style="padding:16px 18px;">
<div style="margin-bottom:10px;">
<div style="font-size:14px;color:var(--sub);margin-bottom:6px;">Provider</div>
<div style="font-size:14px;">${provider}</div>
</div>
<div style="margin-bottom:10px;">
<div style="font-size:14px;color:var(--sub);margin-bottom:6px;">Reason</div>
<div style="font-size:14px;">${reason}</div>
</div>
<div>
<div style="font-size:14px;color:var(--sub);margin-bottom:6px;">Snippet to be sent</div>
<pre style="white-space:pre-wrap;background:var(--muted);border:1px solid var(--border);padding:10px;border-radius:8px;max-height:180px;overflow:auto;">${message.slice(0,300).replace(/[&<>"']/g,(c)=>({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c]))}${message.length>300?'โฆ':''}</pre>
<div style="font-size:12px;color:var(--sub);margin-top:8px;">Only minimal task text is sent for planning; tools execute locally.</div>
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;padding:12px 18px;border-top:1px solid var(--border);">
<button id="cloudCancel" class="btn secondary">Cancel</button>
<button id="cloudProceed" class="btn">Proceed</button>
</div>`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const cleanup = () => { try { document.body.removeChild(overlay); } catch {} };
overlay.querySelector('#cloudCancel').addEventListener('click', () => { cleanup(); resolve(false); });
overlay.querySelector('#cloudProceed').addEventListener('click', () => { cleanup(); resolve(true); });
});
}
async function sendMessage() {
const message = chatInput.value.trim();
if (!message) return;
const toAgentEl = document.getElementById('toAgent');
const isChecked = toAgentEl.checked;
// Sigil-based task detection - clean and explicit
const trimmedMessage = message.trim();
const hasSigil = trimmedMessage.startsWith('@');
// Debug logging for message tracking
if (hasSigil) {
if (trimmedMessage.startsWith('@executive') || trimmedMessage.startsWith('@business')) {
addDebugEntry('info', '๐ฏ', 'Executive request detected', { message: trimmedMessage });
// Show debug panel automatically for executive requests
if (!debugPanelVisible) {
toggleDebugPanel();
}
} else {
addDebugEntry('info', '๐ง', 'Sigil command detected', { sigil: trimmedMessage.split(' ')[0] });
}
} else {
addDebugEntry('info', '๐ญ', 'Regular chat message sent');
}
// Supported sigils for different task types
const taskSigils = [
'@calc', '@todo', '@task', '@email', '@search', '@compile', '@tex',
'@file', '@run', '@shell', '@pdf', '@image', '@video', '@github',
'@whatsapp', '@sms', '@create', '@write', '@read', '@convert', '@build',
'@executive', '@business', '@generate', '@transpile', '@xml', '@xmljs'
];
const isTask = isChecked || hasSigil;
chatInput.value = '';
sendBtn.disabled = true;
addMessage('user', message);
try {
const api = 'http://127.0.0.1:8787';
if (isTask) {
// Ask-before-cloud confirmation (client-side prediction)
const privacy = (settings && settings.privacyProfile) ? settings.privacyProfile : 'ask_before_cloud';
if (privacy === 'ask_before_cloud') {
const pred = predictCloudUse(message);
if (pred.willUse) {
const ok = await showCloudConfirm(pred.provider, pred.reason, message);
if (!ok) {
// user cancelled
addMessage('assistant', 'โ Cloud escalation cancelled. You can switch to Local mode in Settings.');
sendBtn.disabled = false;
return;
}
}
}
// Agent SSE via GET query
const allow = encodeURIComponent(JSON.stringify(["shell.run","script.run","fs.read","fs.write","web.search","jit"]));
// Smart routing: if user hasn't manually overridden provider and mode allows,
// omit provider to let server route by intent
let providerToSend = currentProvider;
try {
const userSelectedProvider = currentProvider;
const defaultProvider = (settings && settings.provider) ? settings.provider : 'llamacpp';
const mode = (settings && settings.defaultMode) ? settings.defaultMode : 'hybrid';
const privacy = (settings && settings.privacyProfile) ? settings.privacyProfile : 'ask_before_cloud';
const userOverrode = userSelectedProvider !== defaultProvider;
if (!userOverrode && mode !== 'local' && privacy !== 'strict_local') {
providerToSend = '';
}
} catch (e) { /* ignore */ }
const url = `${api}/api/agent?prompt=${encodeURIComponent(message)}${providerToSend ? `&provider=${encodeURIComponent(providerToSend)}` : ''}&allow=${allow}`;
const es = new EventSource(url);
let progressMessageId = null;
es.onmessage = (ev) => {
addDebugEntry('info', '๐ฌ', 'SSE Message received', ev.data);
addMessage('assistant', ev.data);
};
es.addEventListener("status", (ev) => {
addDebugEntry('info', '๐', 'SSE Status update', ev.data);
if (progressMessageId) {
const prevMsg = document.getElementById(progressMessageId);
if (prevMsg) prevMsg.remove();
}
progressMessageId = addProgressMessage(ev.data);
// Parse status for executive request tracking
try {
const statusText = ev.data;
if (statusText.includes('๐ง Generating function')) {
addDebugEntry('tool', '๐ง ', 'AI Function Generation started');
} else if (statusText.includes('๐ง Running tool:')) {
const toolMatch = statusText.match(/๐ง Running tool: (\w+)/);
if (toolMatch) {
addDebugEntry('tool', '๐ง', `Tool execution: ${toolMatch[1]}`);
}
} else if (statusText.includes('Calculator execution for:')) {
const exprMatch = statusText.match(/Calculator execution for: (.+)/);
if (exprMatch) {
addDebugEntry('success', '๐งฎ', `Calculator: ${exprMatch[1]}`);
}
} else if (statusText.includes('Calculator result:')) {
const resultMatch = statusText.match(/Calculator result: (.+)/);
if (resultMatch) {
try {
const resultData = JSON.parse(resultMatch[1]);
const interpretation = interpretCalculationResult(resultData.expression, resultData.result);
addDebugEntry('success', 'โ
', `Result: ${resultData.formatted} ${interpretation}`);
} catch (e) {
addDebugEntry('success', 'โ
', `Result: ${resultMatch[1]}`);
}
}
} else if (statusText.includes('functions to registry')) {
const funcMatch = statusText.match(/(\d+) functions to registry/);
if (funcMatch) {
addDebugEntry('success', '๐พ', `Saved ${funcMatch[1]} functions to registry`);
}
}
} catch (e) {
// Silent fail for status parsing
}
});
es.addEventListener("final", (ev) => {
addDebugEntry('success', '๐', 'SSE Final result received');
if (progressMessageId) {
const prevMsg = document.getElementById(progressMessageId);
if (prevMsg) prevMsg.remove();
}
try {
const j = JSON.parse(ev.data);
addMessage('assistant', j.text || ev.data);
// Track executive request completion
if (j.text && j.text.includes('Executive request processed')) {
addDebugEntry('success', '๐ฏ', 'Executive request completed successfully');
}
} catch {
addMessage('assistant', ev.data);
}
es.close();
setStatus('ready', 'Ready');
});
es.addEventListener("error", (ev) => {
addDebugEntry('error', 'โ', 'SSE Error occurred', ev?.data);
if (progressMessageId) {
const prevMsg = document.getElementById(progressMessageId);
if (prevMsg) prevMsg.remove();
}
addMessage('assistant', "Agent error: " + (ev?.data || "check server logs"));
es.close();
setStatus('error', 'Error');
});
} else {
// Regular chat
const chatProgressId = addProgressMessage('๐ค Thinking...');
const r = await fetch(api + "/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider: currentProvider, messages: [{ role:"user", content:message }] })
});
const progressMsg = document.getElementById(chatProgressId);
if (progressMsg) progressMsg.remove();
const j = await r.json().catch(()=>({}));
if (!r.ok || j?.error) {
addMessage('assistant', `Chat error (${currentProvider}): ${j?.error || r.statusText}`);
setStatus('error', 'Provider Error');
} else {
addMessage('assistant', j?.text || j?.content || JSON.stringify(j));
}
}
setStatus('ready', 'Ready');
} catch (error) {
addMessage('assistant', 'Sorry, an error occurred. Please check the server logs.');
setStatus('error', 'Error');
} finally {
sendBtn.disabled = false;
}
}
// Simulate AI response (replace with actual AI service call)
async function simulateAIResponse(message) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
const responses = [
"I understand you're asking about: " + message.substring(0, 50) + "... Let me help you with that.",
"That's an interesting question. Based on what you've asked, here's what I think...",
"I can help you with that. Let me break this down for you...",
"Great question! Here's how I would approach this problem..."
];
return responses[Math.floor(Math.random() * responses.length)];
}
function loadQuickAction(prompt) {
chatInput.value = prompt;
sendMessage();
}
// Load tools list for help page
async function loadToolsList() {
try {
const response = await fetch('/api/tools');
const data = await response.json();
const toolsList = document.getElementById('toolsList');
if (data.tools && data.tools.length > 0) {
toolsList.innerHTML = data.tools.map(tool =>
`<div style="margin-bottom: 8px;"><strong>${tool.name}</strong>: ${tool.description || 'No description'}</div>`
).join('');
} else {
toolsList.innerHTML = 'No tools available';
}
} catch (error) {
console.error('Failed to load tools:', error);
document.getElementById('toolsList').innerHTML = 'Failed to load tools';
}
}
// Functions view
async function loadFunctionsList() {
try {
// Load function statistics
document.getElementById('totalFunctions').textContent = '-';
document.getElementById('activeFunctions').textContent = '-';
document.getElementById('storageUsed').textContent = '-';
// For now, show placeholder until we create the API endpoints
document.getElementById('functionsList').innerHTML = `
<div style="text-align: center; color: var(--sub); padding: 40px;">
<div style="margin-bottom: 16px;">๐</div>
<div style="margin-bottom: 8px;">Function management interface coming soon!</div>
<div style="font-size: 14px;">Functions are currently managed through Smart Chat with @transpile commands.</div>
</div>
`;
updateFunctionStats();
} catch (error) {
console.error('Failed to load functions:', error);
document.getElementById('functionsList').innerHTML = '<div style="color: var(--danger); text-align: center;">Failed to load functions</div>';
}
}
async function updateFunctionStats() {
try {
// This would make an API call to get function stats
// For now, show placeholder data
document.getElementById('totalFunctions').textContent = '2';
document.getElementById('activeFunctions').textContent = '2';
document.getElementById('storageUsed').textContent = '1.2KB';
} catch (error) {
console.error('Failed to update function stats:', error);
}
}
function showCreateFunction() {
// Switch to Smart Chat view and show a helpful message
showView('chat');
addMessage('assistant', `
๐ **Create a new function with XML-Lisp**
Use the @transpile command to create functions. Here's an example:
\`\`\`
@transpile <function name="square">
<params>
<param name="n" type="number"/>
</params>
<body>
<multiply>
<ref>n</ref>
<ref>n</ref>
</multiply>
</body>
</function>
\`\`\`
After transpiling, you can use it with: \`@calc square(5)\`
`);
}
function refreshFunctions() {
if (currentView === 'functions') {
loadFunctionsList();
}
}
function openFunctionsDirectory() {
addMessage('assistant', `๐ Functions are stored at: \`~/.c9ai/functions/\`
You can access them directly with:
\`\`\`bash
ls ~/.c9ai/functions/
cat ~/.c9ai/functions/test.xmlp
\`\`\`
Each function consists of:
- \`.xmlp\` file (XML-Lisp source)
- \`.js\` file (transpiled JavaScript)
- \`registry.json\` (metadata)`);
}
// Event listeners
function setupEventListeners() {
// Quick health check
document.getElementById('btnHealth')?.addEventListener('click', async ()=>{
try {
const r = await fetch('/api/health');
const j = await r.json();
addMessage('assistant', `llama: ${j.llama ? 'ok' : 'down'} ยท claude key: ${j.claude} ยท gemini key: ${j.gemini} ยท openai key: ${j.openai}`);
} catch(e) {
addMessage('assistant', 'Health check failed');
}
});
// Navigation
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function() {
const view = this.dataset.view;
if (view) showView(view);
});
});
// Provider switcher
document.querySelectorAll('[data-provider]').forEach(btn => {
btn.addEventListener('click', function() {
currentProvider = this.dataset.provider;
updateProviderButtons();
setStatus('ready', 'Ready'); // This will show the provider name
});
});
// Chat input
chatInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'n':
e.preventDefault();
startNewConversation();
break;
case ',':
e.preventDefault();
showSettings();
break;
case 'd':
e.preventDefault();
toggleDebugPanel();
addDebugEntry('info', 'โจ๏ธ', 'Debug panel toggled via keyboard shortcut (Ctrl+D)');
break;
}
}
// Function key shortcuts
switch(e.key) {
case 'F12':
e.preventDefault();
toggleDebugPanel();
addDebugEntry('info', '๐ง', 'Debug panel toggled via F12');
break;
}
});
}
// CLI Access Functions
function launchCLI(mode) {
// Get current provider from settings
const currentProvider = settings.provider || 'llamacpp';
let command;
const providerFlag = currentProvider !== 'llamacpp' ? ` --provider ${currentProvider}` : '';
switch(mode) {
case 'interactive':
command = `c9ai${providerFlag}`;
break;
case 'agent':
command = `c9ai agent${providerFlag}`;
break;
case 'models':
command = 'c9ai models list';
break;
case 'stack':
command = 'c9ai stack';
break;
default:
command = `c9ai${providerFlag}`;
}
// Create instruction modal
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center;
z-index: 10000;
`;
const content = document.createElement('div');
content.style.cssText = `
background: var(--panel); border-radius: 12px; padding: 30px;
max-width: 600px; width: 90%; border: 1px solid var(--border);
`;
content.innerHTML = `
<h3 style="margin-bottom: 20px;">๐ฅ๏ธ Launch CLI</h3>
<p style="margin-bottom: 15px;">Open your terminal and run:</p>
<div style="background: var(--muted); padding: 15px; border-radius: 8px; font-family: monospace; font-size: 14px; margin-bottom: 20px; color: var(--primary);">
${command}
</div>
<div style="margin-bottom: 20px; padding: 12px; background: var(--primary-muted); border-radius: 6px; border-left: 4px solid var(--primary);">
<strong>Provider:</strong> ${getProviderDisplayName(currentProvider)}<br>
<small style="opacity: 0.8;">CLI will use your current UI provider settings</small>
</div>
<div style="margin-bottom: 20px; font-size: 14px; color: var(--sub);">
<strong>Installation required?</strong> If the command doesn't work, you may need to install the CLI globally:
<code style="background: var(--muted); padding: 2px 6px; border-radius: 3px; margin: 0 4px;">npm install -g .</code>
</div>
${currentProvider !== 'llamacpp' ? `
<div style="margin-bottom: 20px; padding: 12px; background: var(--muted); border-radius: 8px; font-size: 13px;">
<strong>โ ๏ธ API Key Required:</strong> Your CLI will use <strong>${getProviderDisplayName(currentProvider)}</strong>.
Make sure you have the appropriate API key set as an environment variable or the CLI will fall back to local models.
<br><br>
<strong>Environment variables needed:</strong>
${currentProvider === 'claude' || currentProvider.includes('claude') ? '<br>โข ANTHROPIC_API_KEY' : ''}
${currentProvider === 'openai' || currentProvider.includes('openai') ? '<br>โข OPENAI_API_KEY' : ''}
${currentProvider === 'gemini' || currentProvider.includes('gemini') ? '<br>โข GEMINI_API_KEY' : ''}
${currentProvider === 'deepseek' || currentProvider.includes('deepseek') ? '<br>โข DEEPSEEK_API_KEY' : ''}
</div>
` : ''}
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn secondary" onclick="this.closest('[style*=fixed]').remove()">Close</button>
${currentProvider !== 'llamacpp' ? `
<button class="btn secondary" onclick="showEnvSetup('${currentProvider}')">๐ง Setup Env</button>
` : ''}
<button class="btn" onclick="copyCLICommand('${command}')">๐ Copy Command</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
// Track CLI launch with enhanced conversation sync
trackCLISessionEnhanced({
command: command,
provider: currentProvider,
timestamp: new Date().toISOString(),
launchedFrom: 'ui'
});
}
function copyCLICommand(command) {
navigator.clipboard.writeText(command).then(() => {
// Show feedback
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'โ
Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
function getProviderDisplayName(id) {
if (id.endsWith('-hybrid')) {
const base = id.replace('-hybrid', '');
return `${base.charAt(0).toUpperCase() + base.slice(1)} AI + Local Tools`;
}
switch(id) {
case 'claude': return 'Claude AI';
case 'gemini': return 'Gemini AI';
case 'openai': return 'OpenAI';
case 'deepseek': return 'DeepSeek AI';
case 'llamacpp': return 'Local Model (llama.cpp)';
case 'ollama': return 'Local Model (Ollama)';
default: return id;
}
}
function showEnvSetup(provider) {
const apiKey = getApiKeyFromSettings(provider);
const envVars = getEnvironmentVariables(provider);
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
const content = document.createElement('div');
content.style.cssText = `
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`;
content.innerHTML = `
<h3 style="margin-bottom: 16px;">๐ง Environment Setup for ${getProviderDisplayName(provider)}</h3>
${apiKey ? `
<div style="margin-bottom: 20px; padding: 12px; background: var(--ok); color: white; border-radius: 8px; font-size: 14px;">
โ
API key is configured in UI settings. You can copy the export commands below to use the same key in your terminal.
</div>
` : `
<div style="margin-bottom: 20px; padding: 12px; background: var(--danger); color: white; border-radius: 8px; font-size: 14px;">
โ ๏ธ No API key found in UI settings. Please add it in Settings first, or set up environment variables manually.
</div>
`}
<div style="margin-bottom: 20px;">
<h4 style="margin-bottom: 10px;">Terminal Commands:</h4>
<p style="color: var(--sub); font-size: 14px; margin-bottom: 12px;">
Copy and paste these commands in your terminal before running the CLI:
</p>
${envVars.map(env => `
<div style="margin-bottom: 12px;">
<strong>${env.name}:</strong>
<div style="background: var(--bg); padding: 12px; border-radius: 6px; font-family: monospace; margin-top: 4px; position: relative;">
<span style="font-size: 12px; color: var(--sub);">Bash/Zsh:</span>
<br>
<code>export ${env.name}="${apiKey || 'your_api_key_here'}"</code>
<br><br>
<span style="font-size: 12px; color: var(--sub);">PowerShell:</span>
<br>
<code>$env:${env.name}="${apiKey || 'your_api_key_here'}"</code>
<button onclick="copyEnvCommand('export ${env.name}=\\\"${apiKey || 'your_api_key_here'}\\\"')"
style="position: absolute; top: 8px; right: 8px; background: var(--primary); color: white; border: none; padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer;">
๐ Copy
</button>
</div>
</div>
`).join('')}
</div>
<div style="margin-bottom: 20px; padding: 12px; background: var(--muted); border-radius: 8px; font-size: 13px;">
<strong>๐ก Pro Tip:</strong> Add these export commands to your shell profile (.bashrc, .zshrc, or PowerShell profile)
to make them permanent.
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn secondary" onclick="this.closest('[style*=fixed]').remove()">Close</button>
${apiKey ? `
<button class="btn" onclick="copyAllEnvCommands('${provider}')">๐ Copy All Commands</button>
` : ''}
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
}
function getApiKeyFromSettings(provider) {
const cleanProvider = provider.replace('-hybrid', '');
switch(cleanProvider) {
case 'claude': return settings.apiKeys?.ANTHROPIC_API_KEY;
case 'openai': return settings.apiKeys?.OPENAI_API_KEY;
case 'gemini': return settings.apiKeys?.GEMINI_API_KEY;
case 'deepseek': return settings.apiKeys?.DEEPSEEK_API_KEY;
default: return null;
}
}
function getEnvironmentVariables(provider) {
const cleanProvider = provider.replace('-hybrid', '');
switch(cleanProvider) {
case 'claude': return [{ name: 'ANTHROPIC_API_KEY', description: 'Claude AI API key' }];
case 'openai': return [{ name: 'OPENAI_API_KEY', description: 'OpenAI API key' }];
case 'gemini': return [{ name: 'GEMINI_API_KEY', description: 'Google Gemini API key' }];
case 'deepseek': return [{ name: 'DEEPSEEK_API_KEY', description: 'DeepSeek API key' }];
default: return [];
}
}
function copyEnvCommand(command) {
navigator.clipboard.writeText(command).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'โ
Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
function copyAllEnvCommands(provider) {
const apiKey = getApiKeyFromSettings(provider);
const envVars = getEnvironmentVariables(provider);
const commands = envVars.map(env => `export ${env.name}="${apiKey}"`).join('\n');
navigator.clipboard.writeText(commands).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'โ
All Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
// CLI Session Tracking
let cliSessions = [];
function trackCLISession(session) {
cliSessions.unshift(session);
// Keep only last 50 sessions
if (cliSessions.length > 50) {
cliSessions = cliSessions.slice(0, 50);
}
saveCLISessions();
refreshCLISessions();
}
function saveCLISessions() {
try {
localStorage.setItem('c9ai-cli-sessions', JSON.stringify(cliSessions));
} catch (e) {
console.warn('Failed to save CLI sessions:', e);
}
}
function loadCLISessions() {
try {
const saved = localStorage.getItem('c9ai-cli-sessions');
if (saved) {
cliSessions = JSON.parse(saved);
}
} catch (e) {
console.warn('Failed to load CLI sessions:', e);
cliSessions = [];
}
}
function refreshCLISessions() {
const container = document.getElementById('cliSessions');
if (!container) return;
if (cliSessions.length === 0) {
container.innerHTML = `
<div style="text-align: center; color: var(--sub); padding: 40px;">
No CLI sessions tracked yet. CLI sessions will appear here once you start using the CLI.
</div>
`;
return;
}
const sessionsHTML = cliSessions.map(session => {
const statusColor = session.status === 'active' ? 'var(--ok)' : session.status === 'launched' ? 'var(--primary)' : 'var(--sub)';
const statusText = session.status || 'completed';
const hasConversation = conversations.find(c => c.sessionId === session.sessionId);
return `
<div style="border-bottom: 1px solid var(--border); padding: 12px 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<code style="background: var(--bg); padding: 4px 8px; border-radius: 4px; font-size: 13px;">${session.command}</code>
<small style="color: var(--sub);">${new Date(session.timestamp).toLocaleString()}</small>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 12px; color: var(--sub);">
Provider: ${getProviderDisplayName(session.provider)} | Source: ${session.launchedFrom}
<span style="color: ${statusColor}; margin-left: 8px;">โ ${statusText}</span>
</div>
${hasConversation ? `
<button onclick="loadConversation('cli-${session.sessionId}')" style="background: var(--primary); color: white; border: none; padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer;">
View Chat
</button>
` : ''}
</div>
</div>
`;
}).join('');
container.innerHTML = sessionsHTML;
}
function clearCLISessions() {
if (confirm('Clear all CLI session history?')) {
cliSessions = [];
saveCLISessions();
refreshCLISessions();
}
}
// Load CLI sessions on page load
loadCLISessions();
// CLI Conversation Sync Functions
function createCLIConversation(sessionId, command, provider) {
const newConv = {
id: `cli-${sessionId}`,
title: `CLI: ${command}`,
messages: [],
created: new Date().toISOString(),
updated: new Date().toISOString(),
source: 'cli',
provider: provider,
sessionId: sessionId
};
conversations.unshift(newConv);
saveConversations();
loadConversations();
return newConv;
}
function syncCLIMessage(sessionId, role, content) {
let conv = conversations.find(c => c.sessionId === sessionId && c.source === 'cli');
if (!conv) {
// Create CLI conversation if it doesn't exist
const session = cliSessions.find(s => s.sessionId === sessionId);
if (session) {
conv = createCLIConversation(sessionId, session.command, session.provider);
} else {
return; // Can't sync without session info
}
}
conv.messages.push({
role,
content,
timestamp: new Date().toISOString(),
source: 'cli'
});
conv.updated = new Date().toISOString();
saveConversations();
loadConversations();
// If this CLI conversation is currently active, update the display
if (currentConversationId === conv.id) {
displayConversation(conv);
}
}
function pollCLIConversations() {
// This would connect to a WebSocket or polling endpoint to get CLI conversations
// For now, this is a placeholder for future implementation
fetch('/api/cli/conversations')
.then(response => response.json())
.then(data => {
if (data.conversations) {
data.conversations.forEach(cliConv => {
syncCLIMessage(cliConv.sessionId, cliConv.role, cliConv.content);
});
}
})
.catch(error => {
// Silently handle errors - CLI might not be running
console.debug('CLI conversation sync not available:', error);
});
}
function markCLISessionActive(sessionId) {
// Update CLI session to track when it becomes active
const session = cliSessions.find(s => s.sessionId === sessionId);
if (session) {
session.status = 'active';
session.lastActivity = new Date().toISOString();
saveCLISessions();
refreshCLISessions();
// Create conversation entry immediately
createCLIConversation(sessionId, session.command, session.provider);
}
}
// Enhanced CLI session tracking with conversation sync
function trackCLISessionEnhanced(session) {
// Add unique session ID for tracking
session.sessionId = session.sessionId || `cli-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
session.status = 'launched';
trackCLISession(session);
// Start polling for this session's conversations after a brief delay
setTimeout(() => {
markCLISessionActive(session.sessionId);
}, 2000);
return session.sessionId;
}
// Start polling for CLI conversations every 5 seconds (when in CLI view)
setInterval(() => {
if (currentView === 'cli') {
pollCLIConversations();
}
}, 5000);
// =================================
// TOOL PACKAGE MANAGER UI FUNCTIONS
// =================================
let toolsData = { catalog: null, stats: null };
let currentToolFilter = 'all';
// Load tools data when tools view is opened
// Package Manager Functions
let currentToolSource = 'curated';
let packageManagers = [];
async function switchToolSource(source) {
currentToolSource = source;
// Update active tab
document.querySelectorAll('[data-source]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.source === source);
});
// Show/hide relevant sections
const packageSearch = document.getElementById('packageSearch');
const categoryFilters = document.querySelector('.category-filters');
const toolGrid = document.getElementById('toolGrid');
if (source === 'packages') {
packageSearch.style.display = 'block';
categoryFilters.style.display = 'none';
await loadPackageManagers();
toolGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
Use the search box above to find packages across all available package managers.
<br><br>
Examples: "pandoc", "ffmpeg", "python requests", "node express"
</div>
`;
} else if (source === 'installed') {
packageSearch.style.display = 'none';
categoryFilters.style.display = 'none';
await loadInstalledPackages();
} else {
packageSearch.style.display = 'none';
categoryFilters.style.display = 'block';
await loadToolsData();
}
}
async function loadPackageManagers() {
try {
const response = await fetch('/api/packages/managers');
if (response.ok) {
const data = await response.json();
packageManagers = data.managers;
displayPackageManagerStatus(data);
} else {
throw new Error('Failed to load package managers');
}
} catch (error) {
console.error('Error loading package managers:', error);
document.getElementById('packageManagerStatus').innerHTML = `
<div style="color: var(--danger);">โ Failed to load package managers</div>
`;
}
}
function displayPackageManagerStatus(data) {
const statusDiv = document.getElementById('packageManagerStatus');
const { managers, stats, totalEstimatedPackages } = data;
if (managers.length === 0) {
statusDiv.innerHTML = `
<div style="color: var(--danger); text-align: center; padding: 20px;">
โ No package managers detected. Please install Homebrew, npm, pip, or other package managers.
</div>
`;
return;
}
const managerTags = managers.map(m => {
const typeColors = {
system: 'var(--primary)',
language: 'var(--ok)',
universal: 'var(--sub)'
};
return `
<span style="
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--muted);
border-radius: 16px;
border: 1px solid ${typeColors[m.type] || 'var(--border)'};
font-size: 12px;
color: var(--text);
">
<span style="color: ${typeColors[m.type]};">โ</span>
${m.name} v${m.version}
<span style="opacity: 0.7;">(${m.ecosystem})</span>
</span>
`;
}).join('');
statusDiv.innerHTML = `
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center;">
${managerTags}
<span style="margin-left: 12px; padding: 6px 12px; background: var(--primary); color: white; border-radius: 16px; font-size: 12px; font-weight: 500;">
~${totalEstimatedPackages.toLocaleString()} packages available
</span>
</div>
`;
}
async function searchPackages() {
const query = document.getElementById('packageSearchInput').value.trim();
if (!query) return;
const resultsDiv = document.getElementById('searchResults');
const packageResults = document.getElementById('packageResults');
const countDiv = document.getElementById('searchResultsCount');
// Show loading state
resultsDiv.style.display = 'block';
packageResults.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
๐ Searching for "${query}" across all package managers...
</div>
`;
try {
const response = await fetch(`/api/packages/search?q=${encodeURIComponent(query)}&limit=20`);
if (response.ok) {
const data = await response.json();
displaySearchResults(data);
} else {
throw new Error('Search failed');
}
} catch (error) {
console.error('Package search error:', error);
packageResults.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--danger);">
โ Search failed. Please try again.
</div>
`;
}
}
function displaySearchResults(data) {
const { packages, sources, total, query } = data;
const packageResults = document.getElementById('packageResults');
const countDiv = document.getElementById('searchResultsCount');
countDiv.textContent = `Found ${total} packages from ${sources.length} sources`;
if (packages.length === 0) {
packageResults.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
No packages found for "${query}".
<br><br>
Try searching for:
<br>โข System tools: pandoc, ffmpeg, git, curl
<br>โข Python packages: requests, pandas, numpy
<br>โข Node.js packages: express, react, lodash
<br>โข Development tools: docker, terraform, kubernetes
</div>
`;
return;
}
packageResults.innerHTML = packages.map(pkg => `
<div style="
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
transition: all 0.2s ease;
" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; color: var(--text);">${pkg.name}</h3>
<div style="font-size: 12px; color: var(--sub);">
${pkg.manager} โข ${pkg.source} โข Score: ${pkg.relevanceScore}
</div>
</div>
<button
class="btn primary"
style="font-size: 12px; padding: 6px 12px;"
onclick="installPackage('${pkg.name}', '${pkg.manager}')"
>
๐ฆ Install
</button>
</div>
<p style="margin: 0 0 12px 0; color: var(--sub); font-size: 14px; line-height: 1.4;">
${pkg.description}
</p>
<div style="
background: var(--muted);
padding: 8px 12px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
color: var(--text);
border-left: 3px solid var(--primary);
">
${pkg.installCommand}
</div>
</div>
`).join('');
}
async function installPackage(packageName, manager) {
const installBtn = event.target;
const originalText = installBtn.innerHTML;
// Update button to show loading
installBtn.innerHTML = 'โณ Installing...';
installBtn.disabled = true;
try {
const response = await fetch('/api/packages/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packageName, manager })
});
const result = await response.json();
if (result.success) {
installBtn.innerHTML = 'โ
Installed';
installBtn.style.background = 'var(--ok)';
// Show success message
showNotification(`โ
Successfully installed ${packageName}`, 'success');
// Refresh system tools after installation
setTimeout(() => {
if (typeof refreshToolData === 'function') {
refreshToolData();
}
}, 2000);
} else {
throw new Error(result.error || 'Installation failed');
}
} catch (error) {
console.error('Installation error:', error);
installBtn.innerHTML = 'โ Failed';
installBtn.style.background = 'var(--danger)';
showNotification(`โ Failed to install ${packageName}: ${error.message}`, 'error');
// Reset button after delay
setTimeout(() => {
installBtn.innerHTML = originalText;
installBtn.disabled = false;
installBtn.style.background = '';
}, 3000);
}
}
async function loadInstalledPackages() {
const toolGrid = document.getElementById('toolGrid');
toolGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
๐ฆ Loading installed packages...
</div>
`;
try {
const response = await fetch('/api/packages/installed');
if (response.ok) {
const data = await response.json();
displayInstalledPackages(data.installed);
} else {
throw new Error('Failed to load installed packages');
}
} catch (error) {
console.error('Error loading installed packages:', error);
toolGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--danger);">
โ Failed to load installed packages
</div>
`;
}
}
function displayInstalledPackages(installed) {
const toolGrid = document.getElementById('toolGrid');
const allPackages = [];
// Aggregate packages from all managers
Object.entries(installed).forEach(([manager, packages]) => {
packages.forEach(pkg => {
allPackages.push({ ...pkg, manager });
});
});
if (allPackages.length === 0) {
toolGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
No installed packages found.
<br><br>
Switch to the "System Packages" tab to install new tools.
</div>
`;
return;
}
toolGrid.innerHTML = allPackages.slice(0, 50).map(pkg => `
<div style="
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
transition: all 0.2s ease;
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; color: var(--text);">${pkg.name}</h3>
<div style="font-size: 12px; color: var(--sub);">
${pkg.manager} โข Version: ${pkg.version}
</div>
</div>
<span style="
padding: 4px 8px;
background: var(--ok);
color: white;
border-radius: 12px;
font-size: 10px;
font-weight: 500;
">
โ
INSTALLED
</span>
</div>
</div>
`).join('');
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
background: ${type === 'success' ? 'var(--ok)' : type === 'error' ? 'var(--danger)' : 'var(--primary)'};
color: white;
border-radius: 6px;
z-index: 10000;
font-size: 14px;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
`;
notification.textContent = message;
document.body.appendChild(notification);
// Remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
async function loadToolsData() {
if (currentView !== 'tools') return;
try {
// Load catalog and stats in parallel
const [catalogResponse, statsResponse] = await Promise.all([
fetch('/api/tools/catalog'),
fetch('/api/tools/stats')
]);
if (catalogResponse.ok && statsResponse.ok) {
toolsData.catalog = await catalogResponse.json();
toolsData.stats = await statsResponse.json();
displayToolStats();
displayCategoryFilters();
displayTools();
} else {
displayToolError('Failed to load tools data');
}
} catch (error) {
console.error('Failed to load tools:', error);
displayToolError('Error loading tools: ' + error.message);
}
}
function displayToolStats() {
if (!toolsData.stats) return;
document.getElementById('totalTools').textContent = toolsData.stats.total || 0;
document.getElementById('installedTools').textContent = toolsData.stats.installed || 0;
document.getElementById('availableTools').textContent = toolsData.stats.available || 0;
document.getElementById('builtinTools').textContent = toolsData.stats.builtin || 0;
}
function displayCategoryFilters() {
if (!toolsData.catalog) return;
const filtersContainer = document.getElementById('categoryFilters');
const categories = toolsData.catalog.categories || {};
let filtersHTML = `
<button class="btn pill ${currentToolFilter === 'all' ? 'active' : ''}" data-category="all">
All Tools (${toolsData.stats.total || 0})
</button>
<button class="btn pill ${currentToolFilter === 'available' ? 'active' : ''}" data-category="available">
Available (${toolsData.stats.available || 0})
</button>
<button class="btn pill ${currentToolFilter === 'installed' ? 'active' : ''}" data-category="installed">
Installed (${toolsData.stats.installed || 0})
</button>
`;
// Add category filters
Object.entries(categories).forEach(([catId, category]) => {
const count = toolsData.stats.categories?.[catId]?.total || 0;
filtersHTML += `
<button class="btn pill ${currentToolFilter === catId ? 'active' : ''}" data-category="${catId}">
${category.icon} ${category.name} (${count})
</button>
`;
});
filtersContainer.innerHTML = filtersHTML;
// Add click handlers
filtersContainer.querySelectorAll('[data-category]').forEach(btn => {
btn.addEventListener('click', function() {
currentToolFilter = this.dataset.category;
displayCategoryFilters();
displayTools();
});
});
}
function displayTools() {
if (!toolsData.catalog) return;
const toolGrid = document.getElementById('toolGrid');
let tools = toolsData.catalog.tools || [];
// Filter tools based on current filter
if (currentToolFilter === 'available') {
tools = tools.filter(tool => tool.status === 'available');
} else if (currentToolFilter === 'installed') {
tools = tools.filter(tool => tool.status === 'installed' || tool.builtin);
} else if (currentToolFilter !== 'all') {
tools = tools.filter(tool => tool.category === currentToolFilter);
}
if (tools.length === 0) {
toolGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--sub);">
No tools found for the selected filter.
</div>
`;
return;
}
const toolsHTML = tools.map(tool => createToolCard(tool)).join('');
toolGrid.innerHTML = toolsHTML;
}
function createToolCard(tool) {
const isInstalled = tool.status === 'installed' || tool.builtin;
const categoryInfo = toolsData.catalog.categories[tool.category] || {};
const categoryIcon = categoryInfo.icon || '๐ง';
return `
<div class="tool-card" style="
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
position: relative;
" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'">
<!-- Tool Header -->
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; font-size: 16px; color: var(--text);">${tool.name}</h3>
<div style="font-size: 12px; color: var(--sub);">
${categoryIcon} ${categoryInfo.name || tool.category} โข v${tool.version}
</div>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
${tool.builtin ? '<span style="background: var(--muted); color: var(--text); padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 500;">BUILT-IN</span>' : ''}
${isInstalled ? '<span style="background: var(--ok); color: white; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 500;">INSTALLED</span>' : '<span style="background: var(--primary); color: white; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 500;">AVAILABLE</span>'}
</div>
</div>
<!-- Tool Description -->
<p style="margin: 0 0 16px 0; color: var(--sub); font-size: 14px; line-height: 1.4;">
${tool.description}
</p>
<!-- Tool Details -->
<div style="margin-bottom: 16px;">
${tool.dependencies && tool.dependencies.length > 0 ? `
<div style="margin-bottom: 8px;">
<strong style="font-size: 12px; color: var(--text);">Dependencies:</strong>
<span style="font-size: 12px; color: var(--sub);">${tool.dependencies.join(', ')}</span>
</div>
` : ''}
${tool.author ? `
<div style="margin-bottom: 4px;">
<strong style="font-size: 12px; color: var(--text);">Author:</strong>
<span style="font-size: 12px; color: var(--sub);">${tool.author}</span>
</div>
` : ''}
</div>
<!-- Tool Actions -->
<div style="display: flex; gap: 8px;">
${isInstalled ? `
<button class="btn secondary" onclick="showToolDetails('${tool.id}')" style="flex: 1; font-size: 12px; padding: 8px;">
๐ Details
</button>
${tool.builtin ? '' : `
<button class="btn" onclick="uninstallTool('${tool.id}')" style="background: var(--danger); flex: 1; font-size: 12px; padding: 8px;">
๐๏ธ Remove
</button>
`}
` : `
<button class="btn secondary" onclick="showToolDetails('${tool.id}')" style="flex: 1; font-size: 12px; padding: 8px;">
๐ Details
</button>
<button class="btn" onclick="installTool('${tool.id}')" style="flex: 1; font-size: 12px; padding: 8px;">
๐ฆ Install
</button>
`}
</div>
</div>
`;
}
// Tool Management Functions
async function installTool(toolId) {
try {
const response = await fetch('/api/tools/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toolId })
});
const result = await response.json();
if (result.success) {
alert(`โ
${result.tool.name} installed successfully!`);
refreshToolData();
} else {
alert(`โ Installation failed: ${result.error}`);
}
} catch (error) {
console.error('Install failed:', error);
alert(`โ Installation error: ${error.message}`);
}
}
async function uninstallTool(toolId) {
const tool = toolsData.catalog.tools.find(t => t.id === toolId);
if (!tool) return;
if (!confirm(`Are you sure you want to uninstall "${tool.name}"?`)) {
return;
}
try {
const response = await fetch('/api/tools/uninstall', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toolId })
});
const result = await response.json();
if (result.success) {
alert(`โ
${tool.name} uninstalled successfully!`);
refreshToolData();
} else {
alert(`โ Uninstall failed: ${result.error}`);
}
} catch (error) {
console.error('Uninstall failed:', error);
alert(`โ Uninstall error: ${error.message}`);
}
}
function showToolDetails(toolId) {
const tool = toolsData.catalog.tools.find(t => t.id === toolId);
if (!tool) return;
const isInstalled = tool.status === 'installed' || tool.builtin;
const categoryInfo = toolsData.catalog.categories[tool.category] || {};
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
const content = document.createElement('div');
content.style.cssText = `
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`;
content.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px;">
<div>
<h2 style="margin: 0 0 4px 0;">${tool.name}</h2>
<div style="color: var(--sub);">
${categoryInfo.icon || '๐ง'} ${categoryInfo.name || tool.category} โข v${tool.version} โข ID: ${tool.id}
</div>
</div>
${isInstalled ? '<span style="background: var(--ok); color: white; padding: 4px 12px; border-radius: 16px; font-size: 12px; font-weight: 500;">INSTALLED</span>' : '<span style="background: var(--primary); color: white; padding: 4px 12px; border-radius: 16px; font-size: 12px; font-weight: 500;">AVAILABLE</span>'}
</div>
<p style="color: var(--sub); line-height: 1.6; margin-bottom: 20px;">
${tool.description}
</p>
${tool.dependencies && tool.dependencies.length > 0 ? `
<div style="margin-bottom: 16px;">
<h4 style="margin-bottom: 8px;">Dependencies:</h4>
<div style="background: var(--muted); padding: 12px; border-radius: 8px;">
${tool.dependencies.map(dep => `<code style="background: var(--bg); padding: 2px 6px; border-radius: 4px; margin-right: 8px;">${dep}</code>`).join('')}
</div>
</div>
` : ''}
${tool.schema ? `
<div style="margin-bottom: 16px;">
<h4 style="margin-bottom: 8px;">Parameters:</h4>
<div style="background: var(--muted); padding: 12px; border-radius: 8px; font-family: monospace; font-size: 12px;">
${Object.entries(tool.schema).map(([param, spec]) => `
<div style="margin-bottom: 8px;">
<strong>${param}</strong> (${spec.type}${spec.required ? ', required' : ', optional'})
${spec.description ? `<br><span style="color: var(--sub);">${spec.description}</span>` : ''}
</div>
`).join('')}
</div>
</div>
` : ''}
${tool.author ? `
<div style="margin-bottom: 16px;">
<strong>Author:</strong> <span style="color: var(--sub);">${tool.author}</span>
</div>
` : ''}
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn secondary" onclick="this.closest('[style*=fixed]').remove()">Close</button>
${isInstalled ?
(tool.builtin ? '' : `<button class="btn" onclick="uninstallTool('${tool.id}'); this.closest('[style*=fixed]').remove();" style="background: var(--danger);">๐๏ธ Uninstall</button>`) :
`<button class="btn" onclick="installTool('${tool.id}'); this.closest('[style*=fixed]').remove();">๐ฆ Install</button>`
}
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
// Close on backdrop click
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.remove();
}
});
}
function refreshToolData() {
loadToolsData();
}
function displayToolError(message) {
const toolGrid = document.getElementById('toolGrid');
toolGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--danger);">
โ ${message}
</div>
`;
}
function showBatchInstall() {
// TODO: Implement batch install modal
alert('Batch install feature coming soon!');
}
// Load tools data when switching to tools view
const originalShowView = showView;
showView = function(viewName) {
originalShowView(viewName);
if (viewName === 'tools') {
setTimeout(loadToolsData, 100); // Small delay to ensure view is shown
} else if (viewName === 'vibe') {
setTimeout(loadVibeView, 100); // Small delay to ensure view is shown
}
};
// ======= VIBE SESSION MANAGEMENT =======
let activeSession = null;
let vibeDetectionData = null;
function loadVibeView() {
// Load available templates on view load
refreshTemplates();
}
async function detectCurrentVibe() {
const energyLevel = document.getElementById('energyLevel').value;
const currentMood = document.getElementById('currentMood').value;
const availableTime = parseInt(document.getElementById('availableTime').value);
const workContext = document.getElementById('workContext').value;
if (!energyLevel) {
showNotification('Please select your energy level', 'warning');
return;
}
const contextSignals = {
energyLevel,
mood: currentMood,
availableTime,
workEnvironment: workContext.includes('quiet') ? 'quiet' : 'normal',
workContext: [workContext],
timeOfDay: new Date().getHours(),
recentActivity: ["development", "coding"],
goals: ["build", "create"]
};
try {
const response = await fetch('/api/vibe/detect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contextSignals)
});
if (!response.ok) throw new Error('Vibe detection failed');
const data = await response.json();
vibeDetectionData = data.vibe;
displayVibeResult(data.vibe);
// Auto-match templates based on detected vibe
await matchTemplatesForVibe(contextSignals);
} catch (error) {
console.error('Vibe detection error:', error);
showNotification('Failed to detect vibe: ' + error.message, 'error');
}
}
function displayVibeResult(vibeData) {
const resultDiv = document.getElementById('vibeResult');
const vibeNameDiv = document.getElementById('detectedVibe');
const vibeDescDiv = document.getElementById('vibeDescription');
const vibeConfDiv = document.getElementById('vibeConfidence');
if (vibeData.primaryVibe) {
const [vibeName, vibeInfo] = vibeData.primaryVibe;
vibeNameDiv.textContent = `๐ญ Detected Vibe: ${vibeName.replace('-', ' ')}`;
vibeDescDiv.textContent = vibeInfo.profile.description;
vibeConfDiv.textContent = `Confidence: ${(vibeData.confidence * 100).toFixed(1)}% โข Signals: ${vibeData.detectedSignals.slice(0, 3).join(', ')}`;
} else {
vibeNameDiv.textContent = '๐ญ Balanced Work Mode';
vibeDescDiv.textContent = 'General productive work session detected';
vibeConfDiv.textContent = 'Default recommendation';
}
resultDiv.style.display = 'block';
}
async function matchTemplatesForVibe(contextSignals) {
try {
const response = await fetch('/api/workflows/templates/match', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contextSignals)
});
if (!response.ok) throw new Error('Template matching failed');
const data = await response.json();
displayMatchedTemplates(data.matches);
} catch (error) {
console.error('Template matching error:', error);
showNotification('Failed to match templates: ' + error.message, 'error');
}
}
function displayMatchedTemplates(matches) {
const templatesGrid = document.getElementById('workflowTemplates');
if (matches.length === 0) {
templatesGrid.innerHTML = `
<div style="text-align: center; color: var(--sub); padding: 40px; grid-column: 1 / -1;">
<div style="font-size: 48px; margin-bottom: 16px;">๐ค</div>
<div>No templates matched your current vibe. Try adjusting your context or energy level.</div>
</div>
`;
return;
}
templatesGrid.innerHTML = matches.map(match => `
<div style="background: var(--muted); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<h4 style="margin: 0; color: var(--primary);">${match.template.name}</h4>
<span style="background: var(--primary); color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600;">
${(match.score * 100).toFixed(0)}% match
</span>
</div>
<div style="color: var(--sub); font-size: 13px; margin-bottom: 12px; line-height: 1.4;">
${match.template.description}
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 12px; color: var(--sub);">
<span>โฑ๏ธ ${match.template.vibe.duration}</span>
<span>๐ณ ${match.template.credits?.estimated || 0} credits</span>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
${match.template.vibe.tags?.slice(0, 3).map(tag => `
<span style="background: var(--panel); padding: 2px 6px; border-radius: 6px; font-size: 10px; text-transform: uppercase;">
${tag}
</span>
`).join('') || ''}
</div>
<button class="btn primary" onclick="startVibeSession('${match.template.id}')" style="width: 100%; font-size: 13px;">
Start This Workflow
</button>
</div>
`).join('');
}
async function refreshTemplates() {
try {
const response = await fetch('/api/workflows/templates');
if (!response.ok) throw new Error('Failed to load templates');
const data = await response.json();
displayAllTemplates(data.templates);
} catch (error) {
console.error('Template refresh error:', error);
showNotification('Failed to refresh templates: ' + error.message, 'error');
}
}
async function refreshSlides() {
try {
const r = await fetch('/api/slides');
if (!r.ok) throw new Error('Failed to load slides');
const data = await r.json();
displaySlides(data.slides || []);
} catch (e) {
showNotification('Slides refresh error: ' + (e && e.message ? e.message : e), 'error');
}
}
function displaySlides(slides) {
const grid = document.getElementById('slidesGrid');
if (!slides || slides.length === 0) {
grid.innerHTML = `
<div style="text-align: center; color: var(--sub); padding: 40px; grid-column: 1 / -1;">
<div style="font-size: 42px; margin-bottom: 16px;">๐๏ธ</div>
<div>No slides found. Generate from Markdown, then refresh.</div>
</div>`;
return;
}
grid.innerHTML = slides.map(s => `
<div style="background: var(--muted); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
<h4 style="margin: 0 0 8px 0; color: var(--primary); word-break: break-word;">${s.name}</h4>
<div style="font-size: 12px; color: var(--sub); margin-bottom: 10px;">${s.mtime ? new Date(s.mtime).toLocaleString() : ''}</div>
<div style="display:flex; gap:8px;">
<a class="btn primary" href="${s.url}" target="_blank" rel="noopener noreferrer">Open</a>
<button class="btn" onclick="copySlideLink('${s.url}')">Copy Link</button>
</div>
</div>
`).join('');
}
function copySlideLink(url) {
const full = location.origin + url;
navigator.clipboard.writeText(full).then(() => showNotification('Copied link: ' + full, 'success'));
}
// Generate a workflow from a prompt via cloud AI and save it server-side
function showGenerateWorkflowModal() {
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index:10000;';
const card = document.createElement('div');
card.style.cssText = 'background: var(--panel); border:1px solid var(--border); border-radius:12px; width: min(700px, 92vw); padding:20px;';
card.innerHTML = `
<h3 style="margin-top:0;">โจ Generate Workflow from Prompt</h3>
<div class="form-group"><label class="form-label">Goal</label><textarea id="gwGoal" class="form-input" style="min-height:90px;" placeholder="e.g., Write a research brief on LLM reliability"></textarea></div>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<div style="flex:1; min-width:180px;">
<label class="form-label">Provider</label>
<select id="gwProvider" class="form-select">
<option value="claude">Claude</option>
<option value="gemini">Gemini</option>
<option value="openai">OpenAI</option>
<option value="deepseek">DeepSeek</option>
</select>
</div>
<div style="flex:1; min-width:180px;">
<label class="form-label">Template ID (optional)</label>
<input id="gwId" class="form-input" type="text" placeholder="custom-research-brief"/>
</div>
<div style="flex:1; min-width:180px;">
<label class="form-label">Name (optional)</label>
<input id="gwName" class="form-input" type="text" placeholder="Research Brief"/>
</div>
</div>
<div style="display:flex; gap:8px; justify-content:flex-end; margin-top:14px;">
<button class="btn secondary" onclick="this.closest('[style*=position:fixed]').remove()">Cancel</button>
<button class="btn primary" id="gwBtn">Generate</button>
</div>`;
modal.appendChild(card);
document.body.appendChild(modal);
const btn = card.querySelector('#gwBtn');
btn.addEventListener('click', async () => {
const goal = card.querySelector('#gwGoal').value.trim();
const provider = card.querySelector('#gwProvider').value;
const id = card.querySelector('#gwId').value.trim();
const name = card.querySelector('#gwName').value.trim();
if (!goal) { showNotification('Please enter a goal', 'warning'); return; }
try {
btn.disabled = true; btn.textContent = 'Generatingโฆ';
const resp = await fetch('/api/workflows/templates/generate', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, goal, id: id || undefined, name: name || undefined })
});
const data = await resp.json();
if (!resp.ok || !data.success) throw new Error(data.error || 'Generation failed');
showNotification(`Workflow saved: ${data.template?.id || data.file}`, 'success');
modal.remove();
await refreshTemplates();
} catch (e) {
showNotification('Failed to generate workflow: ' + (e && e.message ? e.message : e), 'error');
btn.disabled = false; btn.textContent = 'Generate';
}
});
}
function displayAllTemplates(templates) {
const templatesGrid = document.getElementById('workflowTemplates');
if (templates.length === 0) {
templatesGrid.innerHTML = `
<div style="text-align: center; color: var(--sub); padding: 40px; grid-column: 1 / -1;">
<div style="font-size: 48px; margin-bottom: 16px;">๐</div>
<div>No workflow templates available</div>
</div>
`;
return;
}
templatesGrid.innerHTML = templates.map(template => `
<div style="background: var(--muted); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
<h4 style="margin: 0 0 8px 0; color: var(--primary);">${template.name}</h4>
<div style="color: var(--sub); font-size: 13px; margin-bottom: 12px; line-height: 1.4;">
${template.description}
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 12px; color: var(--sub);">
<span>โฑ๏ธ ${template.estimatedDuration}</span>
<span>๐ณ ${template.estimatedCredits} credits</span>
<span>๐ ${template.stepCount} steps</span>
</div>
<div style="display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap;">
${template.tags.slice(0, 4).map(tag => `
<span style="background: var(--panel); color: var(--sub); padding: 2px 6px; border-radius: 6px; font-size: 10px; text-transform: uppercase;">
${tag}
</span>
`).join('')}
</div>
<button class="btn secondary" onclick="startVibeSession('${template.id}')" style="width: 100%; font-size: 13px;">
Start Session
</button>
</div>
`).join('');
}
async function startVibeSession(templateId) {
try {
// Get template details first
const templateResponse = await fetch(`/api/workflows/templates/${templateId}`);
if (!templateResponse.ok) throw new Error('Template not found');
const templateData = await templateResponse.json();
const template = templateData.template;
// Initialize interactive session
activeSession = {
sessionId: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
template: template,
currentStep: 0,
completedSteps: [],
stepResults: {},
status: 'active',
startTime: new Date().toISOString()
};
displayInteractiveSession(activeSession);
showNotification(`Started interactive "${template.name}" session!`, 'success');
} catch (error) {
console.error('Start session error:', error);
showNotification('Failed to start session: ' + error.message, 'error');
}
}
function displayInteractiveSession(session) {
const sessionCard = document.getElementById('interactiveSessionCard');
const sessionTitle = document.getElementById('sessionTitle');
const sessionSubtitle = document.getElementById('sessionSubtitle');
const stepsContainer = document.getElementById('workflowStepsContainer');
sessionTitle.textContent = `๐ฏ ${session.template.name}`;
sessionSubtitle.textContent = session.template.description;
// Create interactive steps
stepsContainer.innerHTML = session.template.flow.map((step, index) =>
createInteractiveStep(step, index, session)
).join('');
// Update progress
updateWorkflowProgress(session);
sessionCard.style.display = 'block';
sessionCard.scrollIntoView({ behavior: 'smooth' });
}
function createInteractiveStep(step, index, session) {
const isCompleted = session.completedSteps.includes(index);
const isCurrent = session.currentStep === index;
const canExecute = index === session.currentStep || isCompleted;
return `
<div class="workflow-step" data-step-index="${index}" style="
background: var(--muted);
border: 2px solid ${isCurrent ? 'var(--primary)' : isCompleted ? 'var(--ok)' : 'var(--border)'};
border-radius: 12px;
padding: 20px;
${!canExecute ? 'opacity: 0.6;' : ''}
">
<!-- Step Header -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="
width: 32px; height: 32px;
border-radius: 50%;
background: ${isCompleted ? 'var(--ok)' : isCurrent ? 'var(--primary)' : 'var(--sub)'};
color: white;
display: flex; align-items: center; justify-content: center;
font-weight: 600; font-size: 14px;
">
${isCompleted ? 'โ' : index + 1}
</div>
<div>
<h4 style="margin: 0; color: var(--text);">${step.step.replace('-', ' ')}</h4>
<div style="font-size: 13px; color: var(--sub); margin-top: 2px;">
${step.estimatedTime} โข ${step.vibe} vibe
</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
${step.tools.map(tool => `
<span style="
background: var(--panel);
color: var(--sub);
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
text-transform: uppercase;
font-weight: 500;
">
${tool.split('.')[0]}
</span>
`).join('')}
</div>
</div>
<!-- Step Description -->
<div style="color: var(--sub); margin-bottom: 20px; line-height: 1.5;">
${step.description}
</div>
<!-- Step Inputs -->
<div style="background: var(--panel); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
<!-- File Upload -->
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
๐ Upload Files (Optional)
</label>
<input
type="file"
id="stepFiles_${index}"
multiple
style="
width: 100%;
padding: 8px;
border: 1px dashed var(--border);
border-radius: 6px;
background: var(--muted);
color: var(--text);
font-size: 13px;
"
/>
</div>
<!-- Quick Inputs -->
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
โก Quick Input
</label>
<input
type="text"
id="stepQuickInput_${index}"
placeholder="Enter key info, URLs, or parameters..."
style="
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--muted);
color: var(--text);
font-size: 13px;
"
/>
</div>
</div>
<!-- Custom Prompt -->
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
๐ฌ Custom Instructions
</label>
<textarea
id="stepPrompt_${index}"
placeholder="Customize how this step should be executed..."
rows="3"
style="
width: 100%;
padding: 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--muted);
color: var(--text);
font-size: 13px;
font-family: inherit;
resize: vertical;
"
>${getStepDefaultPrompt(step)}</textarea>
</div>
</div>
<!-- Step Actions -->
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 13px; color: var(--sub);">
${isCompleted ? 'โ
Completed' : canExecute ? 'Ready to execute' : 'Complete previous steps first'}
</div>
<div style="display: flex; gap: 8px;">
${isCompleted ? `
<button class="btn secondary" onclick="viewStepResults(${index})" style="font-size: 13px;">
<span>๐๏ธ</span> View Results
</button>
<button class="btn secondary" onclick="rerunStep(${index})" style="font-size: 13px;">
<span>๐</span> Re-run
</button>
` : canExecute ? `
<button class="btn primary" onclick="executeInteractiveStep(${index})" style="font-size: 13px;">
<span>๐</span> Execute Step
</button>
` : `
<button class="btn secondary" disabled style="font-size: 13px; opacity: 0.5;">
<span>โณ</span> Waiting
</button>
`}
</div>
</div>
<!-- Step Results -->
<div id="stepResults_${index}" style="display: none; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);">
<h5 style="margin: 0 0 12px 0; color: var(--primary);">๐ Step Results</h5>
<div id="stepResultsContent_${index}" style="background: var(--panel); border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; max-height: 300px; overflow-y: auto;"></div>
</div>
</div>
`;
}
function getStepDefaultPrompt(step) {
const prompts = {
'inspiration-gather': 'Research trending topics and gather inspiration for content creation. Focus on current industry trends and engaging angles.',
'draft-creation': 'Create compelling content based on the research and inputs provided. Write in an engaging, professional tone.',
'visual-enhancement': 'Generate or suggest visuals that complement the content and enhance engagement.',
'publish-distribute': 'Prepare content for publication and distribution across selected platforms.',
'data-discovery': 'Analyze and explore the provided data sources. Identify key patterns and insights.',
'data-cleaning': 'Clean and prepare the data for analysis. Handle missing values and inconsistencies.',
'exploratory-analysis': 'Perform comprehensive data analysis and generate visualizations.',
'insight-synthesis': 'Synthesize findings into actionable insights and recommendations.',
'idea-capture': 'Organize and structure the concept for rapid prototyping.',
'rapid-build': 'Create a functional prototype quickly using best practices.',
'instant-deploy': 'Deploy the prototype and make it accessible for testing.',
'feedback-collect': 'Gather user feedback and analyze usability.'
};
return prompts[step.step] || `Execute ${step.step.replace('-', ' ')} step with the provided inputs and context.`;
}
async function executeInteractiveStep(stepIndex) {
if (!activeSession) return;
const step = activeSession.template.flow[stepIndex];
const stepElement = document.querySelector(`[data-step-index="${stepIndex}"]`);
// Get user inputs
const filesInput = document.getElementById(`stepFiles_${stepIndex}`);
const quickInput = document.getElementById(`stepQuickInput_${stepIndex}`);
const customPrompt = document.getElementById(`stepPrompt_${stepIndex}`);
const userInputs = {
files: filesInput.files,
quickInput: quickInput.value,
customPrompt: customPrompt.value,
stepInfo: step
};
// Show execution in progress
const executeBtn = stepElement.querySelector('.btn.primary');
executeBtn.innerHTML = '<span>โณ</span> Executing...';
executeBtn.disabled = true;
try {
// Execute step with real agent
const result = await executeStepWithRealAgent(step, userInputs, stepIndex);
// Mark step as completed
activeSession.completedSteps.push(stepIndex);
activeSession.stepResults[stepIndex] = result;
activeSession.currentStep = stepIndex + 1;
// Show results immediately with enhanced display
displayStepResults(stepIndex, result);
// Auto-scroll to show results after a brief delay
setTimeout(() => {
const resultContainer = document.getElementById(`stepResults_${stepIndex}`);
if (resultContainer) {
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 500);
// Update UI
refreshInteractiveSession();
showNotification(`Step "${step.step}" completed! Results displayed below - click to edit.`, 'success');
} catch (error) {
console.error('Step execution failed:', error);
executeBtn.innerHTML = '<span>โ</span> Failed - Retry';
executeBtn.disabled = false;
showNotification(`Step failed: ${error.message}`, 'error');
}
}
async function executeStepWithRealAgent(step, userInputs, stepIndex) {
return new Promise((resolve, reject) => {
let resultContent = '';
// Use the new workflow API endpoint with tool mapping
fetch('/api/workflows/execute-step', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
step: step,
userInputs: userInputs,
stepIndex: stepIndex,
provider: currentProvider || 'llamacpp'
})
}).then(response => {
if (!response.ok) {
reject(new Error('Agent execution failed'));
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
function readStream() {
reader.read().then(({ done, value }) => {
if (done) {
resolve({
content: resultContent,
timestamp: new Date().toISOString(),
stepIndex: stepIndex,
userInputs: userInputs
});
return;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
console.log('SSE Data received:', data); // Debug log
// Handle different response types
if (data.type === 'response' || data.type === 'chunk') {
const content = data.content || data.text || data.data || '';
resultContent += content;
// Live update results as they stream in
const resultsDiv = document.getElementById(`stepResultsContent_${stepIndex}`);
if (resultsDiv) {
resultsDiv.innerHTML = resultContent.replace(/\n/g, '<br>');
resultsDiv.scrollTop = resultsDiv.scrollHeight;
}
}
if (data.type === 'final') {
console.log('Final result content:', resultContent); // Debug log
resolve({
content: resultContent,
timestamp: new Date().toISOString(),
stepIndex: stepIndex,
userInputs: userInputs,
mappedTools: data.mappedTools,
originalTools: data.originalTools
});
return;
}
if (data.type === 'error') {
reject(new Error(data.error || 'Step execution failed'));
return;
}
} catch (e) {
console.error('JSON parsing error:', e, 'Line:', line); // Better error logging
}
} else if (line.trim() && !line.startsWith('event:')) {
// Handle non-JSON responses
console.log('Non-JSON response:', line);
resultContent += line + '\n';
const resultsDiv = document.getElementById(`stepResultsContent_${stepIndex}`);
if (resultsDiv) {
resultsDiv.innerHTML = resultContent.replace(/\n/g, '<br>');
resultsDiv.scrollTop = resultsDiv.scrollHeight;
}
}
}
readStream();
}).catch(reject);
}
readStream();
}).catch(reject);
});
}
function displayStepResults(stepIndex, result) {
const resultsDiv = document.getElementById(`stepResults_${stepIndex}`);
const resultsContent = document.getElementById(`stepResultsContent_${stepIndex}`);
const content = result.content || 'No content generated - there may have been an issue with the AI response.';
// Create editable content with better styling
resultsContent.innerHTML = `
<div style="position: relative; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;">
<div style="background: var(--muted); padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--sub);">
<span>๐ Step Results</span>
<div>
<button onclick="editStepResult(${stepIndex})" class="edit-btn" style="background: var(--primary); color: var(--primary-ink); border: none; border-radius: 4px; padding: 4px 8px; margin-right: 8px; cursor: pointer; font-size: 11px;">โ๏ธ Edit</button>
<button onclick="copyStepResult(${stepIndex})" class="copy-btn" style="background: var(--ok); color: white; border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 11px;">๐ Copy</button>
</div>
</div>
<div id="stepResultDisplay_${stepIndex}" style="padding: 12px; background: var(--panel); min-height: 60px; line-height: 1.5; white-space: pre-wrap; font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, Consolas, 'Roboto Mono', monospace;">${content}</div>
<textarea id="stepResultEditor_${stepIndex}" style="display: none; width: 100%; min-height: 150px; padding: 12px; border: none; background: var(--panel); color: var(--text); font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, Consolas, 'Roboto Mono', monospace; font-size: 13px; line-height: 1.5; resize: vertical;" placeholder="Edit the step results...">${content}</textarea>
<div id="stepResultActions_${stepIndex}" style="display: none; padding: 8px 12px; background: var(--muted); border-top: 1px solid var(--border);">
<button onclick="saveStepResult(${stepIndex})" class="save-btn" style="background: var(--ok); color: white; border: none; border-radius: 4px; padding: 6px 12px; margin-right: 8px; cursor: pointer;">๐พ Save</button>
<button onclick="cancelEditStepResult(${stepIndex})" class="cancel-btn" style="background: var(--danger); color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer;">โ Cancel</button>
</div>
</div>
`;
resultsDiv.style.display = 'block';
console.log('Displaying step results:', { stepIndex, content: result.content });
}
// Step result editing functions
function editStepResult(stepIndex) {
const displayDiv = document.getElementById(`stepResultDisplay_${stepIndex}`);
const editorTextarea = document.getElementById(`stepResultEditor_${stepIndex}`);
const actionsDiv = document.getElementById(`stepResultActions_${stepIndex}`);
displayDiv.style.display = 'none';
editorTextarea.style.display = 'block';
actionsDiv.style.display = 'block';
// Focus the editor and select all text for easy editing
editorTextarea.focus();
editorTextarea.select();
showNotification('Editing mode enabled - make your changes and click Save', 'info');
}
function saveStepResult(stepIndex) {
const displayDiv = document.getElementById(`stepResultDisplay_${stepIndex}`);
const editorTextarea = document.getElementById(`stepResultEditor_${stepIndex}`);
const actionsDiv = document.getElementById(`stepResultActions_${stepIndex}`);
const newContent = editorTextarea.value;
// Update the display
displayDiv.innerHTML = newContent;
displayDiv.style.display = 'block';
editorTextarea.style.display = 'none';
actionsDiv.style.display = 'none';
// Update the stored result
if (activeSession && activeSession.stepResults[stepIndex]) {
activeSession.stepResults[stepIndex].content = newContent;
activeSession.stepResults[stepIndex].modified = true;
activeSession.stepResults[stepIndex].modifiedAt = new Date().toISOString();
// Save progress
saveWorkflowProgress();
}
showNotification('Step results saved successfully!', 'success');
}
function cancelEditStepResult(stepIndex) {
const displayDiv = document.getElementById(`stepResultDisplay_${stepIndex}`);
const editorTextarea = document.getElementById(`stepResultEditor_${stepIndex}`);
const actionsDiv = document.getElementById(`stepResultActions_${stepIndex}`);
// Revert editor content to original
if (activeSession && activeSession.stepResults[stepIndex]) {
editorTextarea.value = activeSession.stepResults[stepIndex].content || '';
}
displayDiv.style.display = 'block';
editorTextarea.style.display = 'none';
actionsDiv.style.display = 'none';
showNotification('Edit cancelled', 'info');
}
function copyStepResult(stepIndex) {
if (activeSession && activeSession.stepResults[stepIndex]) {
const content = activeSession.stepResults[stepIndex].content || '';
navigator.clipboard.writeText(content).then(() => {
showNotification('Step results copied to clipboard!', 'success');
}).catch(err => {
console.error('Failed to copy to clipboard:', err);
showNotification('Failed to copy to clipboard', 'error');
});
}
}
function refreshInteractiveSession() {
if (!activeSession) return;
// Refresh the entire session display
displayInteractiveSession(activeSession);
}
function updateWorkflowProgress(session) {
const progressBar = document.getElementById('workflowProgressBar');
const progressText = document.getElementById('workflowProgressText');
const totalSteps = session.template.flow.length;
const completedSteps = session.completedSteps.length;
const progress = (completedSteps / totalSteps) * 100;
progressBar.style.width = `${progress}%`;
progressText.textContent = `${completedSteps} of ${totalSteps} steps completed`;
}
function viewStepResults(stepIndex) {
const result = activeSession.stepResults[stepIndex];
if (!result) return;
// Show modal with full results
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8); z-index: 1000;
display: flex; align-items: center; justify-content: center;
padding: 20px;
`;
modal.innerHTML = `
<div style="
background: var(--panel);
border-radius: 12px;
padding: 24px;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
border: 1px solid var(--border);
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: var(--primary);">๐ Step Results</h3>
<button onclick="this.closest('.modal').remove()" style="
background: none; border: none; color: var(--sub);
font-size: 24px; cursor: pointer; padding: 0;
">ร</button>
</div>
<div style="margin-bottom: 16px;">
<strong>Step:</strong> ${activeSession.template.flow[stepIndex].step}<br>
<strong>Completed:</strong> ${new Date(result.timestamp).toLocaleString()}
</div>
<div style="
background: var(--muted);
border-radius: 6px;
padding: 16px;
font-family: monospace;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border);
">
${result.content}
</div>
</div>
`;
modal.className = 'modal';
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
}
function rerunStep(stepIndex) {
if (!activeSession) return;
// Remove from completed steps and reset current step
activeSession.completedSteps = activeSession.completedSteps.filter(i => i !== stepIndex);
activeSession.currentStep = Math.min(activeSession.currentStep, stepIndex);
delete activeSession.stepResults[stepIndex];
refreshInteractiveSession();
showNotification('Step reset - you can now re-run it with different inputs', 'info');
}
function saveWorkflowProgress() {
if (!activeSession) return;
const progressData = {
sessionId: activeSession.sessionId,
templateId: activeSession.template.id,
completedSteps: activeSession.completedSteps,
stepResults: activeSession.stepResults,
currentStep: activeSession.currentStep,
savedAt: new Date().toISOString()
};
localStorage.setItem(`workflow_${activeSession.sessionId}`, JSON.stringify(progressData));
showNotification('Workflow progress saved!', 'success');
}
function endInteractiveSession() {
if (!activeSession) return;
const sessionCard = document.getElementById('interactiveSessionCard');
sessionCard.style.display = 'none';
// Show completion summary if any steps were completed
if (activeSession.completedSteps.length > 0) {
const completedCount = activeSession.completedSteps.length;
const totalCount = activeSession.template.flow.length;
showNotification(`Session ended. Completed ${completedCount}/${totalCount} steps.`, 'info');
// Show workflow results
displayWorkflowSummary();
} else {
showNotification('Session ended', 'info');
}
activeSession = null;
vibeDetectionData = null;
}
function displayWorkflowSummary() {
const resultsDiv = document.getElementById('workflowResults');
const resultsContent = document.getElementById('workflowResultsContent');
if (!activeSession || activeSession.completedSteps.length === 0) return;
const completedSteps = activeSession.completedSteps.map(index => ({
step: activeSession.template.flow[index],
result: activeSession.stepResults[index],
index: index
}));
resultsContent.innerHTML = completedSteps.map(({ step, result, index }) => `
<div style="border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
<h5 style="margin: 0 0 8px 0; color: var(--primary);">
${index + 1}. ${step.step.replace('-', ' ')}
</h5>
<div style="
background: var(--panel);
border-radius: 4px;
padding: 12px;
font-family: monospace;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
">
${result.content.substring(0, 300)}${result.content.length > 300 ? '...' : ''}
</div>
</div>
`).join('');
resultsDiv.style.display = 'block';
}
// Embedded Agent Functions
function showEmbeddedAgent() {
const container = document.getElementById('embeddedAgentContainer');
const iframe = document.getElementById('embeddedAgentFrame');
if (!iframe.src) {
iframe.src = '/agent-test-ui.html';
}
container.style.display = 'block';
container.scrollIntoView({ behavior: 'smooth' });
}
function hideEmbeddedAgent() {
const container = document.getElementById('embeddedAgentContainer');
container.style.display = 'none';
}
// Cloud AI Action Confirmation System
let pendingActions = [];
let currentExecutionSession = null;
function showActionConfirmation(actions, executionPlan) {
pendingActions = actions;
currentExecutionSession = {
sessionId: executionPlan.sessionId || generateSessionId(),
actions: actions,
executionPlan: executionPlan,
approvedActions: new Set(),
results: {}
};
const modal = document.getElementById('actionConfirmationModal');
const content = document.getElementById('actionConfirmationContent');
// Build action list
let actionsHtml = `
<div style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0;">๐ฏ Confirm Actions</h3>
<div style="font-size: 12px; color: var(--sub);">
${actions.length} action${actions.length !== 1 ? 's' : ''} โข
Est. ${executionPlan.estimatedDuration || 'unknown'} โข
Risk: <span style="color: ${getRiskColor(executionPlan.riskAssessment?.overallRiskLevel || 'low')}">${executionPlan.riskAssessment?.overallRiskLevel || 'low'}</span>
</div>
</div>
<div style="max-height: 400px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px;">
`;
actions.forEach((action, index) => {
const riskColor = getRiskColor(action.risk_level || 'low');
const isChecked = true; // Default to selected
actionsHtml += `
<div class="action-item" style="padding: 12px; border-bottom: 1px solid var(--border); display: flex; align-items: flex-start; gap: 12px;">
<input type="checkbox" id="action-${action.id}" ${isChecked ? 'checked' : ''} style="margin-top: 4px;" />
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<strong>${action.sigil}</strong>
<span style="background: ${riskColor}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 500;">
${action.risk_level || 'low'}
</span>
<span style="color: var(--sub); font-size: 12px;">~${action.estimated_time || '10s'}</span>
</div>
<div style="color: var(--sub); font-size: 13px; margin-bottom: 6px;">${action.description}</div>
${action.args ? `<div style="font-family: monospace; font-size: 12px; background: var(--muted); padding: 4px 8px; border-radius: 4px; color: var(--primary);">${action.args}</div>` : ''}
</div>
</div>
`;
});
actionsHtml += `</div></div>`;
// Add execution options
actionsHtml += `
<div style="margin-bottom: 20px;">
<h4 style="margin-bottom: 10px;">Execution Options</h4>
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
<label style="display: flex; align-items: center; gap: 6px;">
<input type="radio" name="executionMode" value="sequential" checked />
<span>Sequential (safer)</span>
</label>
<label style="display: flex; align-items: center; gap: 6px;">
<input type="radio" name="executionMode" value="parallel" />
<span>Parallel (faster)</span>
</label>
<label style="display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="dryRunMode" />
<span>Dry run (preview only)</span>
</label>
</div>
</div>
`;
content.innerHTML = actionsHtml;
modal.style.display = 'flex';
}
function executeApprovedActions() {
const selectedActions = [];
const checkboxes = document.querySelectorAll('#actionConfirmationContent input[type="checkbox"]:checked');
checkboxes.forEach(cb => {
if (cb.id.startsWith('action-')) {
const actionId = cb.id.replace('action-', '');
const action = pendingActions.find(a => a.id === actionId);
if (action) selectedActions.push(action);
}
});
if (selectedActions.length === 0) {
alert('No actions selected');
return;
}
const executionMode = document.querySelector('input[name="executionMode"]:checked').value;
const dryRun = document.getElementById('dryRunMode').checked;
// Close modal and show progress
closeActionConfirmation();
showExecutionProgress(selectedActions, executionMode, dryRun);
// Execute actions
executeActionsWithProgress(selectedActions, executionMode, dryRun);
}
function closeActionConfirmation() {
document.getElementById('actionConfirmationModal').style.display = 'none';
pendingActions = [];
currentExecutionSession = null;
}
function showExecutionProgress(actions, mode, dryRun) {
const modal = document.getElementById('executionProgressModal');
const content = document.getElementById('executionProgressContent');
let progressHtml = `
<div style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0;">โก ${dryRun ? 'Dry Run' : 'Executing'} Actions</h3>
<div style="font-size: 12px; color: var(--sub);">
${actions.length} action${actions.length !== 1 ? 's' : ''} โข Mode: ${mode}
</div>
</div>
<div class="progress-list" style="max-height: 500px; overflow-y: auto;">
`;
actions.forEach((action, index) => {
progressHtml += `
<div class="progress-item" id="progress-${action.id}" style="padding: 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px;">
<div class="progress-status" style="width: 20px; height: 20px; border-radius: 50%; background: var(--sub); display: flex; align-items: center; justify-content: center; font-size: 10px;">
${index + 1}
</div>
<div style="flex: 1;">
<div style="font-weight: 500;">${action.sigil} ${action.description}</div>
<div class="progress-details" style="font-size: 12px; color: var(--sub); margin-top: 4px;">Waiting...</div>
</div>
</div>
`;
});
progressHtml += `
</div>
<div style="margin-top: 20px;">
<button class="btn secondary" onclick="cancelExecution()" style="margin-right: 10px;">Cancel</button>
<button class="btn secondary" onclick="closeExecutionProgress()" style="display: none;" id="closeProgressBtn">Close</button>
</div>
</div>
`;
content.innerHTML = progressHtml;
modal.style.display = 'flex';
}
async function executeActionsWithProgress(actions, mode, dryRun) {
try {
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
updateActionProgress(action.id, 'running', 'Executing...');
// Simulate execution time for demo
await new Promise(resolve => setTimeout(resolve, 1000));
// Mock execution result
const result = {
success: true,
output: `Mock result for ${action.sigil}`,
dryRun: dryRun
};
updateActionProgress(action.id, 'completed', result.output);
currentExecutionSession.results[action.id] = result;
}
// Show completion
document.getElementById('closeProgressBtn').style.display = 'inline-block';
updateProgressStatus('โ
All actions completed successfully');
} catch (error) {
updateProgressStatus('โ Execution failed: ' + error.message);
document.getElementById('closeProgressBtn').style.display = 'inline-block';
}
}
function updateActionProgress(actionId, status, details) {
const progressItem = document.getElementById(`progress-${actionId}`);
if (!progressItem) return;
const statusIcon = progressItem.querySelector('.progress-status');
const detailsEl = progressItem.querySelector('.progress-details');
switch (status) {
case 'running':
statusIcon.style.background = 'var(--primary)';
statusIcon.textContent = 'โก';
statusIcon.style.animation = 'pulse 1.5s infinite';
break;
case 'completed':
statusIcon.style.background = 'var(--ok)';
statusIcon.textContent = 'โ';
statusIcon.style.animation = 'none';
break;
case 'failed':
statusIcon.style.background = 'var(--danger)';
statusIcon.textContent = 'โ';
statusIcon.style.animation = 'none';
break;
}
detailsEl.textContent = details;
}
function updateProgressStatus(message) {
const existingStatus = document.querySelector('.execution-status');
if (existingStatus) {
existingStatus.textContent = message;
} else {
const content = document.getElementById('executionProgressContent');
const statusDiv = document.createElement('div');
statusDiv.className = 'execution-status';
statusDiv.style.cssText = 'text-align: center; padding: 15px; background: var(--panel); border-radius: 8px; margin-top: 15px; font-weight: 500;';
statusDiv.textContent = message;
content.appendChild(statusDiv);
}
}
function closeExecutionProgress() {
document.getElementById('executionProgressModal').style.display = 'none';
}
function cancelExecution() {
closeExecutionProgress();
}
function getRiskColor(riskLevel) {
switch (riskLevel?.toLowerCase()) {
case 'high': return 'var(--danger)';
case 'medium': return '#f39c12';
case 'low': return 'var(--ok)';
default: return 'var(--sub)';
}
}
function generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
// Test function for development
function testActionConfirmation() {
const mockActions = [
{
id: 'test-1',
sigil: '@calc',
args: '22/7',
description: 'Calculate mathematical expression',
risk_level: 'low',
estimated_time: '5s'
},
{
id: 'test-2',
sigil: '@write',
args: 'result.txt -> Result: ${test-1.result}',
description: 'Save calculation result to file',
risk_level: 'medium',
estimated_time: '3s'
},
{
id: 'test-3',
sigil: '@email',
args: 'user@example.com "Results" "The calculation is complete"',
description: 'Email results to user',
risk_level: 'high',
estimated_time: '10s'
}
];
const mockPlan = {
sessionId: 'test-session-123',
estimatedDuration: '18s',
riskAssessment: {
overallRiskLevel: 'medium'
}
};
showActionConfirmation(mockActions, mockPlan);
}
// Code Block Button Handlers
function setupCodeButtonHandlers() {
document.addEventListener('click', async function(e) {
if (e.target.classList.contains('save-code-btn')) {
e.preventDefault();
await handleSaveCode(e.target);
} else if (e.target.classList.contains('run-code-btn')) {
e.preventDefault();
await handleRunCode(e.target);
} else if (e.target.classList.contains('copy-code-btn')) {
e.preventDefault();
await handleCopyCode(e.target);
}
});
}
async function handleSaveCode(button) {
const language = button.dataset.language;
const filename = button.dataset.filename;
const code = decodeURIComponent(button.dataset.code);
try {
const response = await fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: 'save',
args: { language, code, filename }
})
});
if (response.ok) {
const result = await response.json();
if (result.success && result.result.savedFiles && result.result.savedFiles[0]) {
const actualFilename = result.result.savedFiles[0].filename;
// Update the run button with the actual saved filename
const runButton = button.parentElement.querySelector('.run-code-btn');
if (runButton) {
runButton.dataset.filename = actualFilename;
}
button.innerHTML = `โ
Saved as ${actualFilename}!`;
} else {
button.innerHTML = 'โ
Saved!';
}
button.disabled = true;
setTimeout(() => {
button.innerHTML = `๐พ Save as ${filename}`;
button.disabled = false;
}, 2000);
} else {
throw new Error('Save failed');
}
} catch (error) {
button.innerHTML = 'โ Failed';
setTimeout(() => {
button.innerHTML = `๐พ Save as ${filename}`;
}, 2000);
}
}
async function handleRunCode(button) {
const filename = button.dataset.filename;
try {
button.innerHTML = '๐ Running...';
button.disabled = true;
const response = await fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: 'run',
args: { filename }
})
});
if (response.ok) {
button.innerHTML = 'โ
Executed!';
// The result will appear in the chat
} else {
throw new Error('Execution failed');
}
} catch (error) {
button.innerHTML = 'โ Failed';
}
setTimeout(() => {
button.innerHTML = '๐ Run Code';
button.disabled = false;
}, 2000);
}
async function handleCopyCode(button) {
const code = decodeURIComponent(button.dataset.code);
try {
await navigator.clipboard.writeText(code);
button.innerHTML = 'โ
Copied!';
setTimeout(() => {
button.innerHTML = '๐ Copy';
}, 2000);
} catch (error) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = code;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
button.innerHTML = 'โ
Copied!';
setTimeout(() => {
button.innerHTML = '๐ Copy';
}, 2000);
}
}
// Initialize code button handlers
setupCodeButtonHandlers();
</script>
<!-- Action Confirmation Modal -->
<div id="actionConfirmationModal" style="
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
">
<div style="
background: var(--panel);
border-radius: 12px;
border: 1px solid var(--border);
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
">
<div id="actionConfirmationContent" style="padding: 24px;">
<!-- Content will be populated by JavaScript -->
</div>
<div style="padding: 0 24px 24px 24px; display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn secondary" onclick="closeActionConfirmation()">Cancel</button>
<button class="btn primary" onclick="executeApprovedActions()">Execute Selected</button>
</div>
</div>
</div>
<!-- Execution Progress Modal -->
<div id="executionProgressModal" style="
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
">
<div style="
background: var(--panel);
border-radius: 12px;
border: 1px solid var(--border);
max-width: 700px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
">
<div id="executionProgressContent" style="padding: 24px;">
<!-- Content will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Debug Panel -->
<div class="debug-panel" id="debugPanel">
<div class="debug-header">
<div class="debug-title">๐ฌ Debug Console</div>
<div style="display: flex; gap: 8px; align-items: center;">
<button class="debug-close" onclick="clearDebugLog()" title="Clear Log" style="font-size: 14px;">๐งน</button>
<button class="debug-close" onclick="toggleDebugPanel()" title="Close">×</button>
</div>
</div>
<div class="debug-content" id="debugContent">
<div class="debug-entry info">
<span class="debug-timestamp">Ready</span>
<span class="debug-icon">๐ฏ</span>
<span class="debug-message">Debug console initialized. Waiting for executive requests...</span>
</div>
</div>
</div>
<script src="/workflows.js"></script>
<script>
async function runTerminalCmd() {
const cmd = document.getElementById('termCmd').value;
const out = document.getElementById('termOut');
out.textContent = 'Runningโฆ';
try {
const r = await fetch('/api/terminal/run', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ cmd }) });
const j = await r.json();
if (!r.ok) throw new Error(j.error || 'Failed');
out.textContent = (j.stdout || '') + (j.stderr ? ('\n[stderr]\n' + j.stderr) : '');
} catch (e) { out.textContent = 'Error: ' + e.message; }
}
async function loadSystemStatus() {
const el = document.getElementById('systemStatus');
if (!el) return;
el.textContent = 'Loadingโฆ';
try {
const r = await fetch('/api/health');
const j = await r.json();
if (!r.ok) throw new Error('Failed to load');
const rows = [
['llama.cpp', j.llama],
['Claude', j.claude],
['Gemini', j.gemini],
['OpenAI', j.openai]
];
el.innerHTML = rows.map(function(row){
var name=row[0], ok=row[1];
return '<div>' + (ok ? 'โ
' : 'โ') + ' ' + name + '</div>';
}).join('');
} catch (e) { el.textContent = 'Error: ' + e.message; }
}
// Cream API quick actions via workflow executor
async function systemCreamFetch() {
const limitEl = document.getElementById('creamFetchLimit');
const limit = parseInt(limitEl && limitEl.value || '5', 10) || 5;
const out = document.getElementById('creamOut');
if (out) out.textContent = 'Fetchingโฆ';
try {
const r = await fetch('/api/workflows/custom/execute', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ steps: [ { type: 'cream.fetch', args: { limit: limit, raw: raw } } ] })
});
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Failed');
if (out) out.textContent = typeof j.final === 'string' ? j.final : JSON.stringify(j.final, null, 2);
} catch (e) { if (out) out.textContent = 'Error: ' + e.message; }
}
async function systemCreamMail() {
const from_email = (document.getElementById('creamMailFrom')||{}).value || '';
const to_email = (document.getElementById('creamMailTo')||{}).value || '';
const subject = (document.getElementById('creamMailSubject')||{}).value || '';
const body = (document.getElementById('creamMailBody')||{}).value || '';
const out = document.getElementById('creamOut');
if (out) out.textContent = 'Sendingโฆ';
try {
const r = await fetch('/api/workflows/custom/execute', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ steps: [ { type: 'cream.mail', args: { from_email, to_email, subject, body } } ] })
});
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Failed');
if (out) out.textContent = typeof j.final === 'string' ? j.final : JSON.stringify(j.final, null, 2);
} catch (e) { if (out) out.textContent = 'Error: ' + e.message; }
}
async function systemCreamPost() {
const content = (document.getElementById('creamPostContent')||{}).value || '';
const visibility = (document.getElementById('creamPostVisibility')||{}).value || 'public';
const out = document.getElementById('creamOut');
if (out) out.textContent = 'Postingโฆ';
try {
const r = await fetch('/api/workflows/custom/execute', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ steps: [ { type: 'cream.post', args: { content, visibility } } ] })
});
const j = await r.json();
if (!r.ok || j.success === false) throw new Error(j.error || 'Failed');
if (out) out.textContent = typeof j.final === 'string' ? j.final : JSON.stringify(j.final, null, 2);
} catch (e) { if (out) out.textContent = 'Error: ' + e.message; }
}
</script>
<script>
async function systemCreamDiagnose() {
const out = document.getElementById('creamOut');
if (out) out.textContent = 'Diagnosingโฆ';
try {
const r = await fetch('/api/cream/debug');
const j = await r.json();
if (!r.ok) throw new Error('Failed to diagnose');
if (out) out.textContent = JSON.stringify(j, null, 2);
} catch (e) { if (out) out.textContent = 'Error: ' + e.message; }
}
async function systemCreamDiagMail() {
const out = document.getElementById('creamOut');
if (out) out.textContent = 'Diagnosing mailโฆ';
try {
const r = await fetch('/api/cream/diag-mail');
const j = await r.json();
if (!r.ok) throw new Error('Failed to diagnose mail');
if (out) out.textContent = JSON.stringify(j, null, 2);
} catch (e) { if (out) out.textContent = 'Error: ' + e.message; }
}
async function systemCreamDiagPost() {
const out = document.getElementById('creamOut');
if (out) out.textContent = 'Diagnosing postsโฆ';
try {
const r = await fetch('/api/cream/diag-post');
const j = await r.json();
if (!r.ok) throw new Error('Failed to diagnose posts');
if (out) out.textContent = JSON.stringify(j, null, 2);
} catch (e) { if (out) out.textContent = 'Error: ' + e.message; }
}
</script>
</body>
</html>