Add Repositories and Apps tabs to Sources for @mention context in chat
- Added Repositories tab with GitHub/GitLab/Bitbucket support - Added Apps tab for previously created HTMX apps - @mention autocomplete in chat for repos (@botserver) and apps (@myapp) - Task context storage for autonomous task execution - CSS for repo and app cards with connection status - Mention suggestions dropdown with keyboard navigation
This commit is contained in:
parent
88a2610a62
commit
664211d6db
3 changed files with 1296 additions and 296 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
<header class="sources-header">
|
<header class="sources-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>Sources</h1>
|
<h1>Sources</h1>
|
||||||
<p class="header-subtitle">Prompts, Templates, MCP Servers & AI Models</p>
|
<p class="header-subtitle">Repositories, Apps, Prompts, Templates & MCP Servers</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
|
|
@ -32,6 +32,33 @@
|
||||||
<button class="tab-btn active"
|
<button class="tab-btn active"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected="true"
|
aria-selected="true"
|
||||||
|
hx-get="/api/sources/repositories"
|
||||||
|
hx-target="#content-area"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
onclick="setActiveTab(this)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
||||||
|
</svg>
|
||||||
|
Repositories
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
hx-get="/api/sources/apps"
|
||||||
|
hx-target="#content-area"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
onclick="setActiveTab(this)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
Apps
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
hx-get="/api/sources/prompts"
|
hx-get="/api/sources/prompts"
|
||||||
hx-target="#content-area"
|
hx-target="#content-area"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
|
|
@ -120,7 +147,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<main class="content-area" id="content-area" hx-get="/api/sources/prompts" hx-trigger="load" hx-swap="innerHTML">
|
<main class="content-area" id="content-area" hx-get="/api/sources/repositories" hx-trigger="load" hx-swap="innerHTML">
|
||||||
<!-- Content loaded via HTMX -->
|
<!-- Content loaded via HTMX -->
|
||||||
<div class="loading-spinner">
|
<div class="loading-spinner">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty State */
|
/* Empty State */
|
||||||
|
|
@ -651,7 +653,402 @@
|
||||||
color: var(--text-secondary);
|
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 */
|
/* Grids for different content types */
|
||||||
|
.repos-grid,
|
||||||
|
.apps-grid,
|
||||||
.templates-grid,
|
.templates-grid,
|
||||||
.servers-grid,
|
.servers-grid,
|
||||||
.models-grid,
|
.models-grid,
|
||||||
|
|
@ -661,6 +1058,8 @@
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repos-grid.list-view,
|
||||||
|
.apps-grid.list-view,
|
||||||
.templates-grid.list-view,
|
.templates-grid.list-view,
|
||||||
.servers-grid.list-view,
|
.servers-grid.list-view,
|
||||||
.models-grid.list-view,
|
.models-grid.list-view,
|
||||||
|
|
@ -703,6 +1102,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompts-grid,
|
.prompts-grid,
|
||||||
|
.repos-grid,
|
||||||
|
.apps-grid,
|
||||||
.templates-grid,
|
.templates-grid,
|
||||||
.servers-grid,
|
.servers-grid,
|
||||||
.models-grid,
|
.models-grid,
|
||||||
|
|
@ -710,3 +1111,79 @@
|
||||||
grid-template-columns: 1fr;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Sources Module JavaScript
|
* Sources Module JavaScript
|
||||||
* Prompts, Templates, MCP Servers & AI Models
|
* Repositories, Apps, Prompts, Templates, MCP Servers & AI Models
|
||||||
|
* Provides @mention support for chat context
|
||||||
*/
|
*/
|
||||||
(function() {
|
(function () {
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Sources module
|
* Initialize the Sources module
|
||||||
|
|
@ -14,26 +15,29 @@
|
||||||
setupViewToggle();
|
setupViewToggle();
|
||||||
setupKeyboardShortcuts();
|
setupKeyboardShortcuts();
|
||||||
setupHTMXEvents();
|
setupHTMXEvents();
|
||||||
|
setupRepoCards();
|
||||||
|
setupAppCards();
|
||||||
|
setupMentionAutocomplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set active tab
|
* Set active tab
|
||||||
*/
|
*/
|
||||||
window.setActiveTab = function(btn) {
|
window.setActiveTab = function (btn) {
|
||||||
document.querySelectorAll('.tab-btn').forEach(t => {
|
document.querySelectorAll(".tab-btn").forEach((t) => {
|
||||||
t.classList.remove('active');
|
t.classList.remove("active");
|
||||||
t.setAttribute('aria-selected', 'false');
|
t.setAttribute("aria-selected", "false");
|
||||||
});
|
});
|
||||||
btn.classList.add('active');
|
btn.classList.add("active");
|
||||||
btn.setAttribute('aria-selected', 'true');
|
btn.setAttribute("aria-selected", "true");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup tab navigation
|
* Setup tab navigation
|
||||||
*/
|
*/
|
||||||
function setupTabNavigation() {
|
function setupTabNavigation() {
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener("click", function () {
|
||||||
setActiveTab(this);
|
setActiveTab(this);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -43,11 +47,13 @@
|
||||||
* Setup category navigation
|
* Setup category navigation
|
||||||
*/
|
*/
|
||||||
function setupCategoryNavigation() {
|
function setupCategoryNavigation() {
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener("click", function (e) {
|
||||||
const categoryItem = e.target.closest('.category-item');
|
const categoryItem = e.target.closest(".category-item");
|
||||||
if (categoryItem) {
|
if (categoryItem) {
|
||||||
document.querySelectorAll('.category-item').forEach(c => c.classList.remove('active'));
|
document
|
||||||
categoryItem.classList.add('active');
|
.querySelectorAll(".category-item")
|
||||||
|
.forEach((c) => c.classList.remove("active"));
|
||||||
|
categoryItem.classList.add("active");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -56,20 +62,24 @@
|
||||||
* Setup view toggle (grid/list)
|
* Setup view toggle (grid/list)
|
||||||
*/
|
*/
|
||||||
function setupViewToggle() {
|
function setupViewToggle() {
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener("click", function (e) {
|
||||||
const viewBtn = e.target.closest('.view-btn');
|
const viewBtn = e.target.closest(".view-btn");
|
||||||
if (viewBtn) {
|
if (viewBtn) {
|
||||||
const controls = viewBtn.closest('.view-controls');
|
const controls = viewBtn.closest(".view-controls");
|
||||||
if (controls) {
|
if (controls) {
|
||||||
controls.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
controls
|
||||||
viewBtn.classList.add('active');
|
.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');
|
const grid = document.querySelector(
|
||||||
|
".prompts-grid, .templates-grid, .servers-grid, .models-grid, .news-grid",
|
||||||
|
);
|
||||||
if (grid) {
|
if (grid) {
|
||||||
if (viewBtn.title === 'List view') {
|
if (viewBtn.title === "List view") {
|
||||||
grid.classList.add('list-view');
|
grid.classList.add("list-view");
|
||||||
} else {
|
} else {
|
||||||
grid.classList.remove('list-view');
|
grid.classList.remove("list-view");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,17 +91,22 @@
|
||||||
* Setup keyboard shortcuts
|
* Setup keyboard shortcuts
|
||||||
*/
|
*/
|
||||||
function setupKeyboardShortcuts() {
|
function setupKeyboardShortcuts() {
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
// Ctrl+K to focus search
|
// Ctrl+K to focus search
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const searchInput = document.querySelector('.search-box input');
|
const searchInput = document.querySelector(".search-box input");
|
||||||
if (searchInput) searchInput.focus();
|
if (searchInput) searchInput.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab navigation with number keys
|
// Tab navigation with number keys
|
||||||
if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.target.matches('input, textarea')) {
|
if (
|
||||||
const tabs = document.querySelectorAll('.tab-btn');
|
!e.ctrlKey &&
|
||||||
|
!e.metaKey &&
|
||||||
|
!e.altKey &&
|
||||||
|
!e.target.matches("input, textarea")
|
||||||
|
) {
|
||||||
|
const tabs = document.querySelectorAll(".tab-btn");
|
||||||
const num = parseInt(e.key);
|
const num = parseInt(e.key);
|
||||||
if (num >= 1 && num <= tabs.length) {
|
if (num >= 1 && num <= tabs.length) {
|
||||||
tabs[num - 1].click();
|
tabs[num - 1].click();
|
||||||
|
|
@ -99,7 +114,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape to close modals
|
// Escape to close modals
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
closeModals();
|
closeModals();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -109,10 +124,10 @@
|
||||||
* Setup HTMX events
|
* Setup HTMX events
|
||||||
*/
|
*/
|
||||||
function setupHTMXEvents() {
|
function setupHTMXEvents() {
|
||||||
if (typeof htmx === 'undefined') return;
|
if (typeof htmx === "undefined") return;
|
||||||
|
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(e) {
|
document.body.addEventListener("htmx:beforeRequest", function (e) {
|
||||||
if (e.detail.target && e.detail.target.id === 'content-area') {
|
if (e.detail.target && e.detail.target.id === "content-area") {
|
||||||
e.detail.target.innerHTML = `
|
e.detail.target.innerHTML = `
|
||||||
<div class="loading-spinner">
|
<div class="loading-spinner">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
|
|
@ -122,11 +137,13 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function(e) {
|
document.body.addEventListener("htmx:afterSwap", function (e) {
|
||||||
// Re-initialize any dynamic content handlers after content swap
|
// Re-initialize any dynamic content handlers after content swap
|
||||||
setupPromptCards();
|
setupPromptCards();
|
||||||
setupServerCards();
|
setupServerCards();
|
||||||
setupModelCards();
|
setupModelCards();
|
||||||
|
setupRepoCards();
|
||||||
|
setupAppCards();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,10 +151,10 @@
|
||||||
* Setup prompt card interactions
|
* Setup prompt card interactions
|
||||||
*/
|
*/
|
||||||
function setupPromptCards() {
|
function setupPromptCards() {
|
||||||
document.querySelectorAll('.prompt-card').forEach(card => {
|
document.querySelectorAll(".prompt-card").forEach((card) => {
|
||||||
card.addEventListener('click', function(e) {
|
card.addEventListener("click", function (e) {
|
||||||
// Don't trigger if clicking on action buttons
|
// Don't trigger if clicking on action buttons
|
||||||
if (e.target.closest('.prompt-action-btn')) return;
|
if (e.target.closest(".prompt-action-btn")) return;
|
||||||
|
|
||||||
const promptId = this.dataset.id;
|
const promptId = this.dataset.id;
|
||||||
if (promptId) {
|
if (promptId) {
|
||||||
|
|
@ -146,21 +163,21 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.prompt-action-btn').forEach(btn => {
|
document.querySelectorAll(".prompt-action-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', function(e) {
|
btn.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const action = this.title.toLowerCase();
|
const action = this.title.toLowerCase();
|
||||||
const card = this.closest('.prompt-card');
|
const card = this.closest(".prompt-card");
|
||||||
const promptId = card?.dataset.id;
|
const promptId = card?.dataset.id;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'use':
|
case "use":
|
||||||
usePrompt(promptId);
|
usePrompt(promptId);
|
||||||
break;
|
break;
|
||||||
case 'copy':
|
case "copy":
|
||||||
copyPrompt(promptId);
|
copyPrompt(promptId);
|
||||||
break;
|
break;
|
||||||
case 'save':
|
case "save":
|
||||||
savePrompt(promptId);
|
savePrompt(promptId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -172,8 +189,8 @@
|
||||||
* Setup server card interactions
|
* Setup server card interactions
|
||||||
*/
|
*/
|
||||||
function setupServerCards() {
|
function setupServerCards() {
|
||||||
document.querySelectorAll('.server-card').forEach(card => {
|
document.querySelectorAll(".server-card").forEach((card) => {
|
||||||
card.addEventListener('click', function() {
|
card.addEventListener("click", function () {
|
||||||
const serverId = this.dataset.id;
|
const serverId = this.dataset.id;
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
showServerDetail(serverId);
|
showServerDetail(serverId);
|
||||||
|
|
@ -186,8 +203,8 @@
|
||||||
* Setup model card interactions
|
* Setup model card interactions
|
||||||
*/
|
*/
|
||||||
function setupModelCards() {
|
function setupModelCards() {
|
||||||
document.querySelectorAll('.model-card').forEach(card => {
|
document.querySelectorAll(".model-card").forEach((card) => {
|
||||||
card.addEventListener('click', function() {
|
card.addEventListener("click", function () {
|
||||||
const modelId = this.dataset.id;
|
const modelId = this.dataset.id;
|
||||||
if (modelId) {
|
if (modelId) {
|
||||||
showModelDetail(modelId);
|
showModelDetail(modelId);
|
||||||
|
|
@ -200,12 +217,16 @@
|
||||||
* Show prompt detail modal/panel
|
* Show prompt detail modal/panel
|
||||||
*/
|
*/
|
||||||
function showPromptDetail(promptId) {
|
function showPromptDetail(promptId) {
|
||||||
if (typeof htmx !== 'undefined') {
|
if (typeof htmx !== "undefined") {
|
||||||
htmx.ajax('GET', `/api/sources/prompts/${promptId}`, {
|
htmx
|
||||||
target: '#prompt-detail-panel',
|
.ajax("GET", `/api/sources/prompts/${promptId}`, {
|
||||||
swap: 'innerHTML'
|
target: "#prompt-detail-panel",
|
||||||
}).then(() => {
|
swap: "innerHTML",
|
||||||
document.getElementById('prompt-detail-panel')?.classList.remove('hidden');
|
})
|
||||||
|
.then(() => {
|
||||||
|
document
|
||||||
|
.getElementById("prompt-detail-panel")
|
||||||
|
?.classList.remove("hidden");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -214,12 +235,14 @@
|
||||||
* Use a prompt
|
* Use a prompt
|
||||||
*/
|
*/
|
||||||
function usePrompt(promptId) {
|
function usePrompt(promptId) {
|
||||||
if (typeof htmx !== 'undefined') {
|
if (typeof htmx !== "undefined") {
|
||||||
htmx.ajax('POST', `/api/sources/prompts/${promptId}/use`, {
|
htmx
|
||||||
swap: 'none'
|
.ajax("POST", `/api/sources/prompts/${promptId}/use`, {
|
||||||
}).then(() => {
|
swap: "none",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
// Navigate to the appropriate module
|
// Navigate to the appropriate module
|
||||||
window.location.hash = '#research';
|
window.location.hash = "#research";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -228,13 +251,15 @@
|
||||||
* Copy prompt to clipboard
|
* Copy prompt to clipboard
|
||||||
*/
|
*/
|
||||||
function copyPrompt(promptId) {
|
function copyPrompt(promptId) {
|
||||||
if (typeof htmx !== 'undefined') {
|
if (typeof htmx !== "undefined") {
|
||||||
htmx.ajax('GET', `/api/sources/prompts/${promptId}/content`, {
|
htmx
|
||||||
swap: 'none'
|
.ajax("GET", `/api/sources/prompts/${promptId}/content`, {
|
||||||
}).then(response => {
|
swap: "none",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
// Parse response and copy to clipboard
|
// Parse response and copy to clipboard
|
||||||
navigator.clipboard.writeText(response || '');
|
navigator.clipboard.writeText(response || "");
|
||||||
showToast('Prompt copied to clipboard');
|
showToast("Prompt copied to clipboard");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -243,29 +268,486 @@
|
||||||
* Save prompt to collection
|
* Save prompt to collection
|
||||||
*/
|
*/
|
||||||
function savePrompt(promptId) {
|
function savePrompt(promptId) {
|
||||||
const collectionName = prompt('Enter collection name:');
|
const collectionName = prompt("Enter collection name:");
|
||||||
if (collectionName && typeof htmx !== 'undefined') {
|
if (collectionName && typeof htmx !== "undefined") {
|
||||||
htmx.ajax('POST', '/api/sources/prompts/save', {
|
htmx
|
||||||
|
.ajax("POST", "/api/sources/prompts/save", {
|
||||||
values: {
|
values: {
|
||||||
promptId,
|
promptId,
|
||||||
collection: collectionName
|
collection: collectionName,
|
||||||
}
|
},
|
||||||
}).then(() => {
|
})
|
||||||
showToast('Prompt saved to collection');
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => `
|
||||||
|
<div class="mention-suggestion ${index === 0 ? "active" : ""}"
|
||||||
|
data-type="${item.type}"
|
||||||
|
data-name="${item.name}">
|
||||||
|
<div class="mention-suggestion-icon ${item.type}">
|
||||||
|
${
|
||||||
|
item.type === "repo"
|
||||||
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>'
|
||||||
|
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mention-suggestion-info">
|
||||||
|
<div class="mention-suggestion-name">@${item.name}</div>
|
||||||
|
<div class="mention-suggestion-type">${item.type === "repo" ? "Repository" : "App"} • ${item.description || ""}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.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
|
* Show server detail
|
||||||
*/
|
*/
|
||||||
function showServerDetail(serverId) {
|
function showServerDetail(serverId) {
|
||||||
if (typeof htmx !== 'undefined') {
|
if (typeof htmx !== "undefined") {
|
||||||
htmx.ajax('GET', `/api/sources/mcp-servers/${serverId}`, {
|
htmx
|
||||||
target: '#server-detail-panel',
|
.ajax("GET", `/api/sources/mcp-servers/${serverId}`, {
|
||||||
swap: 'innerHTML'
|
target: "#server-detail-panel",
|
||||||
}).then(() => {
|
swap: "innerHTML",
|
||||||
document.getElementById('server-detail-panel')?.classList.remove('hidden');
|
})
|
||||||
|
.then(() => {
|
||||||
|
document
|
||||||
|
.getElementById("server-detail-panel")
|
||||||
|
?.classList.remove("hidden");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,12 +756,16 @@
|
||||||
* Show model detail
|
* Show model detail
|
||||||
*/
|
*/
|
||||||
function showModelDetail(modelId) {
|
function showModelDetail(modelId) {
|
||||||
if (typeof htmx !== 'undefined') {
|
if (typeof htmx !== "undefined") {
|
||||||
htmx.ajax('GET', `/api/sources/models/${modelId}`, {
|
htmx
|
||||||
target: '#model-detail-panel',
|
.ajax("GET", `/api/sources/models/${modelId}`, {
|
||||||
swap: 'innerHTML'
|
target: "#model-detail-panel",
|
||||||
}).then(() => {
|
swap: "innerHTML",
|
||||||
document.getElementById('model-detail-panel')?.classList.remove('hidden');
|
})
|
||||||
|
.then(() => {
|
||||||
|
document
|
||||||
|
.getElementById("model-detail-panel")
|
||||||
|
?.classList.remove("hidden");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -288,35 +774,35 @@
|
||||||
* Close all modals
|
* Close all modals
|
||||||
*/
|
*/
|
||||||
function closeModals() {
|
function closeModals() {
|
||||||
document.querySelectorAll('.modal, .detail-panel').forEach(modal => {
|
document.querySelectorAll(".modal, .detail-panel").forEach((modal) => {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add("hidden");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show toast notification
|
* Show toast notification
|
||||||
*/
|
*/
|
||||||
function showToast(message, type = 'success') {
|
function showToast(message, type = "success") {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement("div");
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
toast.textContent = message;
|
toast.textContent = message;
|
||||||
document.body.appendChild(toast);
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
// Trigger animation
|
// Trigger animation
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
toast.classList.add('show');
|
toast.classList.add("show");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove after delay
|
// Remove after delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.classList.remove('show');
|
toast.classList.remove("show");
|
||||||
setTimeout(() => toast.remove(), 300);
|
setTimeout(() => toast.remove(), 300);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on DOM ready
|
// Initialize on DOM ready
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
} else {
|
} else {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
@ -328,6 +814,16 @@
|
||||||
usePrompt,
|
usePrompt,
|
||||||
copyPrompt,
|
copyPrompt,
|
||||||
savePrompt,
|
savePrompt,
|
||||||
showToast
|
showToast,
|
||||||
|
showRepoDetail,
|
||||||
|
connectRepo,
|
||||||
|
disconnectRepo,
|
||||||
|
browseRepo,
|
||||||
|
showAppDetail,
|
||||||
|
openApp,
|
||||||
|
editApp,
|
||||||
|
insertMention,
|
||||||
|
getTaskContext: window.getTaskContext,
|
||||||
|
clearTaskContext: window.clearTaskContext,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue