mirror of
https://github.com/mudler/LocalAI.git
synced 2026-02-24 02:36:11 -05:00
644 lines
33 KiB
HTML
644 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "views/partials/head" .}}
|
|
|
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
|
<div class="app-layout">
|
|
{{template "views/partials/navbar" .}}
|
|
|
|
<main class="main-content">
|
|
<div class="main-content-inner" x-data="tracesApp()" x-init="init()">
|
|
|
|
<!-- Notifications -->
|
|
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
|
|
<template x-for="notification in notifications" :key="notification.id">
|
|
<div x-show="true"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
:class="notification.type === 'error' ? 'bg-red-500' : 'bg-green-500'"
|
|
class="rounded-lg p-4 text-white flex items-start space-x-3">
|
|
<div class="flex-shrink-0">
|
|
<i :class="notification.type === 'error' ? 'fas fa-exclamation-circle' : 'fas fa-check-circle'" class="text-xl"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium break-words" x-text="notification.message"></p>
|
|
</div>
|
|
<button @click="dismissNotification(notification.id)" class="flex-shrink-0 text-white hover:opacity-80 transition-opacity">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
|
|
|
<!-- Hero Header -->
|
|
<div class="hero-section">
|
|
<div class="hero-content">
|
|
<h1 class="hero-title">
|
|
Traces
|
|
</h1>
|
|
<p class="hero-subtitle">View logged API requests, responses, and backend operations</p>
|
|
<div class="flex flex-wrap justify-center gap-2" x-show="activeTab === 'api'">
|
|
<button type="button" @click="clearTraces()" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
|
<i class="fas fa-trash text-[10px]"></i>
|
|
<span>Clear Traces</span>
|
|
</button>
|
|
<a href="/api/traces" download="traces.json" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
|
<i class="fas fa-download text-[10px]"></i>
|
|
<span>Export Traces</span>
|
|
</a>
|
|
</div>
|
|
<div class="flex flex-wrap justify-center gap-2" x-show="activeTab === 'backend'">
|
|
<button type="button" @click="clearBackendTraces()" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
|
<i class="fas fa-trash text-[10px]"></i>
|
|
<span>Clear Backend Traces</span>
|
|
</button>
|
|
<a href="/api/backend-traces" download="backend-traces.json" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
|
<i class="fas fa-download text-[10px]"></i>
|
|
<span>Export Backend Traces</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Bar -->
|
|
<div class="flex border-b border-[var(--color-border-subtle)] mb-6">
|
|
<button @click="switchTab('api')"
|
|
:class="activeTab === 'api' ? 'border-[var(--color-primary)] text-[var(--color-primary)]' : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
|
|
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors">
|
|
<i class="fas fa-exchange-alt mr-1.5 text-xs"></i>API Traces
|
|
<span class="ml-1 text-xs opacity-70" x-text="'(' + traces.length + ')'"></span>
|
|
</button>
|
|
<button @click="switchTab('backend')"
|
|
:class="activeTab === 'backend' ? 'border-[var(--color-primary)] text-[var(--color-primary)]' : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
|
|
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors">
|
|
<i class="fas fa-cogs mr-1.5 text-xs"></i>Backend Traces
|
|
<span class="ml-1 text-xs opacity-70" x-text="'(' + backendTraces.length + ')'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tracing Settings -->
|
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-lg p-6 mb-8">
|
|
<h2 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
|
|
<i class="fas fa-bug mr-2 text-[var(--color-primary)] text-sm"></i>
|
|
Tracing Settings
|
|
</h2>
|
|
<p class="text-xs text-[var(--color-text-secondary)] mb-4">Configure API and backend tracing</p>
|
|
|
|
<div class="space-y-4">
|
|
<!-- Enable Tracing -->
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<label class="text-sm font-medium text-[var(--color-text-primary)]">Enable Tracing</label>
|
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">Enable tracing of requests and responses</p>
|
|
</div>
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" x-model="settings.enable_tracing"
|
|
@change="updateTracingEnabled()"
|
|
class="sr-only peer">
|
|
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Tracing Max Items -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Tracing Max Items</label>
|
|
<p class="text-xs text-[var(--color-text-secondary)] mb-2">Maximum number of tracing items to keep (0 = unlimited)</p>
|
|
<input type="number" x-model="settings.tracing_max_items"
|
|
min="0"
|
|
placeholder="1000"
|
|
:disabled="!settings.enable_tracing"
|
|
class="w-full px-3 py-2 bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/50"
|
|
:class="!settings.enable_tracing ? 'opacity-50 cursor-not-allowed' : ''">
|
|
</div>
|
|
|
|
<!-- Save Button -->
|
|
<div class="flex justify-end pt-2">
|
|
<button type="button" @click="saveTracingSettings()"
|
|
:disabled="saving"
|
|
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]">
|
|
<i class="fas fa-save text-[10px]" :class="saving ? 'fa-spin fa-spinner' : ''"></i>
|
|
<span x-text="saving ? 'Saving...' : 'Save Settings'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Traces Table -->
|
|
<div class="mt-8" x-show="activeTab === 'api'">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-[var(--color-bg-secondary)]">
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Method</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Path</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Status</th>
|
|
<th class="text-right p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="(trace, index) in traces" :key="index">
|
|
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors">
|
|
<td class="p-2" x-text="trace.request.method"></td>
|
|
<td class="p-2" x-text="trace.request.path"></td>
|
|
<td class="p-2" x-text="trace.response.status"></td>
|
|
<td class="p-2 text-right">
|
|
<button @click="showDetails(index)" class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 rounded p-1 transition-colors">
|
|
<i class="fas fa-eye text-xs"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<div x-show="traces.length === 0" class="text-center py-8 text-[var(--color-text-secondary)] text-sm">
|
|
No API traces recorded yet.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backend Traces Table -->
|
|
<div class="mt-8" x-show="activeTab === 'backend'">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-[var(--color-bg-secondary)]">
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Type</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Timestamp</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Model</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Summary</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Duration</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Status</th>
|
|
<th class="text-right p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="(trace, index) in backendTraces" :key="index">
|
|
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors">
|
|
<td class="p-2">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
|
:class="getTypeClass(trace.type)"
|
|
x-text="trace.type"></span>
|
|
</td>
|
|
<td class="p-2 text-xs text-[var(--color-text-secondary)]" x-text="formatTimestamp(trace.timestamp)"></td>
|
|
<td class="p-2 text-sm" x-text="trace.model_name || '-'"></td>
|
|
<td class="p-2 text-sm max-w-xs truncate" x-text="trace.summary || '-'"></td>
|
|
<td class="p-2 text-xs text-[var(--color-text-secondary)]" x-text="formatDuration(trace.duration)"></td>
|
|
<td class="p-2">
|
|
<template x-if="!trace.error">
|
|
<i class="fas fa-check-circle text-green-500 text-xs"></i>
|
|
</template>
|
|
<template x-if="trace.error">
|
|
<i class="fas fa-times-circle text-red-500 text-xs" :title="trace.error"></i>
|
|
</template>
|
|
</td>
|
|
<td class="p-2 text-right">
|
|
<button @click="showBackendDetails(index)" class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 rounded p-1 transition-colors">
|
|
<i class="fas fa-eye text-xs"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<div x-show="backendTraces.length === 0" class="text-center py-8 text-[var(--color-text-secondary)] text-sm">
|
|
No backend traces recorded yet.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Trace Details Modal -->
|
|
<div x-show="selectedTrace !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="selectedTrace = null">
|
|
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
|
|
<div class="flex justify-between mb-4">
|
|
<h2 class="h3">API Trace Details</h2>
|
|
<button @click="selectedTrace = null" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h3 class="text-lg font-semibold mb-2">Request Body</h3>
|
|
<div id="requestEditor" class="h-96 border border-[var(--color-primary-border)]/20"></div>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold mb-2">Response Body</h3>
|
|
<div id="responseEditor" class="h-96 border border-[var(--color-primary-border)]/20"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backend Trace Details Modal -->
|
|
<div x-show="selectedBackendTrace !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="selectedBackendTrace = null; detailKey = null; detailValue = null;">
|
|
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
|
|
<template x-if="selectedBackendTrace !== null">
|
|
<div>
|
|
<div class="flex justify-between mb-4">
|
|
<h2 class="h3">Backend Trace Details</h2>
|
|
<button @click="selectedBackendTrace = null; detailKey = null; detailValue = null;" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Header info -->
|
|
<div class="grid grid-cols-4 gap-4 mb-4">
|
|
<div class="bg-[var(--color-bg-primary)] rounded p-3">
|
|
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Type</div>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
|
:class="getTypeClass(backendTraces[selectedBackendTrace].type)"
|
|
x-text="backendTraces[selectedBackendTrace].type"></span>
|
|
</div>
|
|
<div class="bg-[var(--color-bg-primary)] rounded p-3">
|
|
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Model</div>
|
|
<div class="text-sm font-medium" x-text="backendTraces[selectedBackendTrace].model_name || '-'"></div>
|
|
</div>
|
|
<div class="bg-[var(--color-bg-primary)] rounded p-3">
|
|
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Backend</div>
|
|
<div class="text-sm font-medium" x-text="backendTraces[selectedBackendTrace].backend || '-'"></div>
|
|
</div>
|
|
<div class="bg-[var(--color-bg-primary)] rounded p-3">
|
|
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Duration</div>
|
|
<div class="text-sm font-medium" x-text="formatDuration(backendTraces[selectedBackendTrace].duration)"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error banner -->
|
|
<div x-show="backendTraces[selectedBackendTrace].error" class="bg-red-500/10 border border-red-500/30 rounded-lg p-3 mb-4">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-exclamation-triangle text-red-500 text-sm"></i>
|
|
<span class="text-sm text-red-400" x-text="backendTraces[selectedBackendTrace].error"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data fields table -->
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-[var(--color-bg-primary)]">
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)] w-1/4">Field</th>
|
|
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="[key, value] in getDataEntries(selectedBackendTrace)" :key="key">
|
|
<tr class="border-b border-[var(--color-bg-primary)] hover:bg-[var(--color-bg-primary)]/50 transition-colors">
|
|
<td class="p-2 text-sm font-mono text-[var(--color-primary)]" x-text="key"></td>
|
|
<td class="p-2 text-sm">
|
|
<template x-if="isLargeValue(value)">
|
|
<button @click="showValueDetail(key, value)"
|
|
class="text-left max-w-full">
|
|
<span class="block truncate max-w-lg text-[var(--color-text-secondary)]" x-text="truncateValue(value, 120)"></span>
|
|
<span class="text-xs text-[var(--color-primary)] hover:underline mt-0.5 inline-block">View full value</span>
|
|
</button>
|
|
</template>
|
|
<template x-if="!isLargeValue(value)">
|
|
<span class="font-mono text-xs" x-text="formatValue(value)"></span>
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Value Detail Modal -->
|
|
<div x-show="detailValue !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-[60]" @click="detailValue = null; detailKey = null;">
|
|
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
|
|
<div class="flex justify-between mb-4">
|
|
<h2 class="h3 font-mono" x-text="detailKey"></h2>
|
|
<button @click="detailValue = null; detailKey = null;" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div id="detailEditor" class="h-[70vh] border border-[var(--color-primary-border)]/20"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
<!-- CodeMirror -->
|
|
<link rel="stylesheet" href="static/assets/codemirror.min.css">
|
|
<script src="static/assets/codemirror.min.js"></script>
|
|
<script src="static/assets/javascript.min.js"></script>
|
|
|
|
<!-- Styles from model-editor -->
|
|
<style>
|
|
.CodeMirror {
|
|
height: 100% !important;
|
|
font-family: monospace;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function tracesApp() {
|
|
return {
|
|
activeTab: 'api',
|
|
traces: [],
|
|
backendTraces: [],
|
|
selectedTrace: null,
|
|
selectedBackendTrace: null,
|
|
detailKey: null,
|
|
detailValue: null,
|
|
requestEditor: null,
|
|
responseEditor: null,
|
|
detailEditor: null,
|
|
notifications: [],
|
|
settings: {
|
|
enable_tracing: false,
|
|
tracing_max_items: 0
|
|
},
|
|
saving: false,
|
|
refreshInterval: null,
|
|
|
|
init() {
|
|
this.loadTracingSettings();
|
|
this.fetchTraces();
|
|
this.fetchBackendTraces();
|
|
this.startAutoRefresh();
|
|
},
|
|
|
|
switchTab(tab) {
|
|
this.activeTab = tab;
|
|
},
|
|
|
|
startAutoRefresh() {
|
|
if (this.refreshInterval) clearInterval(this.refreshInterval);
|
|
this.refreshInterval = setInterval(() => {
|
|
if (this.activeTab === 'api') {
|
|
this.fetchTraces();
|
|
} else {
|
|
this.fetchBackendTraces();
|
|
}
|
|
}, 5000);
|
|
},
|
|
|
|
async loadTracingSettings() {
|
|
try {
|
|
const response = await fetch('/api/settings');
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
this.settings.enable_tracing = data.enable_tracing || false;
|
|
this.settings.tracing_max_items = data.tracing_max_items || 0;
|
|
} else {
|
|
this.addNotification('Failed to load tracing settings: ' + (data.error || 'Unknown error'), 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading tracing settings:', error);
|
|
this.addNotification('Failed to load tracing settings: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
updateTracingEnabled() {
|
|
if (!this.settings.enable_tracing) {
|
|
this.settings.tracing_max_items = 0;
|
|
}
|
|
},
|
|
|
|
async saveTracingSettings() {
|
|
if (this.saving) return;
|
|
|
|
this.saving = true;
|
|
|
|
try {
|
|
const payload = {
|
|
enable_tracing: this.settings.enable_tracing,
|
|
tracing_max_items: parseInt(this.settings.tracing_max_items) || 0
|
|
};
|
|
|
|
const response = await fetch('/api/settings', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
this.addNotification('Tracing settings saved successfully!', 'success');
|
|
} else {
|
|
this.addNotification('Failed to save tracing settings: ' + (data.error || 'Unknown error'), 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving tracing settings:', error);
|
|
this.addNotification('Failed to save tracing settings: ' + error.message, 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
addNotification(message, type = 'success') {
|
|
const id = Date.now();
|
|
this.notifications.push({ id, message, type });
|
|
setTimeout(() => this.dismissNotification(id), 5000);
|
|
},
|
|
|
|
dismissNotification(id) {
|
|
this.notifications = this.notifications.filter(n => n.id !== id);
|
|
},
|
|
|
|
async fetchTraces() {
|
|
try {
|
|
const response = await fetch('/api/traces');
|
|
this.traces = await response.json();
|
|
} catch (e) {
|
|
console.error('Error fetching API traces:', e);
|
|
}
|
|
},
|
|
|
|
async fetchBackendTraces() {
|
|
try {
|
|
const response = await fetch('/api/backend-traces');
|
|
this.backendTraces = await response.json();
|
|
} catch (e) {
|
|
console.error('Error fetching backend traces:', e);
|
|
}
|
|
},
|
|
|
|
async clearTraces() {
|
|
if (confirm('Clear all API traces?')) {
|
|
await fetch('/api/traces/clear', { method: 'POST' });
|
|
this.traces = [];
|
|
}
|
|
},
|
|
|
|
async clearBackendTraces() {
|
|
if (confirm('Clear all backend traces?')) {
|
|
await fetch('/api/backend-traces/clear', { method: 'POST' });
|
|
this.backendTraces = [];
|
|
}
|
|
},
|
|
|
|
showDetails(index) {
|
|
this.selectedTrace = index;
|
|
this.$nextTick(() => {
|
|
const trace = this.traces[index];
|
|
|
|
const decodeBase64 = (base64) => {
|
|
const binaryString = atob(base64);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return new TextDecoder().decode(bytes);
|
|
};
|
|
|
|
const formatBody = (bodyText) => {
|
|
try {
|
|
const json = JSON.parse(bodyText);
|
|
return JSON.stringify(json, null, 2);
|
|
} catch {
|
|
return bodyText;
|
|
}
|
|
};
|
|
|
|
const reqBody = formatBody(decodeBase64(trace.request.body));
|
|
const resBody = formatBody(decodeBase64(trace.response.body));
|
|
|
|
if (!this.requestEditor) {
|
|
this.requestEditor = CodeMirror(document.getElementById('requestEditor'), {
|
|
value: reqBody,
|
|
mode: 'javascript',
|
|
json: true,
|
|
theme: 'default',
|
|
lineNumbers: true,
|
|
readOnly: true,
|
|
lineWrapping: true
|
|
});
|
|
} else {
|
|
this.requestEditor.setValue(reqBody);
|
|
}
|
|
|
|
if (!this.responseEditor) {
|
|
this.responseEditor = CodeMirror(document.getElementById('responseEditor'), {
|
|
value: resBody,
|
|
mode: 'javascript',
|
|
json: true,
|
|
theme: 'default',
|
|
lineNumbers: true,
|
|
readOnly: true,
|
|
lineWrapping: true
|
|
});
|
|
} else {
|
|
this.responseEditor.setValue(resBody);
|
|
}
|
|
});
|
|
},
|
|
|
|
showBackendDetails(index) {
|
|
this.selectedBackendTrace = index;
|
|
},
|
|
|
|
showValueDetail(key, value) {
|
|
this.detailKey = key;
|
|
let formatted = '';
|
|
if (typeof value === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
formatted = JSON.stringify(parsed, null, 2);
|
|
} catch {
|
|
formatted = value;
|
|
}
|
|
} else if (typeof value === 'object') {
|
|
formatted = JSON.stringify(value, null, 2);
|
|
} else {
|
|
formatted = String(value);
|
|
}
|
|
this.detailValue = formatted;
|
|
|
|
this.$nextTick(() => {
|
|
const el = document.getElementById('detailEditor');
|
|
if (el) {
|
|
el.innerHTML = '';
|
|
this.detailEditor = CodeMirror(el, {
|
|
value: formatted,
|
|
mode: 'javascript',
|
|
json: true,
|
|
theme: 'default',
|
|
lineNumbers: true,
|
|
readOnly: true,
|
|
lineWrapping: true
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
formatTimestamp(ts) {
|
|
if (!ts) return '-';
|
|
const d = new Date(ts);
|
|
return d.toLocaleTimeString() + '.' + String(d.getMilliseconds()).padStart(3, '0');
|
|
},
|
|
|
|
formatDuration(ns) {
|
|
if (!ns) return '-';
|
|
const ms = ns / 1000000;
|
|
if (ms < 1000) return ms.toFixed(1) + 'ms';
|
|
return (ms / 1000).toFixed(2) + 's';
|
|
},
|
|
|
|
getTypeClass(type) {
|
|
const classes = {
|
|
'llm': 'bg-blue-500/20 text-blue-400',
|
|
'embedding': 'bg-purple-500/20 text-purple-400',
|
|
'transcription': 'bg-yellow-500/20 text-yellow-400',
|
|
'image_generation': 'bg-green-500/20 text-green-400',
|
|
'video_generation': 'bg-pink-500/20 text-pink-400',
|
|
'tts': 'bg-orange-500/20 text-orange-400',
|
|
'sound_generation': 'bg-teal-500/20 text-teal-400',
|
|
'rerank': 'bg-indigo-500/20 text-indigo-400',
|
|
'tokenize': 'bg-gray-500/20 text-gray-400',
|
|
};
|
|
return classes[type] || 'bg-gray-500/20 text-gray-400';
|
|
},
|
|
|
|
isLargeValue(value) {
|
|
if (typeof value === 'string') return value.length > 120;
|
|
if (typeof value === 'object') return JSON.stringify(value).length > 120;
|
|
return false;
|
|
},
|
|
|
|
truncateValue(value, maxLen) {
|
|
let str = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
if (str.length <= maxLen) return str;
|
|
return str.substring(0, maxLen) + '...';
|
|
},
|
|
|
|
formatValue(value) {
|
|
if (value === null || value === undefined) return 'null';
|
|
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
return String(value);
|
|
},
|
|
|
|
getDataEntries(index) {
|
|
const trace = this.backendTraces[index];
|
|
if (!trace || !trace.data) return [];
|
|
return Object.entries(trace.data);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{{template "views/partials/footer" .}}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|