gradio_livelog / src /frontend /shared /LiveLogPanel.svelte
elismasilva's picture
Upload folder using huggingface_hub
4eda705 verified
<!-- frontend/src/shared/LiveLogPanel.svelte -->
<script lang="ts">
import { afterUpdate, createEventDispatcher } from "svelte";
import { tick } from "svelte";
import Widgets from "./Widgets.svelte";
// -------------------------------------------------------------------------
// Props received from the Gradio backend
// -------------------------------------------------------------------------
/** The incoming value from Gradio. Can be null (for clearing), an array, or a single object. */
export let value: Record<string, any> | Array<Record<string, any>> | null = null;
/** The height of the component. */
export let height: number | string;
/** Whether to automatically scroll to the latest log entry. */
export let autoscroll: boolean;
/** Whether to display line numbers next to log entries. */
export let line_numbers: boolean;
/** The background color of the log display area. */
export let background_color: string;
/** The current display mode: "full", "log", or "progress". */
export let display_mode: "full" | "log" | "progress";
/** Toggles the visibility of the download button. */
export let show_download_button: boolean;
/** Toggles the visibility of the copy button. */
export let show_copy_button: boolean;
/** Toggles the visibility of the clear button. */
export let show_clear_button: boolean;
const dispatch = createEventDispatcher();
let log_container: HTMLElement;
// -------------------------------------------------------------------------
// Internal component state
// -------------------------------------------------------------------------
/** Holds the current state of the progress bar. */
let progress = { visible: true, current: 0, total: 100, desc: "", percentage: 0, rate: 0.0, status: "running", rate_unit: 'it/s', extra_info:''};
/** Accumulates all received log lines. */
let log_lines: { level: string; content: string }[] = [];
/** A plain text representation of all logs for the utility buttons. */
let all_logs_as_text = "";
/** A reactive variable to control the component's height. */
let height_style: string;
/** Stores the initial, fixed description sent from the backend for the progress bar. */
let initial_desc: string = "Processing...";
// -------------------------------------------------------------------------
// Reactive Logic
// -------------------------------------------------------------------------
// Dynamically adjust the component's height style based on the display mode.
$: {
if (display_mode === 'progress' && progress.visible) {
height_style = 'auto'; // Shrink to fit only the progress bar content.
} else {
height_style = typeof height === 'number' ? height + 'px' : height;
}
}
// This is the core reactive block that processes incoming `value` updates from Gradio.
// It uses a debounce to batch rapid updates and prevent UI flickering.
let debounceTimeout: NodeJS.Timeout;
$: {
if (value !== null) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(async () => {
if (value === null) {
log_lines = [];
progress = {
visible: false, current: 0, total: 100, desc: "", percentage: 0,
rate: 0.0, status: "running", rate_unit: 'it/s', extra_info: ''
};
all_logs_as_text = "";
initial_desc = "Processing...";
} else if (value) {
if (Array.isArray(value)) {
// Handles an initial state load if the backend provides a full list.
log_lines = [];
progress.visible = false;
for (const item of value) {
if (item.type === "log") {
log_lines = [...log_lines, { level: item.level || 'INFO', content: item.content }];
} else if (item.type === "progress") {
progress.visible = true;
progress.current = item.current;
progress.total = item.total || 100;
if (item.current === 0 && item.desc && initial_desc === "Processing...") {
initial_desc = item.desc;
}
progress.desc = display_mode === "progress" && log_lines.length > 0
? log_lines[log_lines.length - 1].content
: initial_desc;
progress.rate = item.rate || 0.0;
progress.rate_unit = item.rate_unit || 'it/s';
progress.extra_info = item.extra_info || '';
progress.percentage = progress.total > 0 ? ((item.current / progress.total) * 100) : 0;
progress.status = item.status || "running";
}
}
} else if (typeof value === 'object' && value.type) {
if (value.type === "log") {
log_lines = [...log_lines, { level: value.level || 'INFO', content: value.content }];
} else if (value.type === "progress") {
progress.visible = true;
progress.current = value.current;
progress.total = value.total || 100;
if (value.current === 0 && value.desc && initial_desc === "Processing...") {
initial_desc = value.desc;
}
progress.desc = display_mode === "progress" && log_lines.length > 0
? log_lines[log_lines.length - 1].content
: initial_desc;
progress.rate = value.rate || 0.0;
progress.rate_unit = value.rate_unit || 'it/s';
progress.extra_info = value.extra_info || '';
progress.percentage = progress.total > 0 ? ((value.current / value.total) * 100) : 0;
progress.status = value.status || "running";
log_lines = Array.isArray(value.logs) ? value.logs.map(log => ({
level: log.level || 'INFO',
content: log.content
})) : log_lines;
}
}
all_logs_as_text = log_lines.map(l => l.content).join('\n');
}
await tick();
}, 50);
}
}
// This lifecycle function runs after the DOM has been updated.
afterUpdate(() => {
if (autoscroll && log_container && display_mode !== 'progress') {
// Scroll the log container to the bottom to show the latest entry.
log_container.scrollTop = log_container.scrollHeight;
}
});
</script>
<div class="panel-container" style:height={height_style}>
<!-- Conditionally render the log view based on the display_mode prop. -->
<div class="log-view-container" style:display={display_mode === 'progress' ? 'none' : 'flex'}>
<div class="header">
<Widgets
bind:value={all_logs_as_text}
on:clear={() => dispatch('clear')}
{show_download_button}
{show_copy_button}
{show_clear_button}
/>
</div>
<div class="log-panel" bind:this={log_container} style="background-color: {background_color};">
{#each log_lines as log, i}
<div class="log-line">
{#if line_numbers}<span class="line-number">{i + 1}</span>{/if}
<pre class="log-content log-level-{log.level.toLowerCase()}">{log.content}</pre>
</div>
{/each}
</div>
</div>
<!-- Conditionally render the progress bar view. -->
{#if progress.visible && (display_mode === 'full' || display_mode === 'progress')}
<div class="progress-container">
<div class="progress-label-top">
<span>{progress.desc}</span>
<span class="rate-info">
{progress.rate.toFixed(2)} {progress.rate_unit}
{#if progress.extra_info}
<span class="extra-info">({progress.extra_info})</span>
{/if}
</span>
</div>
<div class="progress-bar-background">
<!-- Conditionally apply CSS classes based on the progress status. -->
<div
class="progress-bar-fill"
class:success={progress.status === 'success'}
class:error={progress.status === 'error'}
style="width: {progress.percentage.toFixed(1)}%;"
></div>
</div>
<div class="progress-label-bottom">
<span>{Math.round(progress.percentage)}%</span>
<span>{progress.current} / {progress.total}</span>
</div>
</div>
{/if}
</div>
<style>
.panel-container {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color-primary);
border-radius: 0 !important;
background-color: var(--background-fill-primary);
overflow: hidden;
}
.log-view-container {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
}
.header {
border-bottom: 1px solid var(--border-color-primary);
background-color: var(--background-fill-secondary);
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.log-panel {
flex-grow: 1;
font-family: var(--font-mono, monospace);
font-size: var(--text-sm);
overflow-y: auto;
color: #f8f8f8;
}
.log-line {
display: flex;
}
.line-number {
opacity: 0.6;
padding-right: var(--spacing-lg);
user-select: none;
text-align: right;
min-width: 3ch;
}
.log-content {
margin: 0;
padding-left: 5px;
white-space: pre-wrap;
word-break: break-word;
}
/* Styles for different log levels */
.log-level-info { color: inherit; }
.log-level-debug { color: #888888; }
.log-level-warning { color: #facc15; }
.log-level-error { color: #ef4444; }
.log-level-critical {
background-color: #ef4444;
color: white;
font-weight: bold;
padding: 0 0.25rem;
}
.log-level-success { color: #22c55e; }
.progress-container {
padding: var(--spacing-sm) var(--spacing-md);
border-top: 1px solid var(--border-color-primary);
background: var(--background-fill-secondary);
}
.progress-label-top, .progress-label-bottom {
display: flex;
justify-content: space-between;
font-size: var(--text-sm);
color: var(--body-text-color-subdued);
}
.progress-label-top {
margin-bottom: var(--spacing-xs);
}
.progress-label-bottom {
margin-top: var(--spacing-xs);
}
.progress-bar-background {
width: 100%;
height: 8px;
background-color: var(--background-fill-primary);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: var(--color-accent); /* Default "running" color */
border-radius: var(--radius-full);
transition: width 0.1s linear, background-color 0.3s ease;
}
/* Styles for different progress bar statuses */
.progress-bar-fill.success {
background-color: var(--color-success, #22c55e);
}
.progress-bar-fill.error {
background-color: var(--color-error, #ef4444);
}
.rate-info {
display: flex;
align-items: center;
gap: 0.5ch;
}
.extra-info {
color: var(--body-text-color-subdued);
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 230px;
}
</style>