diff --git a/examples/web/ui_demo b/examples/web/ui_demo deleted file mode 100755 index 7c800319..00000000 Binary files a/examples/web/ui_demo and /dev/null differ diff --git a/lib/web/ui/factory.v b/lib/web/ui/factory.v index a6493b76..06002ba9 100644 --- a/lib/web/ui/factory.v +++ b/lib/web/ui/factory.v @@ -98,6 +98,12 @@ pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result { return ctx.html(app.render_heroscript()) } +// Chat page +@[get; '/admin/chat'] +pub fn (app &App) admin_chat(mut ctx Context) veb.Result { + return ctx.html(app.render_chat()) +} + // Static CSS files @[get; '/static/css/colors.css'] pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result { @@ -140,6 +146,16 @@ pub fn (app &App) serve_heroscript_js(mut ctx Context) veb.Result { return ctx.text(js_content) } +@[get; '/static/js/chat.js'] +pub fn (app &App) serve_chat_js(mut ctx Context) veb.Result { + js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'chat.js') + js_content := os.read_file(js_path) or { + return ctx.text('/* JS file not found */') + } + ctx.set_content_type('application/javascript') + return ctx.text(js_content) +} + @[get; '/static/css/heroscript.css'] pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result { css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroscript.css') @@ -150,6 +166,16 @@ pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result { return ctx.text(css_content) } +@[get; '/static/css/chat.css'] +pub fn (app &App) serve_chat_css(mut ctx Context) veb.Result { + css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'chat.css') + css_content := os.read_file(css_path) or { + return ctx.text('/* CSS file not found */') + } + ctx.set_content_type('text/css') + return ctx.text(css_content) +} + // Catch-all content under /admin/* @[get; '/admin/:path...'] pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result { @@ -212,6 +238,33 @@ fn (app &App) render_heroscript() string { return result } +// Chat rendering using external template +fn (app &App) render_chat() string { + // Get the template file path relative to the module + template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html') + + // Read the template file + template_content := os.read_file(template_path) or { + // Fallback to basic template if file not found + return app.render_chat_fallback() + } + + // Generate menu HTML + menu_content := menu_html(app.menu, 0, 'm') + + // Simple template variable replacement + mut result := template_content + result = result.replace('{{.title}}', app.title) + result = result.replace('{{.menu_html}}', menu_content) + result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') + result = result.replace('{{.css_main_url}}', '/static/css/main.css') + result = result.replace('{{.css_chat_url}}', '/static/css/chat.css') + result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') + result = result.replace('{{.js_chat_url}}', '/static/js/chat.js') + + return result +} + // Fallback HeroScript rendering method fn (app &App) render_heroscript_fallback() string { return ' @@ -234,6 +287,28 @@ fn (app &App) render_heroscript_fallback() string { ' } +// Fallback Chat rendering method +fn (app &App) render_chat_fallback() string { + return ' + + + + + + ${app.title} - Chat + + + +
+

Chat Assistant

+

Chat template not found. Please check the template files.

+ Back to Admin +
+ + +' +} + // Fallback rendering method (inline template) fn (app &App) render_admin_fallback(path string, heading string) string { return ' @@ -364,6 +439,10 @@ fn default_menu() []MenuItem { title: 'HeroScript' href: '/admin/heroscript' }, + MenuItem{ + title: 'Chat' + href: '/admin/chat' + }, MenuItem{ title: 'Users' children: [ diff --git a/lib/web/ui/templates/chat.html b/lib/web/ui/templates/chat.html new file mode 100644 index 00000000..e662b285 --- /dev/null +++ b/lib/web/ui/templates/chat.html @@ -0,0 +1,200 @@ + + + + + + {{.title}} - Chat + + + + + + + + + + + +
+
+ +
+
+
AI Chat Assistant
+
+ + +
+
+ +
+
+
+ +
+
+
Hello! I'm your AI assistant. How can I help you today?
+
Just now
+
+
+
+ +
+
+ +
+ + + +
+
+
+
+
+ + +
+
+
Voice Recorder
+
+ + + +
+
+ +
+
+ + 00:00 +
+
+
+
+
+ +
+
+
Recordings
+
+ + +
+
+ +
+
+
+ + Recordings +
+
+
+
+ + sample1.mp3 + 2.1 MB +
+
+
+
+ + sample2.wav + 5.3 MB +
+
+
+
+
+
+
+
+
+ + +
+
+ Transcribe +
+
+ Translate +
+
+ Open +
+
+ Move +
+
+ Rename +
+
+
+ Export +
+
+ + + + + + + + + \ No newline at end of file diff --git a/lib/web/ui/templates/css/chat.css b/lib/web/ui/templates/css/chat.css new file mode 100644 index 00000000..21d0655a --- /dev/null +++ b/lib/web/ui/templates/css/chat.css @@ -0,0 +1,489 @@ +/* Chat Widget Styles */ + +/* Chat Container Layout */ +.chat-container { + display: flex; + flex-direction: column; + height: calc(100vh - 44px - 32px); + background-color: var(--bg-primary); + border-radius: 8px; + border: 1px solid var(--border-primary); + overflow: hidden; + margin-bottom: 1rem; +} + +.chat-header { + display: flex; + justify-content: between; + align-items: center; + padding: 1rem; + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-primary); +} + +.chat-controls { + display: flex; + gap: 0.5rem; +} + +/* Chat Messages Area */ +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + background-color: var(--bg-primary); +} + +.message { + display: flex; + gap: 0.75rem; + max-width: 80%; + animation: messageSlideIn 0.3s ease-out; +} + +.message.user { + align-self: flex-end; + flex-direction: row-reverse; +} + +.message.assistant { + align-self: flex-start; +} + +.message-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + flex-shrink: 0; +} + +.message.user .message-avatar { + background-color: var(--link-color); + color: white; +} + +.message.assistant .message-avatar { + background-color: var(--bg-tertiary); + color: var(--text-secondary); +} + +.message-content { + flex: 1; +} + +.message-text { + background-color: var(--card-bg); + padding: 0.75rem 1rem; + border-radius: 1rem; + border: 1px solid var(--border-primary); + color: var(--text-primary); + line-height: 1.4; + word-wrap: break-word; +} + +.message.user .message-text { + background-color: var(--link-color); + color: white; + border-color: var(--link-color); +} + +.message-time { + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 0.25rem; + text-align: right; +} + +.message.user .message-time { + text-align: left; +} + +/* Chat Input Area */ +.chat-input-container { + padding: 1rem; + background-color: var(--bg-secondary); + border-top: 1px solid var(--border-primary); +} + +.chat-input-wrapper { + display: flex; + gap: 0.5rem; + align-items: flex-end; +} + +.chat-input { + flex: 1; + resize: none; + min-height: 40px; + max-height: 120px; + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + border-radius: 20px; + padding: 0.75rem 1rem; +} + +.chat-input:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +.chat-input-actions { + display: flex; + gap: 0.25rem; + align-items: center; +} + +.chat-status { + margin-top: 0.5rem; + font-size: 0.875rem; + color: var(--text-muted); + min-height: 1.2rem; +} + +/* Recorder Container */ +.recorder-container { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: 8px; + overflow: hidden; +} + +.recorder-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-primary); +} + +.recorder-controls { + display: flex; + gap: 0.5rem; +} + +/* Recording Status */ +.recording-status { + padding: 1rem; + display: none; + background-color: var(--bg-primary); + border-bottom: 1px solid var(--border-primary); +} + +.recording-status.active { + display: block; +} + +.recording-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.recording-dot { + width: 12px; + height: 12px; + background-color: var(--danger-color); + border-radius: 50%; + animation: recordingPulse 1s infinite; +} + +.recording-time { + font-family: 'Courier New', monospace; + font-weight: bold; + color: var(--text-primary); +} + +.recording-level { + height: 4px; + background-color: var(--bg-tertiary); + border-radius: 2px; + overflow: hidden; +} + +.level-bar { + height: 100%; + background-color: var(--success-color); + width: 0%; + transition: width 0.1s ease; +} + +/* Recordings Explorer */ +.recordings-explorer { + padding: 1rem; + max-height: 300px; + overflow-y: auto; +} + +.explorer-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.explorer-actions { + display: flex; + gap: 0.25rem; +} + +.explorer-tree { + font-size: 0.875rem; +} + +.tree-item { + margin-bottom: 0.25rem; +} + +.tree-item-content { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.tree-item-content:hover { + background-color: var(--bg-secondary); +} + +.tree-item-content.selected { + background-color: var(--link-color); + color: white; +} + +.tree-item-name { + flex: 1; + color: var(--text-primary); +} + +.tree-item-size { + font-size: 0.75rem; + color: var(--text-muted); +} + +.tree-item-children { + margin-left: 1rem; + border-left: 1px solid var(--border-primary); + padding-left: 0.5rem; +} + +.tree-item.folder > .tree-item-content > i { + color: var(--warning-color); +} + +.tree-item.file > .tree-item-content > i { + color: var(--info-color); +} + +/* Context Menu */ +.context-menu { + position: fixed; + background-color: var(--card-bg); + border: 1px solid var(--border-primary); + border-radius: 6px; + box-shadow: 0 4px 12px var(--card-shadow); + padding: 0.25rem 0; + min-width: 150px; + z-index: 1060; + display: none; +} + +.context-menu-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + cursor: pointer; + color: var(--text-primary); + transition: background-color 0.2s ease; +} + +.context-menu-item:hover { + background-color: var(--bg-secondary); +} + +.context-menu-divider { + height: 1px; + background-color: var(--border-primary); + margin: 0.25rem 0; +} + +/* Animations */ +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes recordingPulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +/* Voice Input States */ +.voice-input-active { + background-color: var(--danger-color) !important; + border-color: var(--danger-color) !important; + color: white !important; +} + +.voice-input-processing { + background-color: var(--warning-color) !important; + border-color: var(--warning-color) !important; + color: white !important; +} + +/* Upload Progress */ +.upload-progress { + margin-top: 1rem; +} + +.progress { + height: 8px; + background-color: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; +} + +.progress-bar { + background-color: var(--link-color); + transition: width 0.3s ease; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .chat-container { + height: calc(100vh - 44px - 16px); + margin-bottom: 0.5rem; + } + + .message { + max-width: 90%; + } + + .chat-input-wrapper { + flex-wrap: wrap; + } + + .chat-input-actions { + order: -1; + width: 100%; + justify-content: center; + margin-bottom: 0.5rem; + } + + .recordings-explorer { + max-height: 200px; + } +} + +/* Dark theme specific adjustments */ +[data-theme="dark"] .chat-input { + background-color: var(--bg-secondary); +} + +[data-theme="dark"] .message.user .message-text { + background-color: var(--link-color); +} + +[data-theme="dark"] .context-menu { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +/* Bootstrap Icons Integration */ +.bi { + font-size: 1em; + line-height: 1; +} + +/* Typing Indicator */ +.typing-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + color: var(--text-muted); + font-style: italic; +} + +.typing-dots { + display: flex; + gap: 0.25rem; +} + +.typing-dot { + width: 6px; + height: 6px; + background-color: var(--text-muted); + border-radius: 50%; + animation: typingBounce 1.4s infinite ease-in-out; +} + +.typing-dot:nth-child(1) { animation-delay: -0.32s; } +.typing-dot:nth-child(2) { animation-delay: -0.16s; } + +@keyframes typingBounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } +} + +/* File attachment preview */ +.file-attachment { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + margin-bottom: 0.5rem; +} + +.file-attachment-icon { + color: var(--info-color); +} + +.file-attachment-info { + flex: 1; +} + +.file-attachment-name { + font-weight: 500; + color: var(--text-primary); +} + +.file-attachment-size { + font-size: 0.75rem; + color: var(--text-muted); +} + +.file-attachment-remove { + color: var(--danger-color); + cursor: pointer; + padding: 0.25rem; +} \ No newline at end of file diff --git a/lib/web/ui/templates/js/chat.js b/lib/web/ui/templates/js/chat.js new file mode 100644 index 00000000..c0e1817b --- /dev/null +++ b/lib/web/ui/templates/js/chat.js @@ -0,0 +1,583 @@ +/** + * Chat Widget JavaScript + * Handles chat functionality, voice recording, and file management + */ + +class ChatWidget { + constructor() { + this.messages = []; + this.isRecording = false; + this.mediaRecorder = null; + this.audioChunks = []; + this.recordingStartTime = null; + this.recordingTimer = null; + this.selectedFiles = []; + this.recordings = []; + + this.init(); + } + + init() { + this.bindEvents(); + this.initializeRecorder(); + this.loadRecordings(); + this.setupContextMenu(); + this.autoResizeTextarea(); + } + + bindEvents() { + // Chat input events + const chatInput = document.getElementById('chatInput'); + const sendButton = document.getElementById('sendMessage'); + + chatInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.sendMessage(); + } + }); + + chatInput.addEventListener('input', () => { + this.autoResizeTextarea(); + }); + + sendButton.addEventListener('click', () => { + this.sendMessage(); + }); + + // Voice input button + document.getElementById('voiceInput').addEventListener('click', () => { + this.toggleVoiceInput(); + }); + + // File attachment + document.getElementById('attachFile').addEventListener('click', () => { + this.showFileUploadModal(); + }); + + // Clear chat + document.getElementById('clearChat').addEventListener('click', () => { + this.clearChat(); + }); + + // Recording controls + document.getElementById('recordBtn').addEventListener('click', () => { + this.startRecording(); + }); + + document.getElementById('stopBtn').addEventListener('click', () => { + this.stopRecording(); + }); + + document.getElementById('playBtn').addEventListener('click', () => { + this.playLastRecording(); + }); + + // Explorer actions + document.getElementById('newFolderBtn').addEventListener('click', () => { + this.createNewFolder(); + }); + + document.getElementById('refreshBtn').addEventListener('click', () => { + this.refreshRecordings(); + }); + + // File upload modal + document.getElementById('uploadBtn').addEventListener('click', () => { + this.uploadFile(); + }); + + // Tree item clicks + document.addEventListener('click', (e) => { + if (e.target.closest('.tree-item-content')) { + this.handleTreeItemClick(e); + } + }); + + // Context menu + document.addEventListener('contextmenu', (e) => { + if (e.target.closest('.tree-item-content')) { + e.preventDefault(); + this.showContextMenu(e); + } + }); + + // Hide context menu on click outside + document.addEventListener('click', () => { + this.hideContextMenu(); + }); + } + + sendMessage() { + const input = document.getElementById('chatInput'); + const message = input.value.trim(); + + if (!message && this.selectedFiles.length === 0) return; + + // Add user message + this.addMessage('user', message, this.selectedFiles); + + // Clear input and files + input.value = ''; + this.selectedFiles = []; + this.autoResizeTextarea(); + + // Show typing indicator + this.showTypingIndicator(); + + // Simulate AI response (replace with actual API call) + setTimeout(() => { + this.hideTypingIndicator(); + this.addMessage('assistant', this.generateAIResponse(message)); + }, 1000 + Math.random() * 2000); + } + + addMessage(sender, text, files = []) { + const messagesContainer = document.getElementById('chatMessages'); + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${sender}`; + + const avatar = document.createElement('div'); + avatar.className = 'message-avatar'; + avatar.innerHTML = sender === 'user' ? '' : ''; + + const content = document.createElement('div'); + content.className = 'message-content'; + + // Add file attachments if any + if (files.length > 0) { + files.forEach(file => { + const fileDiv = document.createElement('div'); + fileDiv.className = 'file-attachment'; + fileDiv.innerHTML = ` + +
+
${file.name}
+
${this.formatFileSize(file.size)}
+
+ `; + content.appendChild(fileDiv); + }); + } + + if (text) { + const textDiv = document.createElement('div'); + textDiv.className = 'message-text'; + textDiv.textContent = text; + content.appendChild(textDiv); + } + + const timeDiv = document.createElement('div'); + timeDiv.className = 'message-time'; + timeDiv.textContent = new Date().toLocaleTimeString(); + content.appendChild(timeDiv); + + messageDiv.appendChild(avatar); + messageDiv.appendChild(content); + + messagesContainer.appendChild(messageDiv); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + + this.messages.push({ sender, text, files, timestamp: new Date() }); + } + + showTypingIndicator() { + const messagesContainer = document.getElementById('chatMessages'); + const typingDiv = document.createElement('div'); + typingDiv.className = 'message assistant typing-indicator'; + typingDiv.id = 'typingIndicator'; + typingDiv.innerHTML = ` +
+ +
+
+
+ AI is typing +
+
+
+
+
+
+
+ `; + messagesContainer.appendChild(typingDiv); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + + hideTypingIndicator() { + const typingIndicator = document.getElementById('typingIndicator'); + if (typingIndicator) { + typingIndicator.remove(); + } + } + + generateAIResponse(userMessage) { + const responses = [ + "I understand your question. Let me help you with that.", + "That's an interesting point. Here's what I think...", + "Based on what you've shared, I'd suggest...", + "I can help you with that. Here are some options...", + "Thank you for the information. Let me process that...", + "I see what you're asking. Here's my response..." + ]; + return responses[Math.floor(Math.random() * responses.length)]; + } + + clearChat() { + if (confirm('Are you sure you want to clear all messages?')) { + document.getElementById('chatMessages').innerHTML = ` +
+
+ +
+
+
Hello! I'm your AI assistant. How can I help you today?
+
Just now
+
+
+ `; + this.messages = []; + } + } + + autoResizeTextarea() { + const textarea = document.getElementById('chatInput'); + textarea.style.height = 'auto'; + textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; + } + + // Voice Recording Functions + async initializeRecorder() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + this.mediaRecorder = new MediaRecorder(stream); + + this.mediaRecorder.ondataavailable = (event) => { + this.audioChunks.push(event.data); + }; + + this.mediaRecorder.onstop = () => { + this.saveRecording(); + }; + + } catch (error) { + console.error('Error accessing microphone:', error); + this.showStatus('Microphone access denied', 'error'); + } + } + + startRecording() { + if (!this.mediaRecorder) { + this.showStatus('Microphone not available', 'error'); + return; + } + + this.audioChunks = []; + this.mediaRecorder.start(); + this.isRecording = true; + this.recordingStartTime = Date.now(); + + // Update UI + document.getElementById('recordBtn').disabled = true; + document.getElementById('stopBtn').disabled = false; + document.getElementById('recordingStatus').classList.add('active'); + + // Start timer + this.recordingTimer = setInterval(() => { + this.updateRecordingTime(); + }, 1000); + + this.showStatus('Recording started...', 'success'); + } + + stopRecording() { + if (!this.isRecording) return; + + this.mediaRecorder.stop(); + this.isRecording = false; + + // Update UI + document.getElementById('recordBtn').disabled = false; + document.getElementById('stopBtn').disabled = true; + document.getElementById('playBtn').disabled = false; + document.getElementById('recordingStatus').classList.remove('active'); + + // Stop timer + clearInterval(this.recordingTimer); + + this.showStatus('Recording stopped', 'success'); + } + + updateRecordingTime() { + const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000); + const minutes = Math.floor(elapsed / 60); + const seconds = elapsed % 60; + const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + document.querySelector('.recording-time').textContent = timeString; + } + + saveRecording() { + const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' }); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `recording-${timestamp}.wav`; + + // Create download link (in real implementation, upload to server) + const url = URL.createObjectURL(audioBlob); + const recording = { + name: filename, + url: url, + size: audioBlob.size, + timestamp: new Date() + }; + + this.recordings.push(recording); + this.updateRecordingsTree(); + this.showStatus(`Recording saved as ${filename}`, 'success'); + } + + playLastRecording() { + if (this.recordings.length === 0) { + this.showStatus('No recordings available', 'warning'); + return; + } + + const lastRecording = this.recordings[this.recordings.length - 1]; + const audio = new Audio(lastRecording.url); + audio.play(); + this.showStatus(`Playing ${lastRecording.name}`, 'info'); + } + + // File Management Functions + showFileUploadModal() { + const modal = new bootstrap.Modal(document.getElementById('fileUploadModal')); + modal.show(); + } + + uploadFile() { + const fileInput = document.getElementById('fileInput'); + const files = Array.from(fileInput.files); + + if (files.length === 0) return; + + // Simulate upload progress + const progressContainer = document.getElementById('uploadProgress'); + const progressBar = progressContainer.querySelector('.progress-bar'); + + progressContainer.style.display = 'block'; + + let progress = 0; + const interval = setInterval(() => { + progress += Math.random() * 20; + if (progress >= 100) { + progress = 100; + clearInterval(interval); + + // Add files to selected files + this.selectedFiles = [...this.selectedFiles, ...files]; + this.showStatus(`${files.length} file(s) attached`, 'success'); + + // Close modal + bootstrap.Modal.getInstance(document.getElementById('fileUploadModal')).hide(); + progressContainer.style.display = 'none'; + progressBar.style.width = '0%'; + fileInput.value = ''; + } + progressBar.style.width = progress + '%'; + }, 100); + } + + // Tree and Context Menu Functions + loadRecordings() { + // Load sample recordings (replace with actual data loading) + this.recordings = [ + { name: 'sample1.mp3', size: 2097152, timestamp: new Date() }, + { name: 'sample2.wav', size: 5242880, timestamp: new Date() } + ]; + this.updateRecordingsTree(); + } + + updateRecordingsTree() { + const tree = document.getElementById('explorerTree'); + const childrenContainer = tree.querySelector('.tree-item-children'); + + // Clear existing items except samples + childrenContainer.innerHTML = ''; + + // Add recordings + this.recordings.forEach(recording => { + const item = document.createElement('div'); + item.className = 'tree-item file'; + item.dataset.path = `/${recording.name}`; + item.innerHTML = ` +
+ + ${recording.name} + ${this.formatFileSize(recording.size)} +
+ `; + childrenContainer.appendChild(item); + }); + } + + handleTreeItemClick(e) { + // Remove previous selection + document.querySelectorAll('.tree-item-content.selected').forEach(item => { + item.classList.remove('selected'); + }); + + // Add selection to clicked item + e.target.closest('.tree-item-content').classList.add('selected'); + } + + setupContextMenu() { + const contextMenu = document.getElementById('contextMenu'); + + contextMenu.addEventListener('click', (e) => { + const action = e.target.dataset.action; + if (action) { + this.handleContextAction(action); + this.hideContextMenu(); + } + }); + } + + showContextMenu(e) { + const contextMenu = document.getElementById('contextMenu'); + contextMenu.style.display = 'block'; + contextMenu.style.left = e.pageX + 'px'; + contextMenu.style.top = e.pageY + 'px'; + } + + hideContextMenu() { + document.getElementById('contextMenu').style.display = 'none'; + } + + handleContextAction(action) { + const selectedItem = document.querySelector('.tree-item-content.selected'); + if (!selectedItem) return; + + const filename = selectedItem.querySelector('.tree-item-name').textContent; + + switch (action) { + case 'transcribe': + this.showStatus(`Transcribing ${filename}...`, 'info'); + break; + case 'translate': + this.showStatus(`Translating ${filename}...`, 'info'); + break; + case 'open': + this.showStatus(`Opening ${filename}...`, 'info'); + break; + case 'move': + this.showStatus(`Moving ${filename}...`, 'info'); + break; + case 'rename': + this.renameFile(filename); + break; + case 'export': + this.exportFile(filename); + break; + } + } + + renameFile(oldName) { + const newName = prompt('Enter new name:', oldName); + if (newName && newName !== oldName) { + this.showStatus(`Renamed ${oldName} to ${newName}`, 'success'); + // Update the recording name in the array and refresh tree + const recording = this.recordings.find(r => r.name === oldName); + if (recording) { + recording.name = newName; + this.updateRecordingsTree(); + } + } + } + + exportFile(filename) { + const recording = this.recordings.find(r => r.name === filename); + if (recording && recording.url) { + const a = document.createElement('a'); + a.href = recording.url; + a.download = filename; + a.click(); + this.showStatus(`Exported ${filename}`, 'success'); + } + } + + createNewFolder() { + const folderName = prompt('Enter folder name:'); + if (folderName) { + this.showStatus(`Created folder: ${folderName}`, 'success'); + // In real implementation, create folder in tree + } + } + + refreshRecordings() { + this.showStatus('Refreshing recordings...', 'info'); + this.loadRecordings(); + } + + // Voice Input Functions + toggleVoiceInput() { + const button = document.getElementById('voiceInput'); + + if (button.classList.contains('voice-input-active')) { + this.stopVoiceInput(); + } else { + this.startVoiceInput(); + } + } + + startVoiceInput() { + const button = document.getElementById('voiceInput'); + button.classList.add('voice-input-active'); + button.innerHTML = ''; + this.showStatus('Listening...', 'info'); + + // Simulate voice recognition (replace with actual implementation) + setTimeout(() => { + this.stopVoiceInput(); + document.getElementById('chatInput').value = 'This is a voice input example'; + this.autoResizeTextarea(); + }, 3000); + } + + stopVoiceInput() { + const button = document.getElementById('voiceInput'); + button.classList.remove('voice-input-active'); + button.innerHTML = ''; + this.showStatus('Voice input stopped', 'info'); + } + + // Utility Functions + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + showStatus(message, type = 'info') { + const statusElement = document.getElementById('chatStatus'); + statusElement.textContent = message; + statusElement.className = `chat-status text-${type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'info'}`; + + // Clear status after 3 seconds + setTimeout(() => { + statusElement.textContent = ''; + statusElement.className = 'chat-status'; + }, 3000); + } +} + +// Initialize chat widget when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + window.chatWidget = new ChatWidget(); +}); + +// Export for external use +if (typeof module !== 'undefined' && module.exports) { + module.exports = ChatWidget; +} \ No newline at end of file