Add cleaner loading / streaming for image loading (#2055)

* add image loading

* clean

* add loading skeleton

* clean up

* clearer comments
This commit is contained in:
pablodanswer 2024-08-06 12:28:48 -07:00 committed by GitHub
parent cc3856ef6d
commit ab564a9ec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 148 additions and 65 deletions

View File

@ -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.

View File

@ -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>
</>
);
}

View File

@ -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 || []} />

View 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>
);
}

View File

@ -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,

View File

@ -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} />

View File

@ -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>
);
}