/** * @fileoverview A simple global store for application context. * Manages shared state like current page, project, team, and selected items. * Used by components like ds-ai-chat to be context-aware. * MVP2: Extended with admin settings and project configuration context. * * ARCHITECTURE NOTE: Context store uses localStorage directly for persistence. * Incognito mode handling is managed at the API client layer (api-client.js). * This removes circular dependencies and initialization order issues. */ /** * Page-specific context prompts for the AI assistant. * These provide contextual awareness for each page in the admin UI. */ const PAGE_CONTEXT_PROMPTS = { dashboard: 'You are helping with the DSS dashboard. The user can see project overview, recent activity, and quick actions.', projects: 'You are helping manage design system projects. The user can create, configure, and monitor projects.', services: 'You are helping analyze design system services and integrations.', 'quick-wins': 'You are helping find quick win opportunities for design system adoption.', chat: 'You are a helpful AI assistant for the Design System Server.', tokens: 'You are helping manage design tokens. The user can extract, validate, sync, and transform tokens between formats.', components: 'You are helping with React components. The user can generate, audit, and validate design system components.', figma: 'You are helping with Figma integration. The user can connect files, sync tokens, and view visual diffs.', docs: 'You are helping with DSS documentation and guides.', teams: 'You are helping manage team access and permissions for design system projects.', audit: 'You are helping with design system audits. The user can identify issues, track adoption, and create quick wins.', plugins: 'You are helping manage DSS plugins and extensions.', settings: 'You are helping configure DSS settings and preferences.', storybook: 'You are helping with Storybook setup. The user can generate stories and configure theme documentation.', }; class ContextStore extends EventTarget { constructor() { super(); this.state = { // MVP1 Core Context (no defaults for project - force selection) projectId: localStorage.getItem('current_project_id') || null, teamId: localStorage.getItem('current_team_id') || 'ui', userId: localStorage.getItem('current_user_id') || 'admin', capabilities: JSON.parse(localStorage.getItem('user_capabilities') || '[]'), // MVP2: Admin settings context adminSettings: { hostname: localStorage.getItem('admin_hostname') || 'localhost', port: parseInt(localStorage.getItem('admin_port')) || 6006, isRemote: localStorage.getItem('admin_isRemote') === 'true' || false, dssSetupType: localStorage.getItem('admin_setupType') || 'local', }, // MVP2: Current project configuration currentProject: null, // Will be set from ProjectStore currentProjectSkin: localStorage.getItem('current_project_skin') || 'default', // Legacy fields (keep for backward compatibility) page: null, project: null, team: 'all', selectedItems: [], // For batch actions on lists pageData: null, // Current page-specific data (tokens, components, etc.) }; } /** * Updates one or more context values and dispatches an event. * @param {Partial} newContext - An object with keys to update. */ setContext(newContext) { let hasChanged = false; const changes = {}; for (const key in newContext) { if (Object.prototype.hasOwnProperty.call(this.state, key)) { const oldValue = this.state[key]; const newValue = newContext[key]; // Deep comparison for arrays/objects const isDifferent = Array.isArray(newValue) || typeof newValue === 'object' ? JSON.stringify(oldValue) !== JSON.stringify(newValue) : oldValue !== newValue; if (isDifferent) { this.state[key] = newValue; changes[key] = { oldValue, newValue }; hasChanged = true; } } } if (hasChanged) { this.dispatchEvent(new CustomEvent('context-change', { detail: { state: { ...this.state }, changes } })); } } /** * Sets a single context value * @param {string} key - The context key * @param {any} value - The new value */ set(key, value) { this.setContext({ [key]: value }); } /** * Gets a single context value * @param {string} key - The context key * @returns {any} */ get(key) { return this.state[key]; } /** * Returns the current state (shallow copy). * @returns {typeof this.state} */ getState() { return { ...this.state }; } /** * Gets a context-aware prompt for the current page. * Used by the AI chat to provide page-specific assistance. * @returns {string} A contextual prompt string. */ getContextPrompt() { const page = this.get('page') || 'dashboard'; return PAGE_CONTEXT_PROMPTS[page] || PAGE_CONTEXT_PROMPTS.chat; } /** * A convenience method to subscribe to context changes. * @param {Function} callback - The function to call on change. * @returns {Function} An unsubscribe function. */ subscribe(callback) { const handler = (event) => callback(event.detail); this.addEventListener('context-change', handler); // Return an unsubscribe function return () => this.removeEventListener('context-change', handler); } /** * Subscribe to changes on a specific key only * @param {string} key - The key to watch * @param {Function} callback - Called with (newValue, oldValue) when key changes * @returns {Function} Unsubscribe function */ subscribeToKey(key, callback) { const handler = (event) => { const { changes } = event.detail; if (changes[key]) { callback(changes[key].newValue, changes[key].oldValue); } }; this.addEventListener('context-change', handler); return () => this.removeEventListener('context-change', handler); } /** * Clears selection state */ clearSelection() { this.setContext({ selectedItems: [] }); } /** * Adds items to selection * @param {Array} items - Items to add */ addToSelection(items) { const current = this.state.selectedItems || []; const newItems = Array.isArray(items) ? items : [items]; this.setContext({ selectedItems: [...new Set([...current, ...newItems])] }); } /** * Removes items from selection * @param {Array} items - Items to remove */ removeFromSelection(items) { const current = this.state.selectedItems || []; const toRemove = new Set(Array.isArray(items) ? items : [items]); this.setContext({ selectedItems: current.filter(item => !toRemove.has(item)) }); } /** * MVP1: Set active project with storage persistence * @param {string} id - Project ID */ setProject(id) { if (!id) { console.error('[ContextStore] Cannot set null projectId'); return; } localStorage.setItem('current_project_id', id); this.setContext({ projectId: id, project: id }); // Update both for compatibility } /** * MVP1: Set active team with storage persistence * @param {string} id - Team ID (ui, ux, qa, admin) */ setTeam(id) { localStorage.setItem('current_team_id', id); this.setContext({ teamId: id, team: id }); // Update both for compatibility } /** * MVP1: Check if project is selected * @returns {boolean} */ hasProject() { return !!this.state.projectId; } /** * MVP1: Get full context for MCP tool calls * @returns {Object} Context object with projectId, teamId, userId */ getMCPContext() { return { project_id: this.state.projectId, team_id: this.state.teamId, user_id: this.state.userId, capabilities: this.state.capabilities, }; } /** * MVP2: Update admin settings in context * @param {Object} settings - Admin settings object */ updateAdminSettings(settings) { this.setContext({ adminSettings: { ...this.state.adminSettings, ...settings } }); } /** * MVP2: Get current admin settings * @returns {Object} Admin settings */ getAdminSettings() { return { ...this.state.adminSettings }; } /** * MVP2: Update current project information * @param {Object} project - Project object from ProjectStore */ setCurrentProject(project) { this.setContext({ currentProject: project, projectId: project?.id || null, currentProjectSkin: project?.skinSelected || 'default' }); } /** * MVP2: Get current project from context * @returns {Object|null} Current project or null */ getCurrentProject() { return this.state.currentProject || null; } /** * MVP2: Update current project skin * @param {string} skin - Skin name */ setCurrentProjectSkin(skin) { localStorage.setItem('current_project_skin', skin); this.setContext({ currentProjectSkin: skin }); } /** * MVP2: Check if system is configured for remote/headless mode * @returns {boolean} */ isRemoteSetup() { return this.state.adminSettings.isRemote; } /** * MVP2: Check if system is configured for local mode * @returns {boolean} */ isLocalSetup() { return !this.state.adminSettings.isRemote; } /** * MVP2: Get DSS setup type * @returns {string} 'local' or 'remote' */ getDSSSetupType() { return this.state.adminSettings.dssSetupType; } } // Export a singleton instance const contextStore = new ContextStore(); export default contextStore;