diff --git a/ui/suite/sources/index.html b/ui/suite/sources/index.html index c67a3e7..8db8c74 100644 --- a/ui/suite/sources/index.html +++ b/ui/suite/sources/index.html @@ -8,7 +8,7 @@ Sources - Prompts, Templates, MCP Servers & AI Models + Repositories, Apps, Prompts, Templates & MCP Servers @@ -32,6 +32,33 @@ + + + + Repositories + + + + + + + + + Apps + + - + diff --git a/ui/suite/sources/sources.css b/ui/suite/sources/sources.css index 5760d7a..71f20ae 100644 --- a/ui/suite/sources/sources.css +++ b/ui/suite/sources/sources.css @@ -525,7 +525,9 @@ } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } /* Empty State */ @@ -651,7 +653,402 @@ color: var(--text-secondary); } +/* Repository Card */ +.repo-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + transition: all 0.2s; + cursor: pointer; +} + +.repo-card:hover { + border-color: var(--primary); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.repo-card.connected { + border-color: var(--success); +} + +.repo-header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; +} + +.repo-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.repo-icon svg { + width: 20px; + height: 20px; + color: white; +} + +.repo-icon.github { + background: linear-gradient(135deg, #24292e, #40464e); +} + +.repo-icon.gitlab { + background: linear-gradient(135deg, #fc6d26, #e24329); +} + +.repo-icon.bitbucket { + background: linear-gradient(135deg, #0052cc, #2684ff); +} + +.repo-info { + flex: 1; + min-width: 0; +} + +.repo-name { + font-size: 15px; + font-weight: 600; + margin: 0 0 4px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.repo-name .mention-tag { + font-size: 12px; + color: var(--primary); + background: var(--primary-bg); + padding: 2px 8px; + border-radius: 4px; + font-weight: 500; +} + +.repo-owner { + font-size: 13px; + color: var(--text-secondary); +} + +.repo-description { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; + margin-bottom: 12px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.repo-meta { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.repo-meta-item { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--text-secondary); +} + +.repo-meta-item svg { + width: 14px; + height: 14px; +} + +.repo-language { + display: flex; + align-items: center; + gap: 6px; +} + +.repo-language-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.repo-language-dot.rust { + background: #dea584; +} +.repo-language-dot.typescript { + background: #3178c6; +} +.repo-language-dot.javascript { + background: #f7df1e; +} +.repo-language-dot.python { + background: #3776ab; +} +.repo-language-dot.html { + background: #e34f26; +} +.repo-language-dot.css { + background: #1572b6; +} + +.repo-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + padding: 4px 10px; + border-radius: 12px; + font-weight: 500; +} + +.repo-status.connected { + background: var(--success-bg); + color: var(--success); +} + +.repo-status.disconnected { + background: var(--surface-hover); + color: var(--text-secondary); +} + +.repo-actions { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.repo-action-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: transparent; + color: var(--text); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.repo-action-btn:hover { + background: var(--surface-hover); + border-color: var(--primary); +} + +.repo-action-btn.primary { + background: var(--primary); + border-color: var(--primary); + color: white; +} + +.repo-action-btn.primary:hover { + background: var(--primary-hover); +} + +/* App Card */ +.app-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + transition: all 0.2s; + cursor: pointer; +} + +.app-card:hover { + border-color: var(--primary); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.app-preview { + height: 140px; + background: linear-gradient(135deg, var(--surface-hover), var(--surface)); + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.app-preview-placeholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: var(--text-secondary); + opacity: 0.5; +} + +.app-preview-placeholder svg { + width: 40px; + height: 40px; +} + +.app-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.app-content { + padding: 16px; +} + +.app-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 8px; +} + +.app-name { + font-size: 15px; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 8px; +} + +.app-name .mention-tag { + font-size: 11px; + color: var(--primary); + background: var(--primary-bg); + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; +} + +.app-type { + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + font-weight: 500; + background: var(--surface-hover); + color: var(--text-secondary); +} + +.app-type.htmx { + background: #e8f4fd; + color: #0284c7; +} +.app-type.site { + background: #fef3c7; + color: #d97706; +} +.app-type.dashboard { + background: #dcfce7; + color: #16a34a; +} + +.app-description { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; + margin-bottom: 12px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.app-meta { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: var(--text-secondary); +} + +.app-created { + display: flex; + align-items: center; + gap: 4px; +} + +.app-url { + color: var(--primary); + font-family: var(--font-mono); + font-size: 11px; +} + +.app-actions { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.app-action-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: transparent; + color: var(--text); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.app-action-btn:hover { + background: var(--surface-hover); + border-color: var(--primary); +} + +.app-action-btn.primary { + background: var(--primary); + border-color: var(--primary); + color: white; +} + +/* Add Repository Modal */ +.add-repo-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.repo-url-input { + width: 100%; + padding: 12px 16px; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 14px; + background: var(--surface); + color: var(--text); +} + +.repo-url-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-bg); +} + /* Grids for different content types */ +.repos-grid, +.apps-grid, .templates-grid, .servers-grid, .models-grid, @@ -661,6 +1058,8 @@ gap: 16px; } +.repos-grid.list-view, +.apps-grid.list-view, .templates-grid.list-view, .servers-grid.list-view, .models-grid.list-view, @@ -703,6 +1102,8 @@ } .prompts-grid, + .repos-grid, + .apps-grid, .templates-grid, .servers-grid, .models-grid, @@ -710,3 +1111,79 @@ grid-template-columns: 1fr; } } + +/* Mention autocomplete in chat */ +.mention-suggestion { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + cursor: pointer; + transition: background 0.15s; +} + +.mention-suggestion:hover, +.mention-suggestion.active { + background: var(--surface-hover); +} + +.mention-suggestion-icon { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.mention-suggestion-icon.repo { + background: linear-gradient(135deg, #24292e, #40464e); + color: white; +} + +.mention-suggestion-icon.app { + background: linear-gradient(135deg, #06b6d4, #0ea5e9); + color: white; +} + +.mention-suggestion-info { + flex: 1; + min-width: 0; +} + +.mention-suggestion-name { + font-size: 14px; + font-weight: 500; + margin: 0; +} + +.mention-suggestion-type { + font-size: 12px; + color: var(--text-secondary); +} + +/* Inline mention tag in chat */ +.chat-mention { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: var(--primary-bg); + color: var(--primary); + border-radius: 4px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.chat-mention:hover { + background: var(--primary); + color: white; +} + +.chat-mention svg { + width: 14px; + height: 14px; +} diff --git a/ui/suite/sources/sources.js b/ui/suite/sources/sources.js index e720611..6ffc240 100644 --- a/ui/suite/sources/sources.js +++ b/ui/suite/sources/sources.js @@ -1,333 +1,829 @@ /** * Sources Module JavaScript - * Prompts, Templates, MCP Servers & AI Models + * Repositories, Apps, Prompts, Templates, MCP Servers & AI Models + * Provides @mention support for chat context */ -(function() { - 'use strict'; +(function () { + "use strict"; - /** - * Initialize the Sources module - */ - function init() { - setupTabNavigation(); - setupCategoryNavigation(); - setupViewToggle(); - setupKeyboardShortcuts(); - setupHTMXEvents(); - } + /** + * Initialize the Sources module + */ + function init() { + setupTabNavigation(); + setupCategoryNavigation(); + setupViewToggle(); + setupKeyboardShortcuts(); + setupHTMXEvents(); + setupRepoCards(); + setupAppCards(); + setupMentionAutocomplete(); + } - /** - * Set active tab - */ - window.setActiveTab = function(btn) { - document.querySelectorAll('.tab-btn').forEach(t => { - t.classList.remove('active'); - t.setAttribute('aria-selected', 'false'); - }); - btn.classList.add('active'); - btn.setAttribute('aria-selected', 'true'); - }; + /** + * Set active tab + */ + window.setActiveTab = function (btn) { + document.querySelectorAll(".tab-btn").forEach((t) => { + t.classList.remove("active"); + t.setAttribute("aria-selected", "false"); + }); + btn.classList.add("active"); + btn.setAttribute("aria-selected", "true"); + }; - /** - * Setup tab navigation - */ - function setupTabNavigation() { - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', function() { - setActiveTab(this); - }); - }); - } + /** + * Setup tab navigation + */ + function setupTabNavigation() { + document.querySelectorAll(".tab-btn").forEach((btn) => { + btn.addEventListener("click", function () { + setActiveTab(this); + }); + }); + } - /** - * Setup category navigation - */ - function setupCategoryNavigation() { - document.addEventListener('click', function(e) { - const categoryItem = e.target.closest('.category-item'); - if (categoryItem) { - document.querySelectorAll('.category-item').forEach(c => c.classList.remove('active')); - categoryItem.classList.add('active'); + /** + * Setup category navigation + */ + function setupCategoryNavigation() { + document.addEventListener("click", function (e) { + const categoryItem = e.target.closest(".category-item"); + if (categoryItem) { + document + .querySelectorAll(".category-item") + .forEach((c) => c.classList.remove("active")); + categoryItem.classList.add("active"); + } + }); + } + + /** + * Setup view toggle (grid/list) + */ + function setupViewToggle() { + document.addEventListener("click", function (e) { + const viewBtn = e.target.closest(".view-btn"); + if (viewBtn) { + const controls = viewBtn.closest(".view-controls"); + if (controls) { + controls + .querySelectorAll(".view-btn") + .forEach((b) => b.classList.remove("active")); + viewBtn.classList.add("active"); + + const grid = document.querySelector( + ".prompts-grid, .templates-grid, .servers-grid, .models-grid, .news-grid", + ); + if (grid) { + if (viewBtn.title === "List view") { + grid.classList.add("list-view"); + } else { + grid.classList.remove("list-view"); } - }); - } + } + } + } + }); + } - /** - * Setup view toggle (grid/list) - */ - function setupViewToggle() { - document.addEventListener('click', function(e) { - const viewBtn = e.target.closest('.view-btn'); - if (viewBtn) { - const controls = viewBtn.closest('.view-controls'); - if (controls) { - controls.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); - viewBtn.classList.add('active'); + /** + * Setup keyboard shortcuts + */ + function setupKeyboardShortcuts() { + document.addEventListener("keydown", function (e) { + // Ctrl+K to focus search + if ((e.ctrlKey || e.metaKey) && e.key === "k") { + e.preventDefault(); + const searchInput = document.querySelector(".search-box input"); + if (searchInput) searchInput.focus(); + } - const grid = document.querySelector('.prompts-grid, .templates-grid, .servers-grid, .models-grid, .news-grid'); - if (grid) { - if (viewBtn.title === 'List view') { - grid.classList.add('list-view'); - } else { - grid.classList.remove('list-view'); - } - } - } - } - }); - } + // Tab navigation with number keys + if ( + !e.ctrlKey && + !e.metaKey && + !e.altKey && + !e.target.matches("input, textarea") + ) { + const tabs = document.querySelectorAll(".tab-btn"); + const num = parseInt(e.key); + if (num >= 1 && num <= tabs.length) { + tabs[num - 1].click(); + } + } - /** - * Setup keyboard shortcuts - */ - function setupKeyboardShortcuts() { - document.addEventListener('keydown', function(e) { - // Ctrl+K to focus search - if ((e.ctrlKey || e.metaKey) && e.key === 'k') { - e.preventDefault(); - const searchInput = document.querySelector('.search-box input'); - if (searchInput) searchInput.focus(); - } + // Escape to close modals + if (e.key === "Escape") { + closeModals(); + } + }); + } - // Tab navigation with number keys - if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.target.matches('input, textarea')) { - const tabs = document.querySelectorAll('.tab-btn'); - const num = parseInt(e.key); - if (num >= 1 && num <= tabs.length) { - tabs[num - 1].click(); - } - } + /** + * Setup HTMX events + */ + function setupHTMXEvents() { + if (typeof htmx === "undefined") return; - // Escape to close modals - if (e.key === 'Escape') { - closeModals(); - } - }); - } - - /** - * Setup HTMX events - */ - function setupHTMXEvents() { - if (typeof htmx === 'undefined') return; - - document.body.addEventListener('htmx:beforeRequest', function(e) { - if (e.detail.target && e.detail.target.id === 'content-area') { - e.detail.target.innerHTML = ` + document.body.addEventListener("htmx:beforeRequest", function (e) { + if (e.detail.target && e.detail.target.id === "content-area") { + e.detail.target.innerHTML = ` Loading... `; - } - }); + } + }); - document.body.addEventListener('htmx:afterSwap', function(e) { - // Re-initialize any dynamic content handlers after content swap - setupPromptCards(); - setupServerCards(); - setupModelCards(); - }); - } + document.body.addEventListener("htmx:afterSwap", function (e) { + // Re-initialize any dynamic content handlers after content swap + setupPromptCards(); + setupServerCards(); + setupModelCards(); + setupRepoCards(); + setupAppCards(); + }); + } - /** - * Setup prompt card interactions - */ - function setupPromptCards() { - document.querySelectorAll('.prompt-card').forEach(card => { - card.addEventListener('click', function(e) { - // Don't trigger if clicking on action buttons - if (e.target.closest('.prompt-action-btn')) return; - - const promptId = this.dataset.id; - if (promptId) { - showPromptDetail(promptId); - } - }); - }); + /** + * Setup prompt card interactions + */ + function setupPromptCards() { + document.querySelectorAll(".prompt-card").forEach((card) => { + card.addEventListener("click", function (e) { + // Don't trigger if clicking on action buttons + if (e.target.closest(".prompt-action-btn")) return; - document.querySelectorAll('.prompt-action-btn').forEach(btn => { - btn.addEventListener('click', function(e) { - e.stopPropagation(); - const action = this.title.toLowerCase(); - const card = this.closest('.prompt-card'); - const promptId = card?.dataset.id; - - switch (action) { - case 'use': - usePrompt(promptId); - break; - case 'copy': - copyPrompt(promptId); - break; - case 'save': - savePrompt(promptId); - break; - } - }); - }); - } - - /** - * Setup server card interactions - */ - function setupServerCards() { - document.querySelectorAll('.server-card').forEach(card => { - card.addEventListener('click', function() { - const serverId = this.dataset.id; - if (serverId) { - showServerDetail(serverId); - } - }); - }); - } - - /** - * Setup model card interactions - */ - function setupModelCards() { - document.querySelectorAll('.model-card').forEach(card => { - card.addEventListener('click', function() { - const modelId = this.dataset.id; - if (modelId) { - showModelDetail(modelId); - } - }); - }); - } - - /** - * Show prompt detail modal/panel - */ - function showPromptDetail(promptId) { - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/sources/prompts/${promptId}`, { - target: '#prompt-detail-panel', - swap: 'innerHTML' - }).then(() => { - document.getElementById('prompt-detail-panel')?.classList.remove('hidden'); - }); + const promptId = this.dataset.id; + if (promptId) { + showPromptDetail(promptId); } - } + }); + }); - /** - * Use a prompt - */ - function usePrompt(promptId) { - if (typeof htmx !== 'undefined') { - htmx.ajax('POST', `/api/sources/prompts/${promptId}/use`, { - swap: 'none' - }).then(() => { - // Navigate to the appropriate module - window.location.hash = '#research'; - }); + document.querySelectorAll(".prompt-action-btn").forEach((btn) => { + btn.addEventListener("click", function (e) { + e.stopPropagation(); + const action = this.title.toLowerCase(); + const card = this.closest(".prompt-card"); + const promptId = card?.dataset.id; + + switch (action) { + case "use": + usePrompt(promptId); + break; + case "copy": + copyPrompt(promptId); + break; + case "save": + savePrompt(promptId); + break; } - } + }); + }); + } - /** - * Copy prompt to clipboard - */ - function copyPrompt(promptId) { - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/sources/prompts/${promptId}/content`, { - swap: 'none' - }).then(response => { - // Parse response and copy to clipboard - navigator.clipboard.writeText(response || ''); - showToast('Prompt copied to clipboard'); - }); + /** + * Setup server card interactions + */ + function setupServerCards() { + document.querySelectorAll(".server-card").forEach((card) => { + card.addEventListener("click", function () { + const serverId = this.dataset.id; + if (serverId) { + showServerDetail(serverId); } - } + }); + }); + } - /** - * Save prompt to collection - */ - function savePrompt(promptId) { - const collectionName = prompt('Enter collection name:'); - if (collectionName && typeof htmx !== 'undefined') { - htmx.ajax('POST', '/api/sources/prompts/save', { - values: { - promptId, - collection: collectionName - } - }).then(() => { - showToast('Prompt saved to collection'); - }); + /** + * Setup model card interactions + */ + function setupModelCards() { + document.querySelectorAll(".model-card").forEach((card) => { + card.addEventListener("click", function () { + const modelId = this.dataset.id; + if (modelId) { + showModelDetail(modelId); } - } + }); + }); + } - /** - * Show server detail - */ - function showServerDetail(serverId) { - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/sources/mcp-servers/${serverId}`, { - target: '#server-detail-panel', - swap: 'innerHTML' - }).then(() => { - document.getElementById('server-detail-panel')?.classList.remove('hidden'); - }); - } - } - - /** - * Show model detail - */ - function showModelDetail(modelId) { - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/sources/models/${modelId}`, { - target: '#model-detail-panel', - swap: 'innerHTML' - }).then(() => { - document.getElementById('model-detail-panel')?.classList.remove('hidden'); - }); - } - } - - /** - * Close all modals - */ - function closeModals() { - document.querySelectorAll('.modal, .detail-panel').forEach(modal => { - modal.classList.add('hidden'); + /** + * Show prompt detail modal/panel + */ + function showPromptDetail(promptId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/prompts/${promptId}`, { + target: "#prompt-detail-panel", + swap: "innerHTML", + }) + .then(() => { + document + .getElementById("prompt-detail-panel") + ?.classList.remove("hidden"); }); } + } - /** - * Show toast notification - */ - function showToast(message, type = 'success') { - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - toast.textContent = message; - document.body.appendChild(toast); - - // Trigger animation - requestAnimationFrame(() => { - toast.classList.add('show'); + /** + * Use a prompt + */ + function usePrompt(promptId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("POST", `/api/sources/prompts/${promptId}/use`, { + swap: "none", + }) + .then(() => { + // Navigate to the appropriate module + window.location.hash = "#research"; }); + } + } - // Remove after delay - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 3000); + /** + * Copy prompt to clipboard + */ + function copyPrompt(promptId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/prompts/${promptId}/content`, { + swap: "none", + }) + .then((response) => { + // Parse response and copy to clipboard + navigator.clipboard.writeText(response || ""); + showToast("Prompt copied to clipboard"); + }); + } + } + + /** + * Save prompt to collection + */ + function savePrompt(promptId) { + const collectionName = prompt("Enter collection name:"); + if (collectionName && typeof htmx !== "undefined") { + htmx + .ajax("POST", "/api/sources/prompts/save", { + values: { + promptId, + collection: collectionName, + }, + }) + .then(() => { + showToast("Prompt saved to collection"); + }); + } + } + + /** + * Setup repository card interactions + */ + function setupRepoCards() { + document.querySelectorAll(".repo-card").forEach((card) => { + card.addEventListener("click", function (e) { + if (e.target.closest(".repo-action-btn")) return; + + const repoId = this.dataset.id; + if (repoId) { + showRepoDetail(repoId); + } + }); + }); + + document.querySelectorAll(".repo-action-btn").forEach((btn) => { + btn.addEventListener("click", function (e) { + e.stopPropagation(); + const action = this.dataset.action; + const card = this.closest(".repo-card"); + const repoId = card?.dataset.id; + const repoName = card?.dataset.name; + + switch (action) { + case "connect": + connectRepo(repoId); + break; + case "disconnect": + disconnectRepo(repoId); + break; + case "mention": + insertMention("repo", repoName); + break; + case "browse": + browseRepo(repoId); + break; + } + }); + }); + } + + /** + * Setup app card interactions + */ + function setupAppCards() { + document.querySelectorAll(".app-card").forEach((card) => { + card.addEventListener("click", function (e) { + if (e.target.closest(".app-action-btn")) return; + + const appId = this.dataset.id; + if (appId) { + showAppDetail(appId); + } + }); + }); + + document.querySelectorAll(".app-action-btn").forEach((btn) => { + btn.addEventListener("click", function (e) { + e.stopPropagation(); + const action = this.dataset.action; + const card = this.closest(".app-card"); + const appId = card?.dataset.id; + const appName = card?.dataset.name; + + switch (action) { + case "open": + openApp(appId); + break; + case "edit": + editApp(appId); + break; + case "mention": + insertMention("app", appName); + break; + } + }); + }); + } + + /** + * Setup @mention autocomplete for chat + */ + function setupMentionAutocomplete() { + // Listen for @ symbol in chat input + document.addEventListener("input", function (e) { + if (!e.target.matches(".chat-input, .message-input, #chat-input")) return; + + const input = e.target; + const value = input.value; + const cursorPos = input.selectionStart; + + // Find @ before cursor + const textBeforeCursor = value.substring(0, cursorPos); + const atMatch = textBeforeCursor.match(/@(\w*)$/); + + if (atMatch) { + const query = atMatch[1]; + showMentionSuggestions(input, query); + } else { + hideMentionSuggestions(); + } + }); + + // Handle mention selection + document.addEventListener("click", function (e) { + const suggestion = e.target.closest(".mention-suggestion"); + if (suggestion) { + const type = suggestion.dataset.type; + const name = suggestion.dataset.name; + applyMention(type, name); + } + }); + + // Keyboard navigation for suggestions + document.addEventListener("keydown", function (e) { + const suggestions = document.querySelector(".mention-suggestions"); + if (!suggestions || suggestions.classList.contains("hidden")) return; + + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + navigateMentionSuggestions(e.key === "ArrowDown" ? 1 : -1); + } else if (e.key === "Enter" || e.key === "Tab") { + const active = suggestions.querySelector(".mention-suggestion.active"); + if (active) { + e.preventDefault(); + active.click(); + } + } else if (e.key === "Escape") { + hideMentionSuggestions(); + } + }); + } + + /** + * Show mention suggestions dropdown + */ + function showMentionSuggestions(input, query) { + let suggestions = document.querySelector(".mention-suggestions"); + + if (!suggestions) { + suggestions = document.createElement("div"); + suggestions.className = "mention-suggestions"; + suggestions.style.cssText = ` + position: absolute; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + max-height: 300px; + overflow-y: auto; + z-index: 1000; + `; + document.body.appendChild(suggestions); } - // Initialize on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); + // Fetch matching repos and apps + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/mentions?q=${encodeURIComponent(query)}`, { + swap: "none", + }) + .then((response) => { + try { + const data = JSON.parse(response); + renderMentionSuggestions(suggestions, data, input); + } catch (e) { + // Fallback to showing cached data + renderMentionSuggestions( + suggestions, + getMockMentions(query), + input, + ); + } + }); } else { - init(); + renderMentionSuggestions(suggestions, getMockMentions(query), input); + } + } + + /** + * Get mock mentions for development + */ + function getMockMentions(query) { + const allMentions = [ + { type: "repo", name: "botserver", description: "Core API server" }, + { type: "repo", name: "botui", description: "Web UI components" }, + { type: "repo", name: "botbook", description: "Documentation" }, + { type: "repo", name: "bottemplates", description: "Bot templates" }, + { type: "app", name: "crm", description: "Customer management app" }, + { type: "app", name: "dashboard", description: "Analytics dashboard" }, + { type: "app", name: "myapp", description: "Custom application" }, + ]; + + if (!query) return allMentions.slice(0, 5); + + return allMentions + .filter((m) => m.name.toLowerCase().includes(query.toLowerCase())) + .slice(0, 5); + } + + /** + * Render mention suggestions + */ + function renderMentionSuggestions(container, data, input) { + if (!data || data.length === 0) { + container.classList.add("hidden"); + return; } - // Expose for external use - window.Sources = { - setActiveTab, - showPromptDetail, - usePrompt, - copyPrompt, - savePrompt, - showToast - }; + const rect = input.getBoundingClientRect(); + container.style.top = `${rect.bottom + 4}px`; + container.style.left = `${rect.left}px`; + container.style.width = `${Math.min(rect.width, 320)}px`; + + container.innerHTML = data + .map( + (item, index) => ` + + + ${ + item.type === "repo" + ? '' + : '' + } + + + @${item.name} + ${item.type === "repo" ? "Repository" : "App"} • ${item.description || ""} + + + `, + ) + .join(""); + + container.classList.remove("hidden"); + } + + /** + * Hide mention suggestions + */ + function hideMentionSuggestions() { + const suggestions = document.querySelector(".mention-suggestions"); + if (suggestions) { + suggestions.classList.add("hidden"); + } + } + + /** + * Navigate mention suggestions with keyboard + */ + function navigateMentionSuggestions(direction) { + const suggestions = document.querySelectorAll(".mention-suggestion"); + const current = document.querySelector(".mention-suggestion.active"); + let index = Array.from(suggestions).indexOf(current); + + index += direction; + if (index < 0) index = suggestions.length - 1; + if (index >= suggestions.length) index = 0; + + suggestions.forEach((s) => s.classList.remove("active")); + suggestions[index]?.classList.add("active"); + suggestions[index]?.scrollIntoView({ block: "nearest" }); + } + + /** + * Apply selected mention to input + */ + function applyMention(type, name) { + const input = document.querySelector( + ".chat-input, .message-input, #chat-input", + ); + if (!input) return; + + const value = input.value; + const cursorPos = input.selectionStart; + const textBeforeCursor = value.substring(0, cursorPos); + const textAfterCursor = value.substring(cursorPos); + + // Replace @query with @name + const newTextBefore = textBeforeCursor.replace(/@\w*$/, `@${name} `); + input.value = newTextBefore + textAfterCursor; + input.selectionStart = input.selectionEnd = newTextBefore.length; + input.focus(); + + hideMentionSuggestions(); + + // Store context for the task + storeTaskContext(type, name); + } + + /** + * Insert mention from Sources page + */ + function insertMention(type, name) { + // Navigate to chat and insert mention + const chatInput = document.querySelector( + ".chat-input, .message-input, #chat-input", + ); + if (chatInput) { + chatInput.value += `@${name} `; + chatInput.focus(); + storeTaskContext(type, name); + showToast(`Added @${name} to chat context`); + } else { + // Store for next chat session + sessionStorage.setItem("pendingMention", JSON.stringify({ type, name })); + showToast(`@${name} will be added when you open chat`); + } + } + + /** + * Store context for autonomous tasks + */ + function storeTaskContext(type, name) { + let context = JSON.parse(sessionStorage.getItem("taskContext") || "[]"); + + // Avoid duplicates + if (!context.find((c) => c.type === type && c.name === name)) { + context.push({ type, name, addedAt: Date.now() }); + sessionStorage.setItem("taskContext", JSON.stringify(context)); + } + } + + /** + * Get current task context + */ + window.getTaskContext = function () { + return JSON.parse(sessionStorage.getItem("taskContext") || "[]"); + }; + + /** + * Clear task context + */ + window.clearTaskContext = function () { + sessionStorage.removeItem("taskContext"); + }; + + /** + * Show repository detail + */ + function showRepoDetail(repoId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/repositories/${repoId}`, { + target: "#repo-detail-panel", + swap: "innerHTML", + }) + .then(() => { + document + .getElementById("repo-detail-panel") + ?.classList.remove("hidden"); + }); + } + } + + /** + * Connect a repository + */ + function connectRepo(repoId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("POST", `/api/sources/repositories/${repoId}/connect`, { + swap: "none", + }) + .then(() => { + showToast("Repository connected"); + // Refresh the repo card + htmx.ajax("GET", "/api/sources/repositories", { + target: "#content-area", + swap: "innerHTML", + }); + }); + } + } + + /** + * Disconnect a repository + */ + function disconnectRepo(repoId) { + if (confirm("Disconnect this repository?")) { + if (typeof htmx !== "undefined") { + htmx + .ajax("DELETE", `/api/sources/repositories/${repoId}/connect`, { + swap: "none", + }) + .then(() => { + showToast("Repository disconnected"); + htmx.ajax("GET", "/api/sources/repositories", { + target: "#content-area", + swap: "innerHTML", + }); + }); + } + } + } + + /** + * Browse repository files + */ + function browseRepo(repoId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/repositories/${repoId}/files`, { + target: "#repo-browser-panel", + swap: "innerHTML", + }) + .then(() => { + document + .getElementById("repo-browser-panel") + ?.classList.remove("hidden"); + }); + } + } + + /** + * Show app detail + */ + function showAppDetail(appId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/apps/${appId}`, { + target: "#app-detail-panel", + swap: "innerHTML", + }) + .then(() => { + document + .getElementById("app-detail-panel") + ?.classList.remove("hidden"); + }); + } + } + + /** + * Open an app in new tab + */ + function openApp(appId) { + window.open(`/apps/${appId}`, "_blank"); + } + + /** + * Edit an app (opens in Tasks with context) + */ + function editApp(appId) { + // Store app context and navigate to tasks + storeTaskContext("app", appId); + window.location.hash = "#tasks"; + showToast(`Editing @${appId} - describe your changes`); + } + + /** + * Show server detail + */ + function showServerDetail(serverId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/mcp-servers/${serverId}`, { + target: "#server-detail-panel", + swap: "innerHTML", + }) + .then(() => { + document + .getElementById("server-detail-panel") + ?.classList.remove("hidden"); + }); + } + } + + /** + * Show model detail + */ + function showModelDetail(modelId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("GET", `/api/sources/models/${modelId}`, { + target: "#model-detail-panel", + swap: "innerHTML", + }) + .then(() => { + document + .getElementById("model-detail-panel") + ?.classList.remove("hidden"); + }); + } + } + + /** + * Close all modals + */ + function closeModals() { + document.querySelectorAll(".modal, .detail-panel").forEach((modal) => { + modal.classList.add("hidden"); + }); + } + + /** + * Show toast notification + */ + function showToast(message, type = "success") { + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + // Trigger animation + requestAnimationFrame(() => { + toast.classList.add("show"); + }); + + // Remove after delay + setTimeout(() => { + toast.classList.remove("show"); + setTimeout(() => toast.remove(), 300); + }, 3000); + } + + // Initialize on DOM ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + + // Expose for external use + window.Sources = { + setActiveTab, + showPromptDetail, + usePrompt, + copyPrompt, + savePrompt, + showToast, + showRepoDetail, + connectRepo, + disconnectRepo, + browseRepo, + showAppDetail, + openApp, + editApp, + insertMention, + getTaskContext: window.getTaskContext, + clearTaskContext: window.clearTaskContext, + }; })();
Prompts, Templates, MCP Servers & AI Models
Repositories, Apps, Prompts, Templates & MCP Servers
Loading...