Files
dss/admin-ui/js/components/tools/ds-chat-panel.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

356 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ds-chat-panel.js
* AI chatbot panel with team+project context
* MVP1: Integrates claude-service with ContextStore for team-aware assistance
*/
import claudeService from '../../services/claude-service.js';
import contextStore from '../../stores/context-store.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSChatPanel extends HTMLElement {
constructor() {
super();
this.messages = [];
this.isLoading = false;
}
async connectedCallback() {
// Sync claude-service with ContextStore
const context = contextStore.getMCPContext();
if (context.project_id) {
claudeService.setProject(context.project_id);
}
// Subscribe to project changes
this.unsubscribe = contextStore.subscribeToKey('projectId', (newProjectId) => {
if (newProjectId) {
claudeService.setProject(newProjectId);
this.showSystemMessage(`Switched to project: ${newProjectId}`);
}
});
// Initialize MCP tools in background
this.initializeMcpTools();
this.render();
this.setupEventListeners();
this.loadHistory();
this.showWelcomeMessage();
}
/**
* Initialize MCP tools to enable AI tool awareness
*/
async initializeMcpTools() {
try {
console.log('[DSChatPanel] Initializing MCP tools...');
await claudeService.getMcpTools();
console.log('[DSChatPanel] MCP tools initialized successfully');
} catch (error) {
console.warn('[DSChatPanel] Failed to load MCP tools (non-blocking):', error.message);
}
}
/**
* Set context from parent component (ds-ai-chat-sidebar)
* @param {Object} context - Context object with project, team, page
*/
setContext(context) {
if (!context) return;
// Handle project context (could be object with id or string id)
if (context.project) {
const projectId = typeof context.project === 'object'
? context.project.id
: context.project;
if (projectId && projectId !== claudeService.currentProjectId) {
claudeService.setProject(projectId);
console.log('[DSChatPanel] Context updated via setContext:', { projectId });
}
}
// Store team and page context for reference
if (context.team) {
this.currentTeam = context.team;
}
if (context.page) {
this.currentPage = context.page;
}
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
setupEventListeners() {
const input = this.querySelector('#chat-input');
const sendBtn = this.querySelector('#chat-send-btn');
const clearBtn = this.querySelector('#chat-clear-btn');
const exportBtn = this.querySelector('#chat-export-btn');
if (sendBtn && input) {
sendBtn.addEventListener('click', () => this.sendMessage());
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
}
if (clearBtn) {
clearBtn.addEventListener('click', () => this.clearChat());
}
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportChat());
}
}
loadHistory() {
const history = claudeService.getHistory();
if (history && history.length > 0) {
this.messages = history;
this.renderMessages();
}
}
showWelcomeMessage() {
if (this.messages.length === 0) {
const context = contextStore.getMCPContext();
const teamId = context.team_id || 'ui';
const teamGreetings = {
ui: 'I can help with token extraction, component audits, Storybook comparisons, and quick wins analysis.',
ux: 'I can assist with Figma syncing, design tokens, asset management, and navigation flows.',
qa: 'I can help with visual regression testing, accessibility audits, and ESRE validation.',
admin: 'I can help manage projects, configure integrations, and oversee the design system.'
};
const greeting = teamGreetings[teamId] || teamGreetings.admin;
this.showSystemMessage(
`👋 Welcome to the ${teamId.toUpperCase()} team workspace!\n\n${greeting}\n\nI have access to all MCP tools for the active project.`
);
}
}
showSystemMessage(text) {
this.messages.push({
role: 'system',
content: text,
timestamp: new Date().toISOString()
});
this.renderMessages();
}
async sendMessage() {
const input = this.querySelector('#chat-input');
const message = input?.value.trim();
if (!message || this.isLoading) return;
// Check project context
const context = contextStore.getMCPContext();
if (!context.project_id) {
ComponentHelpers.showToast?.('Please select a project before chatting', 'error');
return;
}
// Add user message
this.messages.push({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
// Clear input
input.value = '';
// Render and scroll
this.renderMessages();
this.scrollToBottom();
// Show loading
this.isLoading = true;
this.updateLoadingState();
try {
// Add team context to the request
const teamContext = {
projectId: context.project_id,
teamId: context.team_id,
userId: context.user_id,
page: 'workdesk',
capabilities: context.capabilities
};
// Send to Claude with team+project context
const response = await claudeService.chat(message, teamContext);
// Add assistant response
this.messages.push({
role: 'assistant',
content: response,
timestamp: new Date().toISOString()
});
this.renderMessages();
this.scrollToBottom();
} catch (error) {
console.error('[DSChatPanel] Failed to send message:', error);
ComponentHelpers.showToast?.(`Chat error: ${error.message}`, 'error');
this.messages.push({
role: 'system',
content: `❌ Error: ${error.message}\n\nPlease try again or check your connection.`,
timestamp: new Date().toISOString()
});
this.renderMessages();
} finally {
this.isLoading = false;
this.updateLoadingState();
}
}
clearChat() {
if (!confirm('Clear all chat history?')) return;
claudeService.clearHistory();
this.messages = [];
this.renderMessages();
this.showWelcomeMessage();
ComponentHelpers.showToast?.('Chat history cleared', 'success');
}
exportChat() {
claudeService.exportConversation();
ComponentHelpers.showToast?.('Chat exported successfully', 'success');
}
updateLoadingState() {
const sendBtn = this.querySelector('#chat-send-btn');
const input = this.querySelector('#chat-input');
if (sendBtn) {
sendBtn.disabled = this.isLoading;
sendBtn.textContent = this.isLoading ? '⏳ Sending...' : '📤 Send';
}
if (input) {
input.disabled = this.isLoading;
}
}
scrollToBottom() {
const messagesContainer = this.querySelector('#chat-messages');
if (messagesContainer) {
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 100);
}
}
renderMessages() {
const messagesContainer = this.querySelector('#chat-messages');
if (!messagesContainer) return;
if (this.messages.length === 0) {
messagesContainer.innerHTML = `
<div style="text-align: center; padding: 48px; color: var(--vscode-text-dim);">
<div style="font-size: 48px; margin-bottom: 16px;">💬</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">No messages yet</h3>
<p style="font-size: 12px;">Start a conversation to get help with your design system.</p>
</div>
`;
return;
}
messagesContainer.innerHTML = this.messages.map(msg => {
const isUser = msg.role === 'user';
const isSystem = msg.role === 'system';
const alignStyle = isUser ? 'flex-end' : 'flex-start';
const bgColor = isUser
? 'var(--vscode-button-background)'
: isSystem
? 'rgba(255, 191, 0, 0.1)'
: 'var(--vscode-sidebar)';
const textColor = isUser ? 'var(--vscode-button-foreground)' : 'var(--vscode-text)';
const maxWidth = isSystem ? '100%' : '80%';
const icon = isUser ? '👤' : isSystem ? '' : '🤖';
return `
<div style="display: flex; justify-content: ${alignStyle}; margin-bottom: 16px;">
<div style="max-width: ${maxWidth}; background: ${bgColor}; padding: 12px; border-radius: 8px; color: ${textColor};">
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;">
<span>${icon}</span>
<span>${isUser ? 'You' : isSystem ? 'System' : 'AI Assistant'}</span>
<span>•</span>
<span>${ComponentHelpers.formatRelativeTime(new Date(msg.timestamp))}</span>
</div>
<div style="font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">
${ComponentHelpers.escapeHtml(msg.content)}
</div>
</div>
</div>
`;
}).join('');
}
render() {
this.innerHTML = `
<div style="height: 100%; display: flex; flex-direction: column; background: var(--vscode-bg);">
<!-- Header -->
<div style="padding: 12px 16px; border-bottom: 1px solid var(--vscode-border); display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 4px;">AI Assistant</h3>
<div style="font-size: 10px; color: var(--vscode-text-dim);">Team-contextualized help with MCP tools</div>
</div>
<div style="display: flex; gap: 8px;">
<button id="chat-export-btn" class="button" style="padding: 4px 8px; font-size: 10px;">
📥 Export
</button>
<button id="chat-clear-btn" class="button" style="padding: 4px 8px; font-size: 10px;">
🗑️ Clear
</button>
</div>
</div>
<!-- Messages -->
<div id="chat-messages" style="flex: 1; overflow-y: auto; padding: 16px;">
${ComponentHelpers.renderLoading('Loading chat...')}
</div>
<!-- Input -->
<div style="padding: 16px; border-top: 1px solid var(--vscode-border);">
<div style="display: flex; gap: 8px;">
<textarea
id="chat-input"
placeholder="Ask me anything about your design system..."
class="input"
style="flex: 1; min-height: 60px; resize: vertical; font-size: 12px;"
rows="2"
></textarea>
<button id="chat-send-btn" class="button" style="padding: 8px 16px; font-size: 12px; height: 60px;">
📤 Send
</button>
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 8px;">
Press Enter to send • Shift+Enter for new line
</div>
</div>
</div>
`;
}
}
customElements.define('ds-chat-panel', DSChatPanel);
export default DSChatPanel;