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
7 changed files with 148 additions and 65 deletions

View File

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

View File

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

View File

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

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 = ({ export const PaintingIcon = ({
size = 16, size = 16,
className = defaultTailwindCSS, className = defaultTailwindCSS,

View File

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

View File

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