Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
969
templates/inquire.html
Normal file
969
templates/inquire.html
Normal file
@@ -0,0 +1,969 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
|
||||
<title>Inquire Mode - DictIA</title>
|
||||
<!-- All dependencies bundled locally for offline support -->
|
||||
<script src="{{ url_for('static', filename='vendor/js/tailwind.min.js') }}"></script>
|
||||
<!-- All dependencies bundled locally for offline support -->
|
||||
<script src="{{ url_for('static', filename='vendor/js/vue.global.js') }}"></script>
|
||||
<!-- All dependencies bundled locally for offline support -->
|
||||
<script src="{{ url_for('static', filename='vendor/js/marked.min.js') }}"></script>
|
||||
<!-- All dependencies bundled locally for offline support -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/css/fontawesome.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||
<script src="{{ url_for('static', filename='js/i18n.js') }}"></script>
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='img/icon-32x32.png') }}">
|
||||
|
||||
<!-- Loading overlay to prevent FOUC -->
|
||||
{% include 'includes/loading_overlay.html' %}
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'var(--bg-primary)',
|
||||
secondary: 'var(--bg-secondary)',
|
||||
accent: 'var(--bg-accent)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Custom checkbox styling for better visual feedback */
|
||||
.filter-checkbox {
|
||||
appearance: none;
|
||||
background-color: var(--bg-input);
|
||||
border: 2px solid var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-checkbox:checked {
|
||||
background-color: var(--text-accent);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.filter-checkbox:checked::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-checkbox:focus {
|
||||
outline: 2px solid var(--text-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.filter-checkbox:hover {
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
/* Enhanced markdown styling for Inquire Mode chat messages */
|
||||
.chat-message .prose h1,
|
||||
.chat-message .prose h2,
|
||||
.chat-message .prose h3,
|
||||
.chat-message .prose h4 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.75em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.chat-message .prose h1 { font-size: 1.25em; }
|
||||
.chat-message .prose h2 { font-size: 1.15em; }
|
||||
.chat-message .prose h3 { font-size: 1.05em; }
|
||||
|
||||
.chat-message .prose p {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.chat-message .prose ul,
|
||||
.chat-message .prose ol {
|
||||
color: var(--text-primary);
|
||||
margin: 0.75em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.chat-message .prose li {
|
||||
color: var(--text-primary);
|
||||
margin: 0.25em 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.chat-message .prose strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chat-message .prose ul { list-style-type: disc; }
|
||||
.chat-message .prose ol { list-style-type: decimal; }
|
||||
|
||||
.chat-message .prose code {
|
||||
background-color: var(--bg-accent);
|
||||
color: var(--text-accent);
|
||||
padding: 0.15em 0.3em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full bg-[var(--bg-primary)] text-[var(--text-primary)] transition-colors duration-300">
|
||||
<div id="inquire-app" v-cloak class="h-full flex flex-col"
|
||||
data-current-user-name="{{ (current_user.name or current_user.username) if current_user.is_authenticated else '' }}">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="bg-[var(--bg-secondary)] border-b border-[var(--border-primary)] px-3 sm:px-4 py-3 flex items-center justify-between flex-shrink-0 z-50">
|
||||
<!-- Left side: Menu and logo -->
|
||||
<div class="flex items-center gap-2 sm:gap-4 min-w-0 flex-shrink">
|
||||
<!-- Mobile Sidebar Toggle -->
|
||||
<button @click="isMobileSidebarOpen = !isMobileSidebarOpen"
|
||||
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors duration-200 lg:hidden flex-shrink-0 flex items-center justify-center">
|
||||
<i class="fas fa-bars text-lg"></i>
|
||||
</button>
|
||||
|
||||
<!-- Logo and Title (clickable to go back) -->
|
||||
<a href="/" class="flex items-center gap-2 sm:gap-3 min-w-0 hover:opacity-80 transition-opacity">
|
||||
<img src="{{ url_for('static', filename='img/logo-dictia.png') }}" alt="DictIA" class="w-6 h-6 sm:w-8 sm:h-8 flex-shrink-0">
|
||||
<h1 class="text-lg sm:text-xl font-bold text-[var(--text-primary)] truncate">DictIA</h1>
|
||||
<span class="text-xs sm:text-sm text-[var(--text-muted)] hidden sm:inline">• Inquire Mode</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Right side: User menu -->
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
{% include 'components/token_budget_indicator.html' %}
|
||||
|
||||
<!-- New Recording Button -->
|
||||
<a href="/"
|
||||
class="px-3 py-1.5 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors text-sm flex items-center gap-2">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="hidden sm:inline">${t('nav.newRecording')}</span>
|
||||
</a>
|
||||
|
||||
<!-- User menu -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="relative">
|
||||
<button @click="isUserMenuOpen = !isUserMenuOpen"
|
||||
data-user-menu-toggle
|
||||
class="flex items-center gap-1 sm:gap-2 p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors duration-200"
|
||||
:title="t('admin.userMenu')">
|
||||
<i class="fas fa-user-circle text-lg"></i>
|
||||
<span class="hidden lg:inline text-sm">{{ (current_user.name or current_user.username) if current_user.is_authenticated else 'User' }}</span>
|
||||
<i class="fas fa-chevron-down text-xs hidden sm:inline"></i>
|
||||
</button>
|
||||
|
||||
<!-- User dropdown -->
|
||||
<div v-if="isUserMenuOpen"
|
||||
data-user-menu-dropdown
|
||||
class="absolute right-0 mt-2 w-56 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-lg z-50">
|
||||
<a href="/" class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-home mr-2 w-4 text-center"></i><span v-text="t('nav.home')"></span>
|
||||
</a>
|
||||
<a href="/account" class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-cog mr-2 w-4 text-center"></i><span v-text="t('nav.settings')"></span>
|
||||
</a>
|
||||
<a href="/account#tags" class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-tags mr-2 w-4 text-center"></i><span v-text="t('help.tagManagement')"></span>
|
||||
</a>
|
||||
<button @click="openSharesList" class="w-full text-left px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors flex items-center">
|
||||
<i class="fas fa-share-alt mr-2 w-4 text-center"></i><span v-text="t('modal.sharedTranscripts')"></span>
|
||||
</button>
|
||||
{% if current_user.is_admin or is_group_admin %}
|
||||
<a href="/admin" class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-{% if current_user.is_admin %}user-shield{% else %}users-cog{% endif %} mr-2 w-4 text-center"></i>
|
||||
<span v-text="{% if current_user.is_admin %}t('admin.title'){% else %}t('nav.groupManagement'){% endif %}"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="border-t border-[var(--border-primary)] my-1"></div>
|
||||
<button @click="toggleDarkMode" class="w-full text-left px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors flex items-center">
|
||||
<i :class="isDarkMode ? 'fas fa-sun' : 'fas fa-moon'" class="mr-2 w-4 text-center"></i>
|
||||
<span v-text="isDarkMode ? t('nav.lightMode') : t('nav.darkMode')"></span>
|
||||
</button>
|
||||
<button @click="openColorSchemeModal" class="w-full text-left px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors flex items-center">
|
||||
<i class="fas fa-palette mr-2 w-4 text-center"></i>
|
||||
<span v-text="t('modal.colorScheme')"></span>
|
||||
</button>
|
||||
<div class="border-t border-[var(--border-primary)] my-1"></div>
|
||||
<a href="/logout" class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-sign-out-alt mr-2 w-4 text-center"></i><span v-text="t('nav.signOut')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex overflow-hidden relative">
|
||||
<!-- Mobile Sidebar Overlay -->
|
||||
<div v-if="isMobileSidebarOpen"
|
||||
@click="isMobileSidebarOpen = false"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"></div>
|
||||
|
||||
<!-- Left Panel: Filters -->
|
||||
<div class="w-80 bg-[var(--bg-secondary)] border-r border-[var(--border-primary)] flex flex-col flex-shrink-0 transition-transform duration-300 ease-in-out z-50"
|
||||
:class="[
|
||||
'lg:relative lg:translate-x-0',
|
||||
isMobileSidebarOpen ? 'fixed inset-y-0 left-0 translate-x-0' : 'fixed inset-y-0 left-0 -translate-x-full lg:translate-x-0'
|
||||
]">
|
||||
<!-- Mobile close button -->
|
||||
<div class="lg:hidden flex items-center justify-between p-4 border-b border-[var(--border-primary)]">
|
||||
<h2 class="text-lg font-bold">${t('inquire.filters')}</h2>
|
||||
<button @click="isMobileSidebarOpen = false"
|
||||
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-times text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="flex items-center justify-between lg:block">
|
||||
<h2 class="text-lg font-bold hidden lg:block">${t('inquire.filters')}</h2>
|
||||
<button @click="clearInquireFilters" class="text-xs px-3 py-1 bg-[var(--bg-button)] text-[var(--text-button)] rounded-md hover:bg-[var(--bg-button-hover)] transition-colors flex items-center gap-1 w-full lg:w-auto justify-center">
|
||||
<i class="fas fa-times"></i> ${t('inquire.clearAll')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tag Filters -->
|
||||
<div class="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)] p-3">
|
||||
<h3 class="font-medium mb-2 flex items-center text-[var(--text-primary)]">
|
||||
<i class="fas fa-tags mr-2 text-[var(--text-accent)]"></i>${t('inquire.tags')}
|
||||
</h3>
|
||||
<!-- Tag search input -->
|
||||
<div class="mb-3">
|
||||
<input type="text" v-model="tagSearchQuery" :placeholder="t('form.searchTags')"
|
||||
class="w-full px-2 py-1 text-sm bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-[var(--text-primary)] focus:border-[var(--border-focus)] focus:ring-1 focus:ring-[var(--border-focus)] transition-colors">
|
||||
</div>
|
||||
<div class="space-y-1 max-h-40 overflow-y-auto custom-scrollbar">
|
||||
<label v-for="tag in filteredTags" :key="tag.id" class="flex items-center cursor-pointer hover:bg-[var(--bg-tertiary)] p-2 rounded transition-colors group">
|
||||
<input type="checkbox" :value="tag.id" v-model="inquireFilters.selectedTags"
|
||||
class="filter-checkbox">
|
||||
<span class="ml-2 text-sm font-medium flex-1" :style="{color: tag.color}">${tag.name}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded-full">${tag.recording_count}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Speaker Filters -->
|
||||
<div class="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)] p-3">
|
||||
<h3 class="font-medium mb-2 flex items-center text-[var(--text-primary)]">
|
||||
<i class="fas fa-users mr-2 text-[var(--text-accent)]"></i>${t('inquire.speakers')}
|
||||
</h3>
|
||||
<div v-if="availableFilters.speakers.length > 0">
|
||||
<!-- Speaker search input -->
|
||||
<div class="mb-3">
|
||||
<input type="text" v-model="speakerSearchQuery" :placeholder="t('form.searchSpeakers')"
|
||||
class="w-full px-2 py-1 text-sm bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-[var(--text-primary)] focus:border-[var(--border-focus)] focus:ring-1 focus:ring-[var(--border-focus)] transition-colors">
|
||||
</div>
|
||||
<div class="space-y-1 max-h-40 overflow-y-auto custom-scrollbar">
|
||||
<label v-for="speaker in filteredSpeakers" :key="speaker" class="flex items-center cursor-pointer hover:bg-[var(--bg-tertiary)] p-2 rounded transition-colors group">
|
||||
<input type="checkbox" :value="speaker" v-model="inquireFilters.selectedSpeakers"
|
||||
class="filter-checkbox">
|
||||
<span class="ml-2 text-sm text-[var(--text-primary)] flex-1">${speaker}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Speakers Message -->
|
||||
<div v-if="availableFilters.speakers.length === 0" class="text-center py-3">
|
||||
<i class="fas fa-users text-xl text-[var(--text-muted)] mb-2"></i>
|
||||
<p class="text-sm text-[var(--text-muted)]">${t('inquire.noSpeakerData')}</p>
|
||||
<p class="text-xs text-[var(--text-muted)] mt-1">${t('inquire.speakerRequirement')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filters -->
|
||||
<div class="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)] p-3">
|
||||
<h3 class="font-medium mb-2 flex items-center text-[var(--text-primary)]">
|
||||
<i class="fas fa-calendar mr-2 text-[var(--text-accent)]"></i>${t('inquire.dateRange')}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--text-muted)] mb-1 font-medium">${t('inquire.from')}</label>
|
||||
<input type="date" v-model="inquireFilters.dateFrom" class="w-full px-2 py-1 text-sm bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-[var(--text-primary)] focus:border-[var(--border-focus)] focus:ring-1 focus:ring-[var(--border-focus)] transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--text-muted)] mb-1 font-medium">${t('inquire.to')}</label>
|
||||
<input type="date" v-model="inquireFilters.dateTo" class="w-full px-2 py-1 text-sm bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-[var(--text-primary)] focus:border-[var(--border-focus)] focus:ring-1 focus:ring-[var(--border-focus)] transition-colors">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recording Filters - Commented Out -->
|
||||
<!--
|
||||
<div class="flex-1 flex flex-col px-6 pb-6 min-h-0">
|
||||
<div class="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)] p-4 flex flex-col flex-1 min-h-0">
|
||||
<h3 class="font-medium mb-3 flex items-center flex-shrink-0 text-[var(--text-primary)]">
|
||||
<i class="fas fa-file-audio mr-2 text-[var(--text-accent)]"></i>Recordings
|
||||
</h3>
|
||||
<div class="space-y-1 flex-1 overflow-y-auto custom-scrollbar pr-2 min-h-0">
|
||||
<label v-for="recording in availableFilters.recordings" :key="recording.id" class="flex items-center cursor-pointer hover:bg-[var(--bg-tertiary)] p-3 rounded-md transition-colors border border-transparent hover:border-[var(--border-secondary)]">
|
||||
<input type="checkbox" :value="recording.id" v-model="inquireFilters.selectedRecordings" class="h-4 w-4 rounded border-[var(--border-secondary)] text-[var(--text-accent)] focus:ring-[var(--border-focus)] flex-shrink-0">
|
||||
<div class="ml-3 flex-1 min-w-0">
|
||||
<div class="text-sm font-medium truncate text-[var(--text-primary)]" :title="recording.title">${recording.title}</div>
|
||||
<div v-if="recording.meeting_date" class="text-xs text-[var(--text-muted)] mt-1">${formatDate(recording.meeting_date)}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- Active Filters Summary -->
|
||||
<div v-if="hasActiveFilters" class="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)] p-3">
|
||||
<h4 class="text-sm font-medium mb-2 text-[var(--text-muted)] flex items-center">
|
||||
<i class="fas fa-filter mr-2"></i>${t('inquire.activeFilters')}
|
||||
</h4>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div v-if="inquireFilters.selectedTags.length" class="flex items-center px-2 py-1 bg-[var(--bg-tertiary)] rounded text-[var(--text-secondary)]">
|
||||
<span><i class="fas fa-tags mr-1"></i>${inquireFilters.selectedTags.length} ${t('inquire.tagsCount')}</span>
|
||||
</div>
|
||||
<div v-if="inquireFilters.selectedSpeakers.length" class="flex items-center px-2 py-1 bg-[var(--bg-tertiary)] rounded text-[var(--text-secondary)]">
|
||||
<span><i class="fas fa-users mr-1"></i>${inquireFilters.selectedSpeakers.length} ${t('inquire.speakersCount')}</span>
|
||||
</div>
|
||||
<div v-if="inquireFilters.dateFrom || inquireFilters.dateTo" class="flex items-center px-2 py-1 bg-[var(--bg-tertiary)] rounded text-[var(--text-secondary)]">
|
||||
<span><i class="fas fa-calendar mr-1"></i>${t('inquire.dateRangeActive')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End of p-4 space-y-4 div -->
|
||||
</div><!-- End of flex-1 overflow-y-auto div -->
|
||||
</div><!-- End of w-80 sidebar div -->
|
||||
|
||||
<!-- Right Panel: Chat -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden"
|
||||
:class="isMobileSidebarOpen ? 'hidden lg:flex' : 'flex'">
|
||||
<!-- Chat Messages Area -->
|
||||
<div ref="inquireMessagesRef" @scroll="handleScroll" class="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar">
|
||||
<!-- Welcome Message -->
|
||||
<div v-if="inquireChatMessages.length === 0" class="text-center py-16">
|
||||
<div class="mb-6 text-[var(--text-accent)]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold mb-4">${t('inquire.askQuestions')}</h2>
|
||||
<p class="text-[var(--text-muted)] max-w-lg mx-auto mb-6">
|
||||
${t('inquire.selectFilters')}
|
||||
</p>
|
||||
<div class="bg-[var(--bg-tertiary)] rounded-xl p-4 max-w-md mx-auto">
|
||||
<h4 class="font-medium mb-2">${t('inquire.exampleQuestions')}</h4>
|
||||
<ul class="text-sm text-[var(--text-secondary)] space-y-1 text-left">
|
||||
<li>${t('inquire.exampleQuestion1')}</li>
|
||||
<li>${t('inquire.exampleQuestion2')}</li>
|
||||
<li>${t('inquire.exampleQuestion3')}</li>
|
||||
<li>${t('inquire.exampleQuestion4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div v-for="(message, index) in inquireChatMessages" :key="index"
|
||||
:class="['flex', message.role === 'user' ? 'justify-end' : 'justify-start']">
|
||||
<div :class="['max-w-3xl px-4 py-4 rounded-xl chat-message',
|
||||
message.role === 'user'
|
||||
? 'bg-[var(--bg-accent)] text-[var(--text-accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-primary)]']">
|
||||
<div class="prose prose-sm max-w-none text-[var(--text-primary)]"
|
||||
:class="message.role === 'user' ? 'prose-invert' : ''"
|
||||
v-html="message.html || message.content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator with status -->
|
||||
<div v-if="isInquireChatLoading" class="flex justify-start">
|
||||
<div class="bg-[var(--bg-tertiary)] px-4 py-3 rounded-xl max-w-md">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="animate-spin h-5 w-5 border-2 border-[var(--text-accent)] border-t-transparent rounded-full flex-shrink-0"></div>
|
||||
<span class="text-sm text-[var(--text-muted)]" v-if="showProcessingStatus">${chatProcessingStatus}</span>
|
||||
<span class="text-sm text-[var(--text-muted)]" v-else>Analyzing transcriptions...</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress steps indicator -->
|
||||
<div v-if="showProcessingStatus" class="mt-2 flex space-x-1">
|
||||
<div class="h-1 w-4 bg-[var(--text-accent)] rounded" :class="{ 'animate-pulse': chatProcessingStatus.includes('Analyzing') }"></div>
|
||||
<div class="h-1 w-4 rounded" :class="chatProcessingStatus.includes('Enriching') || chatProcessingStatus.includes('Searching') || chatProcessingStatus.includes('Responding') ? 'bg-[var(--text-accent)]' : 'bg-[var(--border-secondary)]'"></div>
|
||||
<div class="h-1 w-4 rounded" :class="chatProcessingStatus.includes('Searching') || chatProcessingStatus.includes('Responding') ? 'bg-[var(--text-accent)]' : 'bg-[var(--border-secondary)]'"></div>
|
||||
<div class="h-1 w-4 rounded" :class="chatProcessingStatus.includes('Contextualizing') || chatProcessingStatus.includes('Responding') ? 'bg-[var(--text-accent)]' : 'bg-[var(--border-secondary)]'"></div>
|
||||
<div class="h-1 w-4 rounded" :class="chatProcessingStatus.includes('Responding') ? 'bg-[var(--text-accent)] animate-pulse' : 'bg-[var(--border-secondary)]'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Input Area -->
|
||||
<div class="flex-shrink-0 border-t border-[var(--border-primary)] p-6 bg-[var(--bg-secondary)]">
|
||||
<div class="flex gap-4">
|
||||
<textarea v-model="inquireChatInput"
|
||||
@keydown="handleInquireChatKeydown"
|
||||
:placeholder="t('inquire.placeholder')"
|
||||
class="flex-1 px-4 py-3 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-[var(--border-focus)] focus:border-transparent text-[var(--text-primary)]"
|
||||
rows="3"></textarea>
|
||||
<button @click="sendInquireChatMessage"
|
||||
:disabled="!inquireChatInput.trim() || isInquireChatLoading"
|
||||
class="px-6 py-3 bg-[var(--bg-button)] text-[var(--text-button)] rounded-xl hover:bg-[var(--bg-button-hover)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-w-[60px]">
|
||||
<i class="fas fa-paper-plane text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-[var(--text-muted)] mt-3 flex items-center justify-between">
|
||||
<span>${t('inquire.sendHint')}</span>
|
||||
<span v-if="hasActiveFilters" class="text-[var(--text-accent)]">
|
||||
<i class="fas fa-filter mr-1"></i>${t('inquire.filtersActive')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toastContainer" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
||||
|
||||
<!-- Global Error Banner -->
|
||||
<div v-if="globalError" class="fixed top-16 left-1/2 transform -translate-x-1/2 z-50 max-w-lg w-full mx-4">
|
||||
<div class="bg-red-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-exclamation-circle mr-3"></i>
|
||||
<span class="text-sm font-medium">${ globalError }</span>
|
||||
</div>
|
||||
<button @click="globalError = null" class="ml-4 text-white hover:text-gray-200">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Color Scheme Modal -->
|
||||
<div v-if="showColorSchemeModal" class="color-scheme-modal" @click.self="closeColorSchemeModal">
|
||||
<div class="color-scheme-modal-content">
|
||||
<div class="color-scheme-header">
|
||||
<h2 class="color-scheme-title">
|
||||
<i class="fas fa-palette"></i>
|
||||
<span v-text="t('colorScheme.title')"></span>
|
||||
</h2>
|
||||
<p class="color-scheme-subtitle" v-text="t('colorScheme.subtitle')"></p>
|
||||
</div>
|
||||
|
||||
<div class="color-scheme-body">
|
||||
<div class="color-scheme-section">
|
||||
<h3 class="color-scheme-section-title">
|
||||
<i :class="isDarkMode ? 'fas fa-moon' : 'fas fa-sun'"></i>
|
||||
<span v-text="isDarkMode ? t('colorScheme.darkThemes') : t('colorScheme.lightThemes')"></span>
|
||||
</h3>
|
||||
<div class="color-scheme-grid">
|
||||
<div v-for="scheme in colorSchemes[isDarkMode ? 'dark' : 'light']"
|
||||
:key="scheme.id"
|
||||
@click="selectColorScheme(scheme.id)"
|
||||
:class="['color-scheme-option', currentColorScheme === scheme.id ? 'active' : '']">
|
||||
<div class="color-scheme-preview">
|
||||
<div :class="`preview-${isDarkMode ? 'dark-' : ''}${scheme.id}-primary color-scheme-preview-segment`"></div>
|
||||
<div :class="`preview-${isDarkMode ? 'dark-' : ''}${scheme.id}-secondary color-scheme-preview-segment`"></div>
|
||||
<div :class="`preview-${isDarkMode ? 'dark-' : ''}${scheme.id}-tertiary color-scheme-preview-segment`"></div>
|
||||
</div>
|
||||
<div class="color-scheme-name" v-text="t('colorScheme.themes.' + (isDarkMode ? 'dark' : 'light') + '.' + scheme.id + '.name')"></div>
|
||||
<div class="color-scheme-description" v-text="t('colorScheme.themes.' + (isDarkMode ? 'dark' : 'light') + '.' + scheme.id + '.description')"></div>
|
||||
<div v-if="currentColorScheme === scheme.id" class="color-scheme-check">
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-scheme-footer">
|
||||
<button @click="resetColorScheme" class="color-scheme-reset-btn">
|
||||
<i class="fas fa-undo mr-2"></i><span v-text="t('colorScheme.resetToDefault')"></span>
|
||||
</button>
|
||||
<button @click="closeColorSchemeModal" class="color-scheme-close-btn" v-text="t('common.close')">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shares List Modal -->
|
||||
<div v-if="showSharesListModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
|
||||
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
|
||||
<div class="p-6 border-b border-[var(--border-primary)] flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold" v-text="t('modal.sharedTranscripts')"></h3>
|
||||
<button @click="closeSharesList" class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4 overflow-y-auto">
|
||||
<div v-if="isLoadingShares" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
|
||||
</div>
|
||||
<div v-else-if="userShares.length === 0" class="text-center text-[var(--text-muted)]">
|
||||
<span v-text="t('sharedTranscripts.noSharedTranscripts')"></span>
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<div v-for="share in userShares" :key="share.id" class="bg-[var(--bg-tertiary)] p-4 rounded-lg border border-[var(--border-primary)]">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="font-semibold">${share.recording_title}</p>
|
||||
<p class="text-sm text-[var(--text-muted)]">${ t('help.sharedOn') }: ${share.created_at}</p>
|
||||
</div>
|
||||
<button @click="deleteShare(share.id)" class="text-red-500 hover:text-red-700 p-1"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<input :value="'{{ request.url_root }}share/' + share.public_id" readonly class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg text-sm">
|
||||
<button @click="copyShareLink(share.public_id)" class="mt-2 px-3 py-1 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg text-sm hover:bg-[var(--bg-button-hover)]">
|
||||
<i class="fas fa-copy mr-2"></i>Copy Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSRF Token Management -->
|
||||
<script src="{{ url_for('static', filename='js/csrf-refresh.js') }}"></script>
|
||||
|
||||
<!-- Shared Components -->
|
||||
<script src="{{ url_for('static', filename='js/shared-components.js') }}"></script>
|
||||
|
||||
<!-- Inquire Mode Vue App -->
|
||||
<script>
|
||||
const { createApp, ref, reactive, computed, onMounted, onUnmounted, nextTick } = Vue;
|
||||
|
||||
// Initialize i18n
|
||||
let t = (key) => key; // Default fallback
|
||||
|
||||
async function initializeI18n() {
|
||||
const userLanguage = "{{ user_language }}";
|
||||
|
||||
// Initialize the global i18n instance
|
||||
if (window.i18n) {
|
||||
await window.i18n.init(userLanguage);
|
||||
|
||||
// Create a shorthand translation function
|
||||
t = (key, params) => window.i18n.t(key, params);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await initializeI18n();
|
||||
|
||||
const csrfToken = ref(document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'));
|
||||
const i18nInstance = window.i18n;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
// --- State ---
|
||||
const inquireFilters = reactive({
|
||||
selectedTags: [],
|
||||
selectedSpeakers: [],
|
||||
selectedRecordings: [],
|
||||
dateFrom: null,
|
||||
dateTo: null
|
||||
});
|
||||
|
||||
// Search queries for filters
|
||||
const tagSearchQuery = ref('');
|
||||
const speakerSearchQuery = ref('');
|
||||
|
||||
// Use shared components
|
||||
const { isDarkMode, toggleDarkMode, initializeDarkMode } = window.SharedComponents.useDarkMode();
|
||||
const {
|
||||
showColorSchemeModal,
|
||||
currentColorScheme,
|
||||
colorSchemes,
|
||||
openColorSchemeModal,
|
||||
closeColorSchemeModal,
|
||||
selectColorScheme,
|
||||
resetColorScheme,
|
||||
applyColorScheme,
|
||||
initializeColorScheme
|
||||
} = window.SharedComponents.useColorScheme();
|
||||
const {
|
||||
showSharesListModal,
|
||||
userShares,
|
||||
isLoadingShares,
|
||||
openSharesList,
|
||||
closeSharesList,
|
||||
copyShareLink,
|
||||
deleteShare
|
||||
} = window.SharedComponents.useSharesModal();
|
||||
const { isUserMenuOpen, toggleUserMenu, closeUserMenu } = window.SharedComponents.useUserMenu();
|
||||
const isMobileSidebarOpen = ref(false);
|
||||
|
||||
const availableFilters = reactive({
|
||||
tags: [],
|
||||
speakers: [],
|
||||
recordings: []
|
||||
});
|
||||
|
||||
const inquireChatMessages = ref([]);
|
||||
const inquireChatInput = ref('');
|
||||
const isInquireChatLoading = ref(false);
|
||||
const chatProcessingStatus = ref('');
|
||||
const showProcessingStatus = ref(false);
|
||||
const inquireMessagesRef = ref(null);
|
||||
const globalError = ref(null);
|
||||
const shouldAutoScroll = ref(true); // Track if we should auto-scroll
|
||||
const tokenBudget = ref({
|
||||
has_budget: false,
|
||||
budget: null,
|
||||
usage: 0,
|
||||
percentage: 0
|
||||
});
|
||||
|
||||
// --- Computed ---
|
||||
const hasActiveFilters = computed(() => {
|
||||
return inquireFilters.selectedTags.length > 0 ||
|
||||
inquireFilters.selectedSpeakers.length > 0 ||
|
||||
inquireFilters.selectedRecordings.length > 0 ||
|
||||
inquireFilters.dateFrom ||
|
||||
inquireFilters.dateTo;
|
||||
});
|
||||
|
||||
const filteredTags = computed(() => {
|
||||
if (!tagSearchQuery.value) return availableFilters.tags;
|
||||
return availableFilters.tags.filter(tag =>
|
||||
tag.name.toLowerCase().includes(tagSearchQuery.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const filteredSpeakers = computed(() => {
|
||||
if (!speakerSearchQuery.value) return availableFilters.speakers;
|
||||
return availableFilters.speakers.filter(speaker =>
|
||||
speaker.toLowerCase().includes(speakerSearchQuery.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
// --- Functions ---
|
||||
const setGlobalError = (message, duration = 7000) => {
|
||||
globalError.value = message;
|
||||
if (duration > 0) {
|
||||
setTimeout(() => { if (globalError.value === message) globalError.value = null; }, duration);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTokenBudget = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/token-budget');
|
||||
if (response.ok) {
|
||||
tokenBudget.value = await response.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading token budget:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadAvailableFilters = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/inquire/available_filters');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
availableFilters.tags = data.tags;
|
||||
availableFilters.speakers = data.speakers;
|
||||
availableFilters.recordings = data.recordings;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading available filters:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendInquireChatMessage = async () => {
|
||||
if (!inquireChatInput.value.trim() || isInquireChatLoading.value) return;
|
||||
|
||||
const userMessage = inquireChatInput.value.trim();
|
||||
inquireChatMessages.value.push({ role: 'user', content: userMessage, html: userMessage });
|
||||
inquireChatInput.value = '';
|
||||
isInquireChatLoading.value = true;
|
||||
showProcessingStatus.value = true;
|
||||
chatProcessingStatus.value = 'Initializing...';
|
||||
|
||||
// Always scroll to bottom when user sends a message
|
||||
shouldAutoScroll.value = true;
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/inquire/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken.value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: userMessage,
|
||||
message_history: inquireChatMessages.value.slice(0, -1),
|
||||
filter_tags: inquireFilters.selectedTags,
|
||||
filter_speakers: inquireFilters.selectedSpeakers,
|
||||
filter_recording_ids: inquireFilters.selectedRecordings,
|
||||
filter_date_from: inquireFilters.dateFrom,
|
||||
filter_date_to: inquireFilters.dateTo,
|
||||
context_chunks: 8
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Chat request failed');
|
||||
}
|
||||
|
||||
// Handle streaming response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let assistantMessage = reactive({ role: 'assistant', content: '', html: '' });
|
||||
let messageAdded = false;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
if (data.status && data.message) {
|
||||
// Update processing status
|
||||
chatProcessingStatus.value = data.message;
|
||||
} else if (data.delta) {
|
||||
// Hide status and start showing response
|
||||
if (!messageAdded) {
|
||||
inquireChatMessages.value.push(assistantMessage);
|
||||
messageAdded = true;
|
||||
showProcessingStatus.value = false;
|
||||
}
|
||||
assistantMessage.content += data.delta;
|
||||
assistantMessage.html = marked.parse(assistantMessage.content);
|
||||
// Smart scroll during streaming
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
} else if (data.end_of_stream) {
|
||||
return;
|
||||
} else if (data.error) {
|
||||
if (data.budget_exceeded) {
|
||||
throw new Error(t('adminDashboard.tokenBudgetExceeded'));
|
||||
}
|
||||
throw new Error(data.error);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse SSE data:', line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in inquire chat:', error);
|
||||
setGlobalError(error.message || 'Chat failed. Please try again.', 10000);
|
||||
} finally {
|
||||
isInquireChatLoading.value = false;
|
||||
showProcessingStatus.value = false;
|
||||
loadTokenBudget(); // Refresh token usage after chat
|
||||
await nextTick();
|
||||
scrollToBottom(); // Use smart scrolling
|
||||
}
|
||||
};
|
||||
|
||||
const handleInquireChatKeydown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.ctrlKey || event.shiftKey) {
|
||||
return;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
sendInquireChatMessage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearInquireFilters = () => {
|
||||
inquireFilters.selectedTags = [];
|
||||
inquireFilters.selectedSpeakers = [];
|
||||
inquireFilters.selectedRecordings = [];
|
||||
inquireFilters.dateFrom = null;
|
||||
inquireFilters.dateTo = null;
|
||||
inquireChatMessages.value = [];
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// Smart scrolling functions
|
||||
const isNearBottom = () => {
|
||||
if (!inquireMessagesRef.value) return true;
|
||||
const container = inquireMessagesRef.value;
|
||||
const threshold = 100; // pixels from bottom
|
||||
return container.scrollTop + container.clientHeight >= container.scrollHeight - threshold;
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (inquireMessagesRef.value && shouldAutoScroll.value) {
|
||||
inquireMessagesRef.value.scrollTop = inquireMessagesRef.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (inquireMessagesRef.value) {
|
||||
shouldAutoScroll.value = isNearBottom();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- Theme Management (from main app.js) ---
|
||||
const applyThemeFromStorage = () => {
|
||||
const savedDarkMode = localStorage.getItem('darkMode');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = savedDarkMode !== null ? savedDarkMode === 'true' : prefersDark;
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
|
||||
const savedScheme = localStorage.getItem('colorScheme') || 'blue';
|
||||
const currentMode = isDark ? 'dark' : 'light';
|
||||
const themeClass = savedScheme === 'blue' ? '' : `theme-${currentMode}-${savedScheme}`;
|
||||
|
||||
// Remove all existing theme classes
|
||||
document.documentElement.className = document.documentElement.className
|
||||
.replace(/theme-(?:light|dark)-\w+/g, '');
|
||||
|
||||
if (themeClass) {
|
||||
document.documentElement.classList.add(themeClass);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up global click handler to close dropdowns when clicking outside
|
||||
* Provides elegant UX by closing menus when users click elsewhere
|
||||
*/
|
||||
const setupGlobalClickHandler = () => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
|
||||
// Close user menu if clicking outside of it
|
||||
if (isUserMenuOpen.value) {
|
||||
const userMenuButton = target.closest('[data-user-menu-toggle]');
|
||||
const userMenuDropdown = target.closest('[data-user-menu-dropdown]');
|
||||
|
||||
if (!userMenuButton && !userMenuDropdown) {
|
||||
isUserMenuOpen.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// --- Lifecycle ---
|
||||
onMounted(async () => {
|
||||
// Initialize dark mode and color scheme
|
||||
initializeDarkMode();
|
||||
initializeColorScheme(isDarkMode.value);
|
||||
setupGlobalClickHandler();
|
||||
|
||||
// Watch dark mode changes to update color scheme
|
||||
Vue.watch(isDarkMode, (newValue) => {
|
||||
initializeColorScheme(newValue);
|
||||
});
|
||||
|
||||
// Apply theme from localStorage on page load
|
||||
applyThemeFromStorage();
|
||||
|
||||
loadAvailableFilters();
|
||||
loadTokenBudget();
|
||||
|
||||
// Add scroll listener for smart auto-scroll after DOM is ready
|
||||
await nextTick();
|
||||
if (inquireMessagesRef.value) {
|
||||
inquireMessagesRef.value.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (inquireMessagesRef.value) {
|
||||
inquireMessagesRef.value.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// i18n - bind to i18n instance to maintain context
|
||||
t: (key, params) => i18nInstance ? i18nInstance.t(key, params) : key,
|
||||
|
||||
// State
|
||||
inquireFilters,
|
||||
availableFilters,
|
||||
inquireChatMessages,
|
||||
inquireChatInput,
|
||||
isInquireChatLoading,
|
||||
inquireMessagesRef,
|
||||
globalError,
|
||||
tokenBudget,
|
||||
isUserMenuOpen,
|
||||
isMobileSidebarOpen,
|
||||
chatProcessingStatus,
|
||||
showProcessingStatus,
|
||||
tagSearchQuery,
|
||||
speakerSearchQuery,
|
||||
|
||||
// Dark Mode
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
|
||||
// Color Scheme
|
||||
showColorSchemeModal,
|
||||
currentColorScheme,
|
||||
colorSchemes,
|
||||
openColorSchemeModal,
|
||||
closeColorSchemeModal,
|
||||
selectColorScheme,
|
||||
resetColorScheme,
|
||||
|
||||
// Shares Modal
|
||||
showSharesListModal,
|
||||
userShares,
|
||||
isLoadingShares,
|
||||
openSharesList,
|
||||
closeSharesList,
|
||||
copyShareLink,
|
||||
deleteShare,
|
||||
|
||||
// Computed
|
||||
hasActiveFilters,
|
||||
filteredTags,
|
||||
filteredSpeakers,
|
||||
|
||||
// Functions
|
||||
sendInquireChatMessage,
|
||||
handleInquireChatKeydown,
|
||||
clearInquireFilters,
|
||||
loadAvailableFilters,
|
||||
setGlobalError,
|
||||
formatDate,
|
||||
handleScroll
|
||||
};
|
||||
},
|
||||
delimiters: ['${', '}']
|
||||
}).mount('#inquire-app');
|
||||
|
||||
// Hide loading overlay after app mounts
|
||||
Vue.nextTick(() => {
|
||||
if (window.AppLoader) {
|
||||
AppLoader.waitForReady();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user