| class CodeMasterIDE { |
| constructor() { |
| this.files = new Map(); |
| this.openTabs = []; |
| this.activeTab = null; |
| this.editor = null; |
| this.fileCounter = 1; |
| this.aiLearning = new AILearning(); |
| this.primaryFile = null; |
| this.sidebarWidth = 256; |
| this.aiPanelWidth = 0; |
| this.isDirty = new Set(); |
| this.currentModalCallback = null; |
| this.contextMenuTarget = null; |
| |
| |
| this.initialFiles = { |
| 'index.html': `<!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width" /> |
| <title>My static Space</title> |
| <link rel="stylesheet" href="style.css" /> |
| </head> |
| <body> |
| <div class="card"> |
| <h1>Welcome to your static Space!</h1> |
| <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p> |
| <p> |
| Also don't forget to check the |
| <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>. |
| </p> |
| </div> |
| </body> |
| </html>`, |
| 'style.css': `body { |
| padding: 2rem; |
| font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif; |
| } |
| |
| h1 { |
| font-size: 16px; |
| margin-top: 0; |
| } |
| |
| p { |
| color: rgb(107, 114, 128); |
| font-size: 15px; |
| margin-bottom: 10px; |
| margin-top: 5px; |
| } |
| |
| .card { |
| max-width: 620px; |
| margin: 0 auto; |
| padding: 16px; |
| border: 1px solid lightgray; |
| border-radius: 16px; |
| } |
| |
| .card p:last-child { |
| margin-bottom: 0; |
| }` |
| }; |
| |
| this.init(); |
| } |
|
|
| init() { |
| this.loadFiles(); |
| this.initEditor(); |
| this.setupEventListeners(); |
| this.setupAI(); |
| this.setupConsoleCapture(); |
| this.broadcastChannel = this.setupBroadcastChannel(); |
| |
| |
| if (this.files.size === 0) { |
| Object.entries(this.initialFiles).forEach(([name, content]) => { |
| this.files.set(name, { |
| name, |
| content, |
| type: this.getFileType(name), |
| path: name |
| }); |
| }); |
| this.saveFiles(); |
| } |
| |
| this.renderFileTree(); |
| |
| |
| const firstHtml = Array.from(this.files.keys()).find(f => f.endsWith('.html')); |
| const firstFile = firstHtml || this.files.keys().next().value; |
| if (firstFile) { |
| this.openFile(firstFile); |
| if (firstHtml) this.setPrimaryFile(firstHtml); |
| } |
| |
| this.updatePrimaryFileSelect(); |
| lucide.createIcons(); |
| } |
|
|
| setupBroadcastChannel() { |
| if ('BroadcastChannel' in window) { |
| try { |
| const bc = new BroadcastChannel('codemaster_sync'); |
| bc.onmessage = (event) => { |
| if (event.data.type === 'pong') { |
| console.log('Viewer connected'); |
| } |
| }; |
| return bc; |
| } catch (e) { |
| return null; |
| } |
| } |
| return null; |
| } |
|
|
| loadFiles() { |
| try { |
| const saved = localStorage.getItem('codemaster_files'); |
| if (saved) { |
| const parsed = JSON.parse(saved); |
| this.files = new Map(Object.entries(parsed)); |
| } |
| } catch (e) { |
| console.error('Failed to load files:', e); |
| } |
| } |
|
|
| saveFiles() { |
| try { |
| const obj = Object.fromEntries(this.files); |
| localStorage.setItem('codemaster_files', JSON.stringify(obj)); |
| } catch (e) { |
| console.error('Failed to save files:', e); |
| } |
| } |
|
|
| initEditor() { |
| require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' }}); |
| |
| require(['vs/editor/editor.main'], () => { |
| monaco.editor.defineTheme('codemaster-dark', { |
| base: 'vs-dark', |
| inherit: true, |
| rules: [], |
| colors: { |
| 'editor.background': '#020617', |
| 'editor.lineHighlightBackground': '#1e293b', |
| 'editorLineNumber.foreground': '#64748b', |
| 'editorLineNumber.activeForeground': '#94a3b8', |
| } |
| }); |
|
|
| this.editor = monaco.editor.create(document.getElementById('editor-container'), { |
| value: '', |
| language: 'html', |
| theme: 'codemaster-dark', |
| automaticLayout: true, |
| minimap: { enabled: true }, |
| fontSize: 14, |
| fontFamily: 'JetBrains Mono, Consolas, Monaco, monospace', |
| wordWrap: 'on', |
| lineNumbers: 'on', |
| roundedSelection: false, |
| scrollBeyondLastLine: false, |
| renderLineHighlight: 'line', |
| padding: { top: 16 }, |
| bracketPairColorization: { enabled: true }, |
| autoIndent: 'advanced', |
| formatOnPaste: true, |
| formatOnType: true, |
| suggestOnTriggerCharacters: true, |
| quickSuggestions: true, |
| }); |
|
|
| |
| this.editor.onDidType((e) => { |
| this.handleTyping(e); |
| }); |
|
|
| |
| this.editor.onDidChangeModelContent(() => { |
| if (this.activeTab) { |
| this.isDirty.add(this.activeTab); |
| this.updateTabStatus(this.activeTab); |
| this.aiLearning.recordEdit(this.activeTab, this.editor.getValue()); |
| this.debouncedPreviewUpdate(); |
| } |
| }); |
|
|
| |
| if (this.activeTab && this.files.has(this.activeTab)) { |
| const file = this.files.get(this.activeTab); |
| this.editor.setValue(file.content); |
| monaco.editor.setModelLanguage(this.editor.getModel(), file.type); |
| } |
| }); |
| } |
|
|
| handleTyping(text) { |
| const position = this.editor.getPosition(); |
| const model = this.editor.getModel(); |
| const lineContent = model.getLineContent(position.lineNumber); |
| const beforeCursor = lineContent.substring(0, position.column - 1); |
| |
| |
| const suggestion = this.aiLearning.getSuggestion(beforeCursor, text, this.activeTab); |
| |
| if (suggestion) { |
| this.showAISuggestion(suggestion, position); |
| } else { |
| this.hideAISuggestion(); |
| } |
| } |
|
|
| showAISuggestion(suggestion, position) { |
| const coords = this.editor.getScrolledVisiblePosition(position); |
| const editorDom = document.getElementById('editor-container'); |
| const suggestionEl = document.getElementById('ai-suggestion'); |
| |
| suggestionEl.style.left = (editorDom.offsetLeft + coords.left) + 'px'; |
| suggestionEl.style.top = (editorDom.offsetTop + coords.top + 20) + 'px'; |
| suggestionEl.textContent = 'AI: ' + suggestion.text; |
| suggestionEl.classList.remove('hidden'); |
| |
| |
| if (suggestion.confidence > 0.8) { |
| const disposable = this.editor.onKeyDown((e) => { |
| if (e.keyCode === monaco.KeyCode.Tab) { |
| e.preventDefault(); |
| this.insertSuggestion(suggestion); |
| disposable.dispose(); |
| } |
| }); |
| |
| |
| setTimeout(() => disposable.dispose(), 5000); |
| } |
| } |
|
|
| hideAISuggestion() { |
| document.getElementById('ai-suggestion').classList.add('hidden'); |
| } |
|
|
| insertSuggestion(suggestion) { |
| const position = this.editor.getPosition(); |
| this.editor.executeEdits('ai-suggestion', [{ |
| range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), |
| text: suggestion.insert |
| }]); |
| this.hideAISuggestion(); |
| this.aiLearning.recordAcceptance(suggestion); |
| this.updateAIPanel(); |
| } |
|
|
| setupAI() { |
| this.updateAIPanel(); |
| setInterval(() => this.updateAIPanel(), 5000); |
| } |
|
|
| updateAIPanel() { |
| const stats = this.aiLearning.getStats(); |
| document.getElementById('patterns-count').textContent = stats.patterns; |
| document.getElementById('confidence-bar').style.width = stats.confidence + '%'; |
| document.getElementById('confidence-text').textContent = stats.confidence + '%'; |
| |
| const snippetsEl = document.getElementById('frequent-snippets'); |
| snippetsEl.innerHTML = stats.topSnippets.map(s => ` |
| <div class="bg-slate-800 rounded p-2 text-xs"> |
| <div class="text-slate-400 mb-1">${s.trigger}</div> |
| <div class="text-blue-400 font-mono truncate">${s.code}</div> |
| <div class="text-slate-500 mt-1">Used ${s.count} times</div> |
| </div> |
| `).join(''); |
| |
| const historyEl = document.getElementById('suggestions-history'); |
| historyEl.innerHTML = stats.recentSuggestions.map(s => ` |
| <div class="text-slate-400"> |
| <span class="text-xs">${s.time}</span>: ${s.text} |
| </div> |
| `).join(''); |
| } |
|
|
| setupConsoleCapture() { |
| const originalLog = console.log; |
| const originalError = console.error; |
| const originalWarn = console.warn; |
| const self = this; |
|
|
| console.log = function(...args) { |
| self.appendToConsole('log', args); |
| originalLog.apply(console, args); |
| }; |
| console.error = function(...args) { |
| self.appendToConsole('error', args); |
| originalError.apply(console, args); |
| }; |
| console.warn = function(...args) { |
| self.appendToConsole('warn', args); |
| originalWarn.apply(console, args); |
| }; |
|
|
| window.onerror = (msg, url, line) => { |
| this.appendToConsole('error', [msg + ' (line ' + line + ')']); |
| }; |
| } |
|
|
| appendToConsole(level, args) { |
| const output = document.getElementById('console-output'); |
| const line = document.createElement('div'); |
| line.className = 'console-line ' + level; |
| |
| const timestamp = new Date().toLocaleTimeString(); |
| const message = args.map(arg => { |
| if (typeof arg === 'object') { |
| try { |
| return JSON.stringify(arg, null, 2); |
| } catch(e) { |
| return String(arg); |
| } |
| } |
| return String(arg); |
| }).join(' '); |
| |
| line.textContent = '[' + timestamp + '] ' + message; |
| output.appendChild(line); |
| output.scrollTop = output.scrollHeight; |
|
|
| |
| if (level === 'error') { |
| this.updateProblemCount(); |
| } |
| } |
|
|
| updateProblemCount() { |
| const errors = document.querySelectorAll('.console-line.error').length; |
| document.getElementById('problem-count').textContent = errors; |
| } |
|
|
| getFileType(filename) { |
| const ext = filename.split('.').pop().toLowerCase(); |
| const types = { |
| 'html': 'html', |
| 'htm': 'html', |
| 'css': 'css', |
| 'js': 'javascript', |
| 'json': 'json', |
| 'ts': 'typescript', |
| 'py': 'python', |
| 'md': 'markdown', |
| 'xml': 'xml', |
| 'svg': 'xml' |
| }; |
| return types[ext] || 'plaintext'; |
| } |
|
|
| getFileIcon(filename) { |
| const ext = filename.split('.').pop().toLowerCase(); |
| const icons = { |
| 'html': 'file-code', |
| 'css': 'file-code', |
| 'js': 'file-code', |
| 'json': 'file-json', |
| 'md': 'file-text', |
| 'txt': 'file-text', |
| 'jpg': 'image', |
| 'png': 'image', |
| 'gif': 'image', |
| 'svg': 'image' |
| }; |
| return icons[ext] || 'file'; |
| } |
|
|
| renderFileTree() { |
| const tree = document.getElementById('file-tree'); |
| tree.innerHTML = ''; |
| |
| const sortedFiles = Array.from(this.files.keys()).sort(); |
| |
| sortedFiles.forEach(filename => { |
| const item = document.createElement('div'); |
| item.className = 'file-tree-item flex items-center gap-2 px-2 py-1.5 rounded text-sm' + |
| (this.activeTab === filename ? ' active' : ''); |
| item.innerHTML = ` |
| <i data-lucide="${this.getFileIcon(filename)}" class="w-4 h-4 ${this.getFileIconColor(filename)}"></i> |
| <span class="truncate flex-1">${filename}</span> |
| `; |
| item.onclick = () => this.openFile(filename); |
| item.oncontextmenu = (e) => this.showContextMenu(e, filename); |
| tree.appendChild(item); |
| }); |
| |
| lucide.createIcons(); |
| } |
|
|
| getFileIconColor(filename) { |
| const ext = filename.split('.').pop().toLowerCase(); |
| const colors = { |
| 'html': 'text-orange-400', |
| 'css': 'text-blue-400', |
| 'js': 'text-yellow-400', |
| 'json': 'text-green-400' |
| }; |
| return colors[ext] || 'text-slate-400'; |
| } |
|
|
| openFile(filename) { |
| |
| if (this.activeTab && this.isDirty.has(this.activeTab)) { |
| this.saveCurrentFile(); |
| } |
|
|
| |
| document.querySelectorAll('.file-tree-item').forEach(el => { |
| el.classList.remove('active'); |
| if (el.textContent.trim() === filename) { |
| el.classList.add('active'); |
| } |
| }); |
|
|
| |
| if (!this.openTabs.includes(filename)) { |
| this.openTabs.push(filename); |
| this.renderTabs(); |
| } |
|
|
| this.activeTab = filename; |
| this.updateTabStatus(filename); |
|
|
| |
| const file = this.files.get(filename); |
| if (file && this.editor) { |
| this.editor.setValue(file.content); |
| monaco.editor.setModelLanguage(this.editor.getModel(), file.type); |
| } |
|
|
| this.renderTabs(); |
| } |
|
|
| renderTabs() { |
| const tabsContainer = document.getElementById('tabs'); |
| tabsContainer.innerHTML = ''; |
| |
| this.openTabs.forEach(filename => { |
| const tab = document.createElement('div'); |
| const isModified = this.isDirty.has(filename); |
| tab.className = 'tab' + (this.activeTab === filename ? ' active' : '') + (isModified ? ' modified' : ''); |
| tab.innerHTML = ` |
| <i data-lucide="${this.getFileIcon(filename)}" class="w-3.5 h-3.5 ${this.getFileIconColor(filename)}"></i> |
| <span class="truncate ml-1.5 flex-1">${filename}</span> |
| <span class="tab-close" onclick="event.stopPropagation(); app.closeTab('${filename}')"> |
| <i data-lucide="x" class="w-3.5 h-3.5"></i> |
| </span> |
| `; |
| tab.onclick = () => this.openFile(filename); |
| tabsContainer.appendChild(tab); |
| }); |
| |
| lucide.createIcons(); |
| } |
|
|
| updateTabStatus(filename) { |
| this.renderTabs(); |
| } |
|
|
| closeTab(filename) { |
| const index = this.openTabs.indexOf(filename); |
| this.openTabs = this.openTabs.filter(f => f !== filename); |
| |
| if (this.isDirty.has(filename)) { |
| this.saveFile(filename); |
| } |
| |
| if (this.activeTab === filename) { |
| this.activeTab = this.openTabs[Math.max(0, index - 1)] || null; |
| if (this.activeTab) { |
| this.openFile(this.activeTab); |
| } else { |
| this.editor.setValue(''); |
| } |
| } |
| |
| this.renderTabs(); |
| } |
|
|
| saveCurrentFile() { |
| if (this.activeTab && this.editor) { |
| const content = this.editor.getValue(); |
| const file = this.files.get(this.activeTab); |
| if (file) { |
| file.content = content; |
| this.files.set(this.activeTab, file); |
| this.isDirty.delete(this.activeTab); |
| this.saveFiles(); |
| this.updateTabStatus(this.activeTab); |
| this.syncToRemote(); |
| } |
| } |
| } |
|
|
| saveFile(filename) { |
| if (this.editor && this.activeTab === filename) { |
| this.saveCurrentFile(); |
| } |
| } |
|
|
| saveAll() { |
| this.openTabs.forEach(tab => { |
| if (this.isDirty.has(tab)) { |
| this.saveFile(tab); |
| } |
| }); |
| this.showNotification('All files saved'); |
| } |
|
|
| setPrimaryFile(filename) { |
| this.primaryFile = filename; |
| document.getElementById('primary-file').value = filename; |
| this.updatePreview(); |
| this.showNotification('Primary file set to: ' + filename); |
| } |
|
|
| updatePrimaryFileSelect() { |
| const select = document.getElementById('primary-file'); |
| const currentValue = select.value; |
| |
| |
| while (select.options.length > 1) { |
| select.remove(1); |
| } |
| |
| |
| Array.from(this.files.keys()) |
| .filter(f => f.endsWith('.html')) |
| .forEach(f => { |
| const option = document.createElement('option'); |
| option.value = f; |
| option.textContent = f; |
| if (f === currentValue || f === this.primaryFile) { |
| option.selected = true; |
| } |
| select.appendChild(option); |
| }); |
| } |
|
|
| updatePreview() { |
| if (!this.primaryFile || !this.files.has(this.primaryFile)) return; |
| |
| const file = this.files.get(this.primaryFile); |
| let content = file.content; |
|
|
| |
| content = this.processIncludes(content); |
| |
| |
| const frame = document.getElementById('preview-frame'); |
| const blob = new Blob([content], { type: 'text/html' }); |
| const url = URL.createObjectURL(blob); |
| frame.src = url; |
| |
| |
| this.syncToRemote(content); |
| } |
|
|
| processIncludes(html) { |
| |
| let processed = html; |
| |
| |
| const cssLinks = html.match(/href=["']([^"']+\.css)["']/g) || []; |
| cssLinks.forEach(link => { |
| const match = link.match(/href=["']([^"']+)["']/); |
| if (match) { |
| const path = match[1]; |
| const filename = path.replace(/^\.\//, '').replace(/^\//, ''); |
| if (this.files.has(filename)) { |
| const file = this.files.get(filename); |
| const dataUri = 'data:text/css;base64,' + btoa(file.content); |
| processed = processed.replace(path, dataUri); |
| } |
| } |
| }); |
|
|
| |
| const jsScripts = html.match(/src=["']([^"']+\.js)["']/g) || []; |
| jsScripts.forEach(script => { |
| const match = script.match(/src=["']([^"']+)["']/); |
| if (match) { |
| const path = match[1]; |
| if (!path.startsWith('http') && !path.startsWith('//')) { |
| const filename = path.replace(/^\.\//, '').replace(/^\//, ''); |
| if (this.files.has(filename)) { |
| const file = this.files.get(filename); |
| const dataUri = 'data:text/javascript;base64,' + btoa(file.content); |
| processed = processed.replace(path, dataUri); |
| } |
| } |
| } |
| }); |
|
|
| return processed; |
| } |
|
|
| debouncedPreviewUpdate() { |
| clearTimeout(this.previewTimeout); |
| this.previewTimeout = setTimeout(() => this.updatePreview(), 1000); |
| } |
|
|
| syncToRemote(content) { |
| const data = { |
| content: content || (this.files.has(this.primaryFile) ? this.processIncludes(this.files.get(this.primaryFile).content) : ''), |
| timestamp: Date.now(), |
| primaryFile: this.primaryFile |
| }; |
| |
| |
| if (this.broadcastChannel) { |
| this.broadcastChannel.postMessage({ |
| type: 'code_update', |
| data: data |
| }); |
| } |
| |
| |
| try { |
| localStorage.setItem('codemaster_preview', JSON.stringify(data)); |
| } catch (e) { |
| console.error('Failed to sync:', e); |
| } |
| } |
|
|
| newFile() { |
| this.showModal('New File', (name) => { |
| if (name && !this.files.has(name)) { |
| const type = this.getFileType(name); |
| this.files.set(name, { |
| name, |
| content: '', |
| type, |
| path: name |
| }); |
| this.saveFiles(); |
| this.renderFileTree(); |
| this.openFile(name); |
| this.updatePrimaryFileSelect(); |
| } |
| }); |
| } |
|
|
| newFolder() { |
| this.showNotification('Folders are simulated - use "folder/file.ext" naming'); |
| } |
|
|
| deleteFile(filename) { |
| if (confirm('Delete "' + filename + '"?')) { |
| this.files.delete(filename); |
| this.saveFiles(); |
| |
| if (this.openTabs.includes(filename)) { |
| this.closeTab(filename); |
| } |
| |
| if (this.primaryFile === filename) { |
| this.primaryFile = null; |
| document.getElementById('primary-file').value = ''; |
| } |
| |
| this.renderFileTree(); |
| this.updatePrimaryFileSelect(); |
| } |
| } |
|
|
| renameFile(oldName) { |
| this.showModal('Rename File', (newName) => { |
| if (newName && newName !== oldName && !this.files.has(newName)) { |
| const file = this.files.get(oldName); |
| file.name = newName; |
| file.type = this.getFileType(newName); |
| this.files.delete(oldName); |
| this.files.set(newName, file); |
| |
| |
| const tabIndex = this.openTabs.indexOf(oldName); |
| if (tabIndex !== -1) { |
| this.openTabs[tabIndex] = newName; |
| } |
| if (this.activeTab === oldName) { |
| this.activeTab = newName; |
| } |
| |
| if (this.primaryFile === oldName) { |
| this.setPrimaryFile(newName); |
| } |
| |
| this.saveFiles(); |
| this.renderFileTree(); |
| this.renderTabs(); |
| this.updatePrimaryFileSelect(); |
| } |
| }, oldName); |
| } |
|
|
| showModal(title, callback, defaultValue = '') { |
| document.getElementById('modal-title').textContent = title; |
| const input = document.getElementById('modal-input'); |
| input.value = defaultValue; |
| input.placeholder = title === 'New File' ? 'example.html' : 'new-name.html'; |
| document.getElementById('modal').classList.add('active'); |
| input.focus(); |
| input.select(); |
| |
| this.currentModalCallback = callback; |
| |
| |
| input.onkeydown = (e) => { |
| if (e.key === 'Enter') { |
| this.confirmModal(); |
| } else if (e.key === 'Escape') { |
| this.closeModal(); |
| } |
| }; |
| } |
|
|
| closeModal() { |
| document.getElementById('modal').classList.remove('active'); |
| this.currentModalCallback = null; |
| } |
|
|
| confirmModal() { |
| const value = document.getElementById('modal-input').value.trim(); |
| if (value && this.currentModalCallback) { |
| this.currentModalCallback(value); |
| } |
| this.closeModal(); |
| } |
|
|
| showContextMenu(event, filename) { |
| event.preventDefault(); |
| this.contextMenuTarget = filename; |
| |
| const menu = document.getElementById('context-menu'); |
| menu.innerHTML = ` |
| <div class="context-menu-item" onclick="app.openFile('${filename}')"> |
| <i data-lucide="edit" class="w-4 h-4 inline mr-2"></i> Open |
| </div> |
| <div class="context-menu-item" onclick="app.renameFile('${filename}')"> |
| <i data-lucide="edit-3" class="w-4 h-4 inline mr-2"></i> Rename |
| </div> |
| <div class="context-menu-separator"></div> |
| <div class="context-menu-item text-red-400" onclick="app.deleteFile('${filename}')"> |
| <i data-lucide="trash-2" class="w-4 h-4 inline mr-2"></i> Delete |
| </div> |
| `; |
| |
| menu.style.left = event.pageX + 'px'; |
| menu.style.top = event.pageY + 'px'; |
| menu.classList.remove('hidden'); |
| |
| lucide.createIcons(); |
| |
| |
| setTimeout(() => { |
| document.addEventListener('click', () => { |
| menu.classList.add('hidden'); |
| }, { once: true }); |
| }, 0); |
| } |
|
|
| toggleAI() { |
| const panel = document.getElementById('ai-panel'); |
| const btn = document.getElementById('ai-btn'); |
| |
| if (panel.style.width === '300px') { |
| panel.style.width = '0'; |
| btn.classList.remove('bg-purple-700'); |
| btn.classList.add('bg-purple-600'); |
| } else { |
| panel.style.width = '300px'; |
| btn.classList.remove('bg-purple-600'); |
| btn.classList.add('bg-purple-700'); |
| this.updateAIPanel(); |
| } |
| } |
|
|
| showRemotePreview() { |
| const url = window.location.href.replace('index.html', 'viewer.html'); |
| document.getElementById('preview-url').textContent = url; |
| |
| |
| const qrContainer = document.getElementById('qrcode'); |
| qrContainer.innerHTML = ''; |
| new QRCode(qrContainer, { |
| text: url, |
| width: 256, |
| height: 256, |
| colorDark: '#000000', |
| colorLight: '#ffffff', |
| correctLevel: QRCode.CorrectLevel.M |
| }); |
| |
| document.getElementById('remote-modal').classList.add('active'); |
| } |
|
|
| closeRemoteModal() { |
| document.getElementById('remote-modal').classList.remove('active'); |
| } |
|
|
| switchPanel(panel) { |
| document.querySelectorAll('.panel-section').forEach(p => p.classList.add('hidden')); |
| document.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active', 'text-blue-400')); |
| |
| document.getElementById(panel + '-panel').classList.remove('hidden'); |
| document.querySelector('[data-panel="' + panel + '"]').classList.add('active', 'text-blue-400'); |
| |
| if (panel === 'preview') { |
| this.updatePreview(); |
| } |
| } |
|
|
| toggleBottomPanel() { |
| const panel = document.getElementById('bottom-panel'); |
| if (panel.style.height === '40px') { |
| panel.style.height = '192px'; |
| } else { |
| panel.style.height = '40px'; |
| } |
| } |
|
|
| startResize(elementId) { |
| const isSidebar = elementId === 'sidebar'; |
| const startX = event.clientX; |
| const startWidth = isSidebar ? this.sidebarWidth : this.aiPanelWidth; |
| |
| const doDrag = (e) => { |
| if (isSidebar) { |
| this.sidebarWidth = Math.max(150, Math.min(400, startWidth + e.clientX - startX)); |
| document.getElementById('sidebar').style.width = this.sidebarWidth + 'px'; |
| } |
| }; |
| |
| const stopDrag = () => { |
| document.removeEventListener('mousemove', doDrag); |
| document.removeEventListener('mouseup', stopDrag); |
| }; |
| |
| document.addEventListener('mousemove', doDrag); |
| document.addEventListener('mouseup', stopDrag); |
| } |
|
|
| showNotification(message) { |
| const notif = document.createElement('div'); |
| notif.className = 'fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg z-50 text-sm'; |
| notif.textContent = message; |
| document.body.appendChild(notif); |
| setTimeout(() => notif.remove(), 3000); |
| } |
|
|
| setupEventListeners() { |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.ctrlKey || e.metaKey) { |
| switch(e.key.toLowerCase()) { |
| case 's': |
| e.preventDefault(); |
| this.saveAll(); |
| break; |
| case 'n': |
| e.preventDefault(); |
| this.newFile(); |
| break; |
| } |
| } |
| }); |
|
|
| |
| window.addEventListener('beforeunload', () => { |
| if (this.isDirty.size > 0) { |
| this.saveAll(); |
| } |
| }); |
| } |
| } |
|
|
| class AILearning { |
| constructor() { |
| this.patterns = new Map(); |
| this.snippetUsage = new Map(); |
| this.suggestionHistory = []; |
| this.userStyle = { |
| namingConvention: null, |
| preferredQuotes: null, |
| semicolonUsage: null, |
| indentStyle: null |
| }; |
| this.loadData(); |
| } |
|
|
| loadData() { |
| try { |
| const data = localStorage.getItem('codemaster_ai'); |
| if (data) { |
| const parsed = JSON.parse(data); |
| this.patterns = new Map(parsed.patterns || []); |
| this.snippetUsage = new Map(parsed.snippets || []); |
| this.userStyle = parsed.style || this.userStyle; |
| } |
| } catch (e) {} |
| } |
|
|
| saveData() { |
| try { |
| const data = { |
| patterns: Array.from(this.patterns), |
| snippets: Array.from(this.snippetUsage), |
| style: this.userStyle |
| }; |
| localStorage.setItem('codemaster_ai', JSON.stringify(data)); |
| } catch (e) {} |
| } |
|
|
| recordEdit(filename, content) { |
| |
| this.analyzePatterns(content); |
| |
| |
| this.trackSnippets(content); |
| |
| |
| clearTimeout(this.saveTimeout); |
| this.saveTimeout = setTimeout(() => this.saveData(), 5000); |
| } |
|
|
| analyzePatterns(content) { |
| |
| const camelCase = content.match(/[a-z]+[A-Z][a-zA-Z]+/g) || []; |
| const snake_case = content.match(/[a-z]+_[a-z_]+/g) || []; |
| const PascalCase = content.match(/[A-Z][a-z]+[A-Z][a-zA-Z]+/g) || []; |
| |
| if (camelCase.length > snake_case.length && camelCase.length > PascalCase.length) { |
| this.userStyle.namingConvention = 'camelCase'; |
| } else if (snake_case.length > camelCase.length) { |
| this.userStyle.namingConvention = 'snake_case'; |
| } |
|
|
| |
| const singleQuotes = (content.match(/'/g) || []).length; |
| const doubleQuotes = (content.match(/"/g) || []).length; |
| this.userStyle.preferredQuotes = doubleQuotes > singleQuotes ? 'double' : 'single'; |
|
|
| |
| const lines = content.split('\n'); |
| const linesWithSemicolon = lines.filter(l => l.trim().endsWith(';')).length; |
| this.userStyle.semicolonUsage = linesWithSemicolon > lines.length * 0.3; |
|
|
| |
| const spaces = content.match(/^ +/gm) || []; |
| const tabs = content.match(/^\t+/gm) || []; |
| this.userStyle.indentStyle = spaces.length > tabs.length ? 'spaces' : 'tabs'; |
| } |
|
|
| trackSnippets(content) { |
| |
| const patterns = [ |
| { regex: /console\.log\([^)]+\)/g, name: 'console.log' }, |
| { regex: /document\.getElementById\([^)]+\)/g, name: 'getElementById' }, |
| { regex: /document\.querySelector\([^)]+\)/g, name: 'querySelector' }, |
| { regex: /function\s+\w+\s*\([^)]*\)\s*{/g, name: 'function declaration' }, |
| { regex: /const\s+\w+\s*=/g, name: 'const declaration' }, |
| { regex: /let\s+\w+\s*=/g, name: 'let declaration' }, |
| { regex: /if\s*\([^)]+\)\s*{/g, name: 'if statement' }, |
| { regex: /for\s*\([^)]+\)\s*{/g, name: 'for loop' } |
| ]; |
|
|
| patterns.forEach(p => { |
| const matches = content.match(p.regex) || []; |
| matches.forEach(match => { |
| const key = p.name + ':' + match.substring(0, 20); |
| const count = this.snippetUsage.get(key) || { count: 0, code: match.substring(0, 30) }; |
| count.count++; |
| this.snippetUsage.set(key, count); |
| }); |
| }); |
| } |
|
|
| getSuggestion(beforeCursor, typed, filename) { |
| const ext = filename.split('.').pop().toLowerCase(); |
| |
| |
| if (ext === 'html' || ext === 'htm') { |
| if (typed === '<') { |
| return { text: 'div', insert: 'div>', confidence: 0.9 }; |
| } |
| if (beforeCursor.endsWith('<div')) { |
| return { text: '></div>', insert: '></div>', confidence: 0.85 }; |
| } |
| if (beforeCursor.endsWith('<p')) { |
| return { text: '></p>', insert: '></p>', confidence: 0.85 }; |
| } |
| } |
|
|
| if (ext === 'css') { |
| if (beforeCursor.endsWith(' {')) { |
| return { text: ' }', insert: '\n \n}', confidence: 0.9 }; |
| } |
| } |
|
|
| if (ext === 'js' || ext === 'javascript') { |
| |
| if (typed === 'c' && this.snippetUsage.has('console.log')) { |
| const usage = this.snippetUsage.get('console.log'); |
| if (usage.count > 2) { |
| return { text: 'onsole.log()', insert: 'onsole.log()', confidence: Math.min(0.95, 0.7 + usage.count * 0.05) }; |
| } |
| } |
| |
| if (typed === 'f' && beforeCursor.match(/^\s*$/)) { |
| return { text: 'unction name() {}', insert: 'unction ' + this.guessFunctionName() + '() {\n \n}', confidence: 0.8 }; |
| } |
|
|
| if (beforeCursor.endsWith('doc')) { |
| return { text: 'ument.', insert: 'ument.', confidence: 0.85 }; |
| } |
| } |
|
|
| |
| if (this.userStyle.namingConvention === 'camelCase') { |
| |
| } |
|
|
| return null; |
| } |
|
|
| guessFunctionName() { |
| |
| return 'myFunction'; |
| } |
|
|
| recordAcceptance(suggestion) { |
| this.suggestionHistory.unshift({ |
| text: suggestion.text, |
| time: new Date().toLocaleTimeString() |
| }); |
| if (this.suggestionHistory.length > 10) { |
| this.suggestionHistory.pop(); |
| } |
| } |
|
|
| getStats() { |
| const snippets = Array.from(this.snippetUsage.entries()) |
| .sort((a, b) => b[1].count - a[1].count) |
| .slice(0, 5) |
| .map(([key, value]) => ({ |
| trigger: key.split(':')[0], |
| code: value.code, |
| count: value.count |
| })); |
|
|
| const totalPatterns = this.patterns.size + this.snippetUsage.size; |
| const confidence = Math.min(100, Math.floor(totalPatterns * 2)); |
|
|
| return { |
| patterns: totalPatterns, |
| confidence: confidence, |
| topSnippets: snippets, |
| recentSuggestions: this.suggestionHistory, |
| style: this.userStyle |
| }; |
| } |
| } |
|
|
| |
| const app = new CodeMasterIDE(); |