const { createApp, reactive, computed, onMounted, onUnmounted, nextTick } = Vue;
import { BaseComponent } from './components/BaseComponent.js';
import { LoadingSpinner } from './components/LoadingSpinner.js';
import { VirtualScroll } from './components/VirtualScroll.js';
import { SearchFilters } from './components/SearchFilters.js';
import { BreadcrumbNavigation } from './components/BreadcrumbNavigation.js';
// window.apiServiceInstance will be accessed via window.window.apiServiceInstanceInstance
const API_BASE_URL = window.location.origin;
const API_KEY = 'claude_api_secret_2024_change_me';
const store = reactive({
conversations: [],
selectedProject: null,
selectedSession: null,
searchResults: [],
activeView: 'dashboard',
isLoading: false,
loadingMessage: '',
error: null,
searchQuery: '',
searchFilters: {
project: '',
messageType: '',
startDate: '',
endDate: '',
onlyMarked: false,
tags: []
},
liveStats: {
total_messages: 0,
total_sessions: 0,
active_projects: 0,
recent_activity: {
last_messages: [],
messages_last_hour: 0,
active_sessions: 0
}
},
connectionStatus: 'disconnected',
virtualScrollEnabled: true,
pageSize: 50,
isDarkMode: localStorage.getItem('claude-dashboard-theme') === 'dark' ||
(!localStorage.getItem('claude-dashboard-theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
});
class OptimizedApiService {
constructor() {
this.cache = new Map();
this.cacheTTL = 5 * 60 * 1000;
this.requestQueue = new Map();
this.retryAttempts = 3;
this.retryDelay = 1000;
}
async request(endpoint, options = {}) {
const cacheKey = `${endpoint}_${JSON.stringify(options)}`;
if (!options.method || options.method === 'GET') {
const cached = this.getFromCache(cacheKey);
if (cached) {
return cached;
}
}
if (this.requestQueue.has(cacheKey)) {
return this.requestQueue.get(cacheKey);
}
const requestPromise = this.executeRequest(endpoint, options);
this.requestQueue.set(cacheKey, requestPromise);
try {
const result = await requestPromise;
if (!options.method || options.method === 'GET') {
this.setCache(cacheKey, result);
}
return result;
} catch (error) {
throw error;
} finally {
this.requestQueue.delete(cacheKey);
}
}
async executeRequest(endpoint, options = {}) {
const url = `${API_BASE_URL}/api/${endpoint}`;
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
try {
const response = await fetch(url, {
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error(`❌ Request failed (attempt ${attempt}/${this.retryAttempts}):`, error);
if (attempt === this.retryAttempts) {
throw error;
}
await new Promise(resolve =>
setTimeout(resolve, this.retryDelay * Math.pow(2, attempt - 1))
);
}
}
}
getFromCache(key) {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() > cached.expiry) {
this.cache.delete(key);
return null;
}
return cached.data;
}
setCache(key, data) {
this.cache.set(key, {
data,
expiry: Date.now() + this.cacheTTL
});
}
clearCache() {
this.cache.clear();
}
async getConversationTree(filters = {}) {
const params = new URLSearchParams(filters).toString();
const endpoint = `conversations/tree${params ? `?${params}` : ''}`;
return this.request(endpoint);
}
async getConversationDetails(sessionId) {
return this.request(`conversations/${sessionId}`);
}
async searchConversations(query, filters = {}) {
const params = new URLSearchParams();
if (query) params.append('q', query);
Object.entries(filters).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
if (Array.isArray(value)) {
value.forEach(v => params.append(key, v));
} else {
params.append(key, value.toString());
}
}
});
return this.request(`search/advanced?${params.toString()}`);
}
async markConversation(sessionId, isMarked, note = '', tags = []) {
return this.request(`conversations/${sessionId}/mark`, {
method: 'POST',
body: JSON.stringify({
is_marked: isMarked,
note,
tags
})
});
}
async exportConversation(sessionId, format = 'json') {
const params = new URLSearchParams({ format }).toString();
const response = await fetch(`${API_BASE_URL}/api/conversations/${sessionId}/export?${params}`, {
headers: { 'X-API-Key': API_KEY }
});
if (!response.ok) {
throw new Error(`Export failed: ${response.status} ${response.statusText}`);
}
if (format === 'json') {
return response.json();
} else {
return {
content: await response.text(),
filename: `conversation_${sessionId.substring(0, 8)}.${format}`,
mime_type: format === 'markdown' ? 'text/markdown' : 'text/plain'
};
}
}
async getSystemStats() {
return this.request('stats');
}
}
const window.apiServiceInstance = new OptimizedApiService();
const OptimizedDashboard = {
mixins: [BaseComponent],
data() {
return {
store,
breadcrumbItems: [
{ label: 'Dashboard', action: () => this.setActiveView('dashboard') }
],
searchFiltersExpanded: false,
refreshInterval: null,
keyboardShortcuts: new Map(),
lastUpdate: null,
performanceMetrics: {
renderTime: 0,
apiCallCount: 0,
cacheHits: 0
},
_computedCache: {
filteredProjects: {
data: null,
lastConversationsLength: 0,
lastFiltersHash: null
}
}
};
},
computed: {
filteredProjects() {
if (!this.store.conversations || !this.store.conversations.length) return [];
const conversationsLength = this.store.conversations.length;
const filtersHash = JSON.stringify(this.store.searchFilters);
const cache = this._computedCache.filteredProjects;
if (cache.data &&
cache.lastConversationsLength === conversationsLength &&
cache.lastFiltersHash === filtersHash) {
this.performanceMetrics.cacheHits++;
return cache.data;
}
const filtered = this.store.conversations
.filter(project => {
if (!this.store.searchFilters.project) return true;
return project.name.toLowerCase().includes(
this.store.searchFilters.project.toLowerCase()
);
})
.sort((a, b) => new Date(b.last_activity) - new Date(a.last_activity));
cache.data = filtered;
cache.lastConversationsLength = conversationsLength;
cache.lastFiltersHash = filtersHash;
return filtered;
},
activeProjects() {
return this.filteredProjects.filter(project =>
project.sessions && project.sessions.some(session => session.is_active)
);
},
recentSessions() {
const sessions = [];
if (this.store.conversations) {
this.store.conversations.forEach(project => {
if (project.sessions) {
project.sessions.forEach(session => {
sessions.push({
...session,
project_name: project.name
});
});
}
});
}
return sessions
.sort((a, b) => new Date(b.last_activity) - new Date(a.last_activity))
.slice(0, 10);
},
dashboardStats() {
const conversations = this.store.conversations || [];
const totalSessions = conversations.reduce((sum, p) => sum + ((p.sessions && p.sessions.length) || 0), 0);
const totalMessages = conversations.reduce((sum, p) => sum + (p.message_count || 0), 0);
const activeSessions = conversations.reduce((sum, p) =>
sum + ((p.sessions && p.sessions.filter(s => s.is_active) && p.sessions.filter(s => s.is_active).length) || 0), 0
);
const totalTokens = this.calculateTotalTokens();
const estimatedCost = this.calculateEstimatedCost();
const avgMessagesPerSession = totalSessions > 0 ? Math.round(totalMessages / totalSessions) : 0;
return {
totalProjects: conversations.length,
totalSessions,
totalMessages,
activeSessions,
totalTokens,
estimatedCost,
avgMessagesPerSession,
lastUpdate: Date.now()
};
},
calculateTotalTokens() {
const conversations = this.store.conversations || [];
let totalTokens = 0;
conversations.forEach(project => {
if (project.sessions) {
project.sessions.forEach(session => {
if (session.recent_messages) {
session.recent_messages.forEach(message => {
if (message.metadata && message.metadata.usage) {
const usage = message.metadata.usage;
totalTokens += (usage.input_tokens || 0) + (usage.output_tokens || 0);
}
});
}
});
}
});
return totalTokens;
},
calculateEstimatedCost() {
const conversations = this.store.conversations || [];
let totalCost = 0;
conversations.forEach(project => {
if (project.sessions) {
project.sessions.forEach(session => {
if (session.recent_messages) {
session.recent_messages.forEach(message => {
if (message.metadata && message.metadata.cost_usd) {
totalCost += parseFloat(message.metadata.cost_usd) || 0;
}
});
}
});
}
});
return totalCost;
}
},
async mounted() {
this.applyTheme();
this.initializeKeyboardShortcuts();
this.initializeGrpcConnection();
await this.loadInitialData();
this.setupAutoRefresh();
this.startPerformanceMonitoring();
},
beforeUnmount() {
window.window.apiServiceInstanceInstance?.disconnect();
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.removeKeyboardShortcuts();
window.apiServiceInstance.clearCache();
},
methods: {
mergeConversationData(newProjects) {
if (!newProjects || !Array.isArray(newProjects)) {
return;
}
if (!this.store.conversations || this.store.conversations.length === 0) {
this.store.conversations = newProjects;
nextTick(() => {
document.querySelectorAll('.project-card').forEach(el => {
el.classList.add('gpu-accelerated');
});
});
return;
}
const existingProjects = new Map();
this.store.conversations.forEach((project, index) => {
existingProjects.set(project.name, { project, index });
});
const updatedProjects = [];
newProjects.forEach(newProject => {
const existing = existingProjects.get(newProject.name);
if (existing) {
const mergedProject = this.mergeProject(existing.project, newProject);
updatedProjects.push(mergedProject);
existingProjects.delete(newProject.name);
} else {
updatedProjects.push(newProject);
}
});
updatedProjects.sort((a, b) => new Date(b.last_activity) - new Date(a.last_activity));
this.store.conversations.splice(0, this.store.conversations.length, ...updatedProjects);
},
mergeProject(existing, newData) {
if (existing.message_count === newData.message_count &&
existing.last_activity === newData.last_activity &&
(existing.sessions && existing.sessions.length) === (newData.sessions && newData.sessions.length)) {
return existing;
}
const mergedSessions = this.mergeSessions(existing.sessions || [], newData.sessions || []);
return {
...existing,
message_count: newData.message_count,
last_activity: newData.last_activity,
sessions: mergedSessions
};
},
mergeSessions(existingSessions, newSessions) {
const sessionMap = new Map();
existingSessions.forEach(session => {
sessionMap.set(session.session_id, session);
});
const merged = [];
newSessions.forEach(newSession => {
const existing = sessionMap.get(newSession.session_id);
if (existing) {
merged.push({
...existing,
message_count: newSession.message_count,
last_activity: newSession.last_activity,
is_active: newSession.is_active,
recent_messages: newSession.recent_messages || existing.recent_messages
});
} else {
merged.push(newSession);
}
});
return merged.sort((a, b) => new Date(b.last_activity) - new Date(a.last_activity));
},
async loadInitialData() {
await this.handleAsyncOperation(async () => {
const startTime = performance.now();
const [conversationData, statsData] = await Promise.all([
window.apiServiceInstance.getConversationTree({ limit: 50, hours_back: 48 }),
window.apiServiceInstance.getSystemStats()
]);
this.mergeConversationData(conversationData.projects);
this.store.liveStats = {
total_messages: statsData.total_messages || 0,
total_sessions: conversationData.total_sessions || 0,
active_projects: statsData.projects || 0,
recent_activity: statsData.recent_activity || {}
};
this.performanceMetrics.renderTime = performance.now() - startTime;
this.lastUpdate = new Date();
.toFixed(2)}ms`);
}, {
loadingMessage: 'Loading dashboard...',
showLoading: true
});
},
initializeGrpcConnection() {
window.apiServiceInstance.on('connection', (data) => {
this.store.connectionStatus = data.status;
if (data.status === 'connected') {
} else if (data.status === 'error') {
console.error('❌ gRPC connection error');
}
});
window.apiServiceInstance.on('new_message', (message) => {
this.handleNewMessage(message);
});
window.apiServiceInstance.on('live_stats', (stats) => {
this.updateLiveStats(stats);
});
window.apiServiceInstance.on('session_start', (message) => {
this.handleSessionStart(message);
});
window.apiServiceInstance.on('session_end', (message) => {
this.handleSessionEnd(message);
});
this.store.connectionStatus = window.apiServiceInstance.getConnectionStatus();
},
handleNewMessage(message) {
if (message && message.session_id) {
let updated = false;
for (let i = 0; i < this.store.conversations.length; i++) {
const project = this.store.conversations[i];
if (project.sessions) {
for (let j = 0; j < project.sessions.length; j++) {
const session = project.sessions[j];
if (session.session_id === message.session_id) {
session.message_count = (session.message_count || 0) + 1;
session.last_activity = Date.now();
session.is_active = true;
if (!session.recent_messages) {
session.recent_messages = [];
}
session.recent_messages.push(message);
session.recent_messages = session.recent_messages.slice(-3);
project.last_activity = Date.now();
project.message_count = (project.message_count || 0) + 1;
updated = true;
break;
}
}
}
if (updated) break;
}
this.store.liveStats.total_messages++;
if (this.store.liveStats.recent_activity) {
this.store.liveStats.recent_activity.messages_last_hour++;
}
}
},
handleSessionStart(message) {
if (message && message.session_id && message.project_name) {
let project = this.store.conversations.find(p => p.name === message.project_name);
if (!project) {
project = {
name: message.project_name,
message_count: 0,
sessions: [],
last_activity: Date.now()
};
this.store.conversations.push(project);
}
const newSession = {
session_id: message.session_id,
short_id: message.session_id.substring(0, 8),
message_count: 1,
start_time: Date.now(),
last_activity: Date.now(),
is_active: true,
is_marked: false,
recent_messages: [message]
};
project.sessions.unshift(newSession);
project.last_activity = Date.now();
this.store.liveStats.total_sessions++;
if (this.store.liveStats.recent_activity) {
this.store.liveStats.recent_activity.active_sessions++;
}
}
},
handleSessionEnd(message) {
if (message && message.session_id) {
this.store.conversations.forEach(project => {
if (project.sessions) {
const session = project.sessions.find(s => s.session_id === message.session_id);
if (session) {
session.is_active = false;
}
}
});
if (this.store.liveStats.recent_activity) {
this.store.liveStats.recent_activity.active_sessions = Math.max(0,
this.store.liveStats.recent_activity.active_sessions - 1);
}
}
},
updateLiveStats(stats) {
const oldStats = { ...this.store.liveStats };
this.store.liveStats = {
...this.store.liveStats,
...stats
};
this.triggerStatAnimations(oldStats, this.store.liveStats);
},
triggerStatAnimations(oldStats, newStats) {
nextTick(() => {
const statsToCheck = ['total_messages', 'total_sessions', 'active_sessions'];
statsToCheck.forEach(stat => {
if (oldStats[stat] !== newStats[stat]) {
const statCards = document.querySelectorAll('.stat-number');
statCards.forEach(card => {
card.classList.add('stat-update');
setTimeout(() => card.classList.remove('stat-update'), 600);
});
}
});
});
},
async refreshData() {
await this.loadInitialData();
},
setupAutoRefresh() {
this.refreshInterval = setInterval(() => {
if (document.visibilityState === 'visible') {
this.refreshData();
}
}, 30000);
},
applyTheme() {
const htmlElement = document.documentElement;
if (this.store.isDarkMode) {
htmlElement.setAttribute('data-theme', 'dark');
htmlElement.classList.add('dark');
} else {
htmlElement.setAttribute('data-theme', 'light');
htmlElement.classList.remove('dark');
}
localStorage.setItem('claude-dashboard-theme', this.store.isDarkMode ? 'dark' : 'light');
},
toggleTheme() {
this.store.isDarkMode = !this.store.isDarkMode;
this.applyTheme();
},
initializeKeyboardShortcuts() {
this.keyboardShortcuts.set('r', () => this.refreshData());
this.keyboardShortcuts.set('s', () => this.focusSearch());
this.keyboardShortcuts.set('d', () => this.setActiveView('dashboard'));
this.keyboardShortcuts.set('p', () => this.setActiveView('projects'));
this.keyboardShortcuts.set('t', () => this.toggleTheme());
this.keyboardShortcuts.set('/', () => this.focusSearch());
document.addEventListener('keydown', this.handleKeyboardShortcut);
},
removeKeyboardShortcuts() {
document.removeEventListener('keydown', this.handleKeyboardShortcut);
},
handleKeyboardShortcut(event) {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return;
}
const key = event.key.toLowerCase();
const shortcut = this.keyboardShortcuts.get(key);
if (shortcut) {
event.preventDefault();
shortcut();
}
},
focusSearch() {
const searchInput = document.querySelector('input[type="text"]');
if (searchInput) {
searchInput.focus();
}
},
setActiveView(view) {
this.store.activeView = view;
this.updateBreadcrumbs(view);
},
updateBreadcrumbs(view) {
const breadcrumbs = [
{ label: 'Dashboard', action: () => this.setActiveView('dashboard') }
];
if (view === 'projects') {
breadcrumbs.push({ label: 'Projects' });
} else if (view === 'sessions') {
breadcrumbs.push({ label: 'Sessions' });
} else if (view === 'search') {
breadcrumbs.push({ label: 'Search Results' });
}
this.breadcrumbItems = breadcrumbs;
},
async handleSearch(filters) {
await this.handleAsyncOperation(async () => {
const results = await window.apiServiceInstance.searchConversations(filters.query, filters);
this.store.searchResults = results.results || [];
this.setActiveView('search');
}, {
loadingMessage: 'Searching...',
showLoading: true
});
},
async selectProject(project) {
this.store.selectedProject = project;
this.setActiveView('sessions');
this.updateBreadcrumbs('sessions');
},
async selectSession(session) {
await this.handleAsyncOperation(async () => {
const details = await window.apiServiceInstance.getConversationDetails(session.session_id);
this.store.selectedSession = details;
this.setActiveView('details');
}, {
loadingMessage: 'Loading session...',
showLoading: true
});
},
startPerformanceMonitoring() {
const originalRequest = window.apiServiceInstance.request;
window.apiServiceInstance.request = async (...args) => {
this.performanceMetrics.apiCallCount++;
return originalRequest.apply(window.apiServiceInstance, args);
};
},
formatMetric(value, type = 'number') {
if (type === 'number') {
return this.formatNumber(value, { compact: true });
} else if (type === 'time') {
return this.formatTimestamp(value);
}
return value;
},
handleToast(event) {
`);
}
},
components: {
LoadingSpinner,
VirtualScroll,
SearchFilters,
BreadcrumbNavigation
},
template: `
<div id="optimized-dashboard" :class="{ 'dark': store.isDarkMode }">
<!-- Header -->
<header class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<!-- Title and Status -->
<div class="flex items-center space-x-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
Claude Conversation Logger
</h1>
<span class="text-sm text-gray-500 dark:text-gray-400">
v2.1.3 Optimized
</span>
<div :class="['flex items-center space-x-1', store.connectionStatus === 'connected' ? 'text-green-500' : 'text-red-500']">
<i class="fas fa-circle text-xs"></i>
<span class="text-xs font-medium">{{ store.connectionStatus }}</span>
</div>
</div>
<!-- Actions -->
<div class="flex items-center space-x-3">
<!-- Last Update -->
<span v-if="lastUpdate" class="text-xs text-gray-500 dark:text-gray-400">
Updated {{ formatTimestamp(lastUpdate) }}
</span>
<!-- Refresh Button -->
<button @click="refreshData"
:disabled="store.isLoading"
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600
text-sm font-medium rounded-md text-gray-700 dark:text-gray-300
bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
disabled:opacity-50 transition-colors">
<i :class="['fas fa-sync-alt mr-2', store.isLoading ? 'animate-spin' : '']"></i>
Refresh
</button>
<!-- Theme Toggle -->
<button @click="toggleTheme"
class="inline-flex items-center p-2 border border-gray-300 dark:border-gray-600
text-sm font-medium rounded-md text-gray-700 dark:text-gray-300
bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
title="Toggle theme (T)">
<i :class="['fas', store.isDarkMode ? 'fa-sun' : 'fa-moon']"></i>
</button>
</div>
</div>
</div>
</header>
<!-- Breadcrumb Navigation -->
<div class="bg-gray-50 dark:bg-gray-750 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
<BreadcrumbNavigation
:items="breadcrumbItems"
@navigate="handleBreadcrumbNavigation" />
</div>
</div>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Loading Overlay -->
<LoadingSpinner v-if="store.isLoading"
:message="store.loadingMessage"
overlay
type="ring"
size="large" />
<!-- Error State -->
<div v-if="store.error"
class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-800 rounded-md p-4 mb-6">
<div class="flex">
<i class="fas fa-exclamation-triangle text-red-400 mr-3 mt-1"></i>
<div>
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Error</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-300">{{ store.error }}</p>
<button @click="refreshData"
class="mt-2 text-sm text-red-600 dark:text-red-400 hover:text-red-500 underline">
Try Again
</button>
</div>
</div>
</div>
<!-- Dashboard View -->
<div v-if="store.activeView === 'dashboard'" class="space-y-8">
<!-- Stats Cards with Overflow Control -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-4 lg:gap-6">
<!-- Projects Card -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 lg:p-6 shadow-sm
hover:shadow-md transition-all duration-200 overflow-hidden max-w-full stat-card">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 mr-4">
<p class="text-xs lg:text-sm font-medium text-gray-600 dark:text-gray-400 truncate">Projects</p>
<p class="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 stat-number">
{{ formatMetric(dashboardStats.totalProjects) }}
</p>
</div>
<div class="p-2 lg:p-3 bg-blue-100 dark:bg-blue-900 rounded-lg flex-shrink-0">
<i class="fas fa-folder text-blue-600 dark:text-blue-400 text-lg lg:text-xl"></i>
</div>
</div>
</div>
<!-- Sessions Card -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 lg:p-6 shadow-sm
hover:shadow-md transition-all duration-200 overflow-hidden max-w-full stat-card">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 mr-4">
<p class="text-xs lg:text-sm font-medium text-gray-600 dark:text-gray-400 truncate">Sessions</p>
<p class="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 stat-number">
{{ formatMetric(dashboardStats.totalSessions) }}
</p>
</div>
<div class="p-2 lg:p-3 bg-green-100 dark:bg-green-900 rounded-lg flex-shrink-0">
<i class="fas fa-comments text-green-600 dark:text-green-400 text-lg lg:text-xl"></i>
</div>
</div>
</div>
<!-- Messages Card -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 lg:p-6 shadow-sm
hover:shadow-md transition-all duration-200 overflow-hidden max-w-full stat-card">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 mr-4">
<p class="text-xs lg:text-sm font-medium text-gray-600 dark:text-gray-400 truncate">Messages</p>
<p class="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 stat-number">
{{ formatMetric(dashboardStats.totalMessages) }}
</p>
</div>
<div class="p-2 lg:p-3 bg-purple-100 dark:bg-purple-900 rounded-lg flex-shrink-0">
<i class="fas fa-envelope text-purple-600 dark:text-purple-400 text-lg lg:text-xl"></i>
</div>
</div>
</div>
<!-- Active Sessions Card -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 lg:p-6 shadow-sm
hover:shadow-md transition-all duration-200 overflow-hidden max-w-full stat-card">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 mr-4">
<p class="text-xs lg:text-sm font-medium text-gray-600 dark:text-gray-400 truncate">Active</p>
<p class="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 stat-number">
{{ formatMetric(dashboardStats.activeSessions) }}
</p>
</div>
<div class="p-2 lg:p-3 bg-orange-100 dark:bg-orange-900 rounded-lg flex-shrink-0">
<i class="fas fa-bolt text-orange-600 dark:text-orange-400 text-lg lg:text-xl"></i>
</div>
</div>
</div>
<!-- Tokens Card -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 lg:p-6 shadow-sm
hover:shadow-md transition-all duration-200 overflow-hidden max-w-full stat-card">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 mr-4">
<p class="text-xs lg:text-sm font-medium text-gray-600 dark:text-gray-400 truncate">Tokens</p>
<p class="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 stat-number">
{{ formatMetric(dashboardStats.totalTokens, 'number') }}
</p>
</div>
<div class="p-2 lg:p-3 bg-indigo-100 dark:bg-indigo-900 rounded-lg flex-shrink-0">
<i class="fas fa-calculator text-indigo-600 dark:text-indigo-400 text-lg lg:text-xl"></i>
</div>
</div>
</div>
<!-- Cost Card -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 lg:p-6 shadow-sm
hover:shadow-md transition-all duration-200 overflow-hidden max-w-full stat-card">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 mr-4">
<p class="text-xs lg:text-sm font-medium text-gray-600 dark:text-gray-400 truncate">Est. Cost</p>
<p class="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 stat-number">
${{ (dashboardStats.estimatedCost || 0).toFixed(2) }}
</p>
</div>
<div class="p-2 lg:p-3 bg-emerald-100 dark:bg-emerald-900 rounded-lg flex-shrink-0">
<i class="fas fa-dollar-sign text-emerald-600 dark:text-emerald-400 text-lg lg:text-xl"></i>
</div>
</div>
</div>
</div>
<!-- Search Filters -->
<SearchFilters
v-model="store.searchFilters"
:projects="store.conversations"
@search="handleSearch"
@clear="store.searchFilters = {}"
@toast="handleToast" />
<!-- Recent Projects -->
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Recent Projects</h3>
</div>
<VirtualScroll v-if="store.virtualScrollEnabled && filteredProjects.length > 10"
:items="filteredProjects"
:item-height="80"
:container-height="400"
@scroll="handleVirtualScroll">
<template #default="{ item }">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750 cursor-pointer transition-colors"
@click="selectProject(item)">
<div class="flex items-center justify-between">
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ (item.sessions && item.sessions.length) || 0 }} sessions • {{ item.message_count || 0 }} messages
</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ formatTimestamp(item.last_activity) }}
</p>
</div>
</div>
</div>
</template>
</VirtualScroll>
<div v-else class="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto custom-scrollbar">
<div v-for="project in filteredProjects.slice(0, 10)"
:key="project.name"
class="p-4 hover:bg-gray-50 dark:hover:bg-gray-750 cursor-pointer transition-all duration-200
project-item smooth-update"
@click="selectProject(project)">
<div class="flex items-center justify-between min-w-0">
<div class="min-w-0 flex-1 mr-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate"
:title="project.name">
{{ project.name }}
</h4>
<div class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400 mt-1">
<span class="flex items-center">
<i class="fas fa-comments text-xs mr-1"></i>
{{ (project.sessions && project.sessions.length) || 0 }} sessions
</span>
<span class="text-gray-300 dark:text-gray-600">•</span>
<span class="flex items-center">
<i class="fas fa-envelope text-xs mr-1"></i>
{{ formatMetric(project.message_count || 0) }} messages
</span>
</div>
<!-- Recent sessions preview -->
<div v-if="project.sessions && project.sessions.length > 0"
class="mt-2 flex flex-wrap gap-1">
<span v-for="session in project.sessions.slice(0, 3)"
:key="session.session_id"
:class="['inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
session.is_active ?
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300']">
<i v-if="session.is_active" class="fas fa-circle text-xs mr-1 animate-pulse"></i>
{{ session.short_id }}
</span>
<span v-if="project.sessions.length > 3"
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
+{{ project.sessions.length - 3 }} more
</span>
</div>
</div>
<div class="text-right flex-shrink-0">
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ formatTimestamp(project.last_activity) }}
</p>
<div class="mt-1">
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Other views (projects, sessions, search results) would go here -->
<!-- ... -->
</main>
<!-- Performance Debug Info (dev only) -->
<div v-if="false" class="fixed bottom-4 right-4 bg-black bg-opacity-75 text-white text-xs p-2 rounded">
<div>Render: {{ (performanceMetrics.renderTime || 0).toFixed(2) }}ms</div>
<div>API Calls: {{ performanceMetrics.apiCallCount }}</div>
</div>
</div>
`
};
window.optimizedApp = createApp(OptimizedDashboard);
window.optimizedApp.component('LoadingSpinner', LoadingSpinner);
window.optimizedApp.component('VirtualScroll', VirtualScroll);
window.optimizedApp.component('SearchFilters', SearchFilters);
window.optimizedApp.component('BreadcrumbNavigation', BreadcrumbNavigation);
window.optimizedApp.mount('#app');