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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hello! I'm your AI assistant. How can I help you today?
+
Just now
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Recordings
+
+
+
+
+
+ sample1.mp3
+ 2.1 MB
+
+
+
+
+
+ sample2.wav
+ 5.3 MB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 = `
+
+
+
+
+ `;
+ 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