mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-28 21:05:17 +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:
@@ -35,14 +35,12 @@ prompts:
|
|||||||
description: "Generates images based on user prompts!"
|
description: "Generates images based on user prompts!"
|
||||||
system: >
|
system: >
|
||||||
You are an advanced image generation system capable of creating diverse and detailed images.
|
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 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.
|
You always strive to create safe and appropriate content, avoiding any harmful or offensive imagery.
|
||||||
task: >
|
task: >
|
||||||
Generate an image based on the user's description.
|
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.
|
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 { useState } from "react";
|
||||||
import { FullImageModal } from "./FullImageModal";
|
import { FullImageModal } from "./FullImageModal";
|
||||||
import { buildImgUrl } from "./utils";
|
import { buildImgUrl } from "./utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export function InMessageImage({ fileId }: { fileId: string }) {
|
export function InMessageImage({ fileId }: { fileId: string }) {
|
||||||
const [fullImageShowing, setFullImageShowing] = useState(false);
|
const [fullImageShowing, setFullImageShowing] = useState(false);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -15,12 +15,22 @@ export function InMessageImage({ fileId }: { fileId: string }) {
|
|||||||
onOpenChange={(open) => setFullImageShowing(open)}
|
onOpenChange={(open) => setFullImageShowing(open)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<div className="relative w-full h-full max-w-96 max-h-96">
|
||||||
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"
|
{!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)}
|
onClick={() => setFullImageShowing(true)}
|
||||||
src={buildImgUrl(fileId)}
|
src={buildImgUrl(fileId)}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -59,6 +59,7 @@ import { Tooltip } from "@/components/tooltip/Tooltip";
|
|||||||
import { useMouseTracking } from "./hooks";
|
import { useMouseTracking } from "./hooks";
|
||||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
||||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||||
|
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
||||||
|
|
||||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||||
SEARCH_TOOL_NAME,
|
SEARCH_TOOL_NAME,
|
||||||
@@ -153,6 +154,7 @@ export const AIMessage = ({
|
|||||||
handleForceSearch?: () => void;
|
handleForceSearch?: () => void;
|
||||||
retrievalDisabled?: boolean;
|
retrievalDisabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const toolCallGenerating = toolCall && !toolCall.tool_result;
|
||||||
const processContent = (content: string | JSX.Element) => {
|
const processContent = (content: string | JSX.Element) => {
|
||||||
if (typeof content !== "string") {
|
if (typeof content !== "string") {
|
||||||
return content;
|
return content;
|
||||||
@@ -168,7 +170,7 @@ export const AIMessage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return content + (!isComplete ? " [*]() " : "");
|
return content + (!isComplete && !toolCallGenerating ? " [*]() " : "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalContent = processContent(content as string);
|
const finalContent = processContent(content as string);
|
||||||
@@ -344,15 +346,7 @@ export const AIMessage = ({
|
|||||||
|
|
||||||
{toolCall &&
|
{toolCall &&
|
||||||
toolCall.tool_name === IMAGE_GENERATION_TOOL_NAME &&
|
toolCall.tool_name === IMAGE_GENERATION_TOOL_NAME &&
|
||||||
!toolCall.tool_result && (
|
!toolCall.tool_result && <GeneratingImageDisplay />}
|
||||||
<ToolRunDisplay
|
|
||||||
toolName={`Generating images`}
|
|
||||||
toolLogo={
|
|
||||||
<FiImage size={15} className="my-auto mr-1" />
|
|
||||||
}
|
|
||||||
isRunning={!toolCall.tool_result}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toolCall &&
|
{toolCall &&
|
||||||
toolCall.tool_name === INTERNET_SEARCH_TOOL_NAME && (
|
toolCall.tool_name === INTERNET_SEARCH_TOOL_NAME && (
|
||||||
@@ -369,7 +363,7 @@ export const AIMessage = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{content ? (
|
{content || files ? (
|
||||||
<>
|
<>
|
||||||
<FileDisplay files={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 = ({
|
export const PaintingIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
|
@@ -210,7 +210,6 @@ export const FullSearchBar = ({
|
|||||||
<div
|
<div
|
||||||
className={`flex 2xl:justify-end justify-between w-full items-center space-x-3 px-4 pb-2`}
|
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">
|
<div className="2xl:hidden">
|
||||||
{(ccPairs.length > 0 || documentSets.length > 0) && (
|
{(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||||
<HorizontalSourceSelector
|
<HorizontalSourceSelector
|
||||||
@@ -223,8 +222,6 @@ export const FullSearchBar = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* ccPairs, documentSets, filterManager, finalAvailableDocumentSets, finalAvailableSources, tags */}
|
|
||||||
{/* </div>/ */}
|
|
||||||
<div className="flex my-auto gap-x-3">
|
<div className="flex my-auto gap-x-3">
|
||||||
{toggleAgentic && (
|
{toggleAgentic && (
|
||||||
<AnimatedToggle isOn={agentic!} handleToggle={toggleAgentic} />
|
<AnimatedToggle isOn={agentic!} handleToggle={toggleAgentic} />
|
||||||
|
@@ -81,7 +81,7 @@ export function SourceSelector({
|
|||||||
showDocSidebar ? "4xl:block" : "!block"
|
showDocSidebar ? "4xl:block" : "!block"
|
||||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
} 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>
|
<h2 className="font-bold my-auto">Filters</h2>
|
||||||
<FiFilter className="my-auto ml-2" size="16" />
|
<FiFilter className="my-auto ml-2" size="16" />
|
||||||
</div>
|
</div>
|
||||||
@@ -469,46 +469,6 @@ export function HorizontalSourceSelector({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user