@intranefr/superbackend 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +5 -2
- package/src/controllers/admin.controller.js +79 -6
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +243 -74
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware.js +195 -34
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/CacheEntry.js +1 -1
- package/src/models/ConsoleLog.js +1 -1
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/Markdown.js +75 -0
- package/src/models/RateLimitCounter.js +1 -1
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminConsoleManager.routes.js +1 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/blogCronsBootstrap.service.js +7 -6
- package/src/services/consoleManager.service.js +56 -18
- package/src/services/consoleOverride.service.js +1 -0
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/jsonConfigs.service.js +24 -12
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +514 -23
- package/src/services/telegram.service.js +130 -0
- package/src/utils/rbac/rightsRegistry.js +4 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +63 -12
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +817 -6
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +4 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/src/middleware/internalCronAuth.js +0 -29
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const TelegramBot = require('node-telegram-bot-api');
|
|
2
|
+
const TelegramBotModel = require('../models/TelegramBot');
|
|
3
|
+
const Agent = require('../models/Agent');
|
|
4
|
+
const agentService = require('./agent.service');
|
|
5
|
+
|
|
6
|
+
const activeBots = new Map();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Start a telegram bot by its ID
|
|
10
|
+
*/
|
|
11
|
+
async function startBot(botId) {
|
|
12
|
+
try {
|
|
13
|
+
const botDoc = await TelegramBotModel.findById(botId);
|
|
14
|
+
if (!botDoc) throw new Error('Bot not found');
|
|
15
|
+
|
|
16
|
+
if (activeBots.has(botId.toString())) {
|
|
17
|
+
await stopBot(botId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const bot = new TelegramBot(botDoc.token, { polling: true });
|
|
21
|
+
|
|
22
|
+
bot.on('message', async (msg) => {
|
|
23
|
+
if (!msg.text) return;
|
|
24
|
+
|
|
25
|
+
// Security: check if user is allowed
|
|
26
|
+
if (botDoc.allowedUserIds && botDoc.allowedUserIds.length > 0) {
|
|
27
|
+
if (!botDoc.allowedUserIds.includes(msg.from.id.toString())) {
|
|
28
|
+
console.warn(`Unauthorized access attempt from ${msg.from.id} to bot ${botDoc.name}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`[TelegramBot: ${botDoc.name}] Received: ${msg.text} from ${msg.from.id}`);
|
|
34
|
+
|
|
35
|
+
// Delegate to agent service
|
|
36
|
+
try {
|
|
37
|
+
if (msg.text.toLowerCase() === '/start') {
|
|
38
|
+
await bot.sendMessage(msg.chat.id, 'Hi there! I am your SuperBackend Agent. How can I help you today?');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!botDoc.defaultAgentId) {
|
|
43
|
+
console.log(`[TelegramBot: ${botDoc.name}] No agent configured, ignoring message from ${msg.from.id}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response = await agentService.processMessage(botDoc.defaultAgentId, {
|
|
48
|
+
content: msg.text,
|
|
49
|
+
senderId: msg.from.id.toString(),
|
|
50
|
+
chatId: msg.chat.id.toString(),
|
|
51
|
+
metadata: {
|
|
52
|
+
firstName: msg.from.first_name,
|
|
53
|
+
lastName: msg.from.last_name,
|
|
54
|
+
username: msg.from.username
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await bot.sendMessage(msg.chat.id, response.text || response);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Error processing message:', err);
|
|
61
|
+
await bot.sendMessage(msg.chat.id, 'Sorry, I encountered an error processing your request.');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
bot.on('polling_error', (err) => {
|
|
66
|
+
console.error(`Telegram polling error [${botDoc.name}]:`, err);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
activeBots.set(botId.toString(), bot);
|
|
70
|
+
|
|
71
|
+
botDoc.status = 'running';
|
|
72
|
+
botDoc.lastError = null;
|
|
73
|
+
await botDoc.save();
|
|
74
|
+
|
|
75
|
+
console.log(`Telegram bot [${botDoc.name}] started`);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`Failed to start Telegram bot [${botId}]:`, err);
|
|
79
|
+
const botDoc = await TelegramBotModel.findById(botId);
|
|
80
|
+
if (botDoc) {
|
|
81
|
+
botDoc.status = 'error';
|
|
82
|
+
botDoc.lastError = err.message;
|
|
83
|
+
await botDoc.save();
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stop a telegram bot
|
|
91
|
+
*/
|
|
92
|
+
async function stopBot(botId) {
|
|
93
|
+
const bot = activeBots.get(botId.toString());
|
|
94
|
+
if (bot) {
|
|
95
|
+
await bot.stopPolling();
|
|
96
|
+
activeBots.delete(botId.toString());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const botDoc = await TelegramBotModel.findById(botId);
|
|
100
|
+
if (botDoc) {
|
|
101
|
+
botDoc.status = 'stopped';
|
|
102
|
+
await botDoc.save();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`Telegram bot [${botId}] stopped`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initialize all active bots on startup
|
|
110
|
+
*/
|
|
111
|
+
async function init() {
|
|
112
|
+
try {
|
|
113
|
+
const activeBotsDocs = await TelegramBotModel.find({ isActive: true });
|
|
114
|
+
for (const botDoc of activeBotsDocs) {
|
|
115
|
+
try {
|
|
116
|
+
await startBot(botDoc._id);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
// Continue with other bots
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error('Failed to initialize Telegram bots:', err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
startBot,
|
|
128
|
+
stopBot,
|
|
129
|
+
init
|
|
130
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Agents - SuperBackend</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-gray-50 min-h-screen">
|
|
12
|
+
<div id="app" class="p-6 max-w-6xl mx-auto" v-cloak>
|
|
13
|
+
<div class="flex justify-between items-center mb-8">
|
|
14
|
+
<div>
|
|
15
|
+
<h1 class="text-2xl font-bold text-gray-800">AI Agents</h1>
|
|
16
|
+
<p class="text-gray-500">Configure your intelligent agent gateway</p>
|
|
17
|
+
</div>
|
|
18
|
+
<button @click="openCreateModal" class="bg-indigo-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-indigo-700 transition">
|
|
19
|
+
<i class="ti ti-plus"></i>
|
|
20
|
+
Create Agent
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- Agent List -->
|
|
25
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
26
|
+
<div v-for="agent in agents" :key="agent._id" class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition">
|
|
27
|
+
<div class="p-6">
|
|
28
|
+
<div class="flex justify-between items-start mb-4">
|
|
29
|
+
<div class="flex items-center gap-3">
|
|
30
|
+
<div class="w-10 h-10 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center text-xl">
|
|
31
|
+
<i class="ti ti-robot"></i>
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<h3 class="font-bold text-gray-800">{{ agent.name }}</h3>
|
|
35
|
+
<p class="text-xs text-gray-400">{{ agent.model }}</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="flex gap-1">
|
|
39
|
+
<button @click="editAgent(agent)" class="p-1.5 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-md transition">
|
|
40
|
+
<i class="ti ti-edit"></i>
|
|
41
|
+
</button>
|
|
42
|
+
<button @click="confirmDelete(agent)" class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition">
|
|
43
|
+
<i class="ti ti-trash"></i>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="space-y-3 text-sm">
|
|
49
|
+
<p class="text-gray-600 line-clamp-2 italic">"{{ agent.systemPrompt }}"</p>
|
|
50
|
+
|
|
51
|
+
<div class="flex flex-wrap gap-1 mt-4">
|
|
52
|
+
<span v-for="tool in agent.tools" :key="tool" class="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs border border-gray-200">
|
|
53
|
+
{{ tool }}
|
|
54
|
+
</span>
|
|
55
|
+
<span v-if="!agent.tools?.length" class="text-xs text-gray-400">No tools enabled</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Empty State -->
|
|
63
|
+
<div v-if="agents.length === 0" class="bg-white rounded-xl border-2 border-dashed border-gray-200 p-12 text-center">
|
|
64
|
+
<i class="ti ti-robot text-6xl text-gray-200 mb-4 inline-block"></i>
|
|
65
|
+
<h3 class="text-lg font-medium text-gray-800">No agents found</h3>
|
|
66
|
+
<p class="text-gray-500 mb-6">Create your first AI agent to start automating</p>
|
|
67
|
+
<button @click="openCreateModal" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
|
|
68
|
+
Create Agent
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Modal -->
|
|
73
|
+
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
74
|
+
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl overflow-hidden max-h-[90vh] flex flex-col">
|
|
75
|
+
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center shrink-0">
|
|
76
|
+
<h3 class="font-bold text-gray-800">{{ editingAgent ? 'Edit Agent' : 'Create Agent' }}</h3>
|
|
77
|
+
<button @click="showModal = false" class="text-gray-400 hover:text-gray-600">
|
|
78
|
+
<i class="ti ti-x"></i>
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<form @submit.prevent="saveAgent" class="p-6 space-y-4 overflow-y-auto">
|
|
82
|
+
<div class="grid grid-cols-2 gap-4">
|
|
83
|
+
<div class="col-span-2">
|
|
84
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Agent Name</label>
|
|
85
|
+
<input v-model="formData.name" type="text" required class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" placeholder="Customer Support AI">
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div>
|
|
89
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">LLM Provider</label>
|
|
90
|
+
<select v-model="formData.providerKey" required class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none">
|
|
91
|
+
<option value="">Select Provider</option>
|
|
92
|
+
<option v-for="(p, key) in providers" :key="key" :value="key">{{ p.label || key }}</option>
|
|
93
|
+
</select>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div>
|
|
97
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
|
98
|
+
<input v-model="formData.model" type="text" required class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" placeholder="gpt-4o, claude-3-5-sonnet...">
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div>
|
|
103
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">System Prompt / Instructions</label>
|
|
104
|
+
<textarea v-model="formData.systemPrompt" rows="6" required class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none font-mono text-sm" placeholder="You are a helpful assistant that..."></textarea>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div>
|
|
108
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">Enabled Tools</label>
|
|
109
|
+
<div class="grid grid-cols-2 gap-2">
|
|
110
|
+
<label v-for="tool in availableTools" :key="tool" class="flex items-center gap-2 p-2 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50">
|
|
111
|
+
<input type="checkbox" :value="tool" v-model="formData.tools" class="w-4 h-4 text-indigo-600 rounded">
|
|
112
|
+
<span class="text-sm font-medium text-gray-700">{{ tool }}</span>
|
|
113
|
+
</label>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="grid grid-cols-2 gap-4">
|
|
118
|
+
<div>
|
|
119
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Temperature ({{ formData.temperature }})</label>
|
|
120
|
+
<input v-model.number="formData.temperature" type="range" min="0" max="2" step="0.1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
|
121
|
+
</div>
|
|
122
|
+
<div>
|
|
123
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Max Tool Iterations</label>
|
|
124
|
+
<input v-model.number="formData.maxIterations" type="number" min="1" max="50" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" placeholder="10">
|
|
125
|
+
<p class="text-[10px] text-gray-400 mt-1">Maximum number of tool call loops allowed per message. Default is 10.</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="flex gap-3 pt-4 shrink-0">
|
|
130
|
+
<button type="button" @click="showModal = false" class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
|
|
131
|
+
Cancel
|
|
132
|
+
</button>
|
|
133
|
+
<button type="submit" :disabled="saving" class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition disabled:opacity-50">
|
|
134
|
+
{{ saving ? 'Saving...' : (editingAgent ? 'Update' : 'Create') }}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</form>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<script>
|
|
143
|
+
const { createApp, ref, onMounted } = Vue;
|
|
144
|
+
|
|
145
|
+
createApp({
|
|
146
|
+
setup() {
|
|
147
|
+
const agents = ref([]);
|
|
148
|
+
const providers = ref({});
|
|
149
|
+
const showModal = ref(false);
|
|
150
|
+
const editingAgent = ref(null);
|
|
151
|
+
const saving = ref(false);
|
|
152
|
+
|
|
153
|
+
const availableTools = ['query_database', 'get_system_stats', 'raw_db_query', 'exec'];
|
|
154
|
+
|
|
155
|
+
const formData = ref({
|
|
156
|
+
name: '',
|
|
157
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
158
|
+
providerKey: '',
|
|
159
|
+
model: '',
|
|
160
|
+
tools: [],
|
|
161
|
+
temperature: 0.7,
|
|
162
|
+
maxIterations: 10
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const baseUrl = '<%= baseUrl %>';
|
|
166
|
+
|
|
167
|
+
const fetchAgents = async () => {
|
|
168
|
+
const res = await fetch(`${baseUrl}/api/admin/agents`);
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
agents.value = data.items;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const fetchLLMConfig = async () => {
|
|
174
|
+
// We need an endpoint to get LLM providers.
|
|
175
|
+
// For now, let's try to get it from the LLM admin API if it exists
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetch(`${baseUrl}/api/admin/llm/providers`);
|
|
178
|
+
if (res.ok) {
|
|
179
|
+
const data = await res.json();
|
|
180
|
+
providers.value = data.providers || {};
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.warn('Failed to fetch LLM providers');
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const openCreateModal = () => {
|
|
188
|
+
editingAgent.value = null;
|
|
189
|
+
formData.value = {
|
|
190
|
+
name: '',
|
|
191
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
192
|
+
providerKey: Object.keys(providers.value)[0] || '',
|
|
193
|
+
model: '',
|
|
194
|
+
tools: [],
|
|
195
|
+
temperature: 0.7,
|
|
196
|
+
maxIterations: 10
|
|
197
|
+
};
|
|
198
|
+
showModal.value = true;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const editAgent = (agent) => {
|
|
202
|
+
editingAgent.value = agent;
|
|
203
|
+
formData.value = {
|
|
204
|
+
name: agent.name,
|
|
205
|
+
systemPrompt: agent.systemPrompt,
|
|
206
|
+
providerKey: agent.providerKey,
|
|
207
|
+
model: agent.model,
|
|
208
|
+
tools: agent.tools || [],
|
|
209
|
+
temperature: agent.temperature || 0.7,
|
|
210
|
+
maxIterations: agent.maxIterations || 10
|
|
211
|
+
};
|
|
212
|
+
showModal.value = true;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const confirmDelete = async (agent) => {
|
|
216
|
+
if (confirm(`Are you sure you want to delete agent ${agent.name}?`)) {
|
|
217
|
+
await fetch(`${baseUrl}/api/admin/agents/${agent._id}`, { method: 'DELETE' });
|
|
218
|
+
fetchAgents();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const saveAgent = async () => {
|
|
223
|
+
saving.value = true;
|
|
224
|
+
try {
|
|
225
|
+
const url = editingAgent.value
|
|
226
|
+
? `${baseUrl}/api/admin/agents/${editingAgent.value._id}`
|
|
227
|
+
: `${baseUrl}/api/admin/agents`;
|
|
228
|
+
|
|
229
|
+
const method = editingAgent.value ? 'PUT' : 'POST';
|
|
230
|
+
|
|
231
|
+
const res = await fetch(url, {
|
|
232
|
+
method,
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
body: JSON.stringify(formData.value)
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
showModal.value = false;
|
|
239
|
+
fetchAgents();
|
|
240
|
+
} else {
|
|
241
|
+
const data = await res.json();
|
|
242
|
+
alert(data.error || 'Failed to save agent');
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
alert(err.message);
|
|
246
|
+
} finally {
|
|
247
|
+
saving.value = false;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
onMounted(() => {
|
|
252
|
+
fetchAgents();
|
|
253
|
+
fetchLLMConfig();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
agents,
|
|
258
|
+
providers,
|
|
259
|
+
showModal,
|
|
260
|
+
editingAgent,
|
|
261
|
+
formData,
|
|
262
|
+
saving,
|
|
263
|
+
availableTools,
|
|
264
|
+
openCreateModal,
|
|
265
|
+
editAgent,
|
|
266
|
+
confirmDelete,
|
|
267
|
+
saveAgent
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}).mount('#app');
|
|
271
|
+
</script>
|
|
272
|
+
</body>
|
|
273
|
+
</html>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
>
|
|
31
31
|
<i v-if="loading.provisioning" class="ti ti-loader-2 animate-spin"></i>
|
|
32
32
|
<i v-else class="ti ti-file-code"></i>
|
|
33
|
-
Provision manage.
|
|
33
|
+
Provision manage.js
|
|
34
34
|
</button>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
@@ -81,24 +81,24 @@ REMOTE_DOMAIN_CONFIG_FILENAME=superlandings.yml</pre>
|
|
|
81
81
|
<div>
|
|
82
82
|
<p class="text-sm font-semibold text-gray-700 mb-2">1. Deploy Application</p>
|
|
83
83
|
<div class="bg-gray-100 p-2 rounded font-mono text-xs flex justify-between items-center">
|
|
84
|
-
<code
|
|
85
|
-
<button @click="copyToClipboard('
|
|
84
|
+
<code>node manage.js deploy</code>
|
|
85
|
+
<button @click="copyToClipboard('node manage.js deploy')" class="p-1 hover:text-blue-600"><i class="ti ti-copy"></i></button>
|
|
86
86
|
</div>
|
|
87
87
|
<p class="text-xs text-gray-500 mt-1">Syncs files via rsync and runs docker compose remotely.</p>
|
|
88
88
|
</div>
|
|
89
89
|
<div>
|
|
90
90
|
<p class="text-sm font-semibold text-gray-700 mb-2">2. Setup Traefik Proxy</p>
|
|
91
91
|
<div class="bg-gray-100 p-2 rounded font-mono text-xs flex justify-between items-center">
|
|
92
|
-
<code
|
|
93
|
-
<button @click="copyToClipboard('
|
|
92
|
+
<code>node manage.js proxy</code>
|
|
93
|
+
<button @click="copyToClipboard('node manage.js proxy')" class="p-1 hover:text-blue-600"><i class="ti ti-copy"></i></button>
|
|
94
94
|
</div>
|
|
95
95
|
<p class="text-xs text-gray-500 mt-1">Generates the Traefik YAML configuration file locally.</p>
|
|
96
96
|
</div>
|
|
97
97
|
<div>
|
|
98
98
|
<p class="text-sm font-semibold text-gray-700 mb-2">3. Deploy Domain</p>
|
|
99
99
|
<div class="bg-gray-100 p-2 rounded font-mono text-xs flex justify-between items-center">
|
|
100
|
-
<code
|
|
101
|
-
<button @click="copyToClipboard('
|
|
100
|
+
<code>node manage.js domain</code>
|
|
101
|
+
<button @click="copyToClipboard('node manage.js domain')" class="p-1 hover:text-blue-600"><i class="ti ti-copy"></i></button>
|
|
102
102
|
</div>
|
|
103
103
|
<p class="text-xs text-gray-500 mt-1">Uploads the Traefik YAML to the gateway server.</p>
|
|
104
104
|
</div>
|
|
@@ -145,7 +145,7 @@ REMOTE_DOMAIN_CONFIG_FILENAME=superlandings.yml`;
|
|
|
145
145
|
|
|
146
146
|
const provisionScript = async (overwrite = false) => {
|
|
147
147
|
if (overwrite) {
|
|
148
|
-
if (!confirm('Are you sure you want to overwrite the existing manage.
|
|
148
|
+
if (!confirm('Are you sure you want to overwrite the existing manage.js?')) {
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<body class="bg-gray-50 overflow-hidden">
|
|
16
16
|
<div id="app" class="h-screen flex flex-col" v-cloak>
|
|
17
17
|
<!-- Top Header -->
|
|
18
|
-
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0">
|
|
18
|
+
<header v-if="!globalZenMode" class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0">
|
|
19
19
|
<div class="flex items-center gap-4">
|
|
20
20
|
<i class="ti ti-layout-dashboard text-2xl text-blue-600"></i>
|
|
21
21
|
<h1 class="text-xl font-bold text-gray-800">
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
<span class="ml-1">to search</span>
|
|
31
31
|
</div>
|
|
32
32
|
<div class="flex items-center gap-4">
|
|
33
|
+
<button @click="globalZenMode = !globalZenMode" class="text-gray-500 hover:text-blue-600 p-1.5 rounded-lg border border-gray-200 bg-white" title="Toggle Global Zen Mode (ESC ESC to exit)">
|
|
34
|
+
<i class="ti ti-maximize"></i>
|
|
35
|
+
</button>
|
|
33
36
|
<span class="text-sm text-gray-500">v1.0.0</span>
|
|
34
37
|
<a :href="baseUrl + adminBase + '/api/test'" target="_blank" class="text-sm text-blue-600 hover:underline">API Test</a>
|
|
35
38
|
</div>
|
|
@@ -37,11 +40,15 @@
|
|
|
37
40
|
</header>
|
|
38
41
|
|
|
39
42
|
<div class="flex-1 flex overflow-hidden">
|
|
40
|
-
|
|
43
|
+
<template v-if="!globalZenMode">
|
|
44
|
+
<%- include('partials/dashboard/sidebar') %>
|
|
45
|
+
</template>
|
|
41
46
|
|
|
42
47
|
<!-- Main Content Area -->
|
|
43
48
|
<main class="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
44
|
-
|
|
49
|
+
<template v-if="!globalZenMode">
|
|
50
|
+
<%- include('partials/dashboard/tab-bar') %>
|
|
51
|
+
</template>
|
|
45
52
|
|
|
46
53
|
<div class="flex-1 relative">
|
|
47
54
|
<!-- Iframes -->
|
|
@@ -89,6 +96,11 @@
|
|
|
89
96
|
// Tabs state
|
|
90
97
|
const tabs = ref([]);
|
|
91
98
|
const activeTabId = ref(null);
|
|
99
|
+
const globalZenMode = ref(false);
|
|
100
|
+
|
|
101
|
+
// ESC ESC to exit Zen Mode
|
|
102
|
+
let escCount = 0;
|
|
103
|
+
let escTimeout = null;
|
|
92
104
|
|
|
93
105
|
// localStorage utilities
|
|
94
106
|
const STORAGE_KEY = 'adminDashboardTabs';
|
|
@@ -222,6 +234,10 @@
|
|
|
222
234
|
const paletteQuery = ref('');
|
|
223
235
|
const paletteCursor = ref(0);
|
|
224
236
|
const paletteInput = ref(null);
|
|
237
|
+
|
|
238
|
+
// Toggle safeguards and debugging
|
|
239
|
+
let toggleTimeout = null;
|
|
240
|
+
let lastToggleTime = 0;
|
|
225
241
|
|
|
226
242
|
// Flattened modules for search
|
|
227
243
|
const allModules = computed(() => {
|
|
@@ -285,19 +301,34 @@
|
|
|
285
301
|
};
|
|
286
302
|
|
|
287
303
|
// Palette methods
|
|
288
|
-
const togglePalette = () => {
|
|
304
|
+
const togglePalette = (source = 'unknown') => {
|
|
305
|
+
// Prevent rapid successive toggles
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
if (now - lastToggleTime < 100) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
lastToggleTime = now;
|
|
311
|
+
|
|
312
|
+
clearTimeout(toggleTimeout);
|
|
313
|
+
|
|
289
314
|
showPalette.value = !showPalette.value;
|
|
290
315
|
if (showPalette.value) {
|
|
291
316
|
paletteQuery.value = '';
|
|
292
317
|
paletteCursor.value = 0;
|
|
293
|
-
nextTick
|
|
294
|
-
|
|
295
|
-
|
|
318
|
+
// Use setTimeout instead of nextTick for better timing
|
|
319
|
+
toggleTimeout = setTimeout(() => {
|
|
320
|
+
if (paletteInput.value) {
|
|
321
|
+
paletteInput.value.focus();
|
|
322
|
+
}
|
|
323
|
+
}, 50);
|
|
296
324
|
}
|
|
297
325
|
};
|
|
298
326
|
|
|
299
|
-
const closePalette = () => {
|
|
300
|
-
showPalette.value
|
|
327
|
+
const closePalette = (source = 'unknown') => {
|
|
328
|
+
if (showPalette.value) {
|
|
329
|
+
showPalette.value = false;
|
|
330
|
+
clearTimeout(toggleTimeout);
|
|
331
|
+
}
|
|
301
332
|
};
|
|
302
333
|
|
|
303
334
|
const navigatePalette = (dir) => {
|
|
@@ -323,15 +354,34 @@
|
|
|
323
354
|
|
|
324
355
|
// Keyboard events
|
|
325
356
|
const handleKeydown = (e) => {
|
|
357
|
+
if (e.key === 'Escape') {
|
|
358
|
+
escCount++;
|
|
359
|
+
clearTimeout(escTimeout);
|
|
360
|
+
if (escCount >= 2) {
|
|
361
|
+
globalZenMode.value = false;
|
|
362
|
+
escCount = 0;
|
|
363
|
+
} else {
|
|
364
|
+
escTimeout = setTimeout(() => {
|
|
365
|
+
escCount = 0;
|
|
366
|
+
}, 500);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
326
370
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
327
371
|
e.preventDefault();
|
|
328
|
-
|
|
372
|
+
e.stopPropagation();
|
|
373
|
+
togglePalette('keyboard-ctrl-k');
|
|
329
374
|
}
|
|
330
375
|
};
|
|
331
376
|
|
|
332
377
|
const handleMessage = (e) => {
|
|
333
|
-
if (e.data && e.data.type === 'keydown'
|
|
334
|
-
|
|
378
|
+
if (e.data && e.data.type === 'keydown') {
|
|
379
|
+
if (e.data.ctrlK) {
|
|
380
|
+
togglePalette('iframe-message');
|
|
381
|
+
}
|
|
382
|
+
if (e.data.key === 'Escape') {
|
|
383
|
+
handleKeydown({ key: 'Escape' });
|
|
384
|
+
}
|
|
335
385
|
}
|
|
336
386
|
};
|
|
337
387
|
|
|
@@ -369,6 +419,7 @@
|
|
|
369
419
|
navSections,
|
|
370
420
|
tabs,
|
|
371
421
|
activeTabId,
|
|
422
|
+
globalZenMode,
|
|
372
423
|
openTab,
|
|
373
424
|
closeTab,
|
|
374
425
|
showPalette,
|