feat: Split chat.html into modular JS modules for #495 switcher support

- Split partials/chat.html (1513→70 lines) into 8 JS modules:
  chat-state.js, chat-switchers.js, chat-mentions.js,
  chat-messages.js, chat-suggestions.js, chat-theme.js,
  chat-websocket.js, chat-init.js
- Centralized state in ChatState global object
- Switcher chips auto-activate on switch_context suggestion action
- active_switchers sent in every WS message payload
- Removed old chat-main.js (merged into modules)
- Split vibe.html into vibe/ module directory with CSS extraction
- Updated standalone chat/chat.html to use same modules
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-04-25 07:03:26 -03:00
parent 1bb96f1923
commit 28c48eeabf
21 changed files with 3184 additions and 3659 deletions

View file

@ -0,0 +1,126 @@
function sendMessage(messageContent) {
var input = document.getElementById("messageInput");
if (!input) return;
var content = messageContent || input.value.trim();
if (!content) return;
if (ChatState.isStreaming && ChatState.streamingMessageId) {
finalizeStreaming();
ChatState.isStreaming = false;
}
if (!messageContent) {
hideMentionDropdown();
input.value = "";
input.focus();
}
addMessage("user", content);
if (ChatState.ws && ChatState.ws.readyState === WebSocket.OPEN) {
ChatState.ws.send(JSON.stringify({
bot_id: ChatState.currentBotId,
user_id: ChatState.currentUserId,
session_id: ChatState.currentSessionId,
channel: "web",
content: content,
message_type: MessageType.USER,
active_switchers: Array.from(ChatState.activeSwitchers),
timestamp: new Date().toISOString(),
}));
} else {
notify("Not connected to server. Message not sent.", "warning");
}
}
window.sendMessage = sendMessage;
window.getChatSessionInfo = function () {
return {
ws: ChatState.ws,
currentBotId: ChatState.currentBotId,
currentUserId: ChatState.currentUserId,
currentSessionId: ChatState.currentSessionId,
currentBotName: ChatState.currentBotName,
};
};
function proceedWithChatInit() {
var botName = window.__INITIAL_BOT_NAME__ || "default";
var storageKey = "gb_chat_" + botName;
var stored = {};
try { stored = JSON.parse(localStorage.getItem(storageKey) || "{}"); } catch (e) {}
var authUrl = "/api/auth?bot_name=" + encodeURIComponent(botName);
if (stored.user_id) authUrl += "&user_id=" + encodeURIComponent(stored.user_id);
if (stored.session_id) authUrl += "&session_id=" + encodeURIComponent(stored.session_id);
fetch(authUrl)
.then(function (response) { return response.json(); })
.then(function (auth) {
ChatState.currentUserId = auth.user_id;
ChatState.currentSessionId = auth.session_id;
ChatState.currentBotId = auth.bot_id || "default";
ChatState.currentBotName = botName;
try {
localStorage.setItem(storageKey, JSON.stringify({ user_id: auth.user_id, session_id: auth.session_id }));
} catch (e) {}
connectWebSocket();
})
.catch(function () {
notify("Failed to connect to chat server", "error");
setTimeout(proceedWithChatInit, 3000);
});
}
function setupEventHandlers() {
var form = document.getElementById("chatForm");
var input = document.getElementById("messageInput");
var sendBtn = document.getElementById("sendBtn");
if (form) {
form.onsubmit = function (e) { e.preventDefault(); sendMessage(); return false; };
}
if (input) {
input.addEventListener("input", handleMentionInput);
input.onkeydown = function (e) {
if (handleMentionKeydown(e)) return;
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
};
}
if (sendBtn) {
sendBtn.onclick = function (e) { e.preventDefault(); sendMessage(); };
}
var scrollBtn = document.getElementById("scrollToBottom");
if (scrollBtn) {
scrollBtn.addEventListener("click", function () { scrollToBottom(true); ChatState.isUserScrolling = false; });
}
var messagesEl = document.getElementById("messages");
if (messagesEl) {
messagesEl.addEventListener("scroll", function () {
ChatState.isUserScrolling = true;
updateScrollButton();
clearTimeout(messagesEl.scrollTimeout);
messagesEl.scrollTimeout = setTimeout(function () { ChatState.isUserScrolling = false; }, 1000);
});
}
document.addEventListener("click", function (e) {
if (!e.target.closest("#mentionDropdown") && !e.target.closest("#messageInput")) {
hideMentionDropdown();
}
});
}
function initChat() {
loadBotConfig();
proceedWithChatInit();
}
setupEventHandlers();
initChat();

View file

@ -1,97 +0,0 @@
// Chat Main Module - Initializes chat and coordinates between modules
(function () {
"use strict";
function notify(message, type) {
type = type || "info";
if (window.GBAlerts) {
window.GBAlerts.show(message, type);
} else {
console.log("[" + type + "]", message);
}
}
function initChat() {
console.log("Chat module initialized");
setupEventHandlers();
}
function setupEventHandlers() {
var form = document.getElementById("chatForm");
var input = document.getElementById("messageInput");
if (form) {
form.onsubmit = function (e) {
e.preventDefault();
sendMessage();
return false;
};
}
if (input) {
input.addEventListener("input", handleMentionInput);
}
var scrollBtn = document.getElementById("scrollToBottom");
if (scrollBtn) {
scrollBtn.addEventListener("click", function() {
scrollToBottom(true);
});
}
document.addEventListener("click", function (e) {
if (
!e.target.closest("#mentionDropdown") &&
!e.target.closest("#messageInput")
) {
hideMentionDropdown();
}
});
}
function sendMessage() {
var input = document.getElementById("messageInput");
if (!input) return;
var content = input.value.trim();
if (!content) return;
// Get active switchers
var activeSwitcherIds = getActiveSwitcherIds();
console.log('Sending message with active_switchers:', activeSwitcherIds);
// Add user message
addMessage("user", content);
// Clear input
input.value = "";
input.focus();
// Send via WebSocket
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: content,
message_type: MessageType.USER,
active_switchers: activeSwitcherIds,
timestamp: new Date().toISOString()
}));
} else {
notify("Not connected to server. Message not sent.", "warning");
}
}
// Expose to global scope
window.sendMessage = sendMessage;
window.initChat = initChat;
// Initialize on load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initChat);
} else {
initChat();
}
})();

View file

@ -0,0 +1,249 @@
function showMentionDropdown() {
var dropdown = document.getElementById("mentionDropdown");
if (dropdown) dropdown.classList.add("visible");
}
function hideMentionDropdown() {
var dropdown = document.getElementById("mentionDropdown");
if (dropdown) dropdown.classList.remove("visible");
ChatState.mentionState.active = false;
ChatState.mentionState.query = "";
ChatState.mentionState.startPos = -1;
ChatState.mentionState.selectedIndex = 0;
ChatState.mentionState.results = [];
}
function searchEntities(query) {
if (!query || query.length < 1) {
var defaultResults = Object.keys(EntityTypes).map(function (type) {
return { type: type, name: EntityTypes[type].label, icon: EntityTypes[type].icon, isTypeHint: true };
});
renderMentionResults(defaultResults);
return;
}
var colonIndex = query.indexOf(":");
if (colonIndex > 0) {
var entityType = query.substring(0, colonIndex).toLowerCase();
var searchTerm = query.substring(colonIndex + 1);
if (EntityTypes[entityType]) {
fetchEntitiesOfType(entityType, searchTerm);
return;
}
}
var filteredTypes = Object.keys(EntityTypes)
.filter(function (type) {
return type.toLowerCase().indexOf(query.toLowerCase()) === 0 ||
EntityTypes[type].label.toLowerCase().indexOf(query.toLowerCase()) === 0;
})
.map(function (type) {
return { type: type, name: EntityTypes[type].label, icon: EntityTypes[type].icon, isTypeHint: true };
});
renderMentionResults(filteredTypes);
}
function fetchEntitiesOfType(type, searchTerm) {
fetch("/api/search/entities?type=" + encodeURIComponent(type) + "&q=" + encodeURIComponent(searchTerm || ""))
.then(function (r) { return r.json(); })
.then(function (data) {
var results = (data.results || []).map(function (item) {
return {
type: type, name: item.name || item.title || item.number,
id: item.id, icon: EntityTypes[type].icon,
subtitle: item.subtitle || item.status || "", isTypeHint: false,
};
});
if (results.length === 0) {
results = [{ type: type, name: "No results for '" + searchTerm + "'", icon: "\u274C", isTypeHint: false, disabled: true }];
}
renderMentionResults(results);
})
.catch(function () {
renderMentionResults([{ type: type, name: "Search unavailable", icon: "\u26A0\uFE0F", isTypeHint: false, disabled: true }]);
});
}
function renderMentionResults(results) {
var container = document.getElementById("mentionResults");
if (!container) return;
ChatState.mentionState.results = results;
ChatState.mentionState.selectedIndex = 0;
container.innerHTML = results.map(function (item, index) {
var classes = "mention-item";
if (index === ChatState.mentionState.selectedIndex) classes += " selected";
if (item.disabled) classes += " disabled";
var subtitle = item.subtitle ? '<span class="mention-item-subtitle">' + escapeHtml(item.subtitle) + "</span>" : "";
var hint = item.isTypeHint ? '<span class="mention-item-hint">Type : to search</span>' : "";
return '<div class="' + classes + '" data-index="' + index + '" data-type="' + item.type +
'" data-name="' + escapeHtml(item.name) + '" data-is-type="' + item.isTypeHint + '">' +
'<span class="mention-item-icon">' + item.icon + "</span>" +
'<span class="mention-item-content">' +
'<span class="mention-item-name">' + escapeHtml(item.name) + "</span>" +
subtitle + hint + "</span></div>";
}).join("");
container.querySelectorAll(".mention-item:not(.disabled)").forEach(function (item) {
item.addEventListener("click", function () {
selectMentionItem(parseInt(this.getAttribute("data-index")));
});
});
}
function selectMentionItem(index) {
var item = ChatState.mentionState.results[index];
if (!item || item.disabled) return;
var input = document.getElementById("messageInput");
if (!input) return;
var value = input.value;
var beforeMention = value.substring(0, ChatState.mentionState.startPos);
var afterMention = value.substring(input.selectionStart);
var insertText;
if (item.isTypeHint) {
insertText = "@" + item.type + ":";
ChatState.mentionState.query = item.type + ":";
ChatState.mentionState.startPos = beforeMention.length;
input.value = beforeMention + insertText + afterMention;
input.setSelectionRange(beforeMention.length + insertText.length, beforeMention.length + insertText.length);
searchEntities(ChatState.mentionState.query);
return;
} else {
insertText = "@" + item.type + ":" + item.name + " ";
input.value = beforeMention + insertText + afterMention;
input.setSelectionRange(beforeMention.length + insertText.length, beforeMention.length + insertText.length);
hideMentionDropdown();
}
input.focus();
}
function updateMentionSelection(direction) {
var enabledResults = ChatState.mentionState.results.filter(function (r) { return !r.disabled; });
if (enabledResults.length === 0) return;
var currentEnabled = 0;
for (var i = 0; i < ChatState.mentionState.selectedIndex; i++) {
if (!ChatState.mentionState.results[i].disabled) currentEnabled++;
}
currentEnabled += direction;
if (currentEnabled < 0) currentEnabled = enabledResults.length - 1;
if (currentEnabled >= enabledResults.length) currentEnabled = 0;
var newIndex = 0;
var count = 0;
for (var j = 0; j < ChatState.mentionState.results.length; j++) {
if (!ChatState.mentionState.results[j].disabled) {
if (count === currentEnabled) { newIndex = j; break; }
count++;
}
}
ChatState.mentionState.selectedIndex = newIndex;
var items = document.querySelectorAll("#mentionResults .mention-item");
items.forEach(function (item, idx) { item.classList.toggle("selected", idx === newIndex); });
var selectedItem = document.querySelector("#mentionResults .mention-item.selected");
if (selectedItem) selectedItem.scrollIntoView({ block: "nearest" });
}
function handleMentionInput(e) {
var input = e.target;
var value = input.value;
var cursorPos = input.selectionStart;
var textBeforeCursor = value.substring(0, cursorPos);
var atIndex = textBeforeCursor.lastIndexOf("@");
if (atIndex >= 0) {
var charBeforeAt = atIndex > 0 ? textBeforeCursor[atIndex - 1] : " ";
if (charBeforeAt === " " || atIndex === 0) {
var query = textBeforeCursor.substring(atIndex + 1);
if (!query.includes(" ")) {
ChatState.mentionState.active = true;
ChatState.mentionState.startPos = atIndex;
ChatState.mentionState.query = query;
showMentionDropdown();
searchEntities(query);
return;
}
}
}
if (ChatState.mentionState.active) hideMentionDropdown();
}
function handleMentionKeydown(e) {
if (!ChatState.mentionState.active) return false;
if (e.key === "ArrowDown") { e.preventDefault(); updateMentionSelection(1); return true; }
if (e.key === "ArrowUp") { e.preventDefault(); updateMentionSelection(-1); return true; }
if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); selectMentionItem(ChatState.mentionState.selectedIndex); return true; }
if (e.key === "Escape") { e.preventDefault(); hideMentionDropdown(); return true; }
return false;
}
function setupMentionClickHandlers(container) {
var mentions = container.querySelectorAll(".mention-tag");
mentions.forEach(function (mention) {
mention.addEventListener("click", function (e) {
e.preventDefault();
var type = this.getAttribute("data-type");
var name = this.getAttribute("data-name");
navigateToEntity(type, name);
});
mention.addEventListener("mouseenter", function () {
var type = this.getAttribute("data-type");
var name = this.getAttribute("data-name");
showEntityCard(type, name, e.target);
});
mention.addEventListener("mouseleave", function () { hideEntityCard(); });
});
}
function navigateToEntity(type, name) {
var entityType = EntityTypes[type.toLowerCase()];
if (entityType) {
var route = entityType.route;
window.location.hash = "#" + route;
var htmxLink = document.querySelector('a[data-section="' + route + '"]');
if (htmxLink) htmx.trigger(htmxLink, "click");
}
}
function showEntityCard(type, name, targetEl) {
var card = document.getElementById("entityCardTooltip");
var entityType = EntityTypes[type.toLowerCase()];
if (!card || !entityType) return;
card.querySelector(".entity-card-type").textContent = entityType.label;
card.querySelector(".entity-card-type").style.background = entityType.color;
card.querySelector(".entity-card-title").textContent = entityType.icon + " " + name;
card.querySelector(".entity-card-status").textContent = "";
card.querySelector(".entity-card-details").textContent = "Loading...";
var rect = targetEl.getBoundingClientRect();
card.style.left = rect.left + "px";
card.style.top = rect.top - card.offsetHeight - 8 + "px";
card.classList.add("visible");
fetchEntityDetails(type, name).then(function (details) {
if (card.classList.contains("visible")) {
card.querySelector(".entity-card-details").innerHTML = details;
}
});
}
function hideEntityCard() {
var card = document.getElementById("entityCardTooltip");
if (card) card.classList.remove("visible");
}
function fetchEntityDetails(type, name) {
return fetch("/api/search/entity?type=" + encodeURIComponent(type) + "&name=" + encodeURIComponent(name))
.then(function (r) { return r.json(); })
.then(function (data) { return data && data.details ? data.details : "No additional details available"; })
.catch(function () { return "Unable to load details"; });
}

View file

@ -1,53 +1,165 @@
// Chat Messages Module - Handles message rendering and display
function addMessage(role, content, messageId) {
var messages = document.getElementById("messages");
if (!messages) return;
var messageDiv = document.createElement("div");
messageDiv.className = "message message-" + role;
if (messageId) {
messageDiv.dataset.messageId = messageId;
}
if (role === "bot") {
messageDiv.innerHTML = marked.parse(content);
} else {
messageDiv.textContent = content;
}
messages.appendChild(messageDiv);
scrollToBottom(false);
}
function scrollToBottom(animate) {
var messages = document.getElementById("messages");
if (!messages) return;
var messages = document.getElementById("messages");
if (messages) {
if (animate) {
messages.scrollTo({
top: messages.scrollHeight,
behavior: "smooth"
});
messages.scrollTo({ top: messages.scrollHeight, behavior: "smooth" });
} else {
messages.scrollTop = messages.scrollHeight;
messages.scrollTop = messages.scrollHeight;
}
}
}
function showThinking() {
var messages = document.getElementById("messages");
if (!messages) return;
var thinking = document.createElement("div");
thinking.className = "message message-bot thinking";
thinking.id = "thinking-indicator";
thinking.textContent = "Thinking...";
messages.appendChild(thinking);
scrollToBottom(false);
function updateScrollButton() {
var messages = document.getElementById("messages");
var scrollBtn = document.getElementById("scrollToBottom");
if (!messages || !scrollBtn) return;
var isNearBottom = messages.scrollHeight - messages.scrollTop - messages.clientHeight < 100;
if (isNearBottom) {
scrollBtn.classList.remove("visible");
} else {
scrollBtn.classList.add("visible");
}
}
function hideThinking() {
var thinking = document.getElementById("thinking-indicator");
if (thinking) {
thinking.remove();
function renderMentionInMessage(content) {
return content.replace(/@(\w+):([^\s]+)/g, function (match, type, name) {
var entityType = EntityTypes[type.toLowerCase()];
if (entityType) {
return '<span class="mention-tag" data-type="' + type + '" data-name="' + escapeHtml(name) + '">' +
'<span class="mention-icon">' + entityType.icon + "</span>" +
'<span class="mention-text">@' + type + ":" + escapeHtml(name) + "</span>" +
"</span>";
}
return match;
});
}
function stripMarkdownBlocks(content) {
var htmlMatch = content.match(/^```(?:html|xml)?\s*\n([\s\S]+?)\n?```$/i);
if (htmlMatch) return htmlMatch[1].trim();
return content;
}
function addMessage(sender, content, msgId) {
var messages = document.getElementById("messages");
if (!messages) return;
var div = document.createElement("div");
div.className = "message " + sender;
if (msgId) div.id = msgId;
if (sender === "user") {
var processedContent = renderMentionInMessage(escapeHtml(content));
div.innerHTML = '<div class="message-content user-message">' + processedContent + "</div>";
} else {
var cleanContent = stripMarkdownBlocks(content);
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
var parsed;
if (msgId) {
parsed = '<div class="streaming-loading"><span class="loading-dots">...</span></div>';
} else if (hasHtmlTags) {
parsed = cleanContent;
} else {
parsed = typeof marked !== "undefined" && marked.parse
? marked.parse(cleanContent)
: escapeHtml(cleanContent);
}
parsed = renderMentionInMessage(parsed);
div.innerHTML = '<div class="message-content bot-message">' + parsed + "</div>";
}
messages.appendChild(div);
if (!ChatState.isUserScrolling) {
scrollToBottom(true);
} else {
updateScrollButton();
}
setupMentionClickHandlers(div);
}
function isTagBalanced(html) {
if (!html) return true;
var lastChevronOpen = html.lastIndexOf('<');
var lastChevronClose = html.lastIndexOf('>');
if (lastChevronOpen > lastChevronClose) return false;
return true;
}
function updateStreaming(content) {
var el = document.getElementById(ChatState.streamingMessageId);
if (!el) return;
var msgContent = el.querySelector(".message-content");
var cleanContent = stripMarkdownBlocks(content);
var isHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
if (isHtml) {
if (isTagBalanced(cleanContent) || (Date.now() - ChatState.lastRenderTime > 2000)) {
msgContent.innerHTML = renderMentionInMessage(cleanContent);
ChatState.lastRenderTime = Date.now();
if (!ChatState.isUserScrolling) scrollToBottom(true);
}
} else {
var parsed = typeof marked !== "undefined" && marked.parse
? marked.parse(cleanContent)
: escapeHtml(cleanContent);
parsed = renderMentionInMessage(parsed);
msgContent.innerHTML = parsed;
if (!ChatState.isUserScrolling) scrollToBottom(true);
}
}
function finalizeStreaming() {
var el = document.getElementById(ChatState.streamingMessageId);
if (el) {
var cleanContent = stripMarkdownBlocks(ChatState.currentStreamingContent);
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
var parsed = hasHtmlTags
? cleanContent
: (typeof marked !== "undefined" && marked.parse
? marked.parse(cleanContent)
: escapeHtml(cleanContent));
parsed = renderMentionInMessage(parsed);
el.querySelector(".message-content").innerHTML = parsed;
el.removeAttribute("id");
setupMentionClickHandlers(el);
if (!ChatState.isUserScrolling) scrollToBottom(true);
}
ChatState.streamingMessageId = null;
ChatState.currentStreamingContent = "";
ChatState.streamingBuffer = "";
}
function processMessage(data) {
if (data.is_complete) {
if (ChatState.isStreaming) {
finalizeStreaming();
} else if (data.content && data.content.trim() !== "") {
addMessage("bot", data.content);
}
ChatState.isStreaming = false;
if (data.suggestions && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
renderSuggestions(data.suggestions);
}
if (data.switchers && Array.isArray(data.switchers) && data.switchers.length > 0) {
renderBotSwitchers(data.switchers);
}
} else {
if (!ChatState.isStreaming) {
ChatState.isStreaming = true;
ChatState.streamingMessageId = "streaming-" + Date.now();
ChatState.currentStreamingContent = data.content || "";
addMessage("bot", ChatState.currentStreamingContent, ChatState.streamingMessageId);
ChatState.lastRenderTime = Date.now();
} else {
ChatState.currentStreamingContent += data.content || "";
var now = Date.now();
if (now - ChatState.lastRenderTime > ChatState.renderInterval) {
updateStreaming(ChatState.currentStreamingContent);
}
}
}
}

View file

@ -0,0 +1,108 @@
var ChatState = {
ws: null,
currentSessionId: null,
currentUserId: null,
currentBotId: "default",
currentBotName: "default",
isStreaming: false,
streamingMessageId: null,
currentStreamingContent: "",
streamingBuffer: "",
lastRenderTime: 0,
renderInterval: 200,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
disconnectNotified: false,
isUserScrolling: false,
activeSwitchers: new Set(),
switcherDefinitions: [],
mentionState: {
active: false,
query: "",
startPos: -1,
selectedIndex: 0,
results: [],
},
};
var WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://";
var WS_URL = WS_BASE_URL + window.location.host + "/ws";
var MessageType = {
EXTERNAL: 0,
USER: 1,
BOT_RESPONSE: 2,
CONTINUE: 3,
SUGGESTION: 4,
CONTEXT_CHANGE: 5,
};
var EntityTypes = {
lead: { icon: "\u{1F464}", color: "#4CAF50", label: "Lead", route: "crm" },
opportunity: {
icon: "\u{1F4B0}",
color: "#FF9800",
label: "Opportunity",
route: "crm",
},
account: {
icon: "\u{1F3E2}",
color: "#2196F3",
label: "Account",
route: "crm",
},
contact: {
icon: "\u{1F4C7}",
color: "#9C27B0",
label: "Contact",
route: "crm",
},
invoice: {
icon: "\u{1F4C4}",
color: "#F44336",
label: "Invoice",
route: "billing",
},
quote: {
icon: "\u{1F4CB}",
color: "#607D8B",
label: "Quote",
route: "billing",
},
case: {
icon: "\u{1F3AB}",
color: "#E91E63",
label: "Case",
route: "tickets",
},
product: {
icon: "\u{1F4E6}",
color: "#795548",
label: "Product",
route: "products",
},
service: {
icon: "\u2699\uFE0F",
color: "#00BCD4",
label: "Service",
route: "products",
},
};
var SWITCHER_ICONS = {
tables: "\u{1F4CA}",
infographic: "\u{1F4CD}",
cards: "\u{1F0CF}",
list: "\u{1F4CB}",
comparison: "\u2696",
timeline: "\u23F0",
markdown: "\u{1F4DD}",
chart: "\u{1F4C8}",
};
function escapeHtml(text) {
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}

View file

@ -0,0 +1,57 @@
function renderSuggestions(suggestions) {
var suggestionsEl = document.getElementById("suggestions");
if (!suggestionsEl) return;
suggestionsEl.innerHTML = "";
suggestionsEl.classList.add("has-bot-suggestions");
suggestions.forEach(function (suggestion) {
var chip = document.createElement("button");
chip.className = "suggestion-chip";
chip.textContent = suggestion.text || "Suggestion";
chip.onclick = (function (sugg) {
return function () {
if (sugg.action) {
try {
var action = typeof sugg.action === "string"
? JSON.parse(sugg.action)
: sugg.action;
if (action.type === "invoke_tool") {
ChatState.ws.send(JSON.stringify({
bot_id: ChatState.currentBotId,
user_id: ChatState.currentUserId,
session_id: ChatState.currentSessionId,
channel: "web",
content: action.tool,
message_type: 6,
active_switchers: Array.from(ChatState.activeSwitchers),
timestamp: new Date().toISOString(),
}));
return;
} else if (action.type === "switch_context" && action.switcher) {
if (!ChatState.activeSwitchers.has(action.switcher)) {
ChatState.activeSwitchers.add(action.switcher);
renderSwitcherChips();
}
window.sendMessage(sugg.text);
} else if (action.type === "send_message") {
window.sendMessage(action.message || sugg.text);
} else if (action.type === "select_context") {
window.sendMessage(action.context);
} else {
window.sendMessage(sugg.text);
}
} catch (e) {
window.sendMessage(sugg.text);
}
} else {
window.sendMessage(sugg.text);
}
};
})(suggestion);
suggestionsEl.appendChild(chip);
});
}

View file

@ -1,90 +1,55 @@
// Chat Switchers Module - Manages format switchers (tables, infographic, cards, etc.)
// Uses event delegation for better reliability with dynamic content
var activeSwitchers = new Set();
var switcherDefinitions = [];
function renderBotSwitchers(switchers) {
if (!switchers || switchers.length === 0) return;
var existingIds = {};
switcherDefinitions.forEach(function(sw) { existingIds[sw.id] = true; });
switchers.forEach(function(sw) {
if (!existingIds[sw.id]) {
switcherDefinitions.push({
id: sw.id,
label: sw.label || sw.id,
icon: sw.icon || '🔀',
color: sw.color || '#666'
});
existingIds[sw.id] = true;
}
});
renderSwitchers();
var container = document.getElementById("switchers");
if (container && switcherDefinitions.length > 0) {
container.style.display = '';
}
}
function renderSwitchers() {
var container = document.getElementById("switcherChips");
if (!container) return;
container.innerHTML = switcherDefinitions.map(function(sw) {
var isActive = activeSwitchers.has(sw.id);
return (
'<div class="switcher-chip' + (isActive ? ' active' : '') + '" ' +
'data-switch-id="' + sw.id + '" ' +
'style="--switcher-color: ' + sw.color + '; ' +
(isActive ? 'color: ' + sw.color + ' background: ' + sw.color + '; ' : '') + '">' +
'<span class="switcher-chip-icon">' + sw.icon + '</span>' +
'<span>' + sw.label + '</span>' +
'</div>'
);
}).join('');
// Event delegation - attach once to parent
if (!container.dataset.hasClickHandler) {
container.addEventListener('click', function(e) {
var chip = e.target.closest('.switcher-chip');
if (chip) {
var switcherId = chip.getAttribute('data-switch-id');
if (switcherId) {
toggleSwitcher(switcherId);
}
}
});
container.dataset.hasClickHandler = 'true';
}
function renderSwitcherChips() {
var container = document.getElementById("switcherChips");
if (!container) return;
container.innerHTML = ChatState.switcherDefinitions.map(function (sw) {
var isActive = ChatState.activeSwitchers.has(sw.id);
return '<div class="switcher-chip' + (isActive ? ' active' : '') + '" ' +
'data-switch-id="' + sw.id + '" ' +
'style="--switcher-color: ' + (sw.color || '#666') + '; ' +
(isActive ? 'color: white; background: ' + (sw.color || '#666') + '; ' : '') + '">' +
'<span class="switcher-chip-icon">' + (sw.icon || '\u{1F500}') + '</span>' +
'<span>' + sw.label + '</span></div>';
}).join('');
}
function toggleSwitcher(switcherId) {
console.log('toggleSwitcher called with:', switcherId);
console.log('activeSwitchers before:', Array.from(activeSwitchers));
if (activeSwitchers.has(switcherId)) {
activeSwitchers.delete(switcherId);
console.log('Deleted switcher:', switcherId);
} else {
activeSwitchers.add(switcherId);
console.log('Added switcher:', switcherId);
if (ChatState.activeSwitchers.has(switcherId)) {
ChatState.activeSwitchers.delete(switcherId);
} else {
ChatState.activeSwitchers.add(switcherId);
}
renderSwitcherChips();
}
function renderBotSwitchers(switchers) {
if (!switchers || switchers.length === 0) return;
var existingIds = {};
ChatState.switcherDefinitions.forEach(function (sw) { existingIds[sw.id] = true; });
switchers.forEach(function (sw) {
if (!existingIds[sw.id]) {
ChatState.switcherDefinitions.push({
id: sw.id,
label: sw.label || sw.id,
icon: sw.icon || SWITCHER_ICONS[sw.id] || "\u{1F500}",
color: sw.color || "#666",
});
existingIds[sw.id] = true;
}
console.log('activeSwitchers after:', Array.from(activeSwitchers));
// Re-render to show active state
renderSwitchers();
}
function getActiveSwitcherIds() {
var ids = Array.from(activeSwitchers);
console.log('getActiveSwitcherIds returning:', ids);
return ids;
}
function clearSwitchers() {
activeSwitchers.clear();
renderSwitchers();
});
renderSwitcherChips();
var container = document.getElementById("switchers");
if (container && ChatState.switcherDefinitions.length > 0) {
container.style.display = '';
}
var chipsContainer = document.getElementById("switcherChips");
if (chipsContainer && !chipsContainer.dataset.hasClickHandler) {
chipsContainer.addEventListener("click", function (e) {
var chip = e.target.closest(".switcher-chip");
if (chip) {
var switcherId = chip.getAttribute("data-switch-id");
if (switcherId) toggleSwitcher(switcherId);
}
});
chipsContainer.dataset.hasClickHandler = "true";
}
}

View file

@ -0,0 +1,78 @@
function getContrastYIQ(hexcolor) {
if (!hexcolor) return "#ffffff";
var temp = document.createElement("div");
temp.style.color = hexcolor;
temp.style.display = "none";
document.body.appendChild(temp);
var style = window.getComputedStyle(temp).color;
document.body.removeChild(temp);
var rgb = style.match(/\d+/g);
if (!rgb || rgb.length < 3) return "#ffffff";
var r = parseInt(rgb[0]);
var g = parseInt(rgb[1]);
var b = parseInt(rgb[2]);
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? "#000000" : "#ffffff";
}
function applyThemeData(themeData) {
var color1 = themeData.color1 || themeData.data && themeData.data.color1 || "black";
var color2 = themeData.color2 || themeData.data && themeData.data.color2 || "white";
var logo = themeData.logo_url || themeData.data && themeData.data.logo_url || "";
var title = themeData.title || themeData.data && themeData.data.title || window.__INITIAL_BOT_NAME__ || "Chat";
document.documentElement.style.setProperty("--chat-color1", color1);
document.documentElement.style.setProperty("--chat-color2", color2);
document.documentElement.style.setProperty("--suggestion-color", color1);
document.documentElement.style.setProperty("--suggestion-bg", color2);
document.documentElement.style.setProperty("--color1", color1);
document.documentElement.style.setProperty("--color2", color2);
document.documentElement.style.setProperty("--primary", color1);
document.documentElement.style.setProperty("--accent", color1);
document.documentElement.style.setProperty("--chat-fg1", getContrastYIQ(color1));
document.documentElement.style.setProperty("--chat-fg2", getContrastYIQ(color2));
}
function loadBotConfig() {
var botName = window.__INITIAL_BOT_NAME__ || "default";
fetch("/api/bot/config?bot_name=" + encodeURIComponent(botName))
.then(function (response) { return response.json(); })
.then(function (config) {
if (!config) return;
var botId = botName.toLowerCase();
var botThemeKey = "gb-theme-" + botId;
var localStorageTheme = localStorage.getItem(botThemeKey);
var configThemeBase = config.theme_base || config["theme-base"] || "light";
var useBotConfigColors = !localStorageTheme || localStorageTheme === "default" || localStorageTheme === configThemeBase;
var color1 = config.theme_color1 || config["theme-color1"] || config["Theme Color"] || "#3b82f6";
var color2 = config.theme_color2 || config["theme-color2"] || config["theme-color2"] || "#f5deb3";
var title = config.theme_title || config["theme-title"] || botName;
var logo = config.theme_logo || config["theme-logo"] || "";
if (useBotConfigColors) {
document.documentElement.setAttribute("data-has-bot-colors", "true");
document.documentElement.style.setProperty("--chat-color1", color1);
document.documentElement.style.setProperty("--chat-color2", color2);
document.documentElement.style.setProperty("--suggestion-color", color1);
document.documentElement.style.setProperty("--suggestion-bg", color2);
document.documentElement.style.setProperty("--color1", color1);
document.documentElement.style.setProperty("--color2", color2);
document.documentElement.style.setProperty("--primary", color1);
document.documentElement.style.setProperty("--accent", color1);
document.documentElement.style.setProperty("--chat-fg1", getContrastYIQ(color1));
document.documentElement.style.setProperty("--chat-fg2", getContrastYIQ(color2));
}
if (logo) {
var logoImg = document.querySelector(".logo-icon-img");
if (logoImg) { logoImg.src = logo; logoImg.alt = title || botName; logoImg.style.display = "block"; }
var logoSvg = document.querySelector(".logo-icon-svg");
if (logoSvg) logoSvg.style.display = "none";
}
})
.catch(function () {});
}

View file

@ -0,0 +1,106 @@
function notify(message, type) {
type = type || "info";
if (window.GBAlerts) {
if (type === "error") {
window.GBAlerts.warning("Chat", message);
} else {
window.GBAlerts.info("Chat", message);
}
}
}
function updateConnectionStatus(status) {
var statusEl = document.getElementById("connectionStatus");
if (!statusEl) return;
statusEl.className = "connection-status " + status;
var statusText = statusEl.querySelector(".connection-text");
if (statusText) {
switch (status) {
case "connected":
statusText.textContent = "Connected";
statusEl.style.display = "none";
break;
case "disconnected":
statusText.textContent = "Disconnected";
statusEl.style.display = "flex";
break;
case "connecting":
statusText.textContent = "Connecting...";
statusEl.style.display = "flex";
break;
}
}
}
function connectWebSocket() {
if (ChatState.ws) ChatState.ws.close();
updateConnectionStatus("connecting");
var url = WS_URL +
"?session_id=" + ChatState.currentSessionId +
"&user_id=" + ChatState.currentUserId +
"&bot_name=" + ChatState.currentBotName;
ChatState.ws = new WebSocket(url);
ChatState.ws.onopen = function () {
ChatState.disconnectNotified = false;
updateConnectionStatus("connected");
var loadingOverlay = document.getElementById("chatLoadingOverlay");
var contentWrapper = document.getElementById("chatContentWrapper");
if (loadingOverlay) loadingOverlay.style.display = "none";
if (contentWrapper) contentWrapper.style.display = "flex";
};
ChatState.ws.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
if (data.type === "connected") {
ChatState.reconnectAttempts = 0;
return;
}
if (data.event) {
if (data.event === "change_theme") applyThemeData(data.data || {});
return;
}
if (data.content && typeof data.content === "string") {
try {
var contentObj = JSON.parse(data.content);
if (contentObj.event === "change_theme") {
applyThemeData(contentObj.data || {});
return;
}
} catch (e) {}
}
if (window.AgentMode && data.type &&
["thought_process", "terminal_output", "browser_ready", "step_progress", "step_complete", "todo_update", "agent_status", "file_created"].indexOf(data.type) !== -1) {
window.AgentMode.handleMessage(data);
}
if (data.message_type === MessageType.BOT_RESPONSE) {
processMessage(data);
}
} catch (e) {}
};
ChatState.ws.onclose = function () {
updateConnectionStatus("disconnected");
if (!ChatState.disconnectNotified) {
notify("Disconnected from chat server", "error");
ChatState.disconnectNotified = true;
}
if (ChatState.reconnectAttempts < ChatState.maxReconnectAttempts) {
ChatState.reconnectAttempts++;
updateConnectionStatus("connecting");
setTimeout(connectWebSocket, 1000 * ChatState.reconnectAttempts);
}
};
ChatState.ws.onerror = function () {
updateConnectionStatus("disconnected");
};
}

View file

@ -1,59 +1,70 @@
<link rel="stylesheet" href="/suite/chat/chat.css?v=10" />
<link rel="stylesheet" href="/suite/css/markdown-message.css" />
<script src="/suite/js/vendor/marked.min.js"></script>
<link rel="stylesheet" href="/suite/css/chat-agent-mode.css" />
<div class="chat-layout" id="chat-app">
<div class="connection-status connecting" id="connectionStatus" style="display: none">
<span class="connection-status-dot"></span>
<span class="connection-text">Connecting...</span>
<div class="chat-loading-overlay" id="chatLoadingOverlay">
<div class="chat-loading-spinner"></div>
<div class="chat-loading-text">Carregando...</div>
</div>
<main id="messages"></main>
<footer>
<div class="chat-footer-content">
<div class="suggestions-container" id="suggestions"></div>
<div class="switchers-container" id="switchers" style="display:none">
<div class="switchers-label">Formato:</div>
<div class="switchers-chips" id="switchersChips"></div>
</div>
<div class="chat-content-wrapper" id="chatContentWrapper" style="display: none;">
<div class="connection-status connecting" id="connectionStatus" style="display: none">
<span class="connection-status-dot"></span>
<span class="connection-text">Connecting...</span>
</div>
<div class="mention-dropdown" id="mentionDropdown">
<div class="mention-header">
<span class="mention-title" data-i18n="chat-mention-title">Reference Entity</span>
<main id="messages"></main>
<footer>
<div class="chat-footer-content">
<div class="suggestions-container" id="suggestions"></div>
<div class="switchers-container" id="switchers" style="display:none">
<div class="switchers-label">Formato:</div>
<div class="switchers-chips" id="switcherChips"></div>
</div>
</div>
<div class="mention-results" id="mentionResults"></div>
</div>
<form class="input-container" id="chatForm">
<input name="content" id="messageInput" type="text"
placeholder="Message... (type @ to mention)"
data-i18n-placeholder="chat-placeholder"
autofocus autocomplete="off" />
<button type="submit" id="sendBtn" title="Send" data-i18n-title="chat-send"></button>
</form>
</footer>
<button class="scroll-to-bottom" id="scrollToBottom" title="Scroll to bottom">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<div class="entity-card-tooltip" id="entityCardTooltip">
<div class="entity-card-header">
<span class="entity-card-type"></span>
<span class="entity-card-status"></span>
<div class="mention-dropdown" id="mentionDropdown">
<div class="mention-header">
<span class="mention-title" data-i18n="chat-mention-title">Reference Entity</span>
</div>
<div class="mention-results" id="mentionResults"></div>
</div>
<form class="input-container" id="chatForm">
<input name="content" id="messageInput" type="text"
placeholder="Message... (type @ to mention)"
data-i18n-placeholder="chat-placeholder" autofocus autocomplete="off" />
<button type="submit" id="sendBtn" title="Send" data-i18n-title="chat-send">&#8593;</button>
</form>
</footer>
<button class="scroll-to-bottom" id="scrollToBottom" title="Scroll to bottom">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<div class="entity-card-title"></div>
<div class="entity-card-details"></div>
<div class="entity-card-actions">
<button class="entity-card-btn" data-action="view" data-i18n="action-view">View</button>
<div class="entity-card-tooltip" id="entityCardTooltip">
<div class="entity-card-header">
<span class="entity-card-type"></span>
<span class="entity-card-status"></span>
</div>
<div class="entity-card-title"></div>
<div class="entity-card-details"></div>
<div class="entity-card-actions">
<button class="entity-card-btn" data-action="view" data-i18n="action-view">View</button>
</div>
</div>
</div>
<!-- Chat Modules -->
<script src="/suite/js/vendor/marked.min.js"></script>
<script src="/suite/chat/chat-state.js"></script>
<script src="/suite/chat/chat-switchers.js"></script>
<script src="/suite/chat/chat-mentions.js"></script>
<script src="/suite/chat/chat-messages.js"></script>
<script src="/suite/chat/chat-main.js"></script>
<script src="/suite/chat/chat-suggestions.js"></script>
<script src="/suite/chat/chat-theme.js"></script>
<script src="/suite/chat/chat-websocket.js"></script>
<script src="/suite/chat/chat-init.js"></script>
<script src="/suite/js/chat-agent-mode.js"></script>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,101 @@
function updateMantis1(status, detail) {
var card = document.querySelector(
'.as-agent-card[data-agent-id="1"]',
);
if (!card) return;
var bar = card.querySelector(".as-agent-bar .as-bar-fill");
if (status === "working") {
card.style.borderLeftColor = "#f59e0b";
if (!card.querySelector(".as-agent-bar")) {
var barWrapper = document.createElement("div");
barWrapper.className = "as-agent-bar";
barWrapper.innerHTML =
'<div class="as-bar-fill bred" style="width:0%;transition:width 0.5s;"></div>';
card.appendChild(barWrapper);
}
} else if (status === "done") {
card.style.borderLeftColor = "var(--accent)";
bar = card.querySelector(".as-bar-fill");
if (bar) bar.style.width = "100%";
setTimeout(function () {
var b = card.querySelector(".as-agent-bar");
if (b) b.remove();
}, 2000);
}
}
function updateAgentCard(agentId, status, detail) {
var card = document.querySelector(
'.as-agent-card[data-agent-id="' + agentId + '"]',
);
if (!card) return;
card.style.opacity = "1";
var badge = card.querySelector(".as-badge");
var dot = card.querySelector(".as-status-dot");
if (status === "WORKING") {
card.style.borderLeft = "3px solid #f59e0b";
if (dot) {
dot.className = "as-status-dot yellow";
}
if (badge) {
badge.textContent = "WORKING";
badge.className = "as-badge badge-bred";
}
if (!card.querySelector(".as-agent-bar")) {
var barWrapper = document.createElement("div");
barWrapper.className = "as-agent-bar";
barWrapper.innerHTML =
'<div class="as-bar-fill bred" style="width:0%;transition:width 0.5s;"></div>';
card.appendChild(barWrapper);
}
} else if (status === "EVOLVED" || status === "DONE") {
card.style.borderLeft = "3px solid var(--accent)";
if (dot) {
dot.className = "as-status-dot green";
}
if (badge) {
badge.textContent = "EVOLVED";
badge.className = "as-badge badge-evolved";
}
var agBar = card.querySelector(".as-bar-fill");
if (agBar) agBar.style.width = "100%";
setTimeout(function () {
var b = card.querySelector(".as-agent-bar");
if (b) b.remove();
}, 2000);
} else if (status === "BRED") {
card.style.borderLeft = "3px solid #f59e0b";
if (dot) {
dot.className = "as-status-dot yellow";
}
if (badge) {
badge.textContent = "BRED";
badge.className = "as-badge badge-bred";
}
} else if (status === "FAILED") {
card.style.borderLeft = "3px solid #ef4444";
if (dot) {
dot.className = "as-status-dot red";
}
if (badge) {
badge.textContent = "FAILED";
badge.className = "as-badge badge-bred";
badge.style.background = "#ef4444";
}
}
if (detail) {
var detailEl = card.querySelector(".as-agent-detail");
if (!detailEl) {
detailEl = document.createElement("span");
detailEl.className = "as-agent-detail";
detailEl.style.cssText =
"font-size:10px;color: var(--text-muted);display:block;padding:0 12px 4px;";
var body = card.querySelector(".as-agent-body");
if (body) body.after(detailEl);
}
detailEl.textContent = detail;
}
}

View file

@ -0,0 +1,144 @@
function callAutotask(intent) {
updateMantis1("working");
vibeAddMsg("system", "🔄 Mantis #1 is analyzing your request…");
connectTaskProgressWs(null);
var breadcrumb = document.querySelector(
".vibe-canvas div:first-child",
);
if (breadcrumb) {
currentProject = intent
.substring(0, 40)
.replace(/[^a-zA-Z0-9 ]/g, "");
breadcrumb.innerHTML =
'// DASHBOARD <span style="color: var(--text-secondary);margin:0 6px;">&gt;</span> // ' +
esc(currentProject.toUpperCase()) +
' <div style="float:right;"><button style="border: 1px solid var(--border);background: var(--bg);border-radius:4px;padding:2px 8px;cursor:pointer;">-</button><span style="font-size:11px;margin:0 8px;color: var(--text);">100%</span><button style="border: 1px solid var(--border);background: var(--bg);border-radius:4px;padding:2px 8px;cursor:pointer;">+</button></div>';
}
var token =
localStorage.getItem("gb-access-token") ||
sessionStorage.getItem("gb-access-token");
fetch("/api/autotask/classify", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({ intent: intent, auto_process: true }),
})
.then(function (r) {
return r.json();
})
.then(function (data) {
updateMantis1("done");
if (data.success && data.result) {
var r = data.result;
if (r.task_id) {
connectTaskProgressWs(r.task_id);
}
if (
r.created_resources &&
r.created_resources.length > 0
) {
r.created_resources.forEach(function (res, i) {
setTimeout(function () {
addTaskNode(
res.name || res.resource_type,
res.resource_type +
(res.path ? " → " + res.path : ""),
{ status: "Done" },
);
}, i * 400);
});
} else {
addTaskNode(
"Project Setup",
"Setting up: " + intent,
{ status: "Planning" },
);
}
vibeAddMsg(
"bot",
r.message || "Done! Your project is ready.",
);
if (r.app_url) {
vibeAddMsg(
"system",
'✅ App available at <a href="' +
r.app_url +
'" target="_blank" style="color: var(--accent);text-decoration:underline;">' +
esc(r.app_url) +
"</a>",
);
var preview =
document.getElementById("vibePreview");
var urlBar =
document.getElementById("vibePreviewUrl");
var content =
document.getElementById("vibePreviewContent");
if (preview) preview.style.display = "";
if (urlBar) urlBar.value = r.app_url;
if (content)
content.innerHTML =
'<iframe src="' +
r.app_url +
'" style="width:100%;height:100%;border:none;"></iframe>';
}
if (r.next_steps && r.next_steps.length > 0) {
vibeAddMsg(
"bot",
"**Next steps:**\n" +
r.next_steps
.map(function (s) { return "• " + s; })
.join("\n"),
);
}
} else {
vibeAddMsg(
"bot",
"I classified your intent as **" +
(data.intent_type || "UNKNOWN") +
"**. " +
(data.error || "Processing complete."),
);
addTaskNode("Analysis", intent, { status: "Planning" });
}
})
.catch(function (err) {
updateMantis1("done");
vibeAddMsg(
"system",
"⚠️ Backend unavailable — showing plan preview.",
);
var words = intent.split(/[.,;]/);
addTaskNode(
"Project Setup",
"Create project structure and install dependencies",
{ status: "Planning" },
);
if (words.length > 1) {
setTimeout(function () {
addTaskNode(
"Database Schema",
"Define tables for: " + words.slice(0, 3).join(", "),
{ status: "Pending" },
);
}, 500);
}
vibeAddMsg(
"bot",
"I've created a preliminary plan with " +
Math.min(words.length + 1, 5) +
" nodes. Once the backend is available, I'll process the full build.",
);
});
}

View file

@ -0,0 +1,131 @@
function addTaskNode(title, description, meta) {
var stepsContainer = document.getElementById("vibeSteps");
if (!stepsContainer) return;
stepsContainer.style.display = "flex";
var emptyState = document.getElementById("vibeCanvasEmpty");
if (emptyState) emptyState.style.display = "none";
nodeIdCounter++;
meta = meta || {};
var fileCount =
meta.estimated_files ||
meta.files ||
Math.floor(Math.random() * 15 + 3);
var time =
meta.estimated_time ||
meta.time ||
Math.floor(Math.random() * 20 + 5) + "m";
var tokens =
meta.estimated_tokens ||
meta.tokens ||
"~" + Math.floor(Math.random() * 30 + 10) + "k tokens";
var status = meta.status || "Planning";
var fileList = meta.fileList || [];
var isFirst = stepsContainer.children.length === 0;
var nodeId = "vibe-node-" + nodeIdCounter;
var statusBg =
status === "Done"
? "var(--accent)"
: status === "Planning"
? "var(--success-light, #eef8eb)"
: "var(--warning-light, var(--bg)3cd)";
var statusColor =
status === "Done"
? "var(--bg)"
: status === "Planning"
? "var(--accent)"
: "var(--warning, #856404)";
var subTasksHtml = "";
if (fileList.length > 0) {
subTasksHtml =
'<div id="' +
nodeId +
'-files" style="display:none;padding:8px 16px;border-top:1px solid var(--border);font-size:10px;color:var(--text-muted, #555);">';
for (var fi = 0; fi < fileList.length; fi++) {
subTasksHtml +=
'<div style="padding:2px 0;display:flex;align-items:center;gap:4px;"><span style="color: var(--accent);">📄</span> ' +
esc(fileList[fi]) +
"</div>";
}
subTasksHtml += "</div>";
}
var node = document.createElement("div");
node.className = "vibe-task-node";
node.style.cssText =
"background: var(--bg);border:" +
(isFirst
? "2px solid var(--accent)"
: "1px solid var(--border)") +
";border-radius:8px;width:280px;box-shadow:0 " +
(isFirst ? "4" : "2") +
"px 12px rgba(" +
(isFirst ? "132,214,105,0.15" : "0,0,0,0.05") +
");position:relative;flex-shrink:0;animation:nodeIn 0.4s ease;";
node.innerHTML =
'<div style="padding:12px 16px;border-bottom: 1px solid var(--border);">' +
'<div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:10px;color: var(--text-muted);">' +
"<span>" +
fileCount +
" files</span><span>" +
time +
"</span><span>" +
tokens +
"</span>" +
"</div>" +
'<h4 style="margin:0 0 8px 0;font-size:14px;color: var(--text);font-weight:700;">' +
esc(title) +
"</h4>" +
'<p style="margin:0;font-size:11px;color: var(--text-muted);line-height:1.4;">' +
esc(description) +
"</p>" +
"</div>" +
'<div style="padding:10px 16px;background: var(--surface);border-bottom: 1px solid var(--border);font-size:11px;">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
'<span style="color: var(--text-muted);">Status</span>' +
'<span style="background:' +
statusBg +
";color:" +
statusColor +
';padding:2px 8px;border-radius:12px;font-weight:600;">' +
esc(status) +
"</span>" +
"</div>" +
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
'<span style="color: var(--text-muted);">Mantis Manager</span>' +
'<span style="display:flex;align-items:center;gap:4px;"><span class="as-status-dot green"></span> Mantis #1</span>' +
"</div>" +
"</div>" +
'<div style="padding:8px 16px;font-size:10px;font-weight:700;color: var(--text-muted);">' +
'<div data-toggle="' +
nodeId +
"-files\" style=\"padding:4px 0;cursor:pointer;user-select:none;\" onclick=\"(function(el){var t=document.getElementById(el.getAttribute('data-toggle'));if(t){t.style.display=t.style.display==='none'?'':'none';var a=el.querySelector('span');if(a)a.textContent=t.style.display==='none'?'▶':'▼';}})(this)\">// SUB-TASKS <span style=\"float:right;\">▶</span></div>" +
'<div style="padding:4px 0;cursor:pointer;">// LOGS <span style="float:right;">▶</span></div>' +
"</div>" +
subTasksHtml;
if (isFirst || stepsContainer.children.length > 0) {
var line = document.createElement("div");
line.style.cssText =
"position:absolute;right:-60px;top:50%;width:60px;height:2px;background:var(--accent);z-index:10;";
node.appendChild(line);
if (!isFirst) {
var dot = document.createElement("div");
dot.style.cssText =
"position:absolute;left:-5px;top:50%;transform:translateY(-50%);width:10px;height:10px;border-radius:50%;background:var(--accent);z-index:20;";
node.appendChild(dot);
}
}
stepsContainer.appendChild(node);
stepsContainer.scrollLeft = stepsContainer.scrollWidth;
taskNodes.push({
title: title,
description: description,
meta: meta,
});
return node;
}

View file

@ -0,0 +1,122 @@
function selectDeploymentTarget(target) {
selectedDeploymentTarget = target;
var internalOption = document.getElementById("deploymentInternal");
var externalOption = document.getElementById("deploymentExternal");
var internalConfig = document.getElementById(
"deploymentInternalConfig",
);
var externalConfig = document.getElementById(
"deploymentExternalConfig",
);
if (target === "internal") {
internalOption.style.borderColor = "var(--accent)";
internalOption.style.background = "rgba(132, 214, 105, 0.06)";
externalOption.style.borderColor = "var(--border)";
externalOption.style.background = "transparent";
internalConfig.style.display = "block";
externalConfig.style.display = "none";
} else {
externalOption.style.borderColor = "var(--accent)";
externalOption.style.background = "rgba(132, 214, 105, 0.06)";
internalOption.style.borderColor = "var(--border)";
internalOption.style.background = "transparent";
internalConfig.style.display = "none";
externalConfig.style.display = "block";
}
}
function showDeploymentModal() {
var modal = document.getElementById("vibeDeploymentModal");
if (modal) {
modal.style.display = "block";
selectDeploymentTarget("internal");
}
}
function closeDeploymentModal() {
var modal = document.getElementById("vibeDeploymentModal");
if (modal) {
modal.style.display = "none";
}
}
async function executeDeployment() {
var deployButton = document.getElementById("deployButton");
if (deployButton) {
deployButton.textContent = "Deploying...";
deployButton.disabled = true;
}
let payload = {
app_name: document.getElementById('deployRepoName')?.value || document.getElementById('deployRoute')?.value || "my-app",
target: {},
environment: "production",
manifest: {}
};
if (selectedDeploymentTarget === 'external') {
payload.target = {
External: {
repo_url: "https://alm.pragmatismo.com.br/" + payload.app_name,
custom_domain: document.getElementById('deployCustomDomain')?.value || null,
ci_cd_enabled: document.getElementById('deployCiCd')?.checked ?? true
}
};
payload.app_type = document.getElementById('deployAppType')?.value || "htmx";
} else {
let route = document.getElementById('deployRoute')?.value || "my-app";
payload.target = {
Internal: {
route: "/apps/" + route,
shared_resources: document.getElementById('deploySharedResources')?.checked ?? true
}
};
payload.app_type = "gb-native";
}
try {
vibeAddMsg("system", "🚀 Initiating deployment API call...");
const response = await fetch('/api/deployment/deploy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (response.ok && (result.success || result.status === 'Deployed' || result.status === 'Building')) {
closeDeploymentModal();
vibeAddMsg("system", "✅ Deployment Successful! " + (result.url ? result.url : ""));
var previewUrl = document.getElementById("vibePreviewUrl");
var previewPanel = document.getElementById("vibePreview");
if (previewUrl && previewPanel && result.url) {
previewUrl.value = result.url;
previewPanel.style.display = "block";
}
} else {
vibeAddMsg("system", "❌ Deployment failed: " + (result.error || result.status));
}
} catch (e) {
vibeAddMsg("system", "❌ Deployment error: " + e.message);
} finally {
if (deployButton) {
deployButton.textContent = "Deploy Now";
deployButton.disabled = false;
}
}
}
if (document.readyState !== "loading") {
var routeInput = document.getElementById("deployRoute");
if (routeInput) {
routeInput.addEventListener("input", function () {
var preview = document.getElementById("deployRoutePreview");
if (preview) {
preview.textContent = this.value || "my-app";
}
});
}
}

View file

@ -0,0 +1,104 @@
function esc(text) {
var d = document.createElement("div");
d.textContent = text || "";
return d.innerHTML;
}
function vibeAddMsg(role, text) {
var box = document.getElementById("vibeChatMessages");
if (!box) return;
var div = document.createElement("div");
if (role === "user") {
div.style.cssText =
"align-self:flex-end;background:var(--accent);color:var(--surface);font-weight:500;padding:10px 14px;border-radius:12px 12px 0 12px;max-width:85%;word-wrap:break-word;";
div.textContent = text;
} else if (role === "system") {
div.style.cssText =
"align-self:center;background:rgba(132,214,105,0.12);color: var(--accent);padding:6px 12px;border-radius:8px;font-size:11px;text-align:center;";
div.innerHTML = text;
} else {
div.style.cssText =
"align-self:flex-start;background:var(--surface-hover);color:var(--text);padding:10px 14px;border-radius:12px 12px 12px 0;max-width:85%;word-wrap:break-word;";
div.className = "vibe-bot-msg";
if (typeof marked !== "undefined" && marked.parse) {
div.innerHTML = marked.parse(text);
} else {
div.textContent = text;
}
}
box.appendChild(div);
box.scrollTop = box.scrollHeight;
return div;
}
function vibeAddStreamStart() {
vibeStreamId = "vibe-stream-" + Date.now();
vibeStreamContent = "";
var el = vibeAddMsg("bot", "▍");
if (el) el.id = vibeStreamId;
return el;
}
function vibeUpdateStream(content) {
vibeStreamContent += content || "";
var el = document.getElementById(vibeStreamId);
if (!el) return;
if (typeof marked !== "undefined" && marked.parse) {
el.innerHTML = marked.parse(vibeStreamContent);
} else {
el.textContent = vibeStreamContent;
}
var box = document.getElementById("vibeChatMessages");
if (box) box.scrollTop = box.scrollHeight;
}
function vibeFinalizeStream() {
var el = document.getElementById(vibeStreamId);
if (el) {
if (typeof marked !== "undefined" && marked.parse) {
el.innerHTML = marked.parse(vibeStreamContent);
} else {
el.textContent = vibeStreamContent;
}
el.removeAttribute("id");
}
vibeStreamId = null;
vibeStreamContent = "";
vibeStreaming = false;
}
function setVibeStatus(status) {
var dot = document.getElementById("vibeChatStatusDot");
var badge = document.getElementById("vibeChatStatusBadge");
if (status === "connected") {
if (dot) {
dot.className = "as-status-dot green";
dot.style.boxShadow = "0 0 8px var(--accent)";
}
if (badge) {
badge.textContent = "EVOLVED";
badge.style.background = "var(--accent)";
badge.style.color = "var(--bg)";
}
} else if (status === "connecting") {
if (dot) {
dot.className = "as-status-dot yellow";
dot.style.boxShadow = "0 0 8px #f59e0b";
}
if (badge) {
badge.textContent = "CONNECTING…";
badge.style.background = "var(--surface-hover)";
badge.style.color = "var(--text-muted)";
}
} else {
if (dot) {
dot.className = "as-status-dot red";
dot.style.boxShadow = "0 0 8px #ef4444";
}
if (badge) {
badge.textContent = "OFFLINE";
badge.style.background = "var(--surface-hover)";
badge.style.color = "var(--text-muted)";
}
}
}

View file

@ -0,0 +1,71 @@
function handleVibeSubmit(e) {
e.preventDefault();
var input = document.getElementById("vibeChatInput");
if (!input) return;
var text = input.value.trim();
if (!text) return;
input.value = "";
vibeAddMsg("user", text);
callAutotask(text);
}
function setupPipelineTabs() {
var container = document.querySelector(".vibe-pipeline");
if (!container) return;
container.addEventListener("click", function (e) {
var tab = e.target.closest(".vibe-pipeline-tab");
if (!tab) return;
container
.querySelectorAll(".vibe-pipeline-tab")
.forEach(function (t) {
t.classList.remove("active");
});
tab.classList.add("active");
});
}
function setupSidebarCollapse() {
var btn = document.getElementById("agentsSidebarCollapse");
var sidebar = document.getElementById("agentsSidebar");
if (!btn || !sidebar) return;
btn.addEventListener("click", function () {
sidebar.classList.toggle("collapsed");
btn.textContent = sidebar.classList.contains("collapsed")
? "▶"
: "◀";
});
}
function setupWorkspaceAccordions() {
var toggles = document.querySelectorAll(".as-workspace-toggle");
toggles.forEach(function (toggle) {
toggle.addEventListener("click", function () {
var body = this.nextElementSibling;
var arrow = this.querySelector(".as-workspace-arrow");
if (body) {
var isOpen = body.style.display !== "none";
body.style.display = isOpen ? "none" : "";
if (arrow) arrow.textContent = isOpen ? "▶" : "▼";
}
});
});
}
function initVibe() {
setupPipelineTabs();
setupSidebarCollapse();
setupWorkspaceAccordions();
var form = document.getElementById("vibeChatForm");
if (form) form.addEventListener("submit", handleVibeSubmit);
connectVibeWs();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initVibe);
} else {
initVibe();
}

View file

@ -0,0 +1,13 @@
var vibeWs = null;
var vibeSessionId = null;
var vibeUserId = null;
var vibeBotId = "default";
var vibeBotName = "default";
var vibeStreaming = false;
var vibeStreamId = null;
var vibeStreamContent = "";
var taskNodes = [];
var currentProject = "My App";
var nodeIdCounter = 0;
var taskProgressWs = null;
var selectedDeploymentTarget = "internal";

View file

@ -0,0 +1,271 @@
function connectVibeWs() {
setVibeStatus("connecting");
var botName = window.__INITIAL_BOT_NAME__ || "default";
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
.then(function (r) {
return r.json();
})
.then(function (auth) {
vibeUserId = auth.user_id;
vibeSessionId = auth.session_id;
vibeBotId = auth.bot_id || "default";
vibeBotName = botName;
var proto =
location.protocol === "https:" ? "wss://" : "ws://";
var url =
proto +
location.host +
"/ws?session_id=" +
vibeSessionId +
"&user_id=" +
vibeUserId +
"&bot_name=" +
vibeBotName;
vibeWs = new WebSocket(url);
vibeWs.onopen = function () {
setVibeStatus("connected");
};
vibeWs.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
if (data.type === "connected") return;
if (data.event) return;
if (data.type === "thought_process") {
vibeAddMsg("system", "💭 " + esc(data.content));
return;
}
if (data.type === "terminal_output") {
vibeAddMsg("system", "🖥️ " + esc(data.line));
return;
}
if (data.type === "step_progress") {
var pct = Math.round(
(data.current / data.total) * 100,
);
updateMantis1("working");
var bar = document.querySelector(
'.as-agent-card[data-agent-id="1"] .as-bar-fill',
);
if (bar) bar.style.width = pct + "%";
return;
}
if (data.message_type === 2) {
if (data.is_complete) {
if (vibeStreaming) {
vibeFinalizeStream();
} else if (
data.content &&
data.content.trim()
) {
vibeAddMsg("bot", data.content);
}
vibeStreaming = false;
} else {
if (!vibeStreaming) {
vibeStreaming = true;
vibeAddStreamStart();
vibeUpdateStream(data.content || "");
} else {
vibeUpdateStream(data.content || "");
}
}
}
} catch (e) {
console.error("Vibe WS parse error:", e);
}
};
vibeWs.onclose = function () {
setVibeStatus("disconnected");
};
vibeWs.onerror = function () {
setVibeStatus("disconnected");
};
})
.catch(function () {
setVibeStatus("disconnected");
vibeAddMsg(
"system",
"⚠️ Could not connect to backend. You can still plan offline.",
);
});
}
function vibeSendWs(content) {
if (vibeWs && vibeWs.readyState === WebSocket.OPEN) {
vibeWs.send(
JSON.stringify({
bot_id: vibeBotId,
user_id: vibeUserId,
session_id: vibeSessionId,
channel: "web",
content: content,
message_type: 1,
timestamp: new Date().toISOString(),
}),
);
}
}
function connectTaskProgressWs(taskId) {
var proto = location.protocol === "https:" ? "wss://" : "ws://";
var url =
proto +
location.host +
"/ws/task-progress" +
(taskId ? "/" + taskId : "");
if (taskProgressWs) {
try {
taskProgressWs.close();
} catch (ignore) { }
}
taskProgressWs = new WebSocket(url);
taskProgressWs.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
if (data.type === "connected") return;
if (
data.event_type === "agent_thought" ||
data.step === "agent_thought"
) {
var agentLabel = (data.details || "mantis_1").replace(
"mantis_",
"Mantis #",
);
vibeAddMsg(
"system",
"💭 " +
agentLabel +
": " +
esc(data.text || data.message || ""),
);
return;
}
if (
data.event_type === "agent_update" ||
data.step === "agent_update"
) {
try {
var info =
typeof data.details === "string"
? JSON.parse(data.details)
: data.details;
if (info) {
updateAgentCard(
info.agent_id,
info.status,
info.detail,
);
}
} catch (ignore) { }
return;
}
if (
data.event_type === "task_node" ||
data.step === "task_node"
) {
try {
var nodeInfo =
typeof data.details === "string"
? JSON.parse(data.details)
: data.details;
if (nodeInfo) {
addTaskNode(
nodeInfo.title || data.message || "Task",
nodeInfo.description || "",
{
status: nodeInfo.status || "Planning",
estimated_files:
nodeInfo.estimated_files,
estimated_time:
nodeInfo.estimated_time,
estimated_tokens:
nodeInfo.estimated_tokens,
fileList: nodeInfo.files || [],
},
);
}
} catch (ignore) {
addTaskNode(data.message || "Task", "", {
status: "Planning",
});
}
return;
}
if (
data.event_type === "step_progress" ||
data.step === "step_progress"
) {
var pct = 0;
if (data.current_step && data.total_steps) {
pct = Math.round(
(data.current_step / data.total_steps) * 100,
);
} else if (data.current && data.total) {
pct = Math.round((data.current / data.total) * 100);
}
updateMantis1("working");
var bar = document.querySelector(
'.as-agent-card[data-agent-id="1"] .as-bar-fill',
);
if (bar) bar.style.width = pct + "%";
var stageMap = {
Planning: "plan",
Building: "build",
Reviewing: "review",
Deploying: "deploy",
Monitoring: "monitor",
};
var stageLabel = data.message || "";
var tabStage = stageMap[stageLabel];
if (tabStage) {
var allTabs =
document.querySelectorAll(".vibe-pipeline-tab");
allTabs.forEach(function (t) {
t.classList.remove("active");
});
var activeTab = document.querySelector(
'.vibe-pipeline-tab[data-stage="' +
tabStage +
'"]',
);
if (activeTab) activeTab.classList.add("active");
}
return;
}
if (
data.event_type === "pipeline_complete" ||
data.step === "pipeline_complete"
) {
updateMantis1("done");
vibeAddMsg(
"system",
"✅ Pipeline complete — all stages finished",
);
return;
}
if (data.event_type === "manifest_update") {
return;
}
} catch (e) {
console.error("Task progress parse error:", e);
}
};
taskProgressWs.onerror = function () { };
taskProgressWs.onclose = function () { };
}

View file

@ -0,0 +1,517 @@
@keyframes nodeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.vibe-quick-btn:hover {
border-color: var(--accent) !important;
color: var(--accent) !important;
background: rgba(132, 214, 105, 0.06) !important;
transform: translateY(-2px);
}
.vibe-quick-btn {
padding: 8px 16px;
border: 1px solid var(--border);
border-radius: 20px;
background: var(--bg);
font-size: 12px;
cursor: pointer;
color: var(--text-muted);
transition: all 0.15s;
font-family: "Fira Code", monospace;
}
.as-logo-section {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.as-logo-section h2 {
margin: 0;
font-size: 15px;
color: var(--text);
font-weight: 800;
display: flex;
align-items: center;
gap: 8px;
}
.as-logo-icons {
display: flex;
gap: 12px;
color: var(--text-muted);
font-size: 14px;
}
.as-logo-icons span {
cursor: pointer;
}
.as-create-btn-full {
width: calc(100% - 16px);
margin: 8px;
}
.mcp-overlay {
width: 280px;
margin-left: 280px;
position: absolute;
left: 0;
bottom: 0;
top: 60px;
pointer-events: none;
z-index: 50;
}
.mcp-overlay-inner {
pointer-events: auto;
position: absolute;
bottom: 24px;
left: 24px;
right: 24px;
}
.vibe-canvas-bg {
background: var(--bg, #fdfdfd);
background-image: radial-gradient(var(--border) 1px, transparent 1px);
background-size: 20px 20px;
position: relative;
}
.vibe-canvas-header {
padding: 16px 24px;
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border);
background: var(--surface);
opacity: 0.95;
backdrop-filter: blur(4px);
}
.vibe-canvas-header-right {
float: right;
}
.vibe-toolbar-btn {
border: 1px solid var(--accent);
background: transparent;
color: var(--text);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
margin-right: 12px;
font-weight: 600;
}
.vibe-toolbar-btn-primary {
border: 1px solid var(--accent);
background: var(--accent);
color: var(--bg);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
margin-right: 12px;
font-weight: 600;
}
.vibe-zoom-btn {
border: 1px solid var(--border);
background: var(--bg);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
}
.vibe-zoom-label {
font-size: 11px;
margin: 0 8px;
color: var(--text);
}
.vibe-steps-container {
padding: 40px;
display: none;
gap: 60px;
align-items: flex-start;
overflow-x: auto;
}
.vibe-canvas-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 60px;
text-align: center;
}
.vibe-empty-icon {
font-size: 56px;
animation: float 3s ease-in-out infinite;
}
.vibe-empty-title {
margin: 0;
font-size: 22px;
font-weight: 800;
color: var(--text);
font-family: "Fira Code", monospace;
}
.vibe-empty-desc {
margin: 0;
font-size: 14px;
color: var(--text-muted);
max-width: 440px;
line-height: 1.6;
}
.vibe-quick-btns {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
margin-top: 12px;
}
.vibe-chat-overlay {
position: absolute;
bottom: 24px;
right: 24px;
width: 380px;
background: var(--surface);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border);
color: var(--text);
z-index: 100;
}
.vibe-chat-header {
padding: 12px 16px;
background: var(--surface-hover);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.vibe-chat-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.vibe-chat-agent-name {
font-size: 12px;
font-weight: 600;
}
.vibe-chat-status-badge {
font-size: 10px;
color: var(--text-muted);
background: var(--surface-hover);
padding: 2px 6px;
border-radius: 4px;
}
.vibe-chat-messages {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
font-size: 12px;
line-height: 1.5;
min-height: 220px;
max-height: 350px;
overflow-y: auto;
font-family: "Segoe UI", system-ui, sans-serif;
}
.vibe-chat-tip {
align-self: center;
background: rgba(132, 214, 105, 0.12);
color: var(--accent);
padding: 8px 14px;
border-radius: 8px;
font-size: 11px;
text-align: center;
}
.vibe-chat-footer {
padding: 12px;
border-top: 1px solid var(--border);
background: var(--surface);
}
.vibe-chat-form {
display: flex;
align-items: center;
gap: 8px;
background: var(--surface-hover);
padding: 8px 12px;
border-radius: 20px;
border: 1px solid var(--border);
}
.vibe-chat-attach {
color: var(--text-muted);
cursor: pointer;
transform: rotate(-45deg);
}
.vibe-chat-input {
flex: 1;
background: transparent;
border: none;
color: var(--text);
font-size: 13px;
outline: none;
font-family: "Segoe UI", system-ui, sans-serif;
}
.vibe-chat-send {
background: var(--accent);
color: var(--surface);
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
font-weight: bold;
border: none;
}
.vibe-panel-modal {
display: none;
position: absolute;
inset: 24px;
z-index: 50;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
}
.vibe-panel-close {
position: absolute;
top: 8px;
right: 16px;
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
z-index: 100;
}
.vibe-terminal-modal {
display: none;
position: absolute;
bottom: 24px;
left: 300px;
right: 24px;
height: 350px;
z-index: 50;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
}
.vibe-terminal-close {
position: absolute;
top: 4px;
right: 8px;
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
z-index: 100;
}
.vibe-deployment-panel-inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--surface);
border-radius: 16px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
width: 600px;
max-width: 90%;
max-height: 80vh;
overflow-y: auto;
border: 1px solid var(--border);
z-index: 1000;
}
.vibe-deploy-header {
padding: 24px;
border-bottom: 1px solid var(--border);
}
.vibe-deploy-header h2 {
margin: 0;
font-size: 20px;
color: var(--text);
display: flex;
align-items: center;
gap: 12px;
}
.vibe-deploy-header p {
margin: 8px 0 0;
font-size: 14px;
color: var(--text-muted);
}
.vibe-deploy-body {
padding: 24px;
}
.vibe-deploy-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.deployment-option {
padding: 20px;
border: 2px solid var(--border);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
}
.deployment-option-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.deployment-option-header h3 {
margin: 0;
font-size: 16px;
color: var(--text);
}
.deployment-option-header span.label {
font-size: 12px;
color: var(--text-muted);
}
.deployment-option ul {
margin: 0;
padding-left: 20px;
font-size: 13px;
color: var(--text-muted);
line-height: 1.6;
}
.vibe-deploy-section h3 {
margin: 0 0 16px;
font-size: 14px;
color: var(--text);
}
.vibe-deploy-field {
margin-bottom: 16px;
}
.vibe-deploy-field label {
display: block;
margin-bottom: 8px;
font-size: 13px;
color: var(--text-muted);
}
.vibe-deploy-field input[type="text"],
.vibe-deploy-field select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 14px;
}
.vibe-deploy-field-hint {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
display: block;
}
.vibe-deploy-checkbox label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text);
cursor: pointer;
}
.vibe-deploy-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.vibe-deploy-cancel {
padding: 10px 20px;
border: 1px solid var(--border);
border-radius: 8px;
background: transparent;
color: var(--text);
font-size: 14px;
cursor: pointer;
}
.vibe-deploy-submit {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: var(--accent);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}