Skip to content

[Draft] Dylan/canvas-expansion #311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .cursor/rules/git-diff-minimization.mdc
Original file line number Diff line number Diff line change
@@ -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
62 changes: 34 additions & 28 deletions apps/web/src/components/artifacts/ArtifactLoading.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import { cn } from "@/lib/utils";
import { Skeleton } from "../ui/skeleton";
import { motion } from "framer-motion";

export function ArtifactLoading() {
return (
<div className="w-full h-full flex flex-col">
<div className="flex items-center justify-between m-4">
<Skeleton className="w-56 h-10" />
<div className="flex items-center justify-center gap-2">
<Skeleton className="w-6 h-6" />
<Skeleton className="w-16 h-6" />
<Skeleton className="w-6 h-6" />
<motion.div
className="w-[80%] max-w-3xl bg-white rounded-lg shadow-lg p-6"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<Skeleton className="w-56 h-8" />
<div className="flex items-center gap-2">
<Skeleton className="w-6 h-6 rounded-full" />
<Skeleton className="w-16 h-6" />
<Skeleton className="w-6 h-6 rounded-full" />
</div>
</div>
<div className="flex items-center justify-end gap-2">
<Skeleton className="w-10 h-10 rounded-md" />
<Skeleton className="w-10 h-10 rounded-md" />

<div className="flex flex-col gap-3">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton
key={i}
className={cn(
"h-6",
["w-1/4", "w-1/3", "w-2/5", "w-1/2", "w-3/5", "w-2/3", "w-3/4"][
Math.floor(Math.random() * 7)
]
)}
/>
))}
</div>

<div className="flex items-center justify-end gap-2 mt-4">
<Skeleton className="w-12 h-12 rounded-2xl" />
<Skeleton className="w-12 h-12 rounded-2xl" />
</div>
</div>
<div className="flex flex-col gap-1 m-4">
{Array.from({ length: 25 }).map((_, i) => (
<Skeleton
key={i}
className={cn(
"h-5",
["w-1/4", "w-1/3", "w-2/5", "w-1/2", "w-3/5", "w-2/3", "w-3/4"][
Math.floor(Math.random() * 7)
]
)}
/>
))}
</div>
<div className="flex items-center justify-end gap-2 mt-auto m-4">
<Skeleton className="w-12 h-12 rounded-2xl" />
<Skeleton className="w-12 h-12 rounded-2xl" />
</div>
</div>
</motion.div>
);
}
20 changes: 18 additions & 2 deletions apps/web/src/components/artifacts/ArtifactRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -288,10 +298,16 @@ function ArtifactRendererComponent(props: ArtifactRendererProps) {
? getArtifactContent(artifact)
: undefined;

if (!artifact && isStreaming) {
return <ArtifactLoading />;
// Show loading state only when generating a new artifact
if (showLoading) {
return (
<div className="w-full h-full flex items-center justify-center">
<ArtifactLoading />
</div>
);
}

// Show empty state when no artifact exists
if (!artifact || !currentArtifactContent) {
return <div className="w-full h-full"></div>;
}
Expand Down
145 changes: 98 additions & 47 deletions apps/web/src/components/canvas/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -67,6 +70,7 @@ export function CanvasComponent() {
return;
}
setChatStarted(true);
setIsArtifactAnimating(true);

let artifactContent: ArtifactCodeV3 | ArtifactMarkdownV3;
if (type === "code" && language) {
Expand All @@ -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 (
Expand All @@ -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<string, any>)?.messages?.length) {
setChatStarted(true);
if (thread?.metadata?.customModelName) {
Expand Down Expand Up @@ -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}
>
Expand All @@ -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<string, any>)?.messages?.length) {
setChatStarted(true);
if (thread?.metadata?.customModelName) {
Expand All @@ -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 {
Expand All @@ -195,44 +199,91 @@ export function CanvasComponent() {
</ResizablePanel>
)}

{chatStarted && (
<>
<ResizableHandle />
<ResizablePanel
defaultSize={chatCollapsed ? 100 : 75}
maxSize={85}
minSize={50}
id="canvas-panel"
order={2}
className="flex flex-row w-full"
>
<div className="w-full ml-auto">
<ArtifactRenderer
chatCollapsed={chatCollapsed}
setChatCollapsed={(c) => {
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}
/>
</div>
<WebSearchResults
open={webSearchResultsOpen}
setOpen={setWebSearchResultsOpen}
/>
</ResizablePanel>
</>
)}
{chatStarted &&
(graphData.artifact ||
(isStreaming && graphData.currentNode === "generateArtifact")) && (
<>
<ResizableHandle />
<ResizablePanel
defaultSize={chatCollapsed ? 100 : 75}
maxSize={85}
minSize={50}
id="canvas-panel"
order={2}
className="relative bg-white"
>
<motion.div
className="absolute inset-0 bg-gradient-to-b from-blue-50/50 to-white"
initial={
isArtifactAnimating
? {
scale: 0.4,
opacity: 0,
x: "50%",
y: "50%",
width: "60%",
height: "60%",
margin: "auto",
borderRadius: "1rem",
boxShadow:
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
}
: false
}
animate={
isArtifactAnimating
? {
scale: 1,
opacity: 1,
x: 0,
y: 0,
width: "100%",
height: "100%",
margin: 0,
borderRadius: 0,
boxShadow: "none",
transition: {
type: "spring",
stiffness: 200,
damping: 25,
duration: 1.5,
},
}
: false
}
>
<div className="w-full h-full">
<ArtifactRenderer
chatCollapsed={chatCollapsed}
setChatCollapsed={(c) => {
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}
/>
</div>
</motion.div>
{!chatCollapsed && (
<div className="absolute right-0 top-0 h-full">
<WebSearchResults
open={webSearchResultsOpen}
setOpen={setWebSearchResultsOpen}
/>
</div>
)}
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
);
}
Expand Down
30 changes: 17 additions & 13 deletions apps/web/src/components/chat-interface/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,23 @@ export const Thread: FC<ThreadProps> = (props: ThreadProps) => {
searchEnabled={props.searchEnabled}
/>
)}
<ThreadPrimitive.Messages
components={{
UserMessage: UserMessage,
AssistantMessage: (prop) => (
<AssistantMessage
{...prop}
feedbackSubmitted={feedbackSubmitted}
setFeedbackSubmitted={setFeedbackSubmitted}
runId={runId}
/>
),
}}
/>
<div className="flex justify-center">
<div className="w-full max-w-2xl">
<ThreadPrimitive.Messages
components={{
UserMessage: UserMessage,
AssistantMessage: (prop) => (
<AssistantMessage
{...prop}
feedbackSubmitted={feedbackSubmitted}
setFeedbackSubmitted={setFeedbackSubmitted}
runId={runId}
/>
),
}}
/>
</div>
</div>
</ThreadPrimitive.Viewport>
<div className="mt-4 flex w-full flex-col items-center justify-end rounded-t-lg bg-inherit pb-4 px-4">
<ThreadScrollToBottom />
Expand Down
Loading