From d29cdbb8d654d76785ee1f0d97c0056873bbc462 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Sun, 20 Oct 2024 18:00:30 -0700 Subject: [PATCH 01/29] feat: Custom quick actions --- src/agent/open-canvas/index.ts | 3 + src/agent/open-canvas/nodes/customAction.ts | 121 ++++++++++++++++++++ src/agent/open-canvas/nodes/generatePath.ts | 6 + src/agent/open-canvas/prompts.ts | 26 +++++ src/agent/open-canvas/state.ts | 4 + src/types.ts | 28 +++++ 6 files changed, 188 insertions(+) create mode 100644 src/agent/open-canvas/nodes/customAction.ts diff --git a/src/agent/open-canvas/index.ts b/src/agent/open-canvas/index.ts index 203f80d5..f90fb11f 100644 --- a/src/agent/open-canvas/index.ts +++ b/src/agent/open-canvas/index.ts @@ -9,6 +9,7 @@ import { updateArtifact } from "./nodes/updateArtifact"; import { respondToQuery } from "./nodes/respondToQuery"; import { rewriteCodeArtifactTheme } from "./nodes/rewriteCodeArtifactTheme"; import { reflectNode } from "./nodes/reflect"; +import { customAction } from "./nodes/customAction"; const defaultInputs: Omit< typeof OpenCanvasGraphAnnotation.State, @@ -25,6 +26,7 @@ const defaultInputs: Omit< fixBugs: undefined, portLanguage: undefined, lastNodeName: undefined, + customQuickActionId: undefined, }; const routeNode = (state: typeof OpenCanvasGraphAnnotation.State) => { @@ -54,6 +56,7 @@ const builder = new StateGraph(OpenCanvasGraphAnnotation) .addNode("rewriteCodeArtifactTheme", rewriteCodeArtifactTheme) .addNode("updateArtifact", updateArtifact) .addNode("generateArtifact", generateArtifact) + .addNode("customAction", customAction) .addNode("generateFollowup", generateFollowup) .addNode("cleanState", cleanState) .addNode("reflect", reflectNode) diff --git a/src/agent/open-canvas/nodes/customAction.ts b/src/agent/open-canvas/nodes/customAction.ts new file mode 100644 index 00000000..f28f4192 --- /dev/null +++ b/src/agent/open-canvas/nodes/customAction.ts @@ -0,0 +1,121 @@ +import { ChatOpenAI } from "@langchain/openai"; +import { OpenCanvasGraphAnnotation, OpenCanvasGraphReturnType } from "../state"; +import { + CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT, + CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX, + CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT, + REFLECTIONS_QUICK_ACTION_PROMPT, +} from "../prompts"; +import { ensureStoreInConfig, formatReflections } from "../../utils"; +import { + ArtifactContent, + CustomQuickAction, + Reflections, +} from "../../../types"; +import { LangGraphRunnableConfig } from "@langchain/langgraph"; +import { BaseMessage } from "@langchain/core/messages"; + +const formatMessages = (messages: BaseMessage[]): string => + messages + .map( + (msg) => + `<${msg.getType()}>\n${msg.content as string}\n` + ) + .join("\n"); + +export const customAction = async ( + state: typeof OpenCanvasGraphAnnotation.State, + config: LangGraphRunnableConfig +): Promise => { + if (!state.customQuickActionId) { + throw new Error("No custom quick action ID found."); + } + + const smallModel = new ChatOpenAI({ + model: "gpt-4o-mini", + temperature: 0.5, + }); + + const store = ensureStoreInConfig(config); + const assistantId = config.configurable?.assistant_id; + if (!assistantId) { + throw new Error("`assistant_id` not found in configurable"); + } + const customActionsNamespace = ["custom_actions", assistantId]; + const actionsKey = "actions"; + + const memoryNamespace = ["memories", assistantId]; + const memoryKey = "reflection"; + + const [customActionsItem, memories] = await Promise.all([ + store.get(customActionsNamespace, actionsKey), + store.get(memoryNamespace, memoryKey), + ]); + if (!customActionsItem?.value) { + throw new Error("No custom actions found."); + } + const customQuickAction = customActionsItem.value[ + state.customQuickActionId + ] as CustomQuickAction | undefined; + if (!customQuickAction) { + throw new Error( + `No custom quick action found from ID ${state.customQuickActionId}` + ); + } + + let currentArtifactContent: ArtifactContent | undefined; + if (state.artifact) { + currentArtifactContent = state.artifact.contents.find( + (art) => art.index === state.artifact.currentContentIndex + ); + } + if (!currentArtifactContent) { + throw new Error("No artifact content found."); + } + + let formattedPrompt = `\n${customQuickAction.prompt}\n`; + if (customQuickAction.includeReflections && memories?.value) { + const memoriesAsString = formatReflections(memories.value as Reflections); + const reflectionsPrompt = REFLECTIONS_QUICK_ACTION_PROMPT.replace( + "{reflections}", + memoriesAsString + ); + formattedPrompt += `\n\n${reflectionsPrompt}`; + } + + if (customQuickAction.includePrefix) { + formattedPrompt = `${CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX}\n\n${formattedPrompt}`; + } + + if (customQuickAction.includeRecentHistory) { + const formattedConversationHistory = + CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT.replace( + "{conversation}", + formatMessages(state.messages.slice(-5)) + ); + formattedPrompt += `\n\n${formattedConversationHistory}`; + } + + formattedPrompt += `\n\n${CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT.replace("{artifactContent}", currentArtifactContent.content)}`; + + const newArtifactValues = await smallModel.invoke([ + { role: "user", content: formattedPrompt }, + ]); + + const newArtifact = { + ...state.artifact, + currentContentIndex: state.artifact.contents.length + 1, + contents: [ + ...state.artifact.contents, + { + ...currentArtifactContent, + index: state.artifact.contents.length + 1, + content: newArtifactValues.content as string, + }, + ], + }; + + return { + artifact: newArtifact, + }; +}; diff --git a/src/agent/open-canvas/nodes/generatePath.ts b/src/agent/open-canvas/nodes/generatePath.ts index cb79c449..d02a7046 100644 --- a/src/agent/open-canvas/nodes/generatePath.ts +++ b/src/agent/open-canvas/nodes/generatePath.ts @@ -45,6 +45,12 @@ export const generatePath = async ( }; } + if (state.customQuickActionId) { + return { + next: "customAction", + }; + } + let currentArtifactContent: ArtifactContent | undefined; if (state.artifact) { currentArtifactContent = state.artifact.contents.find( diff --git a/src/agent/open-canvas/prompts.ts b/src/agent/open-canvas/prompts.ts index 5faa3213..b4a91dfb 100644 --- a/src/agent/open-canvas/prompts.ts +++ b/src/agent/open-canvas/prompts.ts @@ -365,3 +365,29 @@ Rules and guidelines: - Ensure you do not port over language specific modules. E.g if the code contains imports from Node's fs module, you must use the closest equivalent in {newLanguage}. ${DEFAULT_CODE_PROMPT_RULES} `; + +export const REFLECTIONS_QUICK_ACTION_PROMPT = `The following are reflections on the user's style guidelines and general memories/facts about the user. +Use these reflections as context when generating your response. + +{reflections} +`; + +export const CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX = `You are an AI assistant tasked with rewriting a users generated artifact. +They have provided custom instructions on how you should manage rewriting the artifact. The custom instructions are wrapped inside the tags. + +Use this context about the application the user is interacting with when generating your response: + +The name of the application is "Open Canvas". Open Canvas is a web application where users have a chat window and a canvas to display an artifact. +Artifacts can be any sort of writing content, emails, code, or other creative writing work. Think of artifacts as content, or writing you might find on you might find on a blog, Google doc, or other writing platform. +Users only have a single artifact per conversation, however they have the ability to go back and fourth between artifact edits/revisions. +`; + +export const CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT = `Here is the last 5 (or less) messages in the chat history between you and the user: + +{conversation} +`; + +export const CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT = `Here is the full artifact content the user has generated, and is requesting you rewrite according to their custom instructions: + +{artifactContent} +`; diff --git a/src/agent/open-canvas/state.ts b/src/agent/open-canvas/state.ts index ecd3dde1..1d3c4c9e 100644 --- a/src/agent/open-canvas/state.ts +++ b/src/agent/open-canvas/state.ts @@ -59,6 +59,10 @@ export const OpenCanvasGraphAnnotation = Annotation.Root({ * The name of the last node that was executed. */ lastNodeName: Annotation, + /** + * The ID of the custom quick action to use. + */ + customQuickActionId: Annotation, }); export type OpenCanvasGraphReturnType = Partial< diff --git a/src/types.ts b/src/types.ts index a3e04b38..f86d1d86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,3 +82,31 @@ export interface Reflections { */ content: string[]; } + +export interface CustomQuickAction { + /** + * A UUID for the quick action. Used to identify the quick action. + */ + id: string; + /** + * The title of the quick action. Used in the UI + * to display the quick action. + */ + title: string; + /** + * The prompt to use when the quick action is invoked. + */ + prompt: string; + /** + * Whether or not to include the user's reflections in the prompt. + */ + includeReflections: boolean; + /** + * Whether or not to include the default prefix in the prompt. + */ + includePrefix: boolean; + /** + * Whether or not to include the last 5 (or less) messages in the prompt. + */ + includeRecentHistory: boolean; +} From 8d9b0a1d68f36e9b1cdf7e6a47f6c344ebab31a0 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Sun, 20 Oct 2024 18:53:16 -0700 Subject: [PATCH 02/29] add methods to useStore --- src/app/api/store/delete/id/route.ts | 60 +++++++++++++++++ src/app/api/store/delete/route.ts | 19 +----- src/app/api/store/get/route.ts | 19 +----- src/app/api/store/put/route.ts | 42 ++++++++++++ src/hooks/useStore.tsx | 97 +++++++++++++++++++++++++++- 5 files changed, 200 insertions(+), 37 deletions(-) create mode 100644 src/app/api/store/delete/id/route.ts create mode 100644 src/app/api/store/put/route.ts diff --git a/src/app/api/store/delete/id/route.ts b/src/app/api/store/delete/id/route.ts new file mode 100644 index 00000000..47cfcb93 --- /dev/null +++ b/src/app/api/store/delete/id/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Client } from "@langchain/langgraph-sdk"; +import { LANGGRAPH_API_URL } from "@/constants"; +import { User } from "@supabase/supabase-js"; +import { verifyUserAuthenticated } from "../../../../../lib/supabase/verify_user_server"; + +export async function POST(req: NextRequest) { + let user: User | undefined; + try { + user = await verifyUserAuthenticated(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } catch (e) { + console.error("Failed to fetch user", e); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { namespace, key, id } = await req.json(); + + const lgClient = new Client({ + apiKey: process.env.LANGCHAIN_API_KEY, + apiUrl: LANGGRAPH_API_URL, + }); + + try { + const currentItems = await lgClient.store.getItem(namespace, key); + if (!currentItems?.value) { + return new NextResponse( + JSON.stringify({ + error: "Item not found", + success: false, + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const newValues = Object.fromEntries( + Object.entries(currentItems.value).filter(([k]) => k !== id) + ); + + await lgClient.store.putItem(namespace, key, newValues); + + return new NextResponse(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (_) { + return new NextResponse( + JSON.stringify({ error: "Failed to share run after multiple attempts." }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } +} diff --git a/src/app/api/store/delete/route.ts b/src/app/api/store/delete/route.ts index a14aaf92..9478aaa4 100644 --- a/src/app/api/store/delete/route.ts +++ b/src/app/api/store/delete/route.ts @@ -16,30 +16,15 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { assistantId } = await req.json(); - - if (!assistantId) { - return new NextResponse( - JSON.stringify({ - error: "`assistantId` is required to fetch stored data.", - }), - { - status: 400, - headers: { "Content-Type": "application/json" }, - } - ); - } + const { namespace, key } = await req.json(); const lgClient = new Client({ apiKey: process.env.LANGCHAIN_API_KEY, apiUrl: LANGGRAPH_API_URL, }); - const memoryNamespace = ["memories", assistantId]; - const memoryKey = "reflection"; - try { - await lgClient.store.deleteItem(memoryNamespace, memoryKey); + await lgClient.store.deleteItem(namespace, key); return new NextResponse(JSON.stringify({ success: true }), { status: 200, diff --git a/src/app/api/store/get/route.ts b/src/app/api/store/get/route.ts index 2d820e41..b420e10c 100644 --- a/src/app/api/store/get/route.ts +++ b/src/app/api/store/get/route.ts @@ -16,30 +16,15 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { assistantId } = await req.json(); - - if (!assistantId) { - return new NextResponse( - JSON.stringify({ - error: "`assistantId` is required to fetch stored data.", - }), - { - status: 400, - headers: { "Content-Type": "application/json" }, - } - ); - } + const { namespace, key } = await req.json(); const lgClient = new Client({ apiKey: process.env.LANGCHAIN_API_KEY, apiUrl: LANGGRAPH_API_URL, }); - const memoryNamespace = ["memories", assistantId]; - const memoryKey = "reflection"; - try { - const item = await lgClient.store.getItem(memoryNamespace, memoryKey); + const item = await lgClient.store.getItem(namespace, key); return new NextResponse(JSON.stringify({ item }), { status: 200, diff --git a/src/app/api/store/put/route.ts b/src/app/api/store/put/route.ts new file mode 100644 index 00000000..8e46b1f4 --- /dev/null +++ b/src/app/api/store/put/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Client } from "@langchain/langgraph-sdk"; +import { LANGGRAPH_API_URL } from "@/constants"; +import { User } from "@supabase/supabase-js"; +import { verifyUserAuthenticated } from "../../../../lib/supabase/verify_user_server"; + +export async function POST(req: NextRequest) { + let user: User | undefined; + try { + user = await verifyUserAuthenticated(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } catch (e) { + console.error("Failed to fetch user", e); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { namespace, key, value } = await req.json(); + + const lgClient = new Client({ + apiKey: process.env.LANGCHAIN_API_KEY, + apiUrl: LANGGRAPH_API_URL, + }); + + try { + await lgClient.store.putItem(namespace, key, value); + + return new NextResponse(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (_) { + return new NextResponse( + JSON.stringify({ error: "Failed to share run after multiple attempts." }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } +} diff --git a/src/hooks/useStore.tsx b/src/hooks/useStore.tsx index 7661c24e..d06bc5d7 100644 --- a/src/hooks/useStore.tsx +++ b/src/hooks/useStore.tsx @@ -1,4 +1,4 @@ -import { Reflections } from "@/types"; +import { CustomQuickAction, Reflections } from "@/types"; import { useEffect, useState } from "react"; import { useToast } from "./use-toast"; @@ -28,7 +28,10 @@ export function useStore(assistantId: string | undefined) { setIsLoadingReflections(true); const res = await fetch("/api/store/get", { method: "POST", - body: JSON.stringify({ assistantId }), + body: JSON.stringify({ + namespace: ["memories", assistantId], + key: "reflection", + }), headers: { "Content-Type": "application/json", }, @@ -60,7 +63,10 @@ export function useStore(assistantId: string | undefined) { } const res = await fetch("/api/store/delete", { method: "POST", - body: JSON.stringify({ assistantId }), + body: JSON.stringify({ + namespace: ["memories", assistantId], + key: "reflection", + }), headers: { "Content-Type": "application/json", }, @@ -82,10 +88,95 @@ export function useStore(assistantId: string | undefined) { return success; }; + const getCustomQuickActions = async (): Promise< + CustomQuickAction[] | undefined + > => { + if (!assistantId) { + return undefined; + } + setIsLoadingReflections(true); + const res = await fetch("/api/store/get", { + method: "POST", + body: JSON.stringify({ + namespace: ["custom_actions", assistantId], + key: "actions", + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + return undefined; + } + + const { item } = await res.json(); + if (!item?.value) { + return undefined; + } + return Object.values(item?.value); + }; + + const deleteCustomQuickAction = async (id: string): Promise => { + if (!assistantId) { + return false; + } + const res = await fetch("/api/store/delete/id", { + method: "POST", + body: JSON.stringify({ + namespace: ["custom_actions", assistantId], + key: "actions", + id, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + return false; + } + + const { success } = await res.json(); + return success; + }; + + const createCustomQuickAction = async ( + action: CustomQuickAction + ): Promise => { + if (!assistantId) { + return false; + } + setIsLoadingReflections(true); + const res = await fetch("/api/store/put", { + method: "POST", + body: JSON.stringify({ + namespace: ["custom_actions", assistantId], + key: "actions", + value: { + [action.id]: action, + }, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + return false; + } + + const { success } = await res.json(); + return success; + }; + return { isLoadingReflections, reflections, deleteReflections, getReflections, + deleteCustomQuickAction, + getCustomQuickActions, + createCustomQuickAction, }; } From 53a65ec9f814d5bb98e5e99d024cc0049d6d3b64 Mon Sep 17 00:00:00 2001 From: Aadit Kamat Date: Mon, 21 Oct 2024 03:51:23 -0400 Subject: [PATCH 03/29] feat: Add SQL syntax highlighting --- package.json | 4 +++- src/agent/open-canvas/nodes/generateArtifact.ts | 2 +- .../open-canvas/nodes/rewriteCodeArtifactTheme.ts | 3 +++ src/components/ProgrammingLanguageList.tsx | 9 +++++++++ src/components/artifacts/CodeRenderer.tsx | 3 +++ .../actions_toolbar/code/PortToLanguage.tsx | 2 ++ src/lib/get_language_template.ts | 2 ++ src/types.ts | 3 ++- yarn.lock | 12 ++++++++++++ 9 files changed, 37 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a4e9a0ac..58a86df4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-php": "^6.0.1", "@codemirror/lang-python": "^6.1.6", + "@codemirror/lang-sql": "^6.8.0", "@langchain/anthropic": "^0.3.3", "@langchain/core": "^0.3.9", "@langchain/langgraph": "^0.2.10", @@ -82,5 +83,6 @@ "tsx": "^4.19.1", "typescript": "^5", "typescript-eslint": "^8.8.1" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/agent/open-canvas/nodes/generateArtifact.ts b/src/agent/open-canvas/nodes/generateArtifact.ts index b804c152..baf61c12 100644 --- a/src/agent/open-canvas/nodes/generateArtifact.ts +++ b/src/agent/open-canvas/nodes/generateArtifact.ts @@ -45,7 +45,7 @@ export const generateArtifact = async ( "The language of the artifact to generate. " + " If generating code, it should be the programming language. " + "For programming languages, ensure it's one of the following" + - "'javascript' | 'typescript' | 'cpp' | 'java' | 'php' | 'python' | 'html' | 'other'" + "'javascript' | 'typescript' | 'cpp' | 'java' | 'php' | 'python' | 'html' | 'sql' | 'other'" ), artifact: z .string() diff --git a/src/agent/open-canvas/nodes/rewriteCodeArtifactTheme.ts b/src/agent/open-canvas/nodes/rewriteCodeArtifactTheme.ts index d364e494..14427f5e 100644 --- a/src/agent/open-canvas/nodes/rewriteCodeArtifactTheme.ts +++ b/src/agent/open-canvas/nodes/rewriteCodeArtifactTheme.ts @@ -49,6 +49,9 @@ export const rewriteCodeArtifactTheme = async ( case "html": newLanguage = "HTML"; break; + case "sql": + newLanguage = "SQL"; + break; } formattedPrompt = PORT_LANGUAGE_CODE_ARTIFACT_PROMPT.replace( "{newLanguage}", diff --git a/src/components/ProgrammingLanguageList.tsx b/src/components/ProgrammingLanguageList.tsx index 9eb2a2d7..b6290d58 100644 --- a/src/components/ProgrammingLanguageList.tsx +++ b/src/components/ProgrammingLanguageList.tsx @@ -73,6 +73,15 @@ export function ProgrammingLanguageList( >

HTML

+ await props.handleSubmit("sql")} + > +

SQL

+
); } diff --git a/src/components/artifacts/CodeRenderer.tsx b/src/components/artifacts/CodeRenderer.tsx index 99593fad..f955709c 100644 --- a/src/components/artifacts/CodeRenderer.tsx +++ b/src/components/artifacts/CodeRenderer.tsx @@ -9,6 +9,7 @@ import { python } from "@codemirror/lang-python"; import styles from "./CodeRenderer.module.css"; import { cleanContent } from "@/lib/normalize_string"; import { html } from "@codemirror/lang-html"; +import { sql } from "@codemirror/lang-sql"; export interface CodeRendererProps { artifact: Artifact; @@ -32,6 +33,8 @@ export function CodeRenderer(props: Readonly) { extensions = [python()]; } else if (props.artifact.language === "html") { extensions = [html()]; + } else if (props.artifact.language === "sql") { + extensions = [sql()]; } if (!props.artifact.content) { diff --git a/src/components/artifacts/actions_toolbar/code/PortToLanguage.tsx b/src/components/artifacts/actions_toolbar/code/PortToLanguage.tsx index 8e8a8625..73d7875b 100644 --- a/src/components/artifacts/actions_toolbar/code/PortToLanguage.tsx +++ b/src/components/artifacts/actions_toolbar/code/PortToLanguage.tsx @@ -26,6 +26,8 @@ const prettifyLanguage = (language: ProgrammingLanguageOptions) => { return "Python"; case "html": return "HTML"; + case "sql": + return "SQL"; default: return language; } diff --git a/src/lib/get_language_template.ts b/src/lib/get_language_template.ts index 2f1e1084..84af4087 100644 --- a/src/lib/get_language_template.ts +++ b/src/lib/get_language_template.ts @@ -38,6 +38,8 @@ if __name__ == "__main__":

Hello, World!

`; + case "sql": + return `SELECT "Hello, World!";`; default: return "// No quickstart content available for this language"; } diff --git a/src/types.ts b/src/types.ts index e22ba470..b2acedbe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,7 +57,8 @@ export type ProgrammingLanguageOptions = | "java" | "php" | "python" - | "html"; + | "html" + | "sql"; export type ArtifactLengthOptions = "shortest" | "short" | "long" | "longest"; diff --git a/yarn.lock b/yarn.lock index 82f26588..e8488b7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,6 +181,18 @@ "@lezer/common" "^1.2.1" "@lezer/python" "^1.1.4" +"@codemirror/lang-sql@^6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz#1ae68ad49f378605ff88a4cc428ba667ce056068" + integrity sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@lezer/common" "^1.2.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + "@codemirror/language@^6.0.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0": version "6.10.3" resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.3.tgz#eb25fc5ade19032e7bf1dcaa957804e5f1660585" From 4620c093d5f389a60cea207f62fc468cda264e31 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 21 Oct 2024 10:10:04 -0700 Subject: [PATCH 04/29] formatting & add to rewriteArtifact --- src/agent/open-canvas/nodes/rewriteArtifact.ts | 1 + src/lib/get_language_template.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/open-canvas/nodes/rewriteArtifact.ts b/src/agent/open-canvas/nodes/rewriteArtifact.ts index 6eb23739..16769d3e 100644 --- a/src/agent/open-canvas/nodes/rewriteArtifact.ts +++ b/src/agent/open-canvas/nodes/rewriteArtifact.ts @@ -76,6 +76,7 @@ export const rewriteArtifact = async ( "php", "python", "html", + "sql", "other", ]) .optional() diff --git a/src/lib/get_language_template.ts b/src/lib/get_language_template.ts index 84af4087..1d3395f3 100644 --- a/src/lib/get_language_template.ts +++ b/src/lib/get_language_template.ts @@ -39,7 +39,7 @@ if __name__ == "__main__": `; case "sql": - return `SELECT "Hello, World!";`; + return `SELECT "Hello, World!";`; default: return "// No quickstart content available for this language"; } From 2b8bdff0bfe375a989226c1106ecd5b2bbd26224 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 21 Oct 2024 10:20:19 -0700 Subject: [PATCH 05/29] fix build --- src/components/artifacts/CodeRenderer.tsx | 2 +- src/constants.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/artifacts/CodeRenderer.tsx b/src/components/artifacts/CodeRenderer.tsx index c61d1308..9f699269 100644 --- a/src/components/artifacts/CodeRenderer.tsx +++ b/src/components/artifacts/CodeRenderer.tsx @@ -33,7 +33,7 @@ export function CodeRenderer(props: Readonly) { extensions = [python()]; } else if (props.artifactContent.language === "html") { extensions = [html()]; - } else if (props.artifact.language === "sql") { + } else if (props.artifactContent.language === "sql") { extensions = [sql()]; } diff --git a/src/constants.ts b/src/constants.ts index ff02e5b1..45bd1caf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ export const LANGGRAPH_API_URL = - process.env.LANGGRAPH_API_URL ?? "http://localhost:59894"; + process.env.LANGGRAPH_API_URL ?? "http://localhost:54591"; // v2 is tied to the 'open-canvas-prod' deployment. export const ASSISTANT_ID_COOKIE = "oc_assistant_id_v2"; // export const ASSISTANT_ID_COOKIE = "oc_assistant_id"; From 4d1c1ff7c4b976a92261e0e30b14b4cbd05f30bc Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 21 Oct 2024 10:20:48 -0700 Subject: [PATCH 06/29] drop pkg manager --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 2e284edd..9fb91af6 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,5 @@ "tsx": "^4.19.1", "typescript": "^5", "typescript-eslint": "^8.8.1" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } } From 8f4c015dc00c095e348fe62cf16f73c39433281d Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 21 Oct 2024 13:34:07 -0700 Subject: [PATCH 07/29] implemented UI --- package.json | 5 +- src/agent/open-canvas/index.ts | 1 + src/agent/open-canvas/prompts.ts | 2 +- src/app/globals.css | 8 + src/components/artifacts/ArtifactRenderer.tsx | 6 + .../actions_toolbar/custom/FullPrompt.tsx | 144 +++++++++++++ .../custom/NewCustomQuickActionDialog.tsx | 194 ++++++++++++++++++ .../custom/PromptContextTooltip.tsx | 31 +++ .../actions_toolbar/custom/index.tsx | 71 +++++++ src/components/ui/checkbox.tsx | 30 +++ src/components/ui/dropdown-menu.tsx | 2 +- src/components/ui/hover-card.tsx | 29 +++ src/constants.ts | 2 +- src/lib/dummy.ts | 85 ++++++++ yarn.lock | 89 +++++--- 15 files changed, 661 insertions(+), 38 deletions(-) create mode 100644 src/components/artifacts/actions_toolbar/custom/FullPrompt.tsx create mode 100644 src/components/artifacts/actions_toolbar/custom/NewCustomQuickActionDialog.tsx create mode 100644 src/components/artifacts/actions_toolbar/custom/PromptContextTooltip.tsx create mode 100644 src/components/artifacts/actions_toolbar/custom/index.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/hover-card.tsx diff --git a/package.json b/package.json index 9fb91af6..d37dc3e2 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,10 @@ "@langchain/langgraph-sdk": "^0.0.17", "@langchain/openai": "^0.3.5", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-progress": "^1.1.0", @@ -49,6 +51,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "dotenv": "^16.4.5", + "framer-motion": "^11.11.9", "js-cookie": "^3.0.5", "langsmith": "^0.1.61", "lucide-react": "^0.441.0", diff --git a/src/agent/open-canvas/index.ts b/src/agent/open-canvas/index.ts index f90fb11f..106a7f3d 100644 --- a/src/agent/open-canvas/index.ts +++ b/src/agent/open-canvas/index.ts @@ -68,6 +68,7 @@ const builder = new StateGraph(OpenCanvasGraphAnnotation) "respondToQuery", "generateArtifact", "rewriteArtifact", + "customAction", ]) // Edges .addEdge("generateArtifact", "generateFollowup") diff --git a/src/agent/open-canvas/prompts.ts b/src/agent/open-canvas/prompts.ts index b4a91dfb..8032a394 100644 --- a/src/agent/open-canvas/prompts.ts +++ b/src/agent/open-canvas/prompts.ts @@ -380,7 +380,7 @@ Use this context about the application the user is interacting with when generat The name of the application is "Open Canvas". Open Canvas is a web application where users have a chat window and a canvas to display an artifact. Artifacts can be any sort of writing content, emails, code, or other creative writing work. Think of artifacts as content, or writing you might find on you might find on a blog, Google doc, or other writing platform. Users only have a single artifact per conversation, however they have the ability to go back and fourth between artifact edits/revisions. -`; +`; export const CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT = `Here is the last 5 (or less) messages in the chat history between you and the user: diff --git a/src/app/globals.css b/src/app/globals.css index 4f52fd19..2bca7a54 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -122,3 +122,11 @@ a { 1.625em + 2px ); /* Adjust the base value (1.625em) if needed */ } + +.inline-code { + font-family: monospace; + background-color: #f0f0f0; + padding: 2px 4px; + border-radius: 4px; + font-size: 0.9em; +} diff --git a/src/components/artifacts/ArtifactRenderer.tsx b/src/components/artifacts/ArtifactRenderer.tsx index c2067ef1..532709bd 100644 --- a/src/components/artifacts/ArtifactRenderer.tsx +++ b/src/components/artifacts/ArtifactRenderer.tsx @@ -17,6 +17,7 @@ import { ActionsToolbar, CodeToolBar } from "./actions_toolbar"; import { CodeRenderer } from "./CodeRenderer"; import { TextRenderer } from "./TextRenderer"; import { getCurrentArtifactContent } from "@/lib/get_current_artifact"; +import { CustomQuickActions } from "./actions_toolbar/custom"; export interface ArtifactRendererProps { artifact: Artifact | undefined; @@ -408,6 +409,11 @@ export function ArtifactRenderer(props: ArtifactRendererProps) { )} + {currentArtifactContent.type === "text" ? ( ) : null} diff --git a/src/components/artifacts/actions_toolbar/custom/FullPrompt.tsx b/src/components/artifacts/actions_toolbar/custom/FullPrompt.tsx new file mode 100644 index 00000000..982c319a --- /dev/null +++ b/src/components/artifacts/actions_toolbar/custom/FullPrompt.tsx @@ -0,0 +1,144 @@ +import { + CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT, + CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX, + CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT, + REFLECTIONS_QUICK_ACTION_PROMPT, +} from "@/agent/open-canvas/prompts"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { CustomQuickAction } from "@/types"; +import { Dispatch, SetStateAction, useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; + +interface FullPromptProps { + customQuickAction: Omit; + setIncludeReflections: Dispatch>; + setIncludePrefix: Dispatch>; + setIncludeRecentHistory: Dispatch>; +} + +interface HighlightToDeleteTextProps { + text: string; + onClick: () => void; + highlight?: boolean; + isVisible: boolean; +} + +const HighlightToDeleteText = (props: HighlightToDeleteTextProps) => { + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isDeleting, setIsDeleting] = useState(false); + const [isHighlighted, setIsHighlighted] = useState(false); + + useEffect(() => { + if (isInitialLoad) { + setIsInitialLoad(false); + } else if (props.highlight) { + setIsHighlighted(true); + const timer = setTimeout(() => { + setIsHighlighted(false); + }, 1250); + return () => clearTimeout(timer); + } + }, [props.highlight]); + + const handleClick = () => { + setIsDeleting(true); + setTimeout(() => { + props.onClick(); + }, 300); + }; + + return ( + + {props.isVisible && ( + + + + + {props.text} + + + Click to delete + + + )} + + ); +}; + +export const FullPrompt = (props: FullPromptProps) => { + const [highlightPrefix, setHighlightPrefix] = useState( + props.customQuickAction.includePrefix + ); + const [highlightReflections, setHighlightReflections] = useState( + props.customQuickAction.includeReflections + ); + const [highlightRecentHistory, setHighlightRecentHistory] = useState( + props.customQuickAction.includeRecentHistory + ); + + useEffect(() => { + setHighlightPrefix(props.customQuickAction.includePrefix); + }, [props.customQuickAction.includePrefix]); + + useEffect(() => { + setHighlightReflections(props.customQuickAction.includeReflections); + }, [props.customQuickAction.includeReflections]); + + useEffect(() => { + setHighlightRecentHistory(props.customQuickAction.includeRecentHistory); + }, [props.customQuickAction.includeRecentHistory]); + + return ( +
+

+ props.setIncludePrefix(false)} + highlight={highlightPrefix} + isVisible={props.customQuickAction.includePrefix} + /> + {``} +
+ {props.customQuickAction.prompt} +
+ {`
`} + props.setIncludeReflections(false)} + highlight={highlightReflections} + isVisible={props.customQuickAction.includeReflections} + /> + props.setIncludeRecentHistory(false)} + highlight={highlightRecentHistory} + isVisible={props.customQuickAction.includeRecentHistory} + /> + {`\n\n`} + {CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT} +

+
+ ); +}; diff --git a/src/components/artifacts/actions_toolbar/custom/NewCustomQuickActionDialog.tsx b/src/components/artifacts/actions_toolbar/custom/NewCustomQuickActionDialog.tsx new file mode 100644 index 00000000..44656638 --- /dev/null +++ b/src/components/artifacts/actions_toolbar/custom/NewCustomQuickActionDialog.tsx @@ -0,0 +1,194 @@ +import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Eye, EyeOff } from "lucide-react"; +import { Dispatch, SetStateAction, useState } from "react"; +import { FullPrompt } from "./FullPrompt"; +import { InlineContextTooltip } from "./PromptContextTooltip"; + +interface NewCustomQuickActionDialogProps { + assistantId: string; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +interface ViewOrHidePromptIconProps { + showFullPrompt: boolean; + setShowFullPrompt: Dispatch>; +} + +const ViewOrHidePromptIcon = (props: ViewOrHidePromptIconProps) => ( + { + e.preventDefault(); + e.stopPropagation(); + props.setShowFullPrompt((p) => !p); + }} + > + {props.showFullPrompt ? ( + + ) : ( + + )} + +); + +export function NewCustomQuickActionDialog( + props: NewCustomQuickActionDialogProps +) { + const [name, setName] = useState(""); + const [prompt, setPrompt] = useState(""); + const [includeReflections, setIncludeReflections] = useState(true); + const [includePrefix, setIncludePrefix] = useState(true); + const [includeRecentHistory, setIncludeRecentHistory] = useState(true); + const [showFullPrompt, setShowFullPrompt] = useState(true); + + return ( + + + + + New Quick Action + + + Custom quick actions are a way to create your own actions to take + against the selected artifact. + + +
+ + setName(e.target.value)} + /> +
+ +

+ The full prompt includes predefined variables in curly braces + (e.g., {`{artifactContent}`}) + that will be replaced at runtime. Custom variables are not + supported yet. +

+ +
+
+

+ Custom instructions + +

+