Files
LocalAI/core/http/views/partials/inprogress.html
Ettore Di Giacinto 2fabdc08e6 feat(ui): left navbar, dark/light theme (#8594)
* feat(ui): left navbar, dark/light theme

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* darker background

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-18 00:14:39 +01:00

222 lines
12 KiB
HTML

<!-- Global Operations Status Bar -->
<div x-data="operationsStatus()" x-init="init()" x-show="operations.length > 0"
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="sticky top-0 left-0 right-0 z-40 bg-[var(--color-bg-secondary)]/95 backdrop-blur-sm border-b border-[var(--color-primary)]/50">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-2">
<div class="relative">
<i class="fas fa-spinner fa-spin text-[var(--color-primary)] text-lg"></i>
</div>
<h3 class="text-[var(--color-text-primary)] font-semibold text-sm">
Operations in Progress
<span class="ml-2 bg-[var(--color-primary-light)] px-2 py-1 rounded-full text-xs border border-[var(--color-primary-border)]" x-text="operations.length"></span>
</h3>
</div>
</div>
<button @click="collapsed = !collapsed"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
<i class="fas" :class="collapsed ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
</button>
</div>
<!-- Operations List -->
<div x-show="!collapsed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 max-h-0"
x-transition:enter-end="opacity-100 max-h-96"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 max-h-96"
x-transition:leave-end="opacity-0 max-h-0"
class="space-y-2 overflow-y-auto max-h-96">
<template x-for="operation in operations" :key="operation.id">
<div class="bg-[var(--color-bg-primary)]/80 rounded-lg p-3 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/50 transition-colors">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-3 flex-1 min-w-0">
<!-- Icon based on type -->
<div class="flex-shrink-0">
<i class="text-lg"
:class="{
'fas fa-cube text-[var(--color-primary)]': !operation.isBackend && !operation.isDeletion,
'fas fa-cubes text-[var(--color-accent)]': operation.isBackend && !operation.isDeletion,
'fas fa-trash text-[var(--color-error)]': operation.isDeletion
}"></i>
</div>
<!-- Operation details -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2">
<span class="text-[var(--color-text-primary)] font-medium text-sm truncate" x-text="operation.name"></span>
<span class="flex-shrink-0 text-xs px-2 py-0.5 rounded border"
:class="{
'bg-[var(--color-primary-light)] text-[var(--color-primary)]': !operation.isDeletion && !operation.isBackend,
'bg-[var(--color-accent-light)] text-[var(--color-accent)]': !operation.isDeletion && operation.isBackend,
'bg-[var(--color-error-light)] text-[var(--color-error)]': operation.isDeletion
}"
x-text="operation.isBackend ? 'Backend' : 'Model'"></span>
</div>
<!-- Status message -->
<div class="flex items-center space-x-2 mt-1">
<template x-if="operation.isQueued">
<span class="text-xs text-[var(--color-primary)] flex items-center">
<i class="fas fa-clock mr-1"></i>
Queued
</span>
</template>
<template x-if="operation.isCancelled">
<span class="text-xs text-[var(--color-error)] flex items-center">
<i class="fas fa-ban mr-1"></i>
Cancelling...
</span>
</template>
<template x-if="!operation.isQueued && !operation.isCancelled && operation.message">
<span class="text-xs text-[var(--color-text-secondary)] truncate" x-text="operation.message"></span>
</template>
</div>
</div>
<!-- Progress percentage and cancel button -->
<div class="flex-shrink-0 text-right flex items-center space-x-2">
<span class="text-[var(--color-text-primary)] font-bold text-lg" x-text="operation.progress + '%'"></span>
<template x-if="operation.cancellable && !operation.isCancelled">
<button @click="cancelOperation(operation.jobID, operation.id)"
class="text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors p-1 rounded hover:bg-[var(--color-error-light)]"
title="Cancel operation">
<i class="fas fa-times"></i>
</button>
</template>
<template x-if="operation.isCancelled">
<span class="text-[var(--color-error)] text-xs flex items-center">
<i class="fas fa-ban mr-1"></i>
Cancelled
</span>
</template>
</div>
</div>
</div>
<!-- Progress bar -->
<div class="w-full bg-[var(--color-bg-primary)] rounded-full h-2 overflow-hidden border border-[var(--color-border-subtle)]">
<div class="h-full rounded-full transition-all duration-300"
:class="{
'bg-[var(--color-primary)]': !operation.isDeletion && !operation.isCancelled,
'bg-[var(--color-error)]': operation.isDeletion || operation.isCancelled
}"
:style="'width: ' + operation.progress + '%'">
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<script>
function operationsStatus() {
return {
operations: [],
collapsed: false,
pollInterval: null,
init() {
this.fetchOperations();
// Poll every 1s for smooth updates
this.pollInterval = setInterval(() => this.fetchOperations(), 1000);
},
async fetchOperations() {
try {
const response = await fetch('/api/operations');
if (!response.ok) {
throw new Error('Failed to fetch operations');
}
const data = await response.json();
const previousCount = this.operations.length;
this.operations = data.operations || [];
// If we had operations before and now we don't, refresh the page
if (previousCount > 0 && this.operations.length === 0) {
// Small delay to ensure the user sees the completion
setTimeout(() => {
window.location.reload();
}, 1000);
}
// Auto-collapse if there are many operations
if (this.operations.length > 5 && !this.collapsed) {
// Don't auto-collapse, let user control it
}
} catch (error) {
console.error('Error fetching operations:', error);
// Don't clear operations on error, just keep showing last known state
}
},
async cancelOperation(jobID, operationID) {
// Check if operation is already cancelled
const operation = this.operations.find(op => op.jobID === jobID);
if (operation && operation.isCancelled) {
// Already cancelled, no need to do anything
return;
}
try {
const response = await fetch(`/api/operations/${jobID}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
const errorMessage = error.error || 'Failed to cancel operation';
// Don't show alert for "already cancelled" - just update UI silently
if (errorMessage.includes('already cancelled')) {
if (operation) {
operation.isCancelled = true;
operation.cancellable = false;
}
this.fetchOperations();
return;
}
throw new Error(errorMessage);
}
// Update the operation status immediately
if (operation) {
operation.isCancelled = true;
operation.cancellable = false;
operation.message = 'Cancelling...';
}
// Refresh operations to get updated status
this.fetchOperations();
} catch (error) {
console.error('Error cancelling operation:', error);
// Only show alert if it's not an "already cancelled" error
if (!error.message.includes('already cancelled')) {
alert('Failed to cancel operation: ' + error.message);
}
}
},
destroy() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
}
}
</script>