Files
dss/admin-ui/js/components/layout/ds-ai-chat-sidebar.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

270 lines
8.2 KiB
JavaScript

/**
* ds-ai-chat-sidebar.js
* AI Chat Sidebar wrapper component
* Wraps ds-chat-panel with collapse/expand toggle and context binding
* MVP2: Right sidebar integrated with 3-column layout
*/
import contextStore from '../../stores/context-store.js';
import { useUserStore } from '../../stores/user-store.js';
class DSAiChatSidebar extends HTMLElement {
constructor() {
super();
this.userStore = useUserStore();
const preferences = this.userStore.getPreferences();
this.isCollapsed = preferences.chatCollapsedState !== false; // Default to collapsed
this.currentProject = null;
this.currentTeam = null;
this.currentPage = null;
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.initializeContextSubscriptions();
}
initializeContextSubscriptions() {
// Subscribe to context changes to update chat panel context
this.unsubscribe = contextStore.subscribe(({ state }) => {
this.currentProject = state.project;
this.currentTeam = state.team;
this.currentPage = state.page;
// Update chat panel with current context
const chatPanel = this.querySelector('ds-chat-panel');
if (chatPanel && chatPanel.setContext) {
chatPanel.setContext({
project: this.currentProject,
team: this.currentTeam,
page: this.currentPage
});
}
});
// Get initial context
const context = contextStore.getState();
if (context) {
this.currentProject = context.currentProject || context.project || null;
this.currentTeam = context.teamId || context.team || null;
this.currentPage = context.page || null;
}
}
render() {
const buttonClass = this.isCollapsed ? 'rotating' : '';
this.innerHTML = `
<div class="ai-chat-container" style="
display: flex;
flex-direction: column;
height: 100%;
background: var(--vscode-sidebar-background);
border-left: 1px solid var(--vscode-border);
" role="complementary" aria-label="AI Assistant sidebar">
<!-- Header with animated collapse button (shown when expanded) -->
<div style="
padding: 12px;
border-bottom: 1px solid var(--vscode-border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--vscode-bg);
${this.isCollapsed ? 'display: none;' : ''}
">
<div style="
font-weight: 500;
font-size: 13px;
color: var(--vscode-foreground);
">💬 AI Assistant</div>
<button
id="toggle-collapse-btn"
class="ai-chat-toggle-btn ${buttonClass}"
aria-label="Toggle chat sidebar"
aria-expanded="${!this.isCollapsed}"
style="
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 4px 8px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
"
title="Toggle Chat Sidebar">
</button>
</div>
<!-- Chat content (collapsible) -->
<div class="chat-content" style="
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
${this.isCollapsed ? 'display: none;' : ''}
">
<!-- Chat panel will be hydrated here via component registry -->
<div id="chat-panel-container" style="
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
"></div>
</div>
<!-- Collapsed state indicator (shown when collapsed) -->
${this.isCollapsed ? `
<button
id="toggle-collapse-btn-collapsed"
class="ai-chat-toggle-btn ${buttonClass}"
aria-label="Expand chat sidebar"
aria-expanded="false"
style="
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 12px;
font-size: 16px;
text-align: center;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
"
title="Expand Chat Sidebar">
💬
</button>
` : ''}
</div>
`;
}
async setupEventListeners() {
// Handle both expanded and collapsed toggle buttons
const toggleBtn = this.querySelector('#toggle-collapse-btn');
const toggleBtnCollapsed = this.querySelector('#toggle-collapse-btn-collapsed');
const attachToggleListener = (btn) => {
if (btn) {
btn.addEventListener('click', () => {
this.toggleCollapse();
});
btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
btn.click();
}
});
}
};
attachToggleListener(toggleBtn);
attachToggleListener(toggleBtnCollapsed);
// Hydrate chat panel on first connection
const chatContainer = this.querySelector('#chat-panel-container');
if (chatContainer && chatContainer.children.length === 0) {
try {
// Import component registry to load chat panel
const { hydrateComponent } = await import('../../config/component-registry.js');
await hydrateComponent('ds-chat-panel', chatContainer);
console.log('[DSAiChatSidebar] Chat panel loaded');
// Set initial context on chat panel
const chatPanel = chatContainer.querySelector('ds-chat-panel');
if (chatPanel && chatPanel.setContext) {
chatPanel.setContext({
project: this.currentProject,
team: this.currentTeam,
page: this.currentPage
});
}
} catch (error) {
console.error('[DSAiChatSidebar] Failed to load chat panel:', error);
chatContainer.innerHTML = `
<div style="padding: 12px; color: var(--vscode-error); font-size: 12px;">
Failed to load chat panel
</div>
`;
}
}
}
toggleCollapse() {
this.isCollapsed = !this.isCollapsed;
// Persist chat collapsed state to userStore
this.userStore.updatePreferences({ chatCollapsedState: this.isCollapsed });
// Update CSS class for smooth CSS transition (avoid re-render for better UX)
if (this.isCollapsed) {
this.classList.add('collapsed');
} else {
this.classList.remove('collapsed');
}
// Update button classes for rotation animation
const btns = this.querySelectorAll('.ai-chat-toggle-btn');
btns.forEach(btn => {
if (this.isCollapsed) {
btn.classList.add('rotating');
} else {
btn.classList.remove('rotating');
}
btn.setAttribute('aria-expanded', String(!this.isCollapsed));
});
// Update header and content visibility with inline styles
const header = this.querySelector('[style*="padding: 12px"]');
const content = this.querySelector('.chat-content');
if (header) {
if (this.isCollapsed) {
header.style.display = 'none';
} else {
header.style.display = 'flex';
}
}
if (content) {
if (this.isCollapsed) {
content.style.display = 'none';
} else {
content.style.display = 'flex';
}
}
// Toggle collapsed button visibility
let collapsedBtn = this.querySelector('#toggle-collapse-btn-collapsed');
if (!collapsedBtn && this.isCollapsed) {
// Create the collapsed button if needed
this.render();
this.setupEventListeners();
} else if (collapsedBtn && !this.isCollapsed) {
// Remove the collapsed button if needed
this.render();
this.setupEventListeners();
}
// Dispatch event for layout adjustment
this.dispatchEvent(new CustomEvent('chat-sidebar-toggled', {
detail: { isCollapsed: this.isCollapsed },
bubbles: true,
composed: true
}));
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
customElements.define('ds-ai-chat-sidebar', DSAiChatSidebar);