...
This commit is contained in:
Binary file not shown.
@@ -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: [
|
||||
|
||||
200
lib/web/ui/templates/chat.html
Normal file
200
lib/web/ui/templates/chat.html
Normal 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>
|
||||
489
lib/web/ui/templates/css/chat.css
Normal file
489
lib/web/ui/templates/css/chat.css
Normal 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;
|
||||
}
|
||||
583
lib/web/ui/templates/js/chat.js
Normal file
583
lib/web/ui/templates/js/chat.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user