From 99a25dca346114f7bcb33d500c55db052833d385 Mon Sep 17 00:00:00 2001 From: Wang Qi Date: Thu, 18 Jun 2026 09:38:31 +0800 Subject: [PATCH] Fix Chat/Search/Agent bot show image (#16152) Fix Chat/Search/Agent bot show image --- web/src/components/image/index.tsx | 116 +++++++++++++++++- .../reference-image-list.tsx | 25 ++-- 2 files changed, 130 insertions(+), 11 deletions(-) diff --git a/web/src/components/image/index.tsx b/web/src/components/image/index.tsx index e04558936c..78dbbddd2b 100644 --- a/web/src/components/image/index.tsx +++ b/web/src/components/image/index.tsx @@ -1,6 +1,9 @@ +import { Authorization } from '@/constants/authorization'; import { restAPIv1 } from '@/utils/api'; +import { getAuthorization } from '@/utils/authorization-util'; +import { getSearchValue } from '@/utils/common-util'; import classNames from 'classnames'; -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; interface IImage extends React.ImgHTMLAttributes { @@ -9,11 +12,120 @@ interface IImage extends React.ImgHTMLAttributes { label?: string; } +type ImageCacheItem = { + count: number; + objectUrl?: string; + promise?: Promise; + timer?: ReturnType; +}; + +const imageCache = new Map(); + +export const buildDocumentImageUrl = (id: string, t?: string | number) => { + const params = new URLSearchParams(); + + if (t) { + params.set('_t', String(t)); + } + + const query = params.toString(); + return `${restAPIv1}/documents/images/${id}${query ? `?${query}` : ''}`; +}; + +const fetchDocumentImage = (url: string, authorization: string) => { + const cacheKey = `${authorization}:${url}`; + let item = imageCache.get(cacheKey); + + if (!item) { + item = { count: 0 }; + imageCache.set(cacheKey, item); + } + if (item.timer) { + clearTimeout(item.timer); + item.timer = undefined; + } + item.count += 1; + + if (!item.promise) { + item.promise = fetch(url, { headers: { [Authorization]: authorization } }) + .then((response) => { + if (!response.ok) { + throw new Error(response.statusText); + } + return response.blob(); + }) + .then((blob) => { + item.objectUrl = URL.createObjectURL(blob); + return item.objectUrl; + }) + .catch((error) => { + imageCache.delete(cacheKey); + throw error; + }); + } + + return { + promise: item.promise, + release: () => { + item.count -= 1; + if (item.count <= 0) { + item.timer = setTimeout(() => { + if (item.count <= 0) { + if (item.objectUrl) { + URL.revokeObjectURL(item.objectUrl); + } + imageCache.delete(cacheKey); + } + }, 30000); + } + }, + }; +}; + +export const useDocumentImageUrl = (id: string, t?: string | number) => { + const directUrl = useMemo(() => buildDocumentImageUrl(id, t), [id, t]); + const [imageUrl, setImageUrl] = useState(() => + getAuthorization() && getSearchValue('shared_id') ? '' : directUrl, + ); + + useEffect(() => { + const authorization = getAuthorization(); + if (!authorization || !getSearchValue('shared_id')) { + setImageUrl(directUrl); + return; + } + + let ignore = false; + setImageUrl(''); + const { promise, release } = fetchDocumentImage(directUrl, authorization); + promise + .then((url) => { + if (ignore) { + return; + } + setImageUrl(url); + }) + .catch(() => { + if (!ignore) { + setImageUrl(''); + } + }); + + return () => { + ignore = true; + release(); + }; + }, [directUrl]); + + return imageUrl; +}; + const Image = ({ id, t, label, className, ...props }: IImage) => { + const src = useDocumentImageUrl(id, t); const imageElement = ( ); diff --git a/web/src/components/next-message-item/reference-image-list.tsx b/web/src/components/next-message-item/reference-image-list.tsx index 41d0c5603e..00dae1b0e8 100644 --- a/web/src/components/next-message-item/reference-image-list.tsx +++ b/web/src/components/next-message-item/reference-image-list.tsx @@ -1,4 +1,4 @@ -import Image from '@/components/image'; +import Image, { useDocumentImageUrl } from '@/components/image'; import { Carousel, CarouselContent, @@ -7,7 +7,6 @@ import { CarouselPrevious, } from '@/components/ui/carousel'; import { IReferenceChunk } from '@/interfaces/database/chat'; -import { restAPIv1 } from '@/utils/api'; import { isPlainObject } from 'lodash'; import { RotateCw, ZoomIn, ZoomOut } from 'lucide-react'; import { useMemo } from 'react'; @@ -35,6 +34,20 @@ const getButtonVisibilityClass = (imageCount: number) => { return map[imageCount] || (imageCount >= 6 ? '@2xl:hidden' : ''); }; +function ImagePhotoView({ id, index }: ImageItem) { + const src = useDocumentImageUrl(id); + + return ( + + + + ); +} + function ImageCarousel({ images }: { images: ImageItem[] }) { const buttonVisibilityClass = getButtonVisibilityClass(images.length); @@ -79,13 +92,7 @@ function ImageCarousel({ images }: { images: ImageItem[] }) { @2xl:basis-1/6 " > - - - + ))}