fix: show a placeholder when an image cannot be loaded (#27730)

A message referencing a file that no longer exists rendered a broken
image, because nothing anywhere noticed the failed load. What the user saw
depended on the caller's alt text: in a response the alt is the message
content, so the entire reply was rendered inside the image frame, and the
preview still opened full-screen on a dead image.

Image.svelte now tracks a failed load and renders an 'Image unavailable'
placeholder instead, suppressing the preview for a source that cannot be
shown. The failed state resets when the source changes, so a replaced or
corrected URL is retried. An onError callback lets a caller react without
changing behaviour for the eight existing call sites, which are untouched.

Responses now pass the file name (or 'Generated Image') as alt rather than
the message content, which was never a description of the image.
This commit is contained in:
G30
2026-08-11 00:27:13 -04:00
committed by GitHub
parent 0b4b7ae5ff
commit 38a03830c2
3 changed files with 49 additions and 12 deletions
@@ -690,7 +690,7 @@
{#each message.files.filter((f) => ['image', 'file'].includes(f.type)) as file}
<div>
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
<Image src={file.url} alt={message.content} />
<Image src={file.url} alt={file.name || $i18n.t('Generated Image')} />
{:else}
<FileItem
item={file}
+47 -11
View File
@@ -5,6 +5,7 @@
import { settings } from '$lib/stores';
import ImagePreview from './ImagePreview.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import Photo from '$lib/components/icons/Photo.svelte';
import { getContext } from 'svelte';
export let src = '';
@@ -18,27 +19,62 @@
export let dismissible = false;
export let onDismiss = () => {};
/** Called when the image fails to load, e.g. its file has since been deleted. */
export let onError: () => void = () => {};
const i18n = getContext('i18n');
let _src = '';
$: _src = safeImageUrl(src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src, allowExternal);
let showImagePreview = false;
let failed = false;
let attemptedSrc = '';
$: if (_src !== attemptedSrc) {
attemptedSrc = _src;
failed = false;
}
const handleError = () => {
failed = true;
showImagePreview = false;
onError();
};
</script>
<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
{#if !failed}
<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
{/if}
<div class=" relative group w-fit flex items-center">
<button
class={className}
on:click={() => {
showImagePreview = true;
}}
aria-label={$i18n.t('Show image preview')}
type="button"
>
<img src={_src} {alt} class={imageClassName} draggable="false" data-cy="image" />
</button>
{#if failed}
<div
class="{imageClassName} flex items-center gap-2 px-3 py-2.5 border border-dashed border-gray-200 dark:border-gray-800 text-gray-400 dark:text-gray-600"
data-cy="image-unavailable"
>
<Photo className="size-4 shrink-0" strokeWidth="1.5" />
<span class="text-xs">{$i18n.t('Image unavailable')}</span>
</div>
{:else}
<button
class={className}
on:click={() => {
showImagePreview = true;
}}
aria-label={$i18n.t('Show image preview')}
type="button"
>
<img
src={_src}
{alt}
class={imageClassName}
draggable="false"
data-cy="image"
on:error={handleError}
/>
</button>
{/if}
{#if dismissible}
<div class=" absolute -top-1 -right-1">
@@ -1406,6 +1406,7 @@
"Image Prompt Generation": "",
"Image Prompt Generation Prompt": "",
"Image Size": "",
"Image unavailable": "",
"image/*, video/*": "",
"Images": "",
"Import": "",