Skip to content

Commit

Permalink
feat: Automatically save artifact content on change
Browse files Browse the repository at this point in the history
  • Loading branch information
bracesproul committed Oct 24, 2024
1 parent 7d64e3f commit 9ed5e4d
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 32 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"framer-motion": "^11.11.9",
"js-cookie": "^3.0.5",
"langsmith": "^0.1.61",
"lodash": "^4.17.21",
"lucide-react": "^0.441.0",
"next": "14.2.7",
"react": "^18",
Expand All @@ -79,6 +80,7 @@
"@eslint/js": "^9.12.0",
"@types/eslint__js": "^8.42.3",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.12",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
8 changes: 8 additions & 0 deletions src/components/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export function Canvas(props: CanvasProps) {
artifact,
setSelectedBlocks,
isStreaming,
updateRenderedArtifactRequired,
setUpdateRenderedArtifactRequired,
isArtifactSaved,
} = useGraph({
userId: props.user.id,
threadId,
Expand Down Expand Up @@ -187,6 +190,7 @@ export function Canvas(props: CanvasProps) {
{chatStarted && (
<div className="w-full ml-auto">
<ArtifactRenderer
isArtifactSaved={isArtifactSaved}
artifact={artifact}
setArtifact={setArtifact}
setSelectedBlocks={setSelectedBlocks}
Expand All @@ -203,6 +207,10 @@ export function Canvas(props: CanvasProps) {
setMessages={setMessages}
streamMessage={streamMessage}
isStreaming={isStreaming}
updateRenderedArtifactRequired={updateRenderedArtifactRequired}
setUpdateRenderedArtifactRequired={
setUpdateRenderedArtifactRequired
}
/>
</div>
)}
Expand Down
57 changes: 46 additions & 11 deletions src/components/artifacts/ArtifactRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import {
} from "@/types";
import { EditorView } from "@codemirror/view";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { CircleArrowUp, Forward, Copy } from "lucide-react";
import {
CircleArrowUp,
Forward,
Copy,
LoaderCircle,
CircleCheck,
} from "lucide-react";
import {
Dispatch,
FormEvent,
Expand Down Expand Up @@ -51,6 +57,9 @@ export interface ArtifactRendererProps {
handleGetReflections: () => Promise<void>;
setSelectedBlocks: Dispatch<SetStateAction<TextHighlight | undefined>>;
isStreaming: boolean;
updateRenderedArtifactRequired: boolean;
setUpdateRenderedArtifactRequired: Dispatch<SetStateAction<boolean>>;
isArtifactSaved: boolean;
}

interface SelectionBox {
Expand Down Expand Up @@ -261,18 +270,34 @@ export function ArtifactRenderer(props: ArtifactRendererProps) {
}
const currentArtifactContent = getArtifactContent(props.artifact);
const isBackwardsDisabled =
props.artifact.contents.length === 1 || currentArtifactContent.index === 1;
props.artifact.contents.length === 1 ||
currentArtifactContent.index === 1 ||
props.isStreaming;
const isForwardDisabled =
props.artifact.contents.length === 1 ||
currentArtifactContent.index === props.artifact.contents.length;
currentArtifactContent.index === props.artifact.contents.length ||
props.isStreaming;

return (
<div className="relative w-full h-full max-h-screen overflow-auto">
<div className="flex flex-row items-center justify-between">
<div className="pl-[6px] pt-3 flex flex-row items-center justify-start">
<h1 className="text-xl font-medium text-gray-600 ml-[6px]">
<div className="pl-[6px] pt-3 flex flex-col items-start justify-start ml-[6px] gap-1">
<h1 className="text-xl font-medium text-gray-600 ">
{currentArtifactContent.title}
</h1>
<span className="mt-auto">
{props.isArtifactSaved ? (
<span className="flex items-center justify-start gap-1 text-gray-400">
<p className="text-xs font-light">Saved</p>
<CircleCheck className="w-[10px] h-[10px]" />
</span>
) : (
<span className="flex items-center justify-start gap-1 text-gray-400">
<p className="text-xs font-light">Saving</p>
<LoaderCircle className="animate-spin w-[10px] h-[10px]" />
</span>
)}
</span>
</div>
<div className="absolute left-1/2 transform -translate-x-1/2 flex items-center justify-center gap-3 text-gray-600">
<TooltipIconButton
Expand All @@ -281,9 +306,11 @@ export function ArtifactRenderer(props: ArtifactRendererProps) {
variant="ghost"
className="transition-colors w-fit h-fit p-2"
delayDuration={400}
onClick={() =>
props.setSelectedArtifact(currentArtifactContent.index - 1)
}
onClick={() => {
if (!isBackwardsDisabled) {
props.setSelectedArtifact(currentArtifactContent.index - 1);
}
}}
disabled={isBackwardsDisabled}
>
<Forward
Expand All @@ -302,9 +329,11 @@ export function ArtifactRenderer(props: ArtifactRendererProps) {
side="right"
className="transition-colors w-fit h-fit p-2"
delayDuration={400}
onClick={() =>
props.setSelectedArtifact(currentArtifactContent.index + 1)
}
onClick={() => {
if (!isForwardDisabled) {
props.setSelectedArtifact(currentArtifactContent.index + 1);
}
}}
disabled={isForwardDisabled}
>
<Forward
Expand Down Expand Up @@ -375,6 +404,12 @@ export function ArtifactRenderer(props: ArtifactRendererProps) {
setArtifact={props.setArtifact}
setSelectedBlocks={props.setSelectedBlocks}
isEditing={props.isEditing}
updateRenderedArtifactRequired={
props.updateRenderedArtifactRequired
}
setUpdateRenderedArtifactRequired={
props.setUpdateRenderedArtifactRequired
}
/>
) : null}
{currentArtifactContent.type === "code" ? (
Expand Down
30 changes: 18 additions & 12 deletions src/components/artifacts/TextRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";

import { ArtifactMarkdownV3, ArtifactV3, TextHighlight } from "@/types";
import "@blocknote/core/fonts/inter.css";
import {
Expand All @@ -22,11 +21,13 @@ export interface TextRendererProps {
setSelectedBlocks: Dispatch<SetStateAction<TextHighlight | undefined>>;
isStreaming: boolean;
isInputVisible: boolean;
updateRenderedArtifactRequired: boolean;
setUpdateRenderedArtifactRequired: Dispatch<SetStateAction<boolean>>;
}

export function TextRenderer(props: TextRendererProps) {
const editor = useCreateBlockNote({});
const currentContentIndex = useRef<number | undefined>(undefined);

const [manuallyUpdatingArtifact, setManuallyUpdatingArtifact] =
useState(false);

Expand Down Expand Up @@ -77,7 +78,11 @@ export function TextRenderer(props: TextRendererProps) {
if (!props.artifact) {
return;
}
if (!props.isStreaming && !manuallyUpdatingArtifact) {
if (
!props.isStreaming &&
!manuallyUpdatingArtifact &&
!props.updateRenderedArtifactRequired
) {
console.error("Can only update via useEffect when streaming");
return;
}
Expand All @@ -95,23 +100,24 @@ export function TextRenderer(props: TextRendererProps) {
currentContent.fullMarkdown
);
editor.replaceBlocks(editor.document, markdownAsBlocks);
props.setUpdateRenderedArtifactRequired(false);
setManuallyUpdatingArtifact(false);
})();
} finally {
setManuallyUpdatingArtifact(false);
props.setUpdateRenderedArtifactRequired(false);
}
}, [props.artifact]);

useEffect(() => {
if (props.isStreaming) return;
if (props.artifact?.currentIndex === currentContentIndex.current) return;
currentContentIndex.current = props.artifact?.currentIndex;
setManuallyUpdatingArtifact(true);
}, [props.artifact?.currentIndex]);
}, [props.artifact, props.updateRenderedArtifactRequired]);

const isComposition = useRef(false);

const onChange = async () => {
if (props.isStreaming || manuallyUpdatingArtifact) return;
if (
props.isStreaming ||
manuallyUpdatingArtifact ||
props.updateRenderedArtifactRequired
)
return;

const fullMarkdown = await editor.blocksToMarkdownLossy(editor.document);
props.setArtifact((prev) => {
Expand Down
84 changes: 75 additions & 9 deletions src/hooks/use-graph/useGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { AIMessage, BaseMessage } from "@langchain/core/messages";
import { useToast } from "../use-toast";
import { createClient } from "../utils";
Expand Down Expand Up @@ -33,6 +33,7 @@ import {
isArtifactMarkdownContent,
isDeprecatedArtifactType,
} from "@/lib/artifact_content_types";
import { debounce } from "lodash";
// import { DEFAULT_ARTIFACTS, DEFAULT_MESSAGES } from "@/lib/dummy";

export interface GraphInput {
Expand Down Expand Up @@ -80,6 +81,64 @@ export function useGraph(useGraphInput: UseGraphInput) {
const [artifact, setArtifact] = useState<ArtifactV3>();
const [selectedBlocks, setSelectedBlocks] = useState<TextHighlight>();
const [isStreaming, setIsStreaming] = useState(false);
const [updateRenderedArtifactRequired, setUpdateRenderedArtifactRequired] =
useState(false);
const lastSavedArtifact = useRef<ArtifactV3 | undefined>(undefined);
const debouncedAPIUpdate = useRef(
debounce(
(artifact: ArtifactV3, threadId: string) =>
updateArtifact(artifact, threadId),
5000
)
).current;
const [isArtifactSaved, setIsArtifactSaved] = useState(true);

useEffect(() => {
return () => {
debouncedAPIUpdate.cancel();
};
}, [debouncedAPIUpdate]);

useEffect(() => {
if (!artifact) return;
if (!useGraphInput.threadId) return;
if (isStreaming) return;
if (updateRenderedArtifactRequired) return;
const currentIndex = artifact.currentIndex;
const currentContent = artifact.contents.find(
(c) => c.index === currentIndex
);
if (!currentContent) return;

if (
!lastSavedArtifact.current ||
lastSavedArtifact.current.contents !== artifact.contents
) {
setIsArtifactSaved(false);
// This means the artifact in state does not match the last saved artifact
// We need to update
debouncedAPIUpdate(artifact, useGraphInput.threadId);
}
}, [artifact]);

const updateArtifact = async (
artifactToUpdate: ArtifactV3,
threadId: string
) => {
try {
const client = createClient();
await client.threads.updateState(threadId, {
values: {
artifact: artifactToUpdate,
},
});
setIsArtifactSaved(true);
lastSavedArtifact.current = artifactToUpdate;
} catch (e) {
console.error("Failed to update artifact", e);
console.error("Artifact:", artifactToUpdate);
}
};

const clearState = () => {
setMessages([]);
Expand Down Expand Up @@ -539,6 +598,7 @@ export function useGraph(useGraphInput: UseGraphInput) {
);
}
}
lastSavedArtifact.current = artifact;
} catch (e) {
console.error("Failed to stream message", e);
} finally {
Expand Down Expand Up @@ -609,7 +669,7 @@ export function useGraph(useGraphInput: UseGraphInput) {
};

const setSelectedArtifact = (index: number) => {
setIsStreaming(true);
setUpdateRenderedArtifactRequired(true);
setArtifact((prev) => {
if (!prev) {
toast({
Expand All @@ -618,16 +678,17 @@ export function useGraph(useGraphInput: UseGraphInput) {
});
return prev;
}
return {
const newArtifact = {
...prev,
currentIndex: index,
};
lastSavedArtifact.current = newArtifact;
return newArtifact;
});
setIsStreaming(false);
};

const setArtifactContent = (index: number, content: string) => {
setIsStreaming(true);
setUpdateRenderedArtifactRequired(true);
setArtifact((prev) => {
if (!prev) {
toast({
Expand All @@ -636,7 +697,7 @@ export function useGraph(useGraphInput: UseGraphInput) {
});
return prev;
}
return {
const newArtifact = {
...prev,
currentIndex: index,
contents: prev.contents.map((a) => {
Expand All @@ -649,17 +710,19 @@ export function useGraph(useGraphInput: UseGraphInput) {
return a;
}),
};
lastSavedArtifact.current = newArtifact;
return newArtifact;
});
setIsStreaming(false);
};

const switchSelectedThread = (
thread: Thread,
setThreadId: (id: string) => void
) => {
setUpdateRenderedArtifactRequired(true);
setThreadId(thread.thread_id);
setCookie(THREAD_ID_COOKIE_NAME, thread.thread_id);
console.log("thrad.values", thread.values);

const castValues: {
artifact: ArtifactV3 | undefined;
messages: Record<string, any>[] | undefined;
Expand All @@ -677,6 +740,7 @@ export function useGraph(useGraphInput: UseGraphInput) {
} else {
castValues.artifact = undefined;
}
lastSavedArtifact.current = castValues?.artifact;

if (!castValues?.messages?.length) {
setMessages([]);
Expand All @@ -696,7 +760,6 @@ export function useGraph(useGraphInput: UseGraphInput) {
.split("/")[0],
});
}

return msg as BaseMessage;
})
);
Expand All @@ -715,5 +778,8 @@ export function useGraph(useGraphInput: UseGraphInput) {
setArtifactContent,
clearState,
switchSelectedThread,
updateRenderedArtifactRequired,
setUpdateRenderedArtifactRequired,
isArtifactSaved,
};
}
Loading

0 comments on commit 9ed5e4d

Please sign in to comment.