-
-
-
+
+
+
-
-
-
+
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
-
- {Array.from({ length: 25 }).map((_, i) => (
-
- ))}
-
-
-
-
-
-
+
);
}
diff --git a/apps/web/src/components/artifacts/ArtifactRenderer.tsx b/apps/web/src/components/artifacts/ArtifactRenderer.tsx
index 7a2caf4a..93e857b9 100644
--- a/apps/web/src/components/artifacts/ArtifactRenderer.tsx
+++ b/apps/web/src/components/artifacts/ArtifactRenderer.tsx
@@ -64,6 +64,16 @@ function ArtifactRendererComponent(props: ArtifactRendererProps) {
const [inputValue, setInputValue] = useState("");
const [isHoveringOverArtifact, setIsHoveringOverArtifact] = useState(false);
const [isValidSelectionOrigin, setIsValidSelectionOrigin] = useState(false);
+ const [showLoading, setShowLoading] = useState(false);
+
+ useEffect(() => {
+ // Show loading state while generating artifact
+ if (isStreaming && graphData.currentNode === "generateArtifact") {
+ setShowLoading(true);
+ } else {
+ setShowLoading(false);
+ }
+ }, [isStreaming, graphData.currentNode]);
const handleMouseUp = useCallback(() => {
const selection = window.getSelection();
@@ -288,10 +298,16 @@ function ArtifactRendererComponent(props: ArtifactRendererProps) {
? getArtifactContent(artifact)
: undefined;
- if (!artifact && isStreaming) {
- return
;
+ // Show loading state only when generating a new artifact
+ if (showLoading) {
+ return (
+
+ );
}
+ // Show empty state when no artifact exists
if (!artifact || !currentArtifactContent) {
return
;
}
diff --git a/apps/web/src/components/canvas/canvas.tsx b/apps/web/src/components/canvas/canvas.tsx
index 237c0fac..9fbba759 100644
--- a/apps/web/src/components/canvas/canvas.tsx
+++ b/apps/web/src/components/canvas/canvas.tsx
@@ -28,19 +28,22 @@ import {
} from "@/components/ui/resizable";
import { CHAT_COLLAPSED_QUERY_PARAM } from "@/constants";
import { useRouter, useSearchParams } from "next/navigation";
+import { motion, AnimatePresence } from "framer-motion";
export function CanvasComponent() {
const { graphData } = useGraphContext();
const { setModelName, setModelConfig } = useThreadContext();
- const { setArtifact, chatStarted, setChatStarted } = graphData;
+ const { setArtifact, chatStarted, setChatStarted, isStreaming } = graphData;
const { toast } = useToast();
const [isEditing, setIsEditing] = useState(false);
const [webSearchResultsOpen, setWebSearchResultsOpen] = useState(false);
const [chatCollapsed, setChatCollapsed] = useState(false);
+ const [isArtifactAnimating, setIsArtifactAnimating] = useState(false);
const searchParams = useSearchParams();
const router = useRouter();
const chatCollapsedSearchParam = searchParams.get(CHAT_COLLAPSED_QUERY_PARAM);
+
useEffect(() => {
try {
if (chatCollapsedSearchParam) {
@@ -67,6 +70,7 @@ export function CanvasComponent() {
return;
}
setChatStarted(true);
+ setIsArtifactAnimating(true);
let artifactContent: ArtifactCodeV3 | ArtifactMarkdownV3;
if (type === "code" && language) {
@@ -90,11 +94,13 @@ export function CanvasComponent() {
currentIndex: 1,
contents: [artifactContent],
};
- // Do not worry about existing items in state. This should
- // never occur since this action can only be invoked if
- // there are no messages/artifacts in the thread.
setArtifact(newArtifact);
setIsEditing(true);
+
+ // Reset animation state after animation completes
+ setTimeout(() => {
+ setIsArtifactAnimating(false);
+ }, 2000);
};
return (
@@ -110,7 +116,6 @@ export function CanvasComponent() {
router.replace(`?${queryParams.toString()}`, { scroll: false });
}}
switchSelectedThreadCallback={(thread) => {
- // Chat should only be "started" if there are messages present
if ((thread.values as Record
)?.messages?.length) {
setChatStarted(true);
if (thread?.metadata?.customModelName) {
@@ -146,7 +151,7 @@ export function CanvasComponent() {
defaultSize={25}
minSize={15}
maxSize={50}
- className="transition-all duration-700 h-screen mr-auto bg-gray-50/70 shadow-inner-right"
+ className="h-screen bg-gray-50/70 shadow-inner-right"
id="chat-panel-main"
order={1}
>
@@ -162,7 +167,6 @@ export function CanvasComponent() {
router.replace(`?${queryParams.toString()}`, { scroll: false });
}}
switchSelectedThreadCallback={(thread) => {
- // Chat should only be "started" if there are messages present
if ((thread.values as Record)?.messages?.length) {
setChatStarted(true);
if (thread?.metadata?.customModelName) {
@@ -175,9 +179,9 @@ export function CanvasComponent() {
if (thread?.metadata?.modelConfig) {
setModelConfig(
- (thread?.metadata.customModelName ??
+ (thread?.metadata?.customModelName ??
DEFAULT_MODEL_NAME) as ALL_MODEL_NAMES,
- (thread.metadata.modelConfig ??
+ (thread.metadata?.modelConfig ??
DEFAULT_MODEL_CONFIG) as CustomModelConfig
);
} else {
@@ -195,44 +199,91 @@ export function CanvasComponent() {
)}
- {chatStarted && (
- <>
-
-
-
-
{
- setChatCollapsed(c);
- const queryParams = new URLSearchParams(
- searchParams.toString()
- );
- queryParams.set(
- CHAT_COLLAPSED_QUERY_PARAM,
- JSON.stringify(c)
- );
- router.replace(`?${queryParams.toString()}`, {
- scroll: false,
- });
- }}
- setIsEditing={setIsEditing}
- isEditing={isEditing}
- />
-
-
-
- >
- )}
+ {chatStarted &&
+ (graphData.artifact ||
+ (isStreaming && graphData.currentNode === "generateArtifact")) && (
+ <>
+
+
+
+
+
{
+ setChatCollapsed(c);
+ const queryParams = new URLSearchParams(
+ searchParams.toString()
+ );
+ queryParams.set(
+ CHAT_COLLAPSED_QUERY_PARAM,
+ JSON.stringify(c)
+ );
+ router.replace(`?${queryParams.toString()}`, {
+ scroll: false,
+ });
+ }}
+ setIsEditing={setIsEditing}
+ isEditing={isEditing}
+ />
+
+
+ {!chatCollapsed && (
+
+
+
+ )}
+
+ >
+ )}
);
}
diff --git a/apps/web/src/components/chat-interface/thread.tsx b/apps/web/src/components/chat-interface/thread.tsx
index 539a4748..bd2be4e3 100644
--- a/apps/web/src/components/chat-interface/thread.tsx
+++ b/apps/web/src/components/chat-interface/thread.tsx
@@ -149,19 +149,23 @@ export const Thread: FC = (props: ThreadProps) => {
searchEnabled={props.searchEnabled}
/>
)}
- (
-
- ),
- }}
- />
+
diff --git a/apps/web/src/contexts/GraphContext.tsx b/apps/web/src/contexts/GraphContext.tsx
index 645d5b28..aff80f96 100644
--- a/apps/web/src/contexts/GraphContext.tsx
+++ b/apps/web/src/contexts/GraphContext.tsx
@@ -77,6 +77,7 @@ interface GraphData {
artifactUpdateFailed: boolean;
chatStarted: boolean;
searchEnabled: boolean;
+ currentNode: string | undefined;
setSearchEnabled: Dispatch>;
setChatStarted: Dispatch>;
setIsStreaming: Dispatch>;
@@ -142,6 +143,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState(false);
const [artifactUpdateFailed, setArtifactUpdateFailed] = useState(false);
const [searchEnabled, setSearchEnabled] = useState(false);
+ const [currentNode, setCurrentNode] = useState(undefined);
const [_, setWebSearchResultsId] = useQueryState(
WEB_SEARCH_RESULTS_QUERY_PARAM
@@ -458,6 +460,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
langgraphNode
)
) {
+ setCurrentNode(langgraphNode);
const message = extractStreamDataChunk(nodeChunk);
if (!followupMessageId) {
followupMessageId = message.id;
@@ -468,6 +471,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
}
if (langgraphNode === "generateArtifact") {
+ setCurrentNode("generateArtifact");
const message = extractStreamDataChunk(nodeChunk);
// Accumulate content
@@ -1426,6 +1430,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
chatStarted,
artifactUpdateFailed,
searchEnabled,
+ currentNode,
setSearchEnabled,
setChatStarted,
setIsStreaming,