diff --git a/.cursor/rules/git-diff-minimization.mdc b/.cursor/rules/git-diff-minimization.mdc new file mode 100644 index 00000000..31512a55 --- /dev/null +++ b/.cursor/rules/git-diff-minimization.mdc @@ -0,0 +1,16 @@ +--- +description: +globs: +--- +Please make only the necessary inline changes to accomplish this task without reformatting or displacing surrounding code. I need to keep my git diffs minimal. + +When editing, please use this format for your changes: +1. Identify exact line numbers needing modification +2. Show only the specific changes required (not entire functions) +3. Keep existing indentation and formatting + +When making changes: +- Do not reformat entire functions +- Maintain existing indentation style +- Do not adjust blank lines or spacing +- Do not rearrange imports or declarations \ No newline at end of file diff --git a/apps/web/src/components/artifacts/ArtifactLoading.tsx b/apps/web/src/components/artifacts/ArtifactLoading.tsx index a38a2771..015c9c82 100644 --- a/apps/web/src/components/artifacts/ArtifactLoading.tsx +++ b/apps/web/src/components/artifacts/ArtifactLoading.tsx @@ -1,38 +1,44 @@ import { cn } from "@/lib/utils"; import { Skeleton } from "../ui/skeleton"; +import { motion } from "framer-motion"; export function ArtifactLoading() { return ( -
-
- -
- - - + +
+
+ +
+ + + +
-
- - + +
+ {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,