This commit is contained in:
2025-08-11 11:51:51 +02:00
parent b473630ceb
commit ca4127319d
5 changed files with 1351 additions and 0 deletions

Binary file not shown.

View File

@@ -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 '
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${app.title} - Chat</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Chat Assistant</h1>
<p>Chat template not found. Please check the template files.</p>
<a href="/admin" class="btn btn-primary">Back to Admin</a>
</div>
</body>
</html>
'
}
// 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: [

View File

@@ -0,0 +1,200 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.title}} - Chat</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="{{.css_colors_url}}">
<link rel="stylesheet" href="{{.css_main_url}}">
<link rel="stylesheet" href="{{.css_chat_url}}">
<meta name="color-scheme" content="light dark">
</head>
<body>
<nav class="navbar navbar-dark bg-dark fixed-top header px-2">
<div class="d-flex w-100 align-items-center justify-content-between">
<div class="text-white fw-bold">{{.title}}</div>
<div class="text-white-50">Chat</div>
</div>
</nav>
<aside class="sidebar">
<div class="p-2">
<div class="menu-section">Navigation</div>
<div class="list-group list-group-flush">
{{.menu_html}}
</div>
</div>
</aside>
<main class="main">
<div class="container-fluid h-100">
<!-- Chat Section -->
<div class="chat-container">
<div class="chat-header">
<h5 class="mb-0">AI Chat Assistant</h5>
<div class="chat-controls">
<button class="btn btn-sm btn-outline-secondary" id="clearChat">
<i class="bi bi-trash"></i> Clear
</button>
<button class="btn btn-sm btn-outline-secondary" id="voiceToggle">
<i class="bi bi-mic"></i> Voice
</button>
</div>
</div>
<div class="chat-messages" id="chatMessages">
<div class="message assistant">
<div class="message-avatar">
<i class="bi bi-robot"></i>
</div>
<div class="message-content">
<div class="message-text">Hello! I'm your AI assistant. How can I help you today?</div>
<div class="message-time">Just now</div>
</div>
</div>
</div>
<div class="chat-input-container">
<div class="chat-input-wrapper">
<textarea class="form-control chat-input" id="chatInput" placeholder="Type your message here..." rows="1"></textarea>
<div class="chat-input-actions">
<button class="btn btn-outline-secondary btn-sm" id="attachFile" title="Attach file">
<i class="bi bi-paperclip"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" id="voiceInput" title="Voice input">
<i class="bi bi-mic"></i>
</button>
<button class="btn btn-primary btn-sm" id="sendMessage" title="Send message">
<i class="bi bi-send"></i>
</button>
</div>
</div>
<div class="chat-status" id="chatStatus"></div>
</div>
</div>
<!-- Recorder Section -->
<div class="recorder-container">
<div class="recorder-header">
<h6 class="mb-0">Voice Recorder</h6>
<div class="recorder-controls">
<button class="btn btn-sm btn-danger" id="recordBtn">
<i class="bi bi-record-circle"></i> Record
</button>
<button class="btn btn-sm btn-secondary" id="stopBtn" disabled>
<i class="bi bi-stop-circle"></i> Stop
</button>
<button class="btn btn-sm btn-outline-secondary" id="playBtn" disabled>
<i class="bi bi-play-circle"></i> Play
</button>
</div>
</div>
<div class="recording-status" id="recordingStatus">
<div class="recording-indicator">
<span class="recording-dot"></span>
<span class="recording-time">00:00</span>
</div>
<div class="recording-level">
<div class="level-bar" id="levelBar"></div>
</div>
</div>
<div class="recordings-explorer">
<div class="explorer-header">
<h6 class="mb-2">Recordings</h6>
<div class="explorer-actions">
<button class="btn btn-sm btn-outline-secondary" id="newFolderBtn">
<i class="bi bi-folder-plus"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" id="refreshBtn">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
<div class="explorer-tree" id="explorerTree">
<div class="tree-item folder expanded" data-path="/">
<div class="tree-item-content">
<i class="bi bi-folder-open"></i>
<span class="tree-item-name">Recordings</span>
</div>
<div class="tree-item-children">
<div class="tree-item file" data-path="/sample1.mp3">
<div class="tree-item-content">
<i class="bi bi-file-earmark-music"></i>
<span class="tree-item-name">sample1.mp3</span>
<span class="tree-item-size">2.1 MB</span>
</div>
</div>
<div class="tree-item file" data-path="/sample2.wav">
<div class="tree-item-content">
<i class="bi bi-file-earmark-music"></i>
<span class="tree-item-name">sample2.wav</span>
<span class="tree-item-size">5.3 MB</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Context Menu -->
<div class="context-menu" id="contextMenu">
<div class="context-menu-item" data-action="transcribe">
<i class="bi bi-file-text"></i> Transcribe
</div>
<div class="context-menu-item" data-action="translate">
<i class="bi bi-translate"></i> Translate
</div>
<div class="context-menu-item" data-action="open">
<i class="bi bi-folder-open"></i> Open
</div>
<div class="context-menu-item" data-action="move">
<i class="bi bi-arrow-right"></i> Move
</div>
<div class="context-menu-item" data-action="rename">
<i class="bi bi-pencil"></i> Rename
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" data-action="export">
<i class="bi bi-download"></i> Export
</div>
</div>
<!-- File Upload Modal -->
<div class="modal fade" id="fileUploadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Upload File</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="fileInput" class="form-label">Choose file</label>
<input class="form-control" type="file" id="fileInput" accept=".txt,.pdf,.doc,.docx,.md">
</div>
<div class="upload-progress" id="uploadProgress" style="display: none;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="uploadBtn">Upload</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="{{.js_theme_url}}"></script>
<script src="{{.js_chat_url}}"></script>
</body>
</html>

View File

@@ -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;
}

View File

@@ -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' ? '<i class="bi bi-person"></i>' : '<i class="bi bi-robot"></i>';
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 = `
<i class="bi bi-file-earmark file-attachment-icon"></i>
<div class="file-attachment-info">
<div class="file-attachment-name">${file.name}</div>
<div class="file-attachment-size">${this.formatFileSize(file.size)}</div>
</div>
`;
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 = `
<div class="message-avatar">
<i class="bi bi-robot"></i>
</div>
<div class="message-content">
<div class="typing-indicator">
AI is typing
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
</div>
`;
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 = `
<div class="message assistant">
<div class="message-avatar">
<i class="bi bi-robot"></i>
</div>
<div class="message-content">
<div class="message-text">Hello! I'm your AI assistant. How can I help you today?</div>
<div class="message-time">Just now</div>
</div>
</div>
`;
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 = `
<div class="tree-item-content">
<i class="bi bi-file-earmark-music"></i>
<span class="tree-item-name">${recording.name}</span>
<span class="tree-item-size">${this.formatFileSize(recording.size)}</span>
</div>
`;
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 = '<i class="bi bi-mic-fill"></i>';
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 = '<i class="bi bi-mic"></i>';
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;
}