mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-08 03:48:14 +02:00
Add cleaner loading / streaming for image loading (#2055)
* add image loading * clean * add loading skeleton * clean up * clearer comments
This commit is contained in:
parent
cc3856ef6d
commit
ab564a9ec8
@ -35,14 +35,12 @@ prompts:
|
||||
description: "Generates images based on user prompts!"
|
||||
system: >
|
||||
You are an advanced image generation system capable of creating diverse and detailed images.
|
||||
The current date is DANSWER_DATETIME_REPLACEMENT.
|
||||
|
||||
You can interpret user prompts and generate high-quality, creative images that match their descriptions.
|
||||
|
||||
You always strive to create safe and appropriate content, avoiding any harmful or offensive imagery.
|
||||
task: >
|
||||
Generate an image based on the user's description.
|
||||
If the user's request is unclear or too vague, ask for clarification to ensure the best possible result.
|
||||
|
||||
Provide a detailed description of the generated image, including key elements, colors, and composition.
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FullImageModal } from "./FullImageModal";
|
||||
import { buildImgUrl } from "./utils";
|
||||
import Image from "next/image";
|
||||
|
||||
export function InMessageImage({ fileId }: { fileId: string }) {
|
||||
const [fullImageShowing, setFullImageShowing] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -15,12 +15,22 @@ export function InMessageImage({ fileId }: { fileId: string }) {
|
||||
onOpenChange={(open) => setFullImageShowing(open)}
|
||||
/>
|
||||
|
||||
<img
|
||||
className="object-cover object-center overflow-hidden rounded-lg w-full h-full max-w-96 max-h-96 transition-opacity duration-300 opacity-100"
|
||||
onClick={() => setFullImageShowing(true)}
|
||||
src={buildImgUrl(fileId)}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="relative w-full h-full max-w-96 max-h-96">
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 bg-gray-200 animate-pulse rounded-lg" />
|
||||
)}
|
||||
<Image
|
||||
width={1200}
|
||||
height={1200}
|
||||
alt="Chat Message Image"
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
className={`object-cover object-center overflow-hidden rounded-lg w-full h-full max-w-96 max-h-96 transition-opacity duration-300
|
||||
${imageLoaded ? "opacity-100" : "opacity-0"}`}
|
||||
onClick={() => setFullImageShowing(true)}
|
||||
src={buildImgUrl(fileId)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ import { Tooltip } from "@/components/tooltip/Tooltip";
|
||||
import { useMouseTracking } from "./hooks";
|
||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
SEARCH_TOOL_NAME,
|
||||
@ -153,6 +154,7 @@ export const AIMessage = ({
|
||||
handleForceSearch?: () => void;
|
||||
retrievalDisabled?: boolean;
|
||||
}) => {
|
||||
const toolCallGenerating = toolCall && !toolCall.tool_result;
|
||||
const processContent = (content: string | JSX.Element) => {
|
||||
if (typeof content !== "string") {
|
||||
return content;
|
||||
@ -168,7 +170,7 @@ export const AIMessage = ({
|
||||
}
|
||||
}
|
||||
|
||||
return content + (!isComplete ? " [*]() " : "");
|
||||
return content + (!isComplete && !toolCallGenerating ? " [*]() " : "");
|
||||
};
|
||||
|
||||
const finalContent = processContent(content as string);
|
||||
@ -344,15 +346,7 @@ export const AIMessage = ({
|
||||
|
||||
{toolCall &&
|
||||
toolCall.tool_name === IMAGE_GENERATION_TOOL_NAME &&
|
||||
!toolCall.tool_result && (
|
||||
<ToolRunDisplay
|
||||
toolName={`Generating images`}
|
||||
toolLogo={
|
||||
<FiImage size={15} className="my-auto mr-1" />
|
||||
}
|
||||
isRunning={!toolCall.tool_result}
|
||||
/>
|
||||
)}
|
||||
!toolCall.tool_result && <GeneratingImageDisplay />}
|
||||
|
||||
{toolCall &&
|
||||
toolCall.tool_name === INTERNET_SEARCH_TOOL_NAME && (
|
||||
@ -369,7 +363,7 @@ export const AIMessage = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{content ? (
|
||||
{content || files ? (
|
||||
<>
|
||||
<FileDisplay files={files || []} />
|
||||
|
||||
|
102
web/src/app/chat/tools/GeneratingImageDisplay.tsx
Normal file
102
web/src/app/chat/tools/GeneratingImageDisplay.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
|
||||
export default function GeneratingImageDisplay({ isCompleted = false }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const progressRef = useRef(0);
|
||||
const animationRef = useRef<number>();
|
||||
const startTimeRef = useRef<number>(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
// Animation setup
|
||||
let lastUpdateTime = 0;
|
||||
const updateInterval = 500;
|
||||
const animationDuration = 30000;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
const elapsedTime = timestamp - startTimeRef.current;
|
||||
|
||||
// Calculate progress using logarithmic curve
|
||||
const maxProgress = 99.9;
|
||||
const progress =
|
||||
maxProgress * (1 - Math.exp(-elapsedTime / animationDuration));
|
||||
|
||||
// Update progress if enough time has passed
|
||||
if (timestamp - lastUpdateTime > updateInterval) {
|
||||
progressRef.current = progress;
|
||||
setProgress(Math.round(progress * 10) / 10);
|
||||
lastUpdateTime = timestamp;
|
||||
}
|
||||
|
||||
// Continue animation if not completed
|
||||
if (!isCompleted && elapsedTime < animationDuration) {
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
// Start animation
|
||||
startTimeRef.current = performance.now();
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [isCompleted]);
|
||||
|
||||
// Handle completion
|
||||
useEffect(() => {
|
||||
if (isCompleted) {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
setProgress(100);
|
||||
}
|
||||
}, [isCompleted]);
|
||||
|
||||
return (
|
||||
<div className="object-cover object-center border border-neutral-200 bg-neutral-100 items-center justify-center overflow-hidden flex rounded-lg w-96 h-96 transition-opacity duration-300 opacity-100">
|
||||
<div className="m-auto relative flex">
|
||||
<svg className="w-16 h-16 transform -rotate-90" viewBox="0 0 100 100">
|
||||
<circle
|
||||
className="text-gray-200"
|
||||
strokeWidth="8"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
r="44"
|
||||
cx="50"
|
||||
cy="50"
|
||||
/>
|
||||
<circle
|
||||
className="text-gray-800 transition-all duration-300"
|
||||
strokeWidth="8"
|
||||
strokeDasharray={276.46}
|
||||
strokeDashoffset={276.46 * (1 - progress / 100)}
|
||||
strokeLinecap="round"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
r="44"
|
||||
cx="50"
|
||||
cy="50"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-6 h-6 text-neutral-500 animate-pulse-strong"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2278,6 +2278,28 @@ export const NewChatIcon = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PaintingIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
@ -210,7 +210,6 @@ export const FullSearchBar = ({
|
||||
<div
|
||||
className={`flex 2xl:justify-end justify-between w-full items-center space-x-3 px-4 pb-2`}
|
||||
>
|
||||
{/* <div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64"> */}
|
||||
<div className="2xl:hidden">
|
||||
{(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||
<HorizontalSourceSelector
|
||||
@ -223,8 +222,6 @@ export const FullSearchBar = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* ccPairs, documentSets, filterManager, finalAvailableDocumentSets, finalAvailableSources, tags */}
|
||||
{/* </div>/ */}
|
||||
<div className="flex my-auto gap-x-3">
|
||||
{toggleAgentic && (
|
||||
<AnimatedToggle isOn={agentic!} handleToggle={toggleAgentic} />
|
||||
|
@ -81,7 +81,7 @@ export function SourceSelector({
|
||||
showDocSidebar ? "4xl:block" : "!block"
|
||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
||||
>
|
||||
<div className=" mb-4 pb-2 border-b border-border text-emphasis">
|
||||
<div className=" mb-4 pb-2 flex border-b border-border text-emphasis">
|
||||
<h2 className="font-bold my-auto">Filters</h2>
|
||||
<FiFilter className="my-auto ml-2" size="16" />
|
||||
</div>
|
||||
@ -469,46 +469,6 @@ export function HorizontalSourceSelector({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-wrap gap-2">
|
||||
{timeRange && timeRange.selectValue && (
|
||||
<SelectedBubble onClick={() => setTimeRange(null)}>
|
||||
<div className="text-sm flex">{timeRange.selectValue}</div>
|
||||
</SelectedBubble>
|
||||
)}
|
||||
{selectedSources.map((source) => (
|
||||
<SelectedBubble
|
||||
key={source.internalName}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<>
|
||||
<SourceIcon sourceType={source.internalName} iconSize={16} />
|
||||
<span className="ml-2 text-sm">{source.displayName}</span>
|
||||
</>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
{selectedDocumentSets.map((documentSetName) => (
|
||||
<SelectedBubble
|
||||
key={documentSetName}
|
||||
onClick={() => handleDocumentSetSelect(documentSetName)}
|
||||
>
|
||||
<>
|
||||
<FiBookmark />
|
||||
<span className="ml-2 text-sm">{documentSetName}</span>
|
||||
</>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
{selectedTags.map((tag) => (
|
||||
<SelectedBubble
|
||||
key={`${tag.tag_key}=${tag.tag_value}`}
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{tag.tag_key}<b>=</b>{tag.tag_value}
|
||||
</span>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user