├── apps
├── web
│ ├── src
│ │ ├── components
│ │ │ ├── chat-interface
│ │ │ │ ├── index.tsx
│ │ │ │ ├── model-selector
│ │ │ │ │ └── new-badge.tsx
│ │ │ │ └── feedback.tsx
│ │ │ ├── canvas
│ │ │ │ ├── index.ts
│ │ │ │ └── canavas-loading.tsx
│ │ │ ├── artifacts
│ │ │ │ ├── actions_toolbar
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── code
│ │ │ │ │ │ └── PortToLanguage.tsx
│ │ │ │ │ └── text
│ │ │ │ │ │ ├── TranslateOptions.tsx
│ │ │ │ │ │ ├── ReadingLevelOptions.tsx
│ │ │ │ │ │ └── LengthOptions.tsx
│ │ │ │ ├── CodeRenderer.module.css
│ │ │ │ ├── ArtifactLoading.tsx
│ │ │ │ ├── header
│ │ │ │ │ ├── artifact-title.tsx
│ │ │ │ │ ├── navigate-artifact-history.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── TextRenderer.module.css
│ │ │ │ └── components
│ │ │ │ │ └── CopyText.tsx
│ │ │ ├── ui
│ │ │ │ ├── header.tsx
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── progress.tsx
│ │ │ │ ├── inline-context-tooltip.tsx
│ │ │ │ ├── assistant-ui
│ │ │ │ │ ├── attachment-adapters
│ │ │ │ │ │ ├── pdf.ts
│ │ │ │ │ │ ├── video.ts
│ │ │ │ │ │ └── audio.ts
│ │ │ │ │ ├── syntax-highlighter.tsx
│ │ │ │ │ ├── avatar.tsx
│ │ │ │ │ ├── utils
│ │ │ │ │ │ └── withDefaults.tsx
│ │ │ │ │ └── tooltip-icon-button.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── toaster.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ ├── hover-card.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── alert.tsx
│ │ │ │ ├── password-input.tsx
│ │ │ │ ├── resizable.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── accordion.tsx
│ │ │ │ └── slider.tsx
│ │ │ ├── NoSSRWrapper.tsx
│ │ │ ├── assistant-select
│ │ │ │ ├── edit-delete-dropdown.module.css
│ │ │ │ ├── utils.tsx
│ │ │ │ ├── color-picker.tsx
│ │ │ │ ├── assistant-item.tsx
│ │ │ │ └── context-documents
│ │ │ │ │ └── uploaded-file.tsx
│ │ │ ├── tool-hooks
│ │ │ │ ├── AttachmentsToolUI.tsx
│ │ │ │ └── LangSmithLinkToolUI.tsx
│ │ │ ├── auth
│ │ │ │ ├── login
│ │ │ │ │ └── actions.ts
│ │ │ │ └── signup
│ │ │ │ │ ├── actions.ts
│ │ │ │ │ └── success
│ │ │ │ │ └── index.tsx
│ │ │ ├── web-search-results
│ │ │ │ └── loading-cards.tsx
│ │ │ ├── icons
│ │ │ │ ├── svg
│ │ │ │ │ ├── TXTIcon.svg
│ │ │ │ │ ├── MP4Icon.svg
│ │ │ │ │ ├── PDFIcon.svg
│ │ │ │ │ └── MP3Icon.svg
│ │ │ │ └── magic_pencil.tsx
│ │ │ ├── assistant-ui
│ │ │ │ └── tooltip-icon-button.tsx
│ │ │ └── reflections-dialog
│ │ │ │ └── ConfirmClearDialog.tsx
│ │ ├── app
│ │ │ ├── favicon.ico
│ │ │ ├── auth
│ │ │ │ ├── signup
│ │ │ │ │ ├── success
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── login
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── signout
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── callback
│ │ │ │ │ └── route.ts
│ │ │ │ └── confirm
│ │ │ │ │ └── route.ts
│ │ │ ├── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── api
│ │ │ │ ├── store
│ │ │ │ ├── delete
│ │ │ │ │ ├── route.ts
│ │ │ │ │ └── id
│ │ │ │ │ │ └── route.ts
│ │ │ │ ├── get
│ │ │ │ │ └── route.ts
│ │ │ │ └── put
│ │ │ │ │ └── route.ts
│ │ │ │ ├── firecrawl
│ │ │ │ └── scrape
│ │ │ │ │ └── route.ts
│ │ │ │ ├── runs
│ │ │ │ ├── share
│ │ │ │ │ └── route.ts
│ │ │ │ └── feedback
│ │ │ │ │ └── route.ts
│ │ │ │ └── whisper
│ │ │ │ └── audio
│ │ │ │ └── route.ts
│ │ ├── lib
│ │ │ ├── store.ts
│ │ │ ├── utils.ts
│ │ │ ├── supabase
│ │ │ │ ├── client.ts
│ │ │ │ ├── verify_user_server.ts
│ │ │ │ ├── server.ts
│ │ │ │ └── middleware.ts
│ │ │ ├── normalize_string.ts
│ │ │ ├── cookies.ts
│ │ │ └── get_language_template.ts
│ │ ├── hooks
│ │ │ ├── utils.ts
│ │ │ ├── useRuns.tsx
│ │ │ ├── useLocalStorage.tsx
│ │ │ └── useFeedback.ts
│ │ ├── workers
│ │ │ └── graph-stream
│ │ │ │ ├── streamWorker.types.ts
│ │ │ │ ├── streamWorker.ts
│ │ │ │ └── stream.worker.ts
│ │ ├── middleware.ts
│ │ ├── constants.ts
│ │ ├── contexts
│ │ │ └── UserContext.tsx
│ │ └── types.ts
│ ├── public
│ │ ├── lc_logo.jpg
│ │ ├── screenshot.png
│ │ └── lg_studio_graph_diagram.png
│ ├── turbo.json
│ ├── postcss.config.mjs
│ ├── next.config.mjs
│ ├── ls.vitest.config.ts
│ ├── components.json
│ ├── .prettierrc
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── .eslintrc.json
│ ├── LICENSE
│ └── .env.example
└── agents
│ ├── turbo.json
│ ├── src
│ ├── summarizer
│ │ └── state.ts
│ ├── reflection
│ │ ├── state.ts
│ │ └── prompts.ts
│ ├── thread-title
│ │ ├── state.ts
│ │ └── prompts.ts
│ ├── web-search
│ │ ├── state.ts
│ │ ├── nodes
│ │ │ ├── search.ts
│ │ │ ├── classify-message.ts
│ │ │ └── query-generator.ts
│ │ └── index.ts
│ └── open-canvas
│ │ └── nodes
│ │ ├── summarizer.ts
│ │ ├── rewrite-artifact
│ │ ├── schemas.ts
│ │ └── update-meta.ts
│ │ ├── generate-artifact
│ │ ├── schemas.ts
│ │ ├── utils.ts
│ │ └── index.ts
│ │ ├── generateTitle.ts
│ │ ├── reflect.ts
│ │ ├── generateFollowup.ts
│ │ └── replyToGeneralInput.ts
│ ├── .prettierrc
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── .eslintrc.cjs
│ └── package.json
├── static
└── screenshot.png
├── packages
├── evals
│ ├── turbo.json
│ ├── src
│ │ ├── data
│ │ │ ├── codegen.ts
│ │ │ └── query_routing.ts
│ │ ├── highlights.ts
│ │ └── agent.int.test.ts
│ ├── .prettierrc
│ ├── tsconfig.json
│ ├── package.json
│ └── .eslintrc.cjs
└── shared
│ ├── turbo.json
│ ├── .prettierrc
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── src
│ ├── utils
│ │ ├── urls.ts
│ │ └── artifacts.ts
│ ├── prompts
│ │ └── quick-actions.ts
│ └── constants.ts
│ ├── package.json
│ └── .eslintrc.cjs
├── .vscode
└── settings.json
├── turbo.json
├── langgraph.json
├── tsconfig.json
├── .gitignore
├── .env.example
├── package.json
└── LICENSE
/apps/web/src/components/chat-interface/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./thread";
2 |
--------------------------------------------------------------------------------
/static/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sugarforever/blue-canvas/main/static/screenshot.png
--------------------------------------------------------------------------------
/apps/web/src/components/canvas/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./canvas";
2 | export * from "./canavas-loading";
3 |
--------------------------------------------------------------------------------
/apps/web/public/lc_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sugarforever/blue-canvas/main/apps/web/public/lc_logo.jpg
--------------------------------------------------------------------------------
/apps/web/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sugarforever/blue-canvas/main/apps/web/src/app/favicon.ico
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/actions_toolbar/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./text";
2 | export * from "./code";
3 |
--------------------------------------------------------------------------------
/apps/web/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sugarforever/blue-canvas/main/apps/web/public/screenshot.png
--------------------------------------------------------------------------------
/apps/web/public/lg_studio_graph_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sugarforever/blue-canvas/main/apps/web/public/lg_studio_graph_diagram.png
--------------------------------------------------------------------------------
/apps/agents/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["//"],
3 | "tasks": {
4 | "build": {
5 | "outputs": ["**/dist/**"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/evals/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["//"],
3 | "tasks": {
4 | "build": {
5 | "outputs": ["**/dist/**"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/shared/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["//"],
3 | "tasks": {
4 | "build": {
5 | "outputs": ["**/dist/**"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["//"],
3 | "tasks": {
4 | "build": {
5 | "outputs": [".next/**", "!.next/cache/**"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/apps/web/src/lib/store.ts:
--------------------------------------------------------------------------------
1 | export const USER_RULES_STORE_KEY = "rules";
2 |
3 | export const createNamespace = (assistantId: string) => {
4 | return ["assistant_id", assistantId, "userRules"];
5 | };
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "groq",
4 | "langchain",
5 | "langsmith",
6 | "opencanvas",
7 | "Signup",
8 | "sourounding",
9 | "Supabase"
10 | ]
11 | }
--------------------------------------------------------------------------------
/apps/web/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/utils.ts:
--------------------------------------------------------------------------------
1 | import { Client } from "@langchain/langgraph-sdk";
2 |
3 | export const createClient = () => {
4 | const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api";
5 | return new Client({
6 | apiUrl,
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/apps/web/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | webpack: (config, { isServer }) => {
4 | if (!isServer) {
5 | config.output.globalObject = 'self';
6 | }
7 | return config;
8 | },
9 | };
10 |
11 | export default nextConfig;
12 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | export function TighterText({
4 | className,
5 | children,
6 | }: {
7 | className?: string;
8 | children: React.ReactNode;
9 | }) {
10 | return
{children}
;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/evals/src/data/codegen.ts:
--------------------------------------------------------------------------------
1 | import { HumanMessage } from "@langchain/core/messages";
2 |
3 | export const CODEGEN_DATA: Record = {
4 | inputs: {
5 | messages: [
6 | new HumanMessage("Write me code for an LLM agent that does scraping"),
7 | ],
8 | next: "generateArtifact",
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/apps/web/src/app/auth/signup/success/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { UserProvider } from "@/contexts/UserContext";
4 | import { SignupSuccess } from "@/components/auth/signup/success";
5 |
6 | export default function Page() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/src/components/NoSSRWrapper.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import React from "react";
3 |
4 | const NoSSRWrapper: React.FC = (props) => (
5 | {props.children}
6 | );
7 |
8 | export default dynamic(() => Promise.resolve(NoSSRWrapper), {
9 | ssr: false,
10 | });
11 |
--------------------------------------------------------------------------------
/apps/web/src/components/assistant-select/edit-delete-dropdown.module.css:
--------------------------------------------------------------------------------
1 | .dropdownContent {
2 | width: 48px !important;
3 | min-width: 48px !important;
4 | padding: 4px !important;
5 | }
6 |
7 | .dropdownContent > [role="menuitem"] {
8 | width: 100% !important;
9 | padding: 4px !important;
10 | display: flex;
11 | justify-content: center;
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/apps/web/src/app/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Login } from "@/components/auth/login/Login";
4 | import { Suspense } from "react";
5 |
6 | export default function Page() {
7 | return (
8 |
9 | Loading...}>
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/src/app/auth/signup/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Signup } from "@/components/auth/signup/Signup";
4 | import { Suspense } from "react";
5 |
6 | export default function Page() {
7 | return (
8 |
9 | Loading...}>
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/ls.vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 | import path from "path";
3 |
4 | export default defineConfig({
5 | test: {
6 | include: ["**/*.eval.?(c|m)[jt]s"],
7 | reporters: ["langsmith/vitest/reporter"],
8 | setupFiles: ["dotenv/config"],
9 | },
10 | resolve: {
11 | alias: {
12 | "@": path.resolve(__dirname, "./src"),
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/src/components/assistant-select/utils.tsx:
--------------------------------------------------------------------------------
1 | import * as Icons from "lucide-react";
2 | import React from "react";
3 |
4 | export const getIcon = (iconName?: string) => {
5 | if (iconName && Icons[iconName as keyof typeof Icons]) {
6 | return React.createElement(
7 | Icons[iconName as keyof typeof Icons] as React.ElementType
8 | );
9 | }
10 | return React.createElement(Icons.User);
11 | };
12 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env"],
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
8 | },
9 | "lint": {
10 | "dependsOn": ["^lint"]
11 | },
12 | "lint:fix": {
13 | "dependsOn": ["^lint:fix"]
14 | },
15 | "format": {
16 | "dependsOn": ["^format"]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/langgraph.json:
--------------------------------------------------------------------------------
1 | {
2 | "node_version": "20",
3 | "dependencies": [
4 | "."
5 | ],
6 | "graphs": {
7 | "agent": "./apps/agents/src/open-canvas/index.ts:graph",
8 | "reflection": "./apps/agents/src/reflection/index.ts:graph",
9 | "thread_title": "./apps/agents/src/thread-title/index.ts:graph",
10 | "summarizer": "./apps/agents/src/summarizer/index.ts:graph",
11 | "web_search": "./apps/agents/src/web-search/index.ts:graph"
12 | },
13 | "env": ".env"
14 | }
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/apps/web/src/workers/graph-stream/streamWorker.types.ts:
--------------------------------------------------------------------------------
1 | import { ALL_MODEL_NAMES } from "@opencanvas/shared/models";
2 | import { CustomModelConfig, GraphInput } from "@opencanvas/shared/types";
3 |
4 | export interface StreamWorkerMessage {
5 | type: "chunk" | "done" | "error";
6 | data?: string;
7 | error?: string;
8 | }
9 |
10 | export interface StreamConfig {
11 | threadId: string;
12 | assistantId: string;
13 | input: GraphInput;
14 | modelName: ALL_MODEL_NAMES;
15 | modelConfigs: Record;
16 | }
17 |
--------------------------------------------------------------------------------
/apps/agents/src/summarizer/state.ts:
--------------------------------------------------------------------------------
1 | import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
2 |
3 | export const SummarizerGraphAnnotation = Annotation.Root({
4 | /**
5 | * The chat history to reflect on.
6 | */
7 | ...MessagesAnnotation.spec,
8 | /**
9 | * The original thread ID to use to update the message state.
10 | */
11 | threadId: Annotation,
12 | });
13 |
14 | export type SummarizeState = typeof SummarizerGraphAnnotation.State;
15 |
16 | export type SummarizeGraphReturnType = Partial;
17 |
--------------------------------------------------------------------------------
/apps/web/src/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserClient } from "@supabase/ssr";
2 |
3 | export function createSupabaseClient() {
4 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) {
5 | throw new Error("NEXT_PUBLIC_SUPABASE_URL is not defined");
6 | }
7 | if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
8 | throw new Error("NEXT_PUBLIC_SUPABASE_ANON_KEY is not defined");
9 | }
10 |
11 | return createBrowserClient(
12 | process.env.NEXT_PUBLIC_SUPABASE_URL,
13 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/apps/agents/src/reflection/state.ts:
--------------------------------------------------------------------------------
1 | import { ArtifactV3 } from "@opencanvas/shared/types";
2 | import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
3 |
4 | export const ReflectionGraphAnnotation = Annotation.Root({
5 | /**
6 | * The chat history to reflect on.
7 | */
8 | ...MessagesAnnotation.spec,
9 | /**
10 | * The artifact to reflect on.
11 | */
12 | artifact: Annotation,
13 | });
14 |
15 | export type ReflectionGraphReturnType = Partial<
16 | typeof ReflectionGraphAnnotation.State
17 | >;
18 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/CodeRenderer.module.css:
--------------------------------------------------------------------------------
1 | .codeMirrorCustom {
2 | height: 100vh !important;
3 | overflow: hidden;
4 | }
5 |
6 | .codeMirrorCustom :global(.cm-editor) {
7 | height: 100% !important;
8 | border: none !important;
9 | }
10 |
11 | .codeMirrorCustom :global(.cm-scroller) {
12 | overflow: auto;
13 | }
14 |
15 | .codeMirrorCustom :global(.cm-gutters) {
16 | height: 100% !important;
17 | border-right: none !important;
18 | }
19 |
20 | .codeMirrorCustom :global(.cm-focused) {
21 | outline: none !important;
22 | }
23 |
--------------------------------------------------------------------------------
/apps/agents/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "printWidth": 80,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "semi": true,
7 | "singleQuote": false,
8 | "quoteProps": "as-needed",
9 | "jsxSingleQuote": false,
10 | "trailingComma": "es5",
11 | "bracketSpacing": true,
12 | "arrowParens": "always",
13 | "requirePragma": false,
14 | "insertPragma": false,
15 | "proseWrap": "preserve",
16 | "htmlWhitespaceSensitivity": "css",
17 | "vueIndentScriptAndStyle": false,
18 | "endOfLine": "lf"
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "printWidth": 80,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "semi": true,
7 | "singleQuote": false,
8 | "quoteProps": "as-needed",
9 | "jsxSingleQuote": false,
10 | "trailingComma": "es5",
11 | "bracketSpacing": true,
12 | "arrowParens": "always",
13 | "requirePragma": false,
14 | "insertPragma": false,
15 | "proseWrap": "preserve",
16 | "htmlWhitespaceSensitivity": "css",
17 | "vueIndentScriptAndStyle": false,
18 | "endOfLine": "lf"
19 | }
20 |
--------------------------------------------------------------------------------
/packages/evals/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "printWidth": 80,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "semi": true,
7 | "singleQuote": false,
8 | "quoteProps": "as-needed",
9 | "jsxSingleQuote": false,
10 | "trailingComma": "es5",
11 | "bracketSpacing": true,
12 | "arrowParens": "always",
13 | "requirePragma": false,
14 | "insertPragma": false,
15 | "proseWrap": "preserve",
16 | "htmlWhitespaceSensitivity": "css",
17 | "vueIndentScriptAndStyle": false,
18 | "endOfLine": "lf"
19 | }
20 |
--------------------------------------------------------------------------------
/packages/shared/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "printWidth": 80,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "semi": true,
7 | "singleQuote": false,
8 | "quoteProps": "as-needed",
9 | "jsxSingleQuote": false,
10 | "trailingComma": "es5",
11 | "bracketSpacing": true,
12 | "arrowParens": "always",
13 | "requirePragma": false,
14 | "insertPragma": false,
15 | "proseWrap": "preserve",
16 | "htmlWhitespaceSensitivity": "css",
17 | "vueIndentScriptAndStyle": false,
18 | "endOfLine": "lf"
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/lib/supabase/verify_user_server.ts:
--------------------------------------------------------------------------------
1 | import { Session, User } from "@supabase/supabase-js";
2 | import { createClient } from "./server";
3 |
4 | export async function verifyUserAuthenticated(): Promise<
5 | { user: User; session: Session } | undefined
6 | > {
7 | const supabase = createClient();
8 | const {
9 | data: { user },
10 | } = await supabase.auth.getUser();
11 | const {
12 | data: { session },
13 | } = await supabase.auth.getSession();
14 | if (!user || !session) {
15 | return undefined;
16 | }
17 | return { user, session };
18 | }
19 |
--------------------------------------------------------------------------------
/apps/agents/src/thread-title/state.ts:
--------------------------------------------------------------------------------
1 | import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
2 | import { ArtifactV3 } from "@opencanvas/shared/types";
3 |
4 | export const TitleGenerationAnnotation = Annotation.Root({
5 | /**
6 | * The chat history to generate a title for
7 | */
8 | ...MessagesAnnotation.spec,
9 | /**
10 | * The artifact that was generated/updated (if any)
11 | */
12 | artifact: Annotation,
13 | });
14 |
15 | export type TitleGenerationReturnType = Partial<
16 | typeof TitleGenerationAnnotation.State
17 | >;
18 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/useRuns.tsx:
--------------------------------------------------------------------------------
1 | export function useRuns() {
2 | /**
3 | * Generates a public shared run ID for the given run ID.
4 | */
5 | const shareRun = async (runId: string): Promise => {
6 | const res = await fetch("/api/runs/share", {
7 | method: "POST",
8 | body: JSON.stringify({ runId }),
9 | headers: {
10 | "Content-Type": "application/json",
11 | },
12 | });
13 |
14 | if (!res.ok) {
15 | return;
16 | }
17 |
18 | const { sharedRunURL } = await res.json();
19 | return sharedRunURL;
20 | };
21 |
22 | return {
23 | shareRun,
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/src/components/tool-hooks/AttachmentsToolUI.tsx:
--------------------------------------------------------------------------------
1 | import { ContextDocument } from "@opencanvas/shared/types";
2 | import { HumanMessage } from "@langchain/core/messages";
3 | import { UploadedFiles } from "../assistant-select/context-documents/uploaded-file";
4 |
5 | export const ContextDocumentsUI = ({
6 | message,
7 | className,
8 | }: {
9 | message: HumanMessage | undefined;
10 | className?: string;
11 | }) => {
12 | const documents = message?.additional_kwargs?.documents as ContextDocument[];
13 | if (!documents?.length) {
14 | return null;
15 | }
16 |
17 | return ;
18 | };
19 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 | .yarn/cache
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 | .env
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
40 | credentials.json
41 |
42 | # LangGraph API
43 | .langgraph_api
44 |
--------------------------------------------------------------------------------
/apps/agents/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 | .yarn/cache
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 | .env
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
40 | credentials.json
41 |
42 | # LangGraph API
43 | .langgraph_api
44 |
--------------------------------------------------------------------------------
/packages/shared/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 | .yarn/cache
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 | .env
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
40 | credentials.json
41 |
42 | # LangGraph API
43 | .langgraph_api
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from "next/server";
2 | import { updateSession } from "@/lib/supabase/middleware";
3 |
4 | export async function middleware(request: NextRequest) {
5 | return await updateSession(request);
6 | }
7 |
8 | export const config = {
9 | matcher: [
10 | /*
11 | * Match all request paths except for the ones starting with:
12 | * - _next/static (static files)
13 | * - _next/image (image optimization files)
14 | * - favicon.ico (favicon file)
15 | * Feel free to modify this pattern to include more paths.
16 | */
17 | "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
18 | ],
19 | };
20 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | **/node_modules
6 | /.pnp
7 | .pnp.js
8 | .yarn/install-state.gz
9 | .yarn/cache
10 |
11 | # testing
12 | /coverage
13 |
14 | # next.js
15 | /.next/
16 | /out/
17 |
18 | # production
19 | /build
20 | /dist
21 | **/dist
22 | .turbo/
23 |
24 | # misc
25 | .DS_Store
26 | *.pem
27 |
28 | # debug
29 | npm-debug.log*
30 | yarn-debug.log*
31 | yarn-error.log*
32 |
33 | # local env files
34 | .env*.local
35 | .env
36 |
37 | # vercel
38 | .vercel
39 |
40 | # typescript
41 | *.tsbuildinfo
42 | next-env.d.ts
43 |
44 | credentials.json
45 |
46 | # LangGraph API
47 | .langgraph_api
48 |
--------------------------------------------------------------------------------
/apps/web/src/lib/normalize_string.ts:
--------------------------------------------------------------------------------
1 | const actualNewline = `
2 | `;
3 |
4 | export const cleanContent = (content: string): string => {
5 | return content ? content.replace(/\\n/g, actualNewline) : "";
6 | };
7 |
8 | export const reverseCleanContent = (content: string): string => {
9 | return content ? content.replaceAll(actualNewline, "\n") : "";
10 | };
11 |
12 | export const newlineToCarriageReturn = (str: string) =>
13 | // str.replace(actualNewline, "\r\n");
14 | str.replace(actualNewline, [actualNewline, actualNewline].join(""));
15 |
16 | export const emptyLineCount = (content: string): number => {
17 | const liens = content.split("\n");
18 | return liens.filter((line) => line.trim() == "").length;
19 | };
20 |
--------------------------------------------------------------------------------
/apps/web/src/components/auth/login/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { redirect } from "next/navigation";
5 |
6 | import { createClient } from "@/lib/supabase/server";
7 | import { LoginWithEmailInput } from "./Login";
8 |
9 | export async function login(input: LoginWithEmailInput) {
10 | const supabase = createClient();
11 |
12 | const data = {
13 | email: input.email,
14 | password: input.password,
15 | };
16 |
17 | const { error } = await supabase.auth.signInWithPassword(data);
18 |
19 | if (error) {
20 | console.error(error);
21 | redirect("/auth/login?error=true");
22 | }
23 |
24 | revalidatePath("/", "layout");
25 | redirect("/");
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "plugin:@typescript-eslint/recommended"
5 | ],
6 | "parser": "@typescript-eslint/parser",
7 | "plugins": ["@typescript-eslint", "unused-imports", "@typescript-eslint/eslint-plugin"],
8 | "rules": {
9 | "@typescript-eslint/no-unused-vars": [
10 | "error",
11 | {
12 | "argsIgnorePattern": "^_",
13 | "varsIgnorePattern": "^_|^UNUSED_",
14 | "caughtErrorsIgnorePattern": "^_",
15 | "destructuredArrayIgnorePattern": "^_"
16 | }
17 | ],
18 | "@typescript-eslint/no-explicit-any": "off",
19 | "@typescript-eslint/no-empty-object-type": "off",
20 | "unused-imports/no-unused-imports": "error"
21 | }
22 | }
--------------------------------------------------------------------------------
/apps/web/src/lib/cookies.ts:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 |
3 | export const getCookie = (name: string): string | undefined => {
4 | if (typeof window === "undefined") {
5 | return undefined;
6 | }
7 | return Cookies.get(name);
8 | };
9 |
10 | export const setCookie = (
11 | name: string,
12 | value: string,
13 | options?: Cookies.CookieAttributes
14 | ): void => {
15 | if (typeof window === "undefined") {
16 | return;
17 | }
18 | Cookies.set(name, value, {
19 | expires: 365, // Default to 1 year expiration
20 | ...(options || {}),
21 | });
22 | };
23 |
24 | export const removeCookie = (name: string): void => {
25 | if (typeof window === "undefined") {
26 | return;
27 | }
28 | Cookies.remove(name);
29 | };
30 |
--------------------------------------------------------------------------------
/apps/web/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Canvas } from "@/components/canvas";
4 | import { AssistantProvider } from "@/contexts/AssistantContext";
5 | import { GraphProvider } from "@/contexts/GraphContext";
6 | import { ThreadProvider } from "@/contexts/ThreadProvider";
7 | import { UserProvider } from "@/contexts/UserContext";
8 | import { Suspense } from "react";
9 |
10 | export default function Home() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import "./globals.css";
3 | import { Inter } from "next/font/google";
4 | import { cn } from "@/lib/utils";
5 | import { NuqsAdapter } from "nuqs/adapters/next/app";
6 |
7 | const inter = Inter({
8 | subsets: ["latin"],
9 | });
10 |
11 | export const metadata: Metadata = {
12 | title: "Open Canvas",
13 | description: "Open Canvas Chat UX by LangChain",
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | }>) {
21 | return (
22 |
23 |
24 | {children}
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/apps/agents/src/web-search/state.ts:
--------------------------------------------------------------------------------
1 | import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
2 | import { SearchResult } from "@opencanvas/shared/types";
3 |
4 | export const WebSearchGraphAnnotation = Annotation.Root({
5 | /**
6 | * The chat history to search the web for.
7 | * Will use the latest user message as the query.
8 | */
9 | ...MessagesAnnotation.spec,
10 | /**
11 | * The search query.
12 | */
13 | query: Annotation,
14 | /**
15 | * The search results
16 | */
17 | webSearchResults: Annotation,
18 | /**
19 | * Whether or not to search the web based on the user's latest message.
20 | */
21 | shouldSearch: Annotation,
22 | });
23 |
24 | export type WebSearchState = typeof WebSearchGraphAnnotation.State;
25 |
--------------------------------------------------------------------------------
/apps/agents/src/web-search/nodes/search.ts:
--------------------------------------------------------------------------------
1 | import { SearchResult } from "@opencanvas/shared/types";
2 | import { WebSearchState } from "../state.js";
3 | import ExaClient from "exa-js";
4 | import { ExaRetriever } from "@langchain/exa";
5 |
6 | export async function search(
7 | state: WebSearchState
8 | ): Promise> {
9 | const exaClient = new ExaClient(process.env.EXA_API_KEY || "");
10 | const retriever = new ExaRetriever({
11 | client: exaClient,
12 | searchArgs: {
13 | filterEmptyResults: true,
14 | numResults: 5,
15 | },
16 | });
17 |
18 | const query = state.messages[state.messages.length - 1].content as string;
19 | const results = await retriever.invoke(query);
20 |
21 | return {
22 | webSearchResults: results as SearchResult[],
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # This file should contain secrets which are used inside the agent (apps/agents)
2 |
3 | # LangSmith tracing
4 | LANGCHAIN_TRACING_V2="true"
5 | LANGCHAIN_API_KEY=""
6 | # Optional, unless using a non default project
7 | # LANGCHAIN_PROJECT=""
8 |
9 | # ---LLM API Keys---
10 |
11 | # Anthropic
12 | ANTHROPIC_API_KEY=""
13 | # OpenAI
14 | OPENAI_API_KEY=""
15 | # Azure OpenAI
16 | _AZURE_OPENAI_API_KEY=""
17 | _AZURE_OPENAI_API_DEPLOYMENT_NAME=""
18 | _AZURE_OPENAI_API_VERSION=""
19 | _AZURE_OPENAI_API_BASE_PATH=""
20 | # Fireworks
21 | FIREWORKS_API_KEY=""
22 | # Gemini
23 | GOOGLE_API_KEY=""
24 | # Groq - STT
25 | GROQ_API_KEY=""
26 | # ------------------
27 |
28 | # Supabase
29 | NEXT_PUBLIC_SUPABASE_URL=""
30 | NEXT_PUBLIC_SUPABASE_ANON_KEY=""
31 | SUPABASE_SERVICE_ROLE=""
32 |
33 | FIRECRAWL_API_KEY=""
34 |
--------------------------------------------------------------------------------
/packages/evals/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/recommended",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "target": "ES2021",
7 | "lib": [
8 | "ES2021",
9 | "ES2022.Object",
10 | "DOM"
11 | ],
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext",
14 | "esModuleInterop": true,
15 | "declaration": true,
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "useDefineForClassFields": true,
21 | "strictPropertyInitialization": false,
22 | "allowJs": true,
23 | "strict": true
24 | },
25 | "include": [
26 | "src/"
27 | ],
28 | "exclude": [
29 | "node_modules/",
30 | "dist"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/recommended",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "target": "ES2021",
7 | "lib": [
8 | "ES2021",
9 | "ES2022.Object",
10 | "DOM"
11 | ],
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext",
14 | "esModuleInterop": true,
15 | "declaration": true,
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "useDefineForClassFields": true,
21 | "strictPropertyInitialization": false,
22 | "allowJs": true,
23 | "strict": true
24 | },
25 | "include": [
26 | "src/"
27 | ],
28 | "exclude": [
29 | "node_modules/",
30 | "dist"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/apps/agents/src/thread-title/prompts.ts:
--------------------------------------------------------------------------------
1 | export const TITLE_SYSTEM_PROMPT = `You are tasked with generating a concise, descriptive title for a conversation between a user and an AI assistant. The title should capture the main topic or purpose of the conversation.
2 |
3 | Guidelines for title generation:
4 | - Keep titles extremely short (ideally 2-5 words)
5 | - Focus on the main topic or goal of the conversation
6 | - Use natural, readable language
7 | - Avoid unnecessary articles (a, an, the) when possible
8 | - Do not include quotes or special characters
9 | - Capitalize important words
10 |
11 | Use the 'generate_title' tool to output your title.`;
12 |
13 | export const TITLE_USER_PROMPT = `Based on the following conversation, generate a very short and descriptive title for:
14 |
15 | {conversation}
16 |
17 | {artifact_context}`;
18 |
--------------------------------------------------------------------------------
/apps/agents/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/recommended",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "baseUrl": ".",
7 | "target": "ES2021",
8 | "lib": [
9 | "ES2021",
10 | "ES2022.Object",
11 | "DOM",
12 | "es2023"
13 | ],
14 | "module": "NodeNext",
15 | "moduleResolution": "NodeNext",
16 | "esModuleInterop": true,
17 | "declaration": true,
18 | "noImplicitReturns": true,
19 | "noFallthroughCasesInSwitch": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "useDefineForClassFields": true,
23 | "strictPropertyInitialization": false,
24 | "allowJs": true,
25 | "strict": true
26 | },
27 | "include": [
28 | "src/"
29 | ],
30 | "exclude": [
31 | "node_modules/",
32 | "dist"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/summarizer.ts:
--------------------------------------------------------------------------------
1 | import { Client } from "@langchain/langgraph-sdk";
2 | import { OpenCanvasGraphAnnotation } from "../state.js";
3 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
4 |
5 | export async function summarizer(
6 | state: typeof OpenCanvasGraphAnnotation.State,
7 | config: LangGraphRunnableConfig
8 | ) {
9 | if (!config.configurable?.thread_id) {
10 | throw new Error("Missing thread_id in summarizer config.");
11 | }
12 |
13 | const client = new Client({
14 | apiUrl: `http://localhost:${process.env.PORT}`,
15 | });
16 |
17 | const { thread_id } = await client.threads.create();
18 | await client.runs.create(thread_id, "summarizer", {
19 | input: {
20 | messages: state._messages,
21 | threadId: config.configurable.thread_id,
22 | },
23 | });
24 |
25 | return {};
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/packages/shared/src/utils/urls.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Extracts all URLs from a given string
3 | * @param text The string to extract URLs from
4 | * @returns Array of URLs found in the text
5 | */
6 | export function extractUrls(text: string): string[] {
7 | const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
8 | const urls = new Set();
9 |
10 | // First replace all markdown links with spaces to avoid double-matching
11 | const processedText = text.replace(markdownLinkRegex, (match, _, url) => {
12 | urls.add(url);
13 | return " ".repeat(match.length); // Replace with spaces to preserve string length
14 | });
15 |
16 | // Then look for any remaining plain URLs in the text
17 | const plainUrlRegex = /https?:\/\/[^\s<\]]+(?:[^<.,:;"'\]\s)]|(?=\s|$))/g;
18 | const plainUrls = processedText.match(plainUrlRegex) || [];
19 | plainUrls.forEach((url) => urls.add(url));
20 |
21 | return Array.from(urls);
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ProgressPrimitive from "@radix-ui/react-progress";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/inline-context-tooltip.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | HoverCard,
3 | HoverCardContent,
4 | HoverCardTrigger,
5 | } from "@/components/ui/hover-card";
6 | import { cn } from "@/lib/utils";
7 | import { CircleHelp } from "lucide-react";
8 |
9 | export function InlineContextTooltip({
10 | cardContentClassName,
11 | children,
12 | }: {
13 | cardContentClassName?: string;
14 | children: React.ReactNode;
15 | }) {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 | What's this?
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/attachment-adapters/pdf.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AttachmentAdapter,
3 | CompleteAttachment,
4 | PendingAttachment,
5 | } from "@assistant-ui/react";
6 |
7 | export class PDFAttachmentAdapter implements AttachmentAdapter {
8 | public accept = "application/pdf,.pdf";
9 |
10 | public async add(state: { file: File }): Promise {
11 | return {
12 | id: state.file.name,
13 | type: "document",
14 | name: state.file.name,
15 | contentType: state.file.type,
16 | file: state.file,
17 | status: { type: "requires-action", reason: "composer-send" },
18 | };
19 | }
20 |
21 | public async send(
22 | attachment: PendingAttachment
23 | ): Promise {
24 | return {
25 | ...attachment,
26 | status: { type: "complete" },
27 | content: [],
28 | };
29 | }
30 |
31 | public async remove() {
32 | // noop
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/attachment-adapters/video.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AttachmentAdapter,
3 | CompleteAttachment,
4 | PendingAttachment,
5 | } from "@assistant-ui/react";
6 |
7 | export class VideoAttachmentAdapter implements AttachmentAdapter {
8 | public accept = "video/mp4,video/mpeg,video/webm";
9 |
10 | public async add(state: { file: File }): Promise {
11 | return {
12 | id: state.file.name,
13 | type: "document",
14 | name: state.file.name,
15 | contentType: state.file.type,
16 | file: state.file,
17 | status: { type: "requires-action", reason: "composer-send" },
18 | };
19 | }
20 |
21 | public async send(
22 | attachment: PendingAttachment
23 | ): Promise {
24 | return {
25 | ...attachment,
26 | status: { type: "complete" },
27 | content: [],
28 | };
29 | }
30 |
31 | public async remove() {
32 | // noop
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useToast } from "@/hooks/use-toast";
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/attachment-adapters/audio.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AttachmentAdapter,
3 | CompleteAttachment,
4 | PendingAttachment,
5 | } from "@assistant-ui/react";
6 |
7 | export class AudioAttachmentAdapter implements AttachmentAdapter {
8 | public accept =
9 | "audio/mp3,audio/mp4,audio/mpeg,audio/mpga,audio/m4a,audio/wav,audio/webm";
10 |
11 | public async add(state: { file: File }): Promise {
12 | return {
13 | id: state.file.name,
14 | type: "document",
15 | name: state.file.name,
16 | contentType: state.file.type,
17 | file: state.file,
18 | status: { type: "requires-action", reason: "composer-send" },
19 | };
20 | }
21 |
22 | public async send(
23 | attachment: PendingAttachment
24 | ): Promise {
25 | return {
26 | ...attachment,
27 | status: { type: "complete" },
28 | content: [],
29 | };
30 | }
31 |
32 | public async remove() {
33 | // noop
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/syntax-highlighter.tsx:
--------------------------------------------------------------------------------
1 | import { PrismAsyncLight } from "react-syntax-highlighter";
2 | import { makePrismAsyncLightSyntaxHighlighter } from "@assistant-ui/react-syntax-highlighter";
3 |
4 | import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
5 | import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
6 |
7 | import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
8 |
9 | // register languages you want to support
10 | PrismAsyncLight.registerLanguage("js", tsx);
11 | PrismAsyncLight.registerLanguage("jsx", tsx);
12 | PrismAsyncLight.registerLanguage("ts", tsx);
13 | PrismAsyncLight.registerLanguage("tsx", tsx);
14 | PrismAsyncLight.registerLanguage("python", python);
15 |
16 | export const SyntaxHighlighter = makePrismAsyncLightSyntaxHighlighter({
17 | style: coldarkDark,
18 | customStyle: {
19 | margin: 0,
20 | width: "100%",
21 | background: "transparent",
22 | padding: "1.5rem 1rem",
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/apps/web/src/components/auth/signup/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { redirect } from "next/navigation";
4 |
5 | import { createClient } from "@/lib/supabase/server";
6 | import { SignupWithEmailInput } from "./Signup";
7 |
8 | export async function signup(input: SignupWithEmailInput, baseUrl: string) {
9 | const supabase = createClient();
10 |
11 | const data = {
12 | email: input.email,
13 | password: input.password,
14 | // Not possible to set this when signing up with OAuth, so for now we'll omit.
15 | // data: {
16 | // is_open_canvas: true,
17 | // },
18 | options: {
19 | emailRedirectTo: `${baseUrl}/auth/confirm`,
20 | },
21 | };
22 |
23 | const { error } = await supabase.auth.signUp(data);
24 |
25 | if (error) {
26 | console.error(error);
27 | redirect("/auth/signup?error=true");
28 | }
29 |
30 | // Users still need to confirm their email address.
31 | // This page will show a message to check their email.
32 | redirect("/auth/signup/success");
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/src/components/web-search-results/loading-cards.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardDescription,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 | import { Skeleton } from "../ui/skeleton";
9 |
10 | export function LoadingSearchResultCards() {
11 | return (
12 | <>
13 | {Array.from({ length: 5 }).map((_, i) => (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ))}
30 | >
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/web/src/workers/graph-stream/streamWorker.ts:
--------------------------------------------------------------------------------
1 | import { StreamWorkerMessage, StreamConfig } from "./streamWorker.types";
2 |
3 | export class StreamWorkerService {
4 | private worker: Worker;
5 |
6 | constructor() {
7 | this.worker = new Worker(new URL("./stream.worker.ts", import.meta.url));
8 | }
9 |
10 | async *streamData(config: StreamConfig): AsyncGenerator {
11 | this.worker.postMessage(config);
12 |
13 | while (true) {
14 | const event: MessageEvent = await new Promise(
15 | (resolve) => {
16 | this.worker.onmessage = resolve;
17 | }
18 | );
19 |
20 | const { type, data, error } = event.data;
21 |
22 | if (type === "error") {
23 | throw new Error(error);
24 | }
25 |
26 | if (type === "chunk" && data) {
27 | yield JSON.parse(data);
28 | }
29 |
30 | if (type === "done") {
31 | break;
32 | }
33 | }
34 | }
35 |
36 | terminate() {
37 | this.worker.terminate();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/rewrite-artifact/schemas.ts:
--------------------------------------------------------------------------------
1 | import { PROGRAMMING_LANGUAGES } from "@opencanvas/shared/constants";
2 | import { z } from "zod";
3 |
4 | export const OPTIONALLY_UPDATE_ARTIFACT_META_SCHEMA = z
5 | .object({
6 | type: z
7 | .enum(["text", "code"])
8 | .describe("The type of the artifact content."),
9 | title: z
10 | .string()
11 | .optional()
12 | .describe(
13 | "The new title to give the artifact. ONLY update this if the user is making a request which changes the subject/topic of the artifact."
14 | ),
15 | language: z
16 | .enum(
17 | PROGRAMMING_LANGUAGES.map((lang) => lang.language) as [
18 | string,
19 | ...string[],
20 | ]
21 | )
22 | .describe(
23 | "The language of the code artifact. This should be populated with the programming language if the user is requesting code to be written, or 'other', in all other cases."
24 | ),
25 | })
26 | .describe("Update the artifact meta information, if necessary.");
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "open_canvas",
3 | "author": "Brace Sproul",
4 | "homepage": "https://opencanvas.langchain.com",
5 | "repository": "https://github.com/langchain-ai/open-canvas",
6 | "private": true,
7 | "workspaces": ["apps/*", "packages/*"],
8 | "scripts": {
9 | "build": "turbo build",
10 | "turbo:command": "turbo",
11 | "format": "turbo format",
12 | "lint": "turbo lint",
13 | "lint:fix": "turbo lint:fix"
14 | },
15 | "packageManager": "yarn@1.22.22",
16 | "devDependencies": {
17 | "turbo": "latest",
18 | "tsx": "^4.19.1",
19 | "typescript": "^5",
20 | "eslint": "^8.41.0",
21 | "@typescript-eslint/eslint-plugin": "^5.59.8",
22 | "@eslint/eslintrc": "^3.1.0",
23 | "@typescript-eslint/parser": "^5.59.8",
24 | "eslint-config-prettier": "^8.8.0",
25 | "eslint-plugin-import": "^2.27.5",
26 | "eslint-plugin-no-instanceof": "^1.0.1",
27 | "eslint-plugin-prettier": "^4.2.1",
28 | "prettier": "^3.3.3"
29 | },
30 | "resolutions": {
31 | "@langchain/core": "^0.3.38"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/src/app/auth/signout/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { createSupabaseClient } from "@/lib/supabase/client";
5 | import { useRouter } from "next/navigation";
6 |
7 | export default function Page() {
8 | const router = useRouter();
9 | const [errorOccurred, setErrorOccurred] = useState(false);
10 |
11 | useEffect(() => {
12 | async function signOut() {
13 | const client = createSupabaseClient();
14 | const { error } = await client.auth.signOut();
15 | if (error) {
16 | setErrorOccurred(true);
17 | } else {
18 | router.push("/auth/login");
19 | }
20 | }
21 | signOut();
22 | }, []);
23 |
24 | return (
25 | <>
26 | {errorOccurred ? (
27 |
28 |
Sign out error
29 |
30 | There was an error signing out. Please refresh the page to try
31 | again.
32 |
33 |
34 | ) : (
35 | Signing out...
36 | )}
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/apps/agents/src/web-search/index.ts:
--------------------------------------------------------------------------------
1 | import { StateGraph, START, END } from "@langchain/langgraph";
2 | import { WebSearchGraphAnnotation, WebSearchState } from "./state.js";
3 | import { search } from "./nodes/search.js";
4 | import { queryGenerator } from "./nodes/query-generator.js";
5 | import { classifyMessage } from "./nodes/classify-message.js";
6 |
7 | function searchOrEndConditional(
8 | state: WebSearchState
9 | ): "queryGenerator" | typeof END {
10 | if (state.shouldSearch) {
11 | return "queryGenerator";
12 | }
13 | return END;
14 | }
15 |
16 | const builder = new StateGraph(WebSearchGraphAnnotation)
17 | .addNode("classifyMessage", classifyMessage)
18 | .addNode("queryGenerator", queryGenerator)
19 | .addNode("search", search)
20 | .addEdge(START, "classifyMessage")
21 | .addConditionalEdges("classifyMessage", searchOrEndConditional, [
22 | "queryGenerator",
23 | END,
24 | ])
25 | .addEdge("queryGenerator", "search")
26 | .addEdge("search", END);
27 |
28 | export const graph = builder.compile();
29 |
30 | graph.name = "Web Search Graph";
31 |
--------------------------------------------------------------------------------
/apps/web/src/components/tool-hooks/LangSmithLinkToolUI.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalLink } from "lucide-react";
2 | import { LangSmithSVG } from "../icons/langsmith";
3 | import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button";
4 | import { useAssistantToolUI } from "@assistant-ui/react";
5 | import { useCallback } from "react";
6 |
7 | export const useLangSmithLinkToolUI = () =>
8 | useAssistantToolUI({
9 | toolName: "langsmith_tool_ui",
10 | render: useCallback((input) => {
11 | return (
12 | window.open(input.args.sharedRunURL, "_blank")}
18 | >
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }, []),
26 | });
27 |
--------------------------------------------------------------------------------
/apps/web/src/components/chat-interface/model-selector/new-badge.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { cn } from "@/lib/utils";
3 |
4 | export function IsNewBadge() {
5 | return (
6 |
17 |
25 | New!
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) LangChain, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/apps/web/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) LangChain, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/apps/web/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const LANGGRAPH_API_URL =
2 | process.env.LANGGRAPH_API_URL ?? "http://localhost:54367";
3 | // v2 is tied to the 'open-canvas-prod' deployment.
4 | export const ASSISTANT_ID_COOKIE = "oc_assistant_id_v2";
5 | // export const ASSISTANT_ID_COOKIE = "oc_assistant_id";
6 | export const HAS_ASSISTANT_COOKIE_BEEN_SET = "has_oc_assistant_id_been_set";
7 |
8 | export const OC_HAS_SEEN_CUSTOM_ASSISTANTS_ALERT =
9 | "oc_has_seen_custom_assistants_alert";
10 | export const WEB_SEARCH_RESULTS_QUERY_PARAM = "webSearchResults";
11 |
12 | export const ALLOWED_AUDIO_TYPES = new Set([
13 | "audio/mp3",
14 | "audio/mp4",
15 | "audio/mpeg",
16 | "audio/mpga",
17 | "audio/m4a",
18 | "audio/wav",
19 | "audio/webm",
20 | ]);
21 | export const ALLOWED_AUDIO_TYPE_ENDINGS = [
22 | ".mp3",
23 | ".mpga",
24 | ".m4a",
25 | ".wav",
26 | ".webm",
27 | ];
28 | export const ALLOWED_VIDEO_TYPES = new Set([
29 | "video/mp4",
30 | "video/mpeg",
31 | "video/webm",
32 | ]);
33 | export const ALLOWED_VIDEO_TYPE_ENDINGS = [".mp4", ".mpeg", ".webm"];
34 |
35 | export const CHAT_COLLAPSED_QUERY_PARAM = "chatCollapsed";
36 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5 | import { CheckIcon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/apps/web/src/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from "@supabase/ssr";
2 | import { cookies } from "next/headers";
3 |
4 | export function createClient() {
5 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) {
6 | throw new Error("NEXT_PUBLIC_SUPABASE_URL is not defined");
7 | }
8 | if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
9 | throw new Error("NEXT_PUBLIC_SUPABASE_ANON_KEY is not defined");
10 | }
11 |
12 | const cookieStore = cookies();
13 |
14 | return createServerClient(
15 | process.env.NEXT_PUBLIC_SUPABASE_URL,
16 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
17 | {
18 | cookies: {
19 | getAll() {
20 | return cookieStore.getAll();
21 | },
22 | setAll(cookiesToSet) {
23 | try {
24 | cookiesToSet.forEach(({ name, value, options }) =>
25 | cookieStore.set(name, value, options)
26 | );
27 | } catch {
28 | // The `setAll` method was called from a Server Component.
29 | // This can be ignored if you have middleware refreshing
30 | // user sessions.
31 | }
32 | },
33 | },
34 | }
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/src/lib/get_language_template.ts:
--------------------------------------------------------------------------------
1 | import { ProgrammingLanguageOptions } from "@opencanvas/shared/types";
2 |
3 | export const getLanguageTemplate = (
4 | language: ProgrammingLanguageOptions
5 | ): string => {
6 | switch (language) {
7 | case "javascript":
8 | case "typescript":
9 | return `import { } from "module";`;
10 | case "cpp":
11 | return `#include
12 |
13 | int main() {
14 | std::cout << "Hello, World!" << std::endl;
15 | return 0;
16 | }`;
17 | case "java":
18 | return `public class Main {
19 | public static void main(String[] args) {
20 | System.out.println("Hello, World!");
21 | }
22 | }`;
23 | case "php":
24 | return `
36 |
37 |
38 | Hello, World!
39 |
40 | `;
41 | case "sql":
42 | return `SELECT "Hello, World!";`;
43 | default:
44 | return "// No quickstart content available for this language";
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/generate-artifact/schemas.ts:
--------------------------------------------------------------------------------
1 | import { PROGRAMMING_LANGUAGES } from "@opencanvas/shared/constants";
2 | import { z } from "zod";
3 |
4 | export const ARTIFACT_TOOL_SCHEMA = z.object({
5 | type: z
6 | .enum(["code", "text"])
7 | .describe("The content type of the artifact generated."),
8 | language: z
9 | .enum(
10 | PROGRAMMING_LANGUAGES.map((lang) => lang.language) as [
11 | string,
12 | ...string[],
13 | ]
14 | )
15 | .optional()
16 | .describe(
17 | "The language/programming language of the artifact generated.\n" +
18 | "If generating code, it should be one of the options, or 'other'.\n" +
19 | "If not generating code, the language should ALWAYS be 'other'."
20 | ),
21 | isValidReact: z
22 | .boolean()
23 | .optional()
24 | .describe(
25 | "Whether or not the generated code is valid React code. Only populate this field if generating code."
26 | ),
27 | artifact: z.string().describe("The content of the artifact to generate."),
28 | title: z
29 | .string()
30 | .describe(
31 | "A short title to give to the artifact. Should be less than 5 words."
32 | ),
33 | });
34 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default: "border-transparent bg-primary text-primary-foreground shadow",
12 | secondary: "border-transparent bg-secondary text-secondary-foreground",
13 | destructive:
14 | "border-transparent bg-destructive text-destructive-foreground shadow",
15 | outline: "text-foreground",
16 | },
17 | },
18 | defaultVariants: {
19 | variant: "default",
20 | },
21 | }
22 | );
23 |
24 | export interface BadgeProps
25 | extends React.HTMLAttributes,
26 | VariantProps {}
27 |
28 | function Badge({ className, variant, ...props }: BadgeProps) {
29 | return (
30 |
31 | );
32 | }
33 |
34 | export { Badge, badgeVariants };
35 |
--------------------------------------------------------------------------------
/apps/web/src/components/icons/svg/TXTIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
3 | import { withDefaults } from "./utils/withDefaults";
4 |
5 | export type AvatarProps = {
6 | src?: string | undefined;
7 | alt?: string | undefined;
8 | fallback?: string | undefined;
9 | };
10 |
11 | export const Avatar: FC = ({ src, alt, fallback }) => {
12 | if (src == null && fallback == null) return null;
13 |
14 | return (
15 |
16 | {src != null && }
17 | {fallback != null && {fallback} }
18 |
19 | );
20 | };
21 |
22 | Avatar.displayName = "Avatar";
23 |
24 | export const AvatarRoot = withDefaults(AvatarPrimitive.Root, {
25 | className: "aui-avatar-root",
26 | });
27 |
28 | AvatarRoot.displayName = "AvatarRoot";
29 |
30 | export const AvatarImage = withDefaults(AvatarPrimitive.Image, {
31 | className: "aui-avatar-image",
32 | });
33 |
34 | AvatarImage.displayName = "AvatarImage";
35 |
36 | export const AvatarFallback = withDefaults(AvatarPrimitive.Fallback, {
37 | className: "aui-avatar-fallback",
38 | });
39 |
40 | AvatarFallback.displayName = "AvatarFallback";
41 |
--------------------------------------------------------------------------------
/packages/evals/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opencanvas/evals",
3 | "author": "Brace Sproul",
4 | "homepage": "https://opencanvas.langchain.com",
5 | "repository": "https://github.com/langchain-ai/open-canvas",
6 | "version": "0.0.1",
7 | "private": true,
8 | "scripts": {
9 | "build": "yarn clean && tsc",
10 | "clean": "rm -rf ./dist .turbo || true",
11 | "format": "prettier --config .prettierrc --write \"src\"",
12 | "lint": "eslint src",
13 | "lint:fix": "eslint src --fix"
14 | },
15 | "dependencies": {
16 | "@langchain/core": "^0.3.38",
17 | "@langchain/openai": "^0.4.2",
18 | "@opencanvas/agents": "*",
19 | "langsmith": "^0.3.5",
20 | "zod": "^3.24.1",
21 | "dotenv": "^16.4.7"
22 | },
23 | "devDependencies": {
24 | "@eslint/eslintrc": "^3.1.0",
25 | "@typescript-eslint/eslint-plugin": "^7.0.1",
26 | "@typescript-eslint/parser": "^7.0.1",
27 | "eslint": "^8.57.0",
28 | "eslint-config-prettier": "^8.8.0",
29 | "eslint-plugin-import": "^2.27.5",
30 | "eslint-plugin-no-instanceof": "^1.0.1",
31 | "eslint-plugin-prettier": "^4.2.1",
32 | "prettier": "^3.3.3",
33 | "tsx": "^4.19.1",
34 | "turbo": "latest",
35 | "typescript": "^5",
36 | "vitest": "^3.0.4"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/utils/withDefaults.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef, ElementType, forwardRef } from "react";
2 | import classNames from "classnames";
3 | import { ComponentRef } from "react";
4 |
5 | export const withDefaultProps =
6 | ({
7 | className,
8 | ...defaultProps
9 | }: Partial) =>
10 | ({ className: classNameProp, ...props }: TProps) => {
11 | return {
12 | className: classNames(className, classNameProp),
13 | ...defaultProps,
14 | ...props,
15 | } as TProps;
16 | };
17 |
18 | export const withDefaults = (
19 | Component: TComponent,
20 | defaultProps: Partial>
21 | ) => {
22 | type TComponentProps = typeof defaultProps;
23 | const getProps = withDefaultProps(defaultProps);
24 | const WithDefaults = forwardRef, TComponentProps>(
25 | (props, ref) => {
26 | const ComponentAsAny = Component as any;
27 | return ;
28 | }
29 | );
30 | WithDefaults.displayName =
31 | "withDefaults(" +
32 | (typeof Component === "string" ? Component : Component.displayName) +
33 | ")";
34 | return WithDefaults;
35 | };
36 |
--------------------------------------------------------------------------------
/apps/web/src/components/assistant-ui/tooltip-icon-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { forwardRef } from "react";
4 |
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipProvider,
9 | TooltipTrigger,
10 | } from "@/components/ui/tooltip";
11 | import { Button, ButtonProps } from "@/components/ui/button";
12 | import { cn } from "@/lib/utils";
13 |
14 | export type TooltipIconButtonProps = ButtonProps & {
15 | tooltip: string;
16 | side?: "top" | "bottom" | "left" | "right";
17 | };
18 |
19 | export const TooltipIconButton = forwardRef<
20 | HTMLButtonElement,
21 | TooltipIconButtonProps
22 | >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
23 | return (
24 |
25 |
26 |
27 |
34 | {children}
35 | {tooltip}
36 |
37 |
38 | {tooltip}
39 |
40 |
41 | );
42 | });
43 |
44 | TooltipIconButton.displayName = "TooltipIconButton";
45 |
--------------------------------------------------------------------------------
/apps/web/src/workers/graph-stream/stream.worker.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@/hooks/utils";
2 | import { StreamConfig } from "./streamWorker.types";
3 |
4 | // Since workers can't directly access the client SDK, you'll need to recreate/import necessary parts
5 | const ctx: Worker = self as any;
6 |
7 | ctx.addEventListener("message", async (event: MessageEvent) => {
8 | try {
9 | const { threadId, assistantId, input, modelName, modelConfigs } =
10 | event.data;
11 |
12 | const client = createClient();
13 |
14 | const stream = client.runs.stream(threadId, assistantId, {
15 | input: input as Record,
16 | streamMode: "events",
17 | config: {
18 | configurable: {
19 | customModelName: modelName,
20 | modelConfig: modelConfigs[modelName as keyof typeof modelConfigs],
21 | },
22 | },
23 | });
24 |
25 | for await (const chunk of stream) {
26 | // Serialize the chunk and post it back to the main thread
27 | ctx.postMessage({
28 | type: "chunk",
29 | data: JSON.stringify(chunk),
30 | });
31 | }
32 |
33 | ctx.postMessage({ type: "done" });
34 | } catch (error: any) {
35 | ctx.postMessage({
36 | type: "error",
37 | error: error.message,
38 | });
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
--------------------------------------------------------------------------------
/apps/web/src/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | // The client you created from the Server-Side Auth instructions
3 | import { createClient } from "@/lib/supabase/server";
4 |
5 | export async function GET(request: Request) {
6 | const { searchParams, origin } = new URL(request.url);
7 | const code = searchParams.get("code");
8 | // if "next" is in param, use it as the redirect URL
9 | const next = searchParams.get("next") ?? "/";
10 |
11 | if (code) {
12 | const supabase = createClient();
13 | const { error } = await supabase.auth.exchangeCodeForSession(code);
14 | if (!error) {
15 | const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
16 | const isLocalEnv = process.env.NODE_ENV === "development";
17 | if (isLocalEnv) {
18 | // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
19 | return NextResponse.redirect(`${origin}${next}`);
20 | } else if (forwardedHost) {
21 | return NextResponse.redirect(`https://${forwardedHost}${next}`);
22 | } else {
23 | return NextResponse.redirect(`${origin}${next}`);
24 | }
25 | }
26 | }
27 |
28 | // return the user to an error page with instructions
29 | return NextResponse.redirect(`${origin}/auth/auth-code-error`);
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/store/delete/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { Client } from "@langchain/langgraph-sdk";
3 | import { LANGGRAPH_API_URL } from "@/constants";
4 | import { verifyUserAuthenticated } from "../../../../lib/supabase/verify_user_server";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const authRes = await verifyUserAuthenticated();
9 | if (!authRes?.user) {
10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 | }
12 | } catch (e) {
13 | console.error("Failed to fetch user", e);
14 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15 | }
16 |
17 | const { namespace, key } = await req.json();
18 |
19 | const lgClient = new Client({
20 | apiKey: process.env.LANGCHAIN_API_KEY,
21 | apiUrl: LANGGRAPH_API_URL,
22 | });
23 |
24 | try {
25 | await lgClient.store.deleteItem(namespace, key);
26 |
27 | return new NextResponse(JSON.stringify({ success: true }), {
28 | status: 200,
29 | headers: { "Content-Type": "application/json" },
30 | });
31 | } catch (_) {
32 | return new NextResponse(
33 | JSON.stringify({ error: "Failed to share run after multiple attempts." }),
34 | {
35 | status: 500,
36 | headers: { "Content-Type": "application/json" },
37 | }
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/store/get/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { Client } from "@langchain/langgraph-sdk";
3 | import { LANGGRAPH_API_URL } from "@/constants";
4 | import { verifyUserAuthenticated } from "../../../../lib/supabase/verify_user_server";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const authRes = await verifyUserAuthenticated();
9 | if (!authRes?.user) {
10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 | }
12 | } catch (e) {
13 | console.error("Failed to fetch user", e);
14 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15 | }
16 |
17 | const { namespace, key } = await req.json();
18 |
19 | const lgClient = new Client({
20 | apiKey: process.env.LANGCHAIN_API_KEY,
21 | apiUrl: LANGGRAPH_API_URL,
22 | });
23 |
24 | try {
25 | const item = await lgClient.store.getItem(namespace, key);
26 |
27 | return new NextResponse(JSON.stringify({ item }), {
28 | status: 200,
29 | headers: { "Content-Type": "application/json" },
30 | });
31 | } catch (_) {
32 | return new NextResponse(
33 | JSON.stringify({ error: "Failed to share run after multiple attempts." }),
34 | {
35 | status: 500,
36 | headers: { "Content-Type": "application/json" },
37 | }
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/generate-artifact/utils.ts:
--------------------------------------------------------------------------------
1 | import { NEW_ARTIFACT_PROMPT } from "../../prompts.js";
2 | import {
3 | ArtifactCodeV3,
4 | ArtifactMarkdownV3,
5 | ProgrammingLanguageOptions,
6 | } from "@opencanvas/shared/types";
7 | import { z } from "zod";
8 | import { ARTIFACT_TOOL_SCHEMA } from "./schemas.js";
9 |
10 | export const formatNewArtifactPrompt = (
11 | memoriesAsString: string,
12 | modelName: string
13 | ): string => {
14 | return NEW_ARTIFACT_PROMPT.replace("{reflections}", memoriesAsString).replace(
15 | "{disableChainOfThought}",
16 | modelName.includes("claude")
17 | ? "\n\nIMPORTANT: Do NOT preform chain of thought beforehand. Instead, go STRAIGHT to generating the tool response. This is VERY important."
18 | : ""
19 | );
20 | };
21 |
22 | export const createArtifactContent = (
23 | toolCall: z.infer
24 | ): ArtifactCodeV3 | ArtifactMarkdownV3 => {
25 | const artifactType = toolCall?.type;
26 |
27 | if (artifactType === "code") {
28 | return {
29 | index: 1,
30 | type: "code",
31 | title: toolCall?.title,
32 | code: toolCall?.artifact,
33 | language: toolCall?.language as ProgrammingLanguageOptions,
34 | };
35 | }
36 |
37 | return {
38 | index: 1,
39 | type: "text",
40 | title: toolCall?.title,
41 | fullMarkdown: toolCall?.artifact,
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/store/put/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { Client } from "@langchain/langgraph-sdk";
3 | import { LANGGRAPH_API_URL } from "@/constants";
4 | import { verifyUserAuthenticated } from "../../../../lib/supabase/verify_user_server";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const authRes = await verifyUserAuthenticated();
9 | if (!authRes?.user) {
10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 | }
12 | } catch (e) {
13 | console.error("Failed to fetch user", e);
14 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15 | }
16 |
17 | const { namespace, key, value } = await req.json();
18 |
19 | const lgClient = new Client({
20 | apiKey: process.env.LANGCHAIN_API_KEY,
21 | apiUrl: LANGGRAPH_API_URL,
22 | });
23 |
24 | try {
25 | await lgClient.store.putItem(namespace, key, value);
26 |
27 | return new NextResponse(JSON.stringify({ success: true }), {
28 | status: 200,
29 | headers: { "Content-Type": "application/json" },
30 | });
31 | } catch (_) {
32 | return new NextResponse(
33 | JSON.stringify({ error: "Failed to share run after multiple attempts." }),
34 | {
35 | status: 500,
36 | headers: { "Content-Type": "application/json" },
37 | }
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/src/app/auth/confirm/route.ts:
--------------------------------------------------------------------------------
1 | import { type EmailOtpType } from "@supabase/supabase-js";
2 | import { type NextRequest } from "next/server";
3 | import { revalidatePath } from "next/cache";
4 |
5 | import { createClient } from "@/lib/supabase/server";
6 | import { redirect, RedirectType } from "next/navigation";
7 |
8 | export async function GET(request: NextRequest) {
9 | const { searchParams } = new URL(request.url);
10 | const token_hash = searchParams.get("token_hash");
11 | const type = searchParams.get("type") as EmailOtpType | null;
12 | const next = searchParams.get("next") ?? "/";
13 | const code = searchParams.get("code");
14 |
15 | const supabase = createClient();
16 |
17 | if (token_hash && type) {
18 | const { error } = await supabase.auth.verifyOtp({
19 | type,
20 | token_hash,
21 | });
22 | if (!error) {
23 | // redirect user to specified redirect URL or root of app
24 | revalidatePath(next);
25 | redirect(next, RedirectType.push);
26 | }
27 | } else if (code) {
28 | const { error } = await supabase.auth.exchangeCodeForSession(code);
29 | if (!error) {
30 | // redirect user to specified redirect URL or root of app
31 | revalidatePath(next);
32 | redirect(next, RedirectType.push);
33 | }
34 | }
35 |
36 | // redirect the user to an error page with some instructions
37 | redirect("/error");
38 | }
39 |
--------------------------------------------------------------------------------
/packages/shared/src/utils/artifacts.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Artifact,
3 | ArtifactCodeV3,
4 | ArtifactMarkdownV3,
5 | ArtifactV3,
6 | } from "../types.js";
7 |
8 | export const isArtifactCodeContent = (
9 | content: unknown
10 | ): content is ArtifactCodeV3 => {
11 | return !!(
12 | typeof content === "object" &&
13 | content &&
14 | "type" in content &&
15 | content.type === "code"
16 | );
17 | };
18 |
19 | export const isArtifactMarkdownContent = (
20 | content: unknown
21 | ): content is ArtifactMarkdownV3 => {
22 | return !!(
23 | typeof content === "object" &&
24 | content &&
25 | "type" in content &&
26 | content.type === "text"
27 | );
28 | };
29 |
30 | export const isDeprecatedArtifactType = (
31 | artifact: unknown
32 | ): artifact is Artifact => {
33 | return !!(
34 | typeof artifact === "object" &&
35 | artifact &&
36 | "currentContentIndex" in artifact &&
37 | typeof artifact.currentContentIndex === "number"
38 | );
39 | };
40 |
41 | export const getArtifactContent = (
42 | artifact: ArtifactV3
43 | ): ArtifactCodeV3 | ArtifactMarkdownV3 => {
44 | if (!artifact) {
45 | throw new Error("No artifact found.");
46 | }
47 | const currentContent = artifact.contents.find(
48 | (a) => a.index === artifact.currentIndex
49 | );
50 | if (!currentContent) {
51 | return artifact.contents[artifact.contents.length - 1];
52 | }
53 | return currentContent;
54 | };
55 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor;
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ));
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
34 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/ArtifactLoading.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Skeleton } from "../ui/skeleton";
3 |
4 | export function ArtifactLoading() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {Array.from({ length: 25 }).map((_, i) => (
21 |
30 | ))}
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | # Feature flags for hiding/showing specific models
2 | NEXT_PUBLIC_FIREWORKS_ENABLED=true
3 | NEXT_PUBLIC_GEMINI_ENABLED=true
4 | NEXT_PUBLIC_ANTHROPIC_ENABLED=true
5 | NEXT_PUBLIC_OPENAI_ENABLED=true
6 | # Set to false by default since the base OpenAI API is more common than the Azure OpenAI API.
7 | NEXT_PUBLIC_AZURE_ENABLED=false
8 | NEXT_PUBLIC_OLLAMA_ENABLED=false
9 | NEXT_PUBLIC_GROQ_ENABLED=false
10 |
11 | # If using Ollama, set the API URL here. Only needs to be set if using the non default Ollama server port.
12 | # It will default to `http://host.docker.internal:11434` if not set.
13 | # OLLAMA_API_URL="http://host.docker.internal:11434"
14 |
15 | # Supabase for authentication
16 | # Public keys
17 | NEXT_PUBLIC_SUPABASE_URL=
18 | NEXT_PUBLIC_SUPABASE_ANON_KEY=
19 | # For document uploading (can be the same as the above keys)
20 | NEXT_PUBLIC_SUPABASE_URL_DOCUMENTS=
21 | NEXT_PUBLIC_SUPABASE_ANON_KEY_DOCUMENTS=
22 |
23 | # For transcription
24 | GROQ_API_KEY=
25 |
26 | # For web scraping
27 | FIRECRAWL_API_KEY=
28 |
29 | # Azure OpenAI Configuration
30 | # ENSURE THEY ARE PREFIXED WITH AN UNDERSCORE.
31 | # _AZURE_OPENAI_API_KEY=your-azure-openai-api-key
32 | # _AZURE_OPENAI_API_INSTANCE_NAME=your-instance-name
33 | # _AZURE_OPENAI_API_DEPLOYMENT_NAME=your-deployment-name
34 | # _AZURE_OPENAI_API_VERSION=2024-08-01-preview
35 | # Optional: Azure OpenAI Base Path (if using a different domain)
36 | # _AZURE_OPENAI_API_BASE_PATH=https://your-custom-domain.com/openai/deployments
37 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/header/artifact-title.tsx:
--------------------------------------------------------------------------------
1 | import { CircleCheck, CircleX, LoaderCircle } from "lucide-react";
2 |
3 | interface ArtifactTitleProps {
4 | title: string;
5 | isArtifactSaved: boolean;
6 | artifactUpdateFailed: boolean;
7 | }
8 |
9 | export function ArtifactTitle(props: ArtifactTitleProps) {
10 | return (
11 |
12 |
13 | {props.title}
14 |
15 |
16 | {props.isArtifactSaved ? (
17 |
18 | Saved
19 |
20 |
21 | ) : !props.artifactUpdateFailed ? (
22 |
23 | Saving
24 |
25 |
26 | ) : props.artifactUpdateFailed ? (
27 |
28 | Failed to save
29 |
30 |
31 | ) : null}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/generateTitle.ts:
--------------------------------------------------------------------------------
1 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
2 | import { Client } from "@langchain/langgraph-sdk";
3 | import { OpenCanvasGraphAnnotation } from "../state.js";
4 |
5 | export const generateTitleNode = async (
6 | state: typeof OpenCanvasGraphAnnotation.State,
7 | config: LangGraphRunnableConfig
8 | ) => {
9 | if (state.messages.length > 2) {
10 | // Skip if it's not first human ai conversation. Should never occur in practice
11 | // due to the conditional edge which is called before this node.
12 | return {};
13 | }
14 |
15 | try {
16 | const langGraphClient = new Client({
17 | apiUrl: `http://localhost:${process.env.PORT}`,
18 | });
19 |
20 | const titleInput = {
21 | messages: state.messages,
22 | artifact: state.artifact,
23 | };
24 | const titleConfig = {
25 | configurable: {
26 | open_canvas_thread_id: config.configurable?.thread_id,
27 | },
28 | };
29 |
30 | // Create a new thread for title generation
31 | const newThread = await langGraphClient.threads.create();
32 |
33 | // Create a new title generation run in the background
34 | await langGraphClient.runs.create(newThread.thread_id, "thread_title", {
35 | input: titleInput,
36 | config: titleConfig,
37 | multitaskStrategy: "enqueue",
38 | afterSeconds: 0,
39 | });
40 | } catch (e) {
41 | console.error("Failed to call generate title graph\n\n", e);
42 | }
43 |
44 | return {};
45 | };
46 |
--------------------------------------------------------------------------------
/apps/web/src/components/icons/svg/MP4Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/assistant-ui/tooltip-icon-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { forwardRef } from "react";
4 |
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipProvider,
9 | TooltipTrigger,
10 | } from "@/components/ui/tooltip";
11 | import { Button, ButtonProps } from "@/components/ui/button";
12 | import { cn } from "@/lib/utils";
13 |
14 | export type TooltipIconButtonProps = ButtonProps & {
15 | tooltip: string | React.ReactNode;
16 | side?: "top" | "bottom" | "left" | "right";
17 | /**
18 | * @default 700
19 | */
20 | delayDuration?: number;
21 | };
22 |
23 | export const TooltipIconButton = forwardRef<
24 | HTMLButtonElement,
25 | TooltipIconButtonProps
26 | >(
27 | (
28 | { children, tooltip, side = "bottom", className, delayDuration, ...rest },
29 | ref
30 | ) => {
31 | return (
32 |
33 |
34 |
35 |
42 | {children}
43 | {tooltip}
44 |
45 |
46 | {tooltip}
47 |
48 |
49 | );
50 | }
51 | );
52 |
53 | TooltipIconButton.displayName = "TooltipIconButton";
54 |
--------------------------------------------------------------------------------
/apps/agents/src/web-search/nodes/classify-message.ts:
--------------------------------------------------------------------------------
1 | import { ChatAnthropic } from "@langchain/anthropic";
2 | import { WebSearchState } from "../state.js";
3 | import z from "zod";
4 |
5 | const CLASSIFIER_PROMPT = `You're a helpful AI assistant tasked with classifying the user's latest message.
6 | The user has enabled web search for their conversation, however not all messages should be searched.
7 |
8 | Analyze their latest message in isolation and determine if it warrants a web search to include additional context.
9 |
10 |
11 | {message}
12 | `;
13 |
14 | const classificationSchema = z
15 | .object({
16 | shouldSearch: z
17 | .boolean()
18 | .describe(
19 | "Whether or not to search the web based on the user's latest message."
20 | ),
21 | })
22 | .describe("The classification of the user's latest message.");
23 |
24 | export async function classifyMessage(
25 | state: WebSearchState
26 | ): Promise> {
27 | const model = new ChatAnthropic({
28 | model: "claude-3-5-sonnet-latest",
29 | temperature: 0,
30 | }).withStructuredOutput(classificationSchema, {
31 | name: "classify_message",
32 | });
33 |
34 | const latestMessageContent = state.messages[state.messages.length - 1]
35 | .content as string;
36 | const formattedPrompt = CLASSIFIER_PROMPT.replace(
37 | "{message}",
38 | latestMessageContent
39 | );
40 |
41 | const response = await model.invoke([["user", formattedPrompt]]);
42 |
43 | return {
44 | shouldSearch: response.shouldSearch,
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/apps/web/src/components/icons/magic_pencil.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | export const MagicPencilSVG = ({ className }: { className?: string }) => (
4 |
12 |
19 |
26 | {" "}
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opencanvas/shared",
3 | "author": "Brace Sproul",
4 | "repository": "https://github.com/langchain-ai/open-canvas",
5 | "version": "0.0.1",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "types": "./dist/index.d.ts",
12 | "import": "./dist/index.js",
13 | "require": "./dist/index.js"
14 | },
15 | "./*": {
16 | "types": "./dist/*.d.ts",
17 | "import": "./dist/*.js",
18 | "require": "./dist/*.js"
19 | },
20 | "./package.json": "./package.json"
21 | },
22 | "files": [
23 | "dist/**/*"
24 | ],
25 | "license": "MIT",
26 | "private": true,
27 | "scripts": {
28 | "build": "yarn clean && tsc",
29 | "clean": "rm -rf ./dist .turbo || true",
30 | "format": "prettier --config .prettierrc --write \"src\"",
31 | "lint": "eslint src",
32 | "lint:fix": "eslint src --fix"
33 | },
34 | "devDependencies": {
35 | "@eslint/js": "^9.12.0",
36 | "@tsconfig/recommended": "^1.0.8",
37 | "@types/eslint__js": "^8.42.3",
38 | "@types/node": "^20",
39 | "@typescript-eslint/eslint-plugin": "^8.12.2",
40 | "@typescript-eslint/parser": "^8.8.1",
41 | "eslint": "^8",
42 | "eslint-plugin-unused-imports": "^4.1.4",
43 | "prettier": "^3.3.3",
44 | "react": "^19.0.0",
45 | "tsx": "^4.19.1",
46 | "turbo": "latest",
47 | "typescript": "^5",
48 | "typescript-eslint": "^8.8.1"
49 | },
50 | "dependencies": {
51 | "@langchain/core": "^0.3.38"
52 | }
53 | }
--------------------------------------------------------------------------------
/apps/web/src/components/icons/svg/PDFIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/apps/web/src/contexts/UserContext.tsx:
--------------------------------------------------------------------------------
1 | import { createSupabaseClient } from "@/lib/supabase/client";
2 | import { User } from "@supabase/supabase-js";
3 | import {
4 | createContext,
5 | ReactNode,
6 | useContext,
7 | useEffect,
8 | useState,
9 | } from "react";
10 |
11 | type UserContentType = {
12 | getUser: () => Promise;
13 | user: User | undefined;
14 | loading: boolean;
15 | };
16 |
17 | const UserContext = createContext(undefined);
18 |
19 | export function UserProvider({ children }: { children: ReactNode }) {
20 | const [user, setUser] = useState();
21 | const [loading, setLoading] = useState(true);
22 |
23 | useEffect(() => {
24 | if (user || typeof window === "undefined") return;
25 |
26 | getUser();
27 | }, []);
28 |
29 | async function getUser() {
30 | if (user) {
31 | setLoading(false);
32 | return user;
33 | }
34 |
35 | const supabase = createSupabaseClient();
36 |
37 | const {
38 | data: { user: supabaseUser },
39 | } = await supabase.auth.getUser();
40 | setUser(supabaseUser || undefined);
41 | setLoading(false);
42 | return supabaseUser || undefined;
43 | }
44 |
45 | const contextValue: UserContentType = {
46 | getUser,
47 | user,
48 | loading,
49 | };
50 |
51 | return (
52 | {children}
53 | );
54 | }
55 |
56 | export function useUserContext() {
57 | const context = useContext(UserContext);
58 | if (context === undefined) {
59 | throw new Error("useUserContext must be used within a UserProvider");
60 | }
61 | return context;
62 | }
63 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/TextRenderer.module.css:
--------------------------------------------------------------------------------
1 | .mdEditorCustom {
2 | height: 100% !important;
3 | overflow: hidden;
4 | border-radius: 0%;
5 | }
6 |
7 | .mdEditorCustom :global(.w-md-editor) {
8 | height: 100% !important;
9 | border: none !important;
10 | }
11 |
12 | .mdEditorCustom :global(.w-md-editor-content) {
13 | height: 100% !important;
14 | }
15 |
16 | .mdEditorCustom :global(.w-md-editor-text),
17 | .mdEditorCustom :global(.w-md-editor-text-pre),
18 | .mdEditorCustom :global(.w-md-editor-text-input) {
19 | min-height: 100% !important;
20 | height: 100% !important;
21 | }
22 |
23 | .mdEditorCustom :global(.w-md-editor-preview) {
24 | box-shadow: none !important;
25 | }
26 |
27 | .mdEditorCustom :global(.w-md-editor-toolbar) {
28 | border-bottom: none !important;
29 | }
30 |
31 | /* Force full height for text area */
32 | .fullHeightTextArea :global(.w-md-editor-text-input) {
33 | min-height: 100vh !important;
34 | height: 100% !important;
35 | }
36 |
37 | .lightModeOnly {
38 | --color-canvas-default: #ffffff;
39 | --color-canvas-subtle: #f6f8fa;
40 | --color-border-default: #d0d7de;
41 | --color-border-muted: #d8dee4;
42 | --color-neutral-muted: rgba(175, 184, 193, 0.2);
43 | --color-accent-fg: #0969da;
44 | --color-accent-emphasis: #0969da;
45 | --color-attention-subtle: #fff8c5;
46 | --color-danger-fg: #cf222e;
47 | }
48 |
49 | .lightModeOnly :global(.wmde-markdown),
50 | .lightModeOnly :global(.wmde-markdown-var) {
51 | background-color: #ffffff;
52 | color: #24292f;
53 | }
54 |
55 | .lightModeOnly :global(.w-md-editor-text-pre > code),
56 | .lightModeOnly :global(.w-md-editor-text-input) {
57 | color: #24292f !important;
58 | }
59 |
--------------------------------------------------------------------------------
/apps/agents/src/web-search/nodes/query-generator.ts:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 | import { ChatAnthropic } from "@langchain/anthropic";
3 | import { WebSearchState } from "../state.js";
4 | import { formatMessages } from "../../utils.js";
5 |
6 | const QUERY_GENERATOR_PROMPT = `You're a helpful AI assistant tasked with writing a query to search the web.
7 | You're provided with a list of messages between a user and an AI assistant.
8 | The most recent message from the user is the one you should update to be a more search engine friendly query.
9 |
10 | Try to keep the new query as similar to the message as possible, while still being search engine friendly.
11 |
12 | Here is the conversation between the user and the assistant, in order of oldest to newest:
13 |
14 |
15 | {conversation}
16 |
17 |
18 |
19 | {additional_context}
20 |
21 |
22 | Respond ONLY with the search query, and nothing else.`;
23 |
24 | export async function queryGenerator(
25 | state: WebSearchState
26 | ): Promise> {
27 | const model = new ChatAnthropic({
28 | model: "claude-3-5-sonnet-latest",
29 | temperature: 0,
30 | });
31 |
32 | const additionalContext = `The current date is ${format(new Date(), "PPpp")}`;
33 |
34 | const formattedMessages = formatMessages(state.messages);
35 | const formattedPrompt = QUERY_GENERATOR_PROMPT.replace(
36 | "{conversation}",
37 | formattedMessages
38 | ).replace("{additional_context}", additionalContext);
39 |
40 | const response = await model.invoke([["user", formattedPrompt]]);
41 |
42 | return {
43 | query: response.content as string,
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/packages/shared/src/prompts/quick-actions.ts:
--------------------------------------------------------------------------------
1 | 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:
2 |
3 | {artifactContent}
4 | `;
5 |
6 | export const CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX = `You are an AI assistant tasked with rewriting a users generated artifact.
7 | They have provided custom instructions on how you should manage rewriting the artifact. The custom instructions are wrapped inside the tags.
8 |
9 | Use this context about the application the user is interacting with when generating your response:
10 |
11 | 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.
12 | 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.
13 | Users only have a single artifact per conversation, however they have the ability to go back and fourth between artifact edits/revisions.
14 | `;
15 |
16 | export const CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT = `Here is the last 5 (or less) messages in the chat history between you and the user:
17 |
18 | {conversation}
19 | `;
20 |
21 | export const REFLECTIONS_QUICK_ACTION_PROMPT = `The following are reflections on the user's style guidelines and general memories/facts about the user.
22 | Use these reflections as context when generating your response.
23 |
24 | {reflections}
25 | `;
26 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/actions_toolbar/code/PortToLanguage.tsx:
--------------------------------------------------------------------------------
1 | import { ProgrammingLanguageOptions } from "@opencanvas/shared/types";
2 | import { useToast } from "@/hooks/use-toast";
3 | import { ProgrammingLanguageList } from "@/components/ui/programming-lang-dropdown";
4 | import { GraphInput } from "@opencanvas/shared/types";
5 |
6 | export interface PortToLanguageOptionsProps {
7 | streamMessage: (params: GraphInput) => Promise;
8 | handleClose: () => void;
9 | language: ProgrammingLanguageOptions;
10 | }
11 |
12 | const prettifyLanguage = (language: ProgrammingLanguageOptions) => {
13 | switch (language) {
14 | case "php":
15 | return "PHP";
16 | case "typescript":
17 | return "TypeScript";
18 | case "javascript":
19 | return "JavaScript";
20 | case "cpp":
21 | return "C++";
22 | case "java":
23 | return "Java";
24 | case "python":
25 | return "Python";
26 | case "html":
27 | return "HTML";
28 | case "sql":
29 | return "SQL";
30 | default:
31 | return language;
32 | }
33 | };
34 |
35 | export function PortToLanguageOptions(props: PortToLanguageOptionsProps) {
36 | const { streamMessage } = props;
37 | const { toast } = useToast();
38 |
39 | const handleSubmit = async (portLanguage: ProgrammingLanguageOptions) => {
40 | if (portLanguage === props.language) {
41 | toast({
42 | title: "Port language error",
43 | description: `The code is already in ${prettifyLanguage(portLanguage)}`,
44 | duration: 5000,
45 | });
46 | props.handleClose();
47 | return;
48 | }
49 |
50 | props.handleClose();
51 | await streamMessage({
52 | portLanguage,
53 | });
54 | };
55 |
56 | return ;
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | );
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ));
33 | Alert.displayName = "Alert";
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ));
45 | AlertTitle.displayName = "AlertTitle";
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | AlertDescription.displayName = "AlertDescription";
58 |
59 | export { Alert, AlertTitle, AlertDescription };
60 |
--------------------------------------------------------------------------------
/apps/web/src/components/reflections-dialog/ConfirmClearDialog.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogDescription,
7 | DialogTrigger,
8 | } from "../ui/dialog";
9 | import { Button } from "../ui/button";
10 | import { TighterText } from "../ui/header";
11 |
12 | export interface ReflectionsProps {
13 | handleDeleteReflections: () => Promise;
14 | }
15 |
16 | export function ConfirmClearDialog(props: ReflectionsProps) {
17 | const { handleDeleteReflections } = props;
18 | const [open, setOpen] = useState(false);
19 |
20 | return (
21 |
22 |
23 | setOpen(true)} variant="destructive">
24 | Clear reflections
25 |
26 |
27 |
28 |
29 |
30 |
31 | Are you sure you want to clear all reflections? This action can
32 | not be undone.
33 |
34 |
35 |
36 | {
38 | setOpen(false);
39 | await handleDeleteReflections();
40 | }}
41 | variant="destructive"
42 | >
43 | Clear reflections
44 |
45 |
46 | setOpen(false)} variant="outline">
47 | Cancel
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/useLocalStorage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | type SetValue = T | ((val: T) => T);
4 |
5 | function useLocalStorage(
6 | key: string,
7 | initialValue: T
8 | ): [T, (value: SetValue) => void] {
9 | // Get from local storage then
10 | // parse stored json or return initialValue
11 | const readValue = (): T => {
12 | if (typeof window === "undefined") {
13 | return initialValue;
14 | }
15 |
16 | try {
17 | const item = window.localStorage.getItem(key);
18 | return item ? (JSON.parse(item) as T) : initialValue;
19 | } catch (error) {
20 | console.warn(`Error reading localStorage key "${key}":`, error);
21 | return initialValue;
22 | }
23 | };
24 |
25 | // State to store our value
26 | // Pass initial state function to useState so logic is only executed once
27 | const [storedValue, setStoredValue] = useState(readValue);
28 |
29 | // Return a wrapped version of useState's setter function that ...
30 | // ... persists the new value to localStorage.
31 | const setValue = (value: SetValue) => {
32 | try {
33 | // Allow value to be a function so we have same API as useState
34 | const valueToStore =
35 | value instanceof Function ? value(storedValue) : value;
36 |
37 | // Save state
38 | setStoredValue(valueToStore);
39 |
40 | // Save to local storage
41 | if (typeof window !== "undefined") {
42 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
43 | }
44 | } catch (error) {
45 | console.warn(`Error setting localStorage key "${key}":`, error);
46 | }
47 | };
48 |
49 | useEffect(() => {
50 | setStoredValue(readValue());
51 | // eslint-disable-next-line react-hooks/exhaustive-deps
52 | }, []);
53 |
54 | return [storedValue, setValue];
55 | }
56 |
57 | export default useLocalStorage;
58 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/password-input.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { Input, InputProps } from "./input";
7 | import { Button } from "./button";
8 | import { EyeIcon, EyeOffIcon } from "lucide-react";
9 |
10 | export const PasswordInput = React.forwardRef(
11 | ({ className, ...props }, ref) => {
12 | const [showPassword, setShowPassword] = React.useState(false);
13 | const disabled =
14 | props.value === "" || props.value === undefined || props.disabled;
15 |
16 | return (
17 |
18 |
24 | setShowPassword((prev) => !prev)}
30 | disabled={disabled}
31 | >
32 | {showPassword && !disabled ? (
33 |
34 | ) : (
35 |
36 | )}
37 |
38 | {showPassword ? "Hide password" : "Show password"}
39 |
40 |
41 |
42 | {/* hides browsers password toggles */}
43 |
51 |
52 | );
53 | }
54 | );
55 |
56 | PasswordInput.displayName = "PasswordInput";
57 |
--------------------------------------------------------------------------------
/packages/evals/src/data/query_routing.ts:
--------------------------------------------------------------------------------
1 | import { AIMessage, HumanMessage } from "@langchain/core/messages";
2 |
3 | export const QUERY_ROUTING_DATA: Record = {
4 | inputs: {
5 | messages: [
6 | new HumanMessage(
7 | "generate code for an LLM agent that can scrape the web"
8 | ),
9 | new AIMessage(
10 | "I've crafted a web scraper for you that fetches and parses content from a specified URL. Let me know if you need any modifications or additional features!"
11 | ),
12 | new HumanMessage("Where's the LLM?"),
13 | ],
14 | artifact: {
15 | currentIndex: 1,
16 | contents: [
17 | {
18 | index: 1,
19 | type: "code" as const,
20 | title: "Web Scraper LLM Agent",
21 | code: "import requests\nfrom bs4 import BeautifulSoup\n\nclass WebScraper:\n def __init__(self, url):\n self.url = url\n self.content = None\n\n def fetch_content(self):\n try:\n response = requests.get(self.url)\n response.raise_for_status() # Check for HTTP errors\n self.content = response.text\n except requests.RequestException as e:\n print(f\"Error fetching {self.url}: {e}\")\n\n def parse_content(self):\n if self.content:\n soup = BeautifulSoup(self.content, 'html.parser')\n return soup\n else:\n print(\"No content to parse. Please fetch content first.\")\n return None\n\n def scrape(self):\n self.fetch_content()\n return self.parse_content()\n\n# Example usage:\nif __name__ == '__main__':\n url = 'https://example.com'\n scraper = WebScraper(url)\n parsed_content = scraper.scrape()\n print(parsed_content)",
22 | language: "python" as const,
23 | },
24 | ],
25 | },
26 | },
27 | referenceOutputs: {
28 | next: "rewriteArtifact",
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/packages/evals/src/highlights.ts:
--------------------------------------------------------------------------------
1 | import { type Example, Run } from "langsmith";
2 | import { graph } from "@opencanvas/agents/dist/open-canvas/index";
3 | import { evaluate, EvaluationResult } from "langsmith/evaluation";
4 | import "dotenv/config";
5 |
6 | const runGraph = async (
7 | input: Record
8 | ): Promise> => {
9 | // Interrupt after updating the artifact
10 | graph.interruptAfter = ["updateArtifact"];
11 | return await graph.invoke(input);
12 | };
13 |
14 | const evaluateHighlights = (run: Run, example?: Example): EvaluationResult => {
15 | if (!example) {
16 | throw new Error("No example provided");
17 | }
18 | if (!example.outputs) {
19 | throw new Error("No example outputs provided");
20 | }
21 | if (!run.outputs) {
22 | throw new Error("No run outputs provided");
23 | }
24 |
25 | const { expectedGeneration } = example.outputs;
26 | const { highlighted, artifacts } = example.inputs;
27 | const expectedGenerationStart = artifacts[0].content.slice(
28 | 0,
29 | highlighted.startCharIndex
30 | );
31 | const expectedGenerationEnd = artifacts[0].content.slice(
32 | highlighted.endCharIndex
33 | );
34 | const fullExpectedArtifact = `${expectedGenerationStart}${expectedGeneration}${expectedGenerationEnd}`;
35 |
36 | const generatedArtifact = run.outputs.artifacts[0].content;
37 | if (generatedArtifact !== fullExpectedArtifact) {
38 | return {
39 | key: "correct_generation",
40 | score: false,
41 | };
42 | }
43 | return {
44 | key: "correct_generation",
45 | score: true,
46 | };
47 | };
48 |
49 | async function runHighlightEval() {
50 | const datasetName = "open-canvas-deterministic-highlights";
51 | await evaluate(runGraph, {
52 | data: datasetName,
53 | evaluators: [evaluateHighlights],
54 | experimentPrefix: "Highlight generation",
55 | });
56 | }
57 |
58 | runHighlightEval().catch(console.error);
59 |
--------------------------------------------------------------------------------
/apps/web/src/components/icons/svg/MP3Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as ResizablePrimitive from "react-resizable-panels";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { DragHandleDots2Icon } from "@radix-ui/react-icons";
6 |
7 | const ResizablePanelGroup = ({
8 | className,
9 | ...props
10 | }: React.ComponentProps) => (
11 |
18 | );
19 |
20 | const ResizablePanel = ResizablePrimitive.Panel;
21 |
22 | const ResizableHandle = ({
23 | withHandle,
24 | className,
25 | ...props
26 | }: React.ComponentProps & {
27 | withHandle?: boolean;
28 | }) => (
29 | div]:rotate-90",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {withHandle && (
37 |
38 |
39 |
40 | )}
41 |
42 | );
43 |
44 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
45 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/store/delete/id/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { Client } from "@langchain/langgraph-sdk";
3 | import { LANGGRAPH_API_URL } from "@/constants";
4 | import { verifyUserAuthenticated } from "../../../../../lib/supabase/verify_user_server";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const authRes = await verifyUserAuthenticated();
9 | if (!authRes?.user) {
10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 | }
12 | } catch (e) {
13 | console.error("Failed to fetch user", e);
14 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15 | }
16 |
17 | const { namespace, key, id } = await req.json();
18 |
19 | const lgClient = new Client({
20 | apiKey: process.env.LANGCHAIN_API_KEY,
21 | apiUrl: LANGGRAPH_API_URL,
22 | });
23 |
24 | try {
25 | const currentItems = await lgClient.store.getItem(namespace, key);
26 | if (!currentItems?.value) {
27 | return new NextResponse(
28 | JSON.stringify({
29 | error: "Item not found",
30 | success: false,
31 | }),
32 | {
33 | status: 404,
34 | headers: { "Content-Type": "application/json" },
35 | }
36 | );
37 | }
38 |
39 | const newValues = Object.fromEntries(
40 | Object.entries(currentItems.value).filter(([k]) => k !== id)
41 | );
42 |
43 | await lgClient.store.putItem(namespace, key, newValues);
44 |
45 | return new NextResponse(JSON.stringify({ success: true }), {
46 | status: 200,
47 | headers: { "Content-Type": "application/json" },
48 | });
49 | } catch (_) {
50 | return new NextResponse(
51 | JSON.stringify({ error: "Failed to share run after multiple attempts." }),
52 | {
53 | status: 500,
54 | headers: { "Content-Type": "application/json" },
55 | }
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/firecrawl/scrape/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { ContextDocument } from "@opencanvas/shared/types";
3 | import { FireCrawlLoader } from "@langchain/community/document_loaders/web/firecrawl";
4 |
5 | export async function POST(req: NextRequest) {
6 | try {
7 | const body = await req.json();
8 | const { urls } = body as { urls: string[] };
9 |
10 | if (!urls) {
11 | return NextResponse.json(
12 | { error: "`urls` is required." },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | if (!process.env.FIRECRAWL_API_KEY) {
18 | return NextResponse.json(
19 | {
20 | error: "Firecrawl API key is missing",
21 | },
22 | { status: 400 }
23 | );
24 | }
25 |
26 | const contextDocuments: ContextDocument[] = [];
27 |
28 | for (const url of urls) {
29 | const loader = new FireCrawlLoader({
30 | url,
31 | mode: "scrape",
32 | params: {
33 | formats: ["markdown"],
34 | },
35 | });
36 |
37 | const urlObj = new URL(url);
38 | const hostname = urlObj.hostname;
39 | const path = urlObj.pathname;
40 | const cleanedUrl = `${hostname}${path}`;
41 |
42 | const docs = await loader.load();
43 | const text = docs.map((doc) => doc.pageContent).join("\n");
44 |
45 | contextDocuments.push({
46 | name: cleanedUrl,
47 | type: "text",
48 | data: text,
49 | metadata: {
50 | url,
51 | },
52 | });
53 | }
54 |
55 | return NextResponse.json(
56 | { success: true, documents: contextDocuments },
57 | { status: 200 }
58 | );
59 | } catch (error: any) {
60 | console.error("Failed to process feedback request:", error);
61 |
62 | return NextResponse.json(
63 | { error: "Failed to submit feedback." + error.message },
64 | { status: 500 }
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/components/CopyText.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button";
3 | import { useToast } from "@/hooks/use-toast";
4 | import { isArtifactCodeContent } from "@opencanvas/shared/utils/artifacts";
5 | import { ArtifactCodeV3, ArtifactMarkdownV3 } from "@opencanvas/shared/types";
6 | import { Copy } from "lucide-react";
7 |
8 | interface CopyTextProps {
9 | currentArtifactContent: ArtifactCodeV3 | ArtifactMarkdownV3;
10 | }
11 |
12 | export function CopyText(props: CopyTextProps) {
13 | const { toast } = useToast();
14 |
15 | return (
16 |
22 | {
28 | try {
29 | const text = isArtifactCodeContent(props.currentArtifactContent)
30 | ? props.currentArtifactContent.code
31 | : props.currentArtifactContent.fullMarkdown;
32 | navigator.clipboard.writeText(text).then(() => {
33 | toast({
34 | title: "Copied to clipboard",
35 | description: "The canvas content has been copied.",
36 | duration: 5000,
37 | });
38 | });
39 | } catch (_) {
40 | toast({
41 | title: "Copy error",
42 | description:
43 | "Failed to copy the canvas content. Please try again.",
44 | duration: 5000,
45 | });
46 | }
47 | }}
48 | >
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/src/components/auth/signup/success/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { redirect, RedirectType } from "next/navigation";
5 | import { useUserContext } from "@/contexts/UserContext";
6 |
7 | export function SignupSuccess() {
8 | const { getUser, user } = useUserContext();
9 | const [isChecking, setIsChecking] = useState(true);
10 |
11 | useEffect(() => {
12 | if (user) {
13 | return;
14 | }
15 | const startTime = Date.now();
16 | const checkDuration = 3 * 60 * 1000; // 3 minutes in milliseconds
17 | const interval = 4000; // 4 seconds
18 |
19 | const checkUser = async () => {
20 | await getUser();
21 | if (Date.now() - startTime >= checkDuration) {
22 | setIsChecking(false);
23 | }
24 | };
25 |
26 | const intervalId = setInterval(checkUser, interval);
27 |
28 | // Initial check
29 | checkUser();
30 |
31 | // Cleanup function
32 | return () => clearInterval(intervalId);
33 | }, [getUser]);
34 |
35 | useEffect(() => {
36 | if (user) {
37 | redirect("/", RedirectType.push);
38 | }
39 | }, [user]);
40 |
41 | return (
42 |
43 |
44 |
Successfully Signed Up!
45 |
46 | Please check your email for a confirmation link. That link will
47 | redirect you to Open Canvas.
48 |
49 |
50 | If you don't see the email, please check your spam folder.
51 |
52 | {isChecking && (
53 |
54 | Waiting for email confirmation...
55 |
56 | )}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/packages/shared/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { ProgrammingLanguageOptions } from "./types.js";
2 |
3 | export const OC_SUMMARIZED_MESSAGE_KEY = "__oc_summarized_message";
4 | export const OC_HIDE_FROM_UI_KEY = "__oc_hide_from_ui";
5 | export const OC_WEB_SEARCH_RESULTS_MESSAGE_KEY =
6 | "__oc_web_search_results_message";
7 |
8 | export const CONTEXT_DOCUMENTS_NAMESPACE = ["context_documents"];
9 |
10 | export const DEFAULT_INPUTS = {
11 | highlightedCode: undefined,
12 | highlightedText: undefined,
13 | next: undefined,
14 | language: undefined,
15 | artifactLength: undefined,
16 | regenerateWithEmojis: undefined,
17 | readingLevel: undefined,
18 | addComments: undefined,
19 | addLogs: undefined,
20 | fixBugs: undefined,
21 | portLanguage: undefined,
22 | customQuickActionId: undefined,
23 | webSearchEnabled: undefined,
24 | webSearchResults: undefined,
25 | };
26 |
27 | export const PROGRAMMING_LANGUAGES: Array<{
28 | language: ProgrammingLanguageOptions;
29 | label: string;
30 | }> = [
31 | {
32 | language: "typescript",
33 | label: "TypeScript",
34 | },
35 | {
36 | language: "javascript",
37 | label: "JavaScript",
38 | },
39 | {
40 | language: "cpp",
41 | label: "C++",
42 | },
43 | {
44 | language: "java",
45 | label: "Java",
46 | },
47 | {
48 | language: "php",
49 | label: "PHP",
50 | },
51 | {
52 | language: "python",
53 | label: "Python",
54 | },
55 | {
56 | language: "html",
57 | label: "HTML",
58 | },
59 | {
60 | language: "sql",
61 | label: "SQL",
62 | },
63 | {
64 | language: "json",
65 | label: "JSON",
66 | },
67 | {
68 | language: "rust",
69 | label: "Rust",
70 | },
71 | {
72 | language: "xml",
73 | label: "XML",
74 | },
75 | {
76 | language: "clojure",
77 | label: "Clojure",
78 | },
79 | {
80 | language: "csharp",
81 | label: "C#",
82 | },
83 | {
84 | language: "other",
85 | label: "Other",
86 | },
87 | ];
88 |
--------------------------------------------------------------------------------
/apps/agents/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "eslint:recommended",
4 | "prettier",
5 | "plugin:@typescript-eslint/recommended",
6 | ],
7 | parserOptions: {
8 | ecmaVersion: 12,
9 | parser: "@typescript-eslint/parser",
10 | project: "./tsconfig.json",
11 | sourceType: "module",
12 | },
13 | plugins: ["import", "@typescript-eslint", "no-instanceof"],
14 | ignorePatterns: [
15 | ".eslintrc.cjs",
16 | "scripts",
17 | "src/utils/lodash/*",
18 | "node_modules",
19 | "dist",
20 | "dist-cjs",
21 | "*.js",
22 | "*.cjs",
23 | "*.d.ts",
24 | ],
25 | rules: {
26 | "@typescript-eslint/explicit-module-boundary-types": 0,
27 | "@typescript-eslint/no-empty-function": 0,
28 | "@typescript-eslint/no-shadow": 0,
29 | "@typescript-eslint/no-empty-interface": 0,
30 | "@typescript-eslint/no-use-before-define": ["error", "nofunc"],
31 | "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
32 | "@typescript-eslint/no-floating-promises": "error",
33 | "@typescript-eslint/no-misused-promises": "error",
34 | "@typescript-eslint/no-explicit-any": 0,
35 | camelcase: 0,
36 | "class-methods-use-this": 0,
37 | "import/extensions": [2, "ignorePackages"],
38 | "import/no-extraneous-dependencies": [
39 | "error",
40 | { devDependencies: ["**/*.test.ts"] },
41 | ],
42 | "import/no-unresolved": 0,
43 | "import/prefer-default-export": 0,
44 | "keyword-spacing": "error",
45 | "max-classes-per-file": 0,
46 | "max-len": 0,
47 | "no-await-in-loop": 0,
48 | "no-bitwise": 0,
49 | "no-console": 0,
50 | "no-restricted-syntax": 0,
51 | "no-shadow": 0,
52 | "no-continue": 0,
53 | "no-underscore-dangle": 0,
54 | "no-use-before-define": 0,
55 | "no-useless-constructor": 0,
56 | "no-return-await": 0,
57 | "consistent-return": 0,
58 | "no-else-return": 0,
59 | "new-cap": ["error", { properties: false, capIsNew: false }],
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/packages/evals/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "eslint:recommended",
4 | "prettier",
5 | "plugin:@typescript-eslint/recommended",
6 | ],
7 | parserOptions: {
8 | ecmaVersion: 12,
9 | parser: "@typescript-eslint/parser",
10 | project: "./tsconfig.json",
11 | sourceType: "module",
12 | },
13 | plugins: ["import", "@typescript-eslint", "no-instanceof"],
14 | ignorePatterns: [
15 | ".eslintrc.cjs",
16 | "scripts",
17 | "src/utils/lodash/*",
18 | "node_modules",
19 | "dist",
20 | "dist-cjs",
21 | "*.js",
22 | "*.cjs",
23 | "*.d.ts",
24 | ],
25 | rules: {
26 | "@typescript-eslint/explicit-module-boundary-types": 0,
27 | "@typescript-eslint/no-empty-function": 0,
28 | "@typescript-eslint/no-shadow": 0,
29 | "@typescript-eslint/no-empty-interface": 0,
30 | "@typescript-eslint/no-use-before-define": ["error", "nofunc"],
31 | "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
32 | "@typescript-eslint/no-floating-promises": "error",
33 | "@typescript-eslint/no-misused-promises": "error",
34 | "@typescript-eslint/no-explicit-any": 0,
35 | camelcase: 0,
36 | "class-methods-use-this": 0,
37 | "import/extensions": [2, "ignorePackages"],
38 | "import/no-extraneous-dependencies": [
39 | "error",
40 | { devDependencies: ["**/*.test.ts"] },
41 | ],
42 | "import/no-unresolved": 0,
43 | "import/prefer-default-export": 0,
44 | "keyword-spacing": "error",
45 | "max-classes-per-file": 0,
46 | "max-len": 0,
47 | "no-await-in-loop": 0,
48 | "no-bitwise": 0,
49 | "no-console": 0,
50 | "no-restricted-syntax": 0,
51 | "no-shadow": 0,
52 | "no-continue": 0,
53 | "no-underscore-dangle": 0,
54 | "no-use-before-define": 0,
55 | "no-useless-constructor": 0,
56 | "no-return-await": 0,
57 | "consistent-return": 0,
58 | "no-else-return": 0,
59 | "new-cap": ["error", { properties: false, capIsNew: false }],
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/packages/shared/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "eslint:recommended",
4 | "prettier",
5 | "plugin:@typescript-eslint/recommended",
6 | ],
7 | parserOptions: {
8 | ecmaVersion: 12,
9 | parser: "@typescript-eslint/parser",
10 | project: "./tsconfig.json",
11 | sourceType: "module",
12 | },
13 | plugins: ["import", "@typescript-eslint", "no-instanceof"],
14 | ignorePatterns: [
15 | ".eslintrc.cjs",
16 | "scripts",
17 | "src/utils/lodash/*",
18 | "node_modules",
19 | "dist",
20 | "dist-cjs",
21 | "*.js",
22 | "*.cjs",
23 | "*.d.ts",
24 | ],
25 | rules: {
26 | "@typescript-eslint/explicit-module-boundary-types": 0,
27 | "@typescript-eslint/no-empty-function": 0,
28 | "@typescript-eslint/no-shadow": 0,
29 | "@typescript-eslint/no-empty-interface": 0,
30 | "@typescript-eslint/no-use-before-define": ["error", "nofunc"],
31 | "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
32 | "@typescript-eslint/no-floating-promises": "error",
33 | "@typescript-eslint/no-misused-promises": "error",
34 | "@typescript-eslint/no-explicit-any": 0,
35 | camelcase: 0,
36 | "class-methods-use-this": 0,
37 | "import/extensions": [2, "ignorePackages"],
38 | "import/no-extraneous-dependencies": [
39 | "error",
40 | { devDependencies: ["**/*.test.ts"] },
41 | ],
42 | "import/no-unresolved": 0,
43 | "import/prefer-default-export": 0,
44 | "keyword-spacing": "error",
45 | "max-classes-per-file": 0,
46 | "max-len": 0,
47 | "no-await-in-loop": 0,
48 | "no-bitwise": 0,
49 | "no-console": 0,
50 | "no-restricted-syntax": 0,
51 | "no-shadow": 0,
52 | "no-continue": 0,
53 | "no-underscore-dangle": 0,
54 | "no-use-before-define": 0,
55 | "no-useless-constructor": 0,
56 | "no-return-await": 0,
57 | "consistent-return": 0,
58 | "no-else-return": 0,
59 | "new-cap": ["error", { properties: false, capIsNew: false }],
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/runs/share/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { Client } from "langsmith";
3 |
4 | const MAX_RETRIES = 5;
5 | const RETRY_DELAY = 5000; // 5 seconds
6 |
7 | async function shareRunWithRetry(
8 | lsClient: Client,
9 | runId: string
10 | ): Promise {
11 | for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
12 | try {
13 | return await lsClient.shareRun(runId);
14 | } catch (error) {
15 | if (attempt === MAX_RETRIES) {
16 | throw error;
17 | }
18 | console.warn(
19 | `Attempt ${attempt} failed. Retrying in ${RETRY_DELAY / 1000} seconds...`
20 | );
21 | await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
22 | }
23 | }
24 | throw new Error("Max retries reached"); // This line should never be reached due to the throw in the loop
25 | }
26 |
27 | export async function POST(req: NextRequest) {
28 | const { runId } = await req.json();
29 |
30 | if (!runId) {
31 | return new NextResponse(
32 | JSON.stringify({
33 | error: "`runId` is required to share run.",
34 | }),
35 | {
36 | status: 400,
37 | headers: { "Content-Type": "application/json" },
38 | }
39 | );
40 | }
41 |
42 | const lsClient = new Client({
43 | apiKey: process.env.LANGCHAIN_API_KEY,
44 | });
45 |
46 | try {
47 | const sharedRunURL = await shareRunWithRetry(lsClient, runId);
48 |
49 | return new NextResponse(JSON.stringify({ sharedRunURL }), {
50 | status: 200,
51 | headers: { "Content-Type": "application/json" },
52 | });
53 | } catch (error) {
54 | console.error(
55 | `Failed to share run with id ${runId} after ${MAX_RETRIES} attempts:\n`,
56 | error
57 | );
58 | return new NextResponse(
59 | JSON.stringify({ error: "Failed to share run after multiple attempts." }),
60 | {
61 | status: 500,
62 | headers: { "Content-Type": "application/json" },
63 | }
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/header/navigate-artifact-history.tsx:
--------------------------------------------------------------------------------
1 | import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button";
2 | import { Forward } from "lucide-react";
3 |
4 | interface NavigateArtifactHistoryProps {
5 | isBackwardsDisabled: boolean;
6 | isForwardDisabled: boolean;
7 | setSelectedArtifact: (prevState: number) => void;
8 | currentArtifactIndex: number;
9 | totalArtifactVersions: number;
10 | }
11 |
12 | export function NavigateArtifactHistory(props: NavigateArtifactHistoryProps) {
13 | const prevTooltip = `Previous (${props.currentArtifactIndex - 1}/${props.totalArtifactVersions})`;
14 | const nextTooltip = `Next (${props.currentArtifactIndex + 1}/${props.totalArtifactVersions})`;
15 |
16 | return (
17 |
18 | {
24 | if (!props.isBackwardsDisabled) {
25 | props.setSelectedArtifact(props.currentArtifactIndex - 1);
26 | }
27 | }}
28 | disabled={props.isBackwardsDisabled}
29 | className="w-fit h-fit p-2"
30 | >
31 |
35 |
36 | {
42 | if (!props.isForwardDisabled) {
43 | props.setSelectedArtifact(props.currentArtifactIndex + 1);
44 | }
45 | }}
46 | disabled={props.isForwardDisabled}
47 | className="w-fit h-fit p-2"
48 | >
49 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/src/types.ts:
--------------------------------------------------------------------------------
1 | import { ProgrammingLanguageOptions } from "@opencanvas/shared/types";
2 |
3 | export type Message = {
4 | id: string;
5 | text?: string;
6 | rawResponse?: Record;
7 | sender: string;
8 | toolCalls?: ToolCall[];
9 | };
10 |
11 | export interface ToolCall {
12 | id: string;
13 | name: string;
14 | args: string;
15 | result?: any;
16 | }
17 |
18 | export type Model = "gpt-4o-mini" | string; // Add other model options as needed
19 |
20 | export type UserRules = {
21 | styleRules: string[];
22 | contentRules: string[];
23 | };
24 |
25 | export interface ArtifactV2 {
26 | id: string;
27 | contents: (ArtifactMarkdownContent | ArtifactCodeContent)[];
28 | currentContentIndex: number;
29 | }
30 |
31 | export interface MarkdownBlock {
32 | id: string;
33 | content: Array<{
34 | id: string;
35 | type: string;
36 | text: string;
37 | styles: Record;
38 | }>;
39 | type: string;
40 | }
41 |
42 | export interface ArtifactMarkdownContent {
43 | index: number;
44 | blocks: MarkdownBlock[];
45 | title: string;
46 | type: "text";
47 | }
48 |
49 | export interface ArtifactCodeContent {
50 | index: number;
51 | code: string;
52 | title: string;
53 | type: "code";
54 | language: ProgrammingLanguageOptions;
55 | }
56 |
57 | export interface Highlight {
58 | /**
59 | * The index of the first character of the highlighted text
60 | */
61 | startCharIndex: number;
62 | /**
63 | * The index of the last character of the highlighted text
64 | */
65 | endCharIndex: number;
66 | }
67 |
68 | export interface NewMarkdownToolResponse {
69 | blocks: Array<{ block_id?: string; new_text?: string }>;
70 | }
71 |
72 | export interface ModelConfig {
73 | temperature?: number;
74 | modelProvider: string;
75 | maxTokens?: number;
76 | azureConfig?: {
77 | azureOpenAIApiKey: string;
78 | azureOpenAIApiInstanceName: string;
79 | azureOpenAIApiDeploymentName: string;
80 | azureOpenAIApiVersion: string;
81 | azureOpenAIBasePath?: string;
82 | };
83 | }
84 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | );
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean;
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button";
46 | return (
47 |
52 | );
53 | }
54 | );
55 | Button.displayName = "Button";
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ));
42 | CardTitle.displayName = "CardTitle";
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLDivElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = "CardDescription";
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ));
62 | CardContent.displayName = "CardContent";
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | CardFooter.displayName = "CardFooter";
75 |
76 | export {
77 | Card,
78 | CardHeader,
79 | CardFooter,
80 | CardTitle,
81 | CardDescription,
82 | CardContent,
83 | };
84 |
--------------------------------------------------------------------------------
/apps/web/src/components/chat-interface/feedback.tsx:
--------------------------------------------------------------------------------
1 | import { useToast } from "@/hooks/use-toast";
2 | import { FeedbackResponse } from "@/hooks/useFeedback";
3 | import { ThumbsUpIcon, ThumbsDownIcon } from "lucide-react";
4 | import { Dispatch, FC, SetStateAction } from "react";
5 | import { cn } from "@/lib/utils";
6 | import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button";
7 |
8 | interface FeedbackButtonProps {
9 | runId: string;
10 | setFeedbackSubmitted: Dispatch>;
11 | sendFeedback: (
12 | runId: string,
13 | feedbackKey: string,
14 | score: number,
15 | comment?: string
16 | ) => Promise;
17 | feedbackValue: number;
18 | icon: "thumbs-up" | "thumbs-down";
19 | isLoading: boolean;
20 | }
21 |
22 | export const FeedbackButton: FC = ({
23 | runId,
24 | setFeedbackSubmitted,
25 | sendFeedback,
26 | isLoading,
27 | feedbackValue,
28 | icon,
29 | }) => {
30 | const { toast } = useToast();
31 |
32 | const handleClick = async () => {
33 | try {
34 | const res = await sendFeedback(runId, "feedback", feedbackValue);
35 | if (res?.success) {
36 | setFeedbackSubmitted(true);
37 | } else {
38 | toast({
39 | title: "Failed to submit feedback",
40 | description: "Please try again later.",
41 | variant: "destructive",
42 | });
43 | }
44 | } catch (_) {
45 | toast({
46 | title: "Failed to submit feedback",
47 | description: "Please try again later.",
48 | variant: "destructive",
49 | });
50 | }
51 | };
52 |
53 | const tooltip = `Give ${icon === "thumbs-up" ? "positive" : "negative"} feedback on this run`;
54 |
55 | return (
56 |
64 | {icon === "thumbs-up" ? (
65 |
66 | ) : (
67 |
70 | )}
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { cn } from "@/lib/utils";
6 | import { ChevronDownIcon } from "@radix-ui/react-icons";
7 |
8 | const Accordion = AccordionPrimitive.Root;
9 |
10 | const AccordionItem = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
19 | ));
20 | AccordionItem.displayName = "AccordionItem";
21 |
22 | const AccordionTrigger = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef
25 | >(({ className, children, ...props }, ref) => (
26 |
27 | svg]:rotate-180",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {children}
36 |
37 |
38 |
39 | ));
40 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
41 |
42 | const AccordionContent = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef
45 | >(({ className, children, ...props }, ref) => (
46 |
51 | {children}
52 |
53 | ));
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
57 |
--------------------------------------------------------------------------------
/apps/web/src/components/canvas/canavas-loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "../ui/skeleton";
2 |
3 | export function CanvasLoading() {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | >
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/apps/agents/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opencanvas/agents",
3 | "author": "Brace Sproul",
4 | "repository": "https://github.com/langchain-ai/open-canvas",
5 | "version": "0.0.1",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "files": [
10 | "dist/**/*"
11 | ],
12 | "license": "MIT",
13 | "private": true,
14 | "scripts": {
15 | "dev": "npx @langchain/langgraph-cli dev --port 54367 --config ../../langgraph.json",
16 | "build": "yarn clean && tsc",
17 | "clean": "rm -rf ./dist .turbo || true",
18 | "format": "prettier --config .prettierrc --write \"src\"",
19 | "lint": "eslint src",
20 | "lint:fix": "eslint src --fix",
21 | "postinstall": "yarn turbo build"
22 | },
23 | "dependencies": {
24 | "@ffmpeg/ffmpeg": "^0.12.15",
25 | "@ffmpeg/util": "^0.12.2",
26 | "@langchain/anthropic": "^0.3.12",
27 | "@langchain/community": "^0.3.28",
28 | "@langchain/core": "^0.3.38",
29 | "@langchain/exa": "^0.1.0",
30 | "@langchain/google-genai": "^0.1.7",
31 | "@langchain/groq": "^0.1.3",
32 | "@langchain/langgraph": "^0.2.41",
33 | "@langchain/langgraph-sdk": "^0.0.37",
34 | "@langchain/ollama": "^0.1.4",
35 | "@langchain/openai": "^0.4.2",
36 | "@mendable/firecrawl-js": "1.10.1",
37 | "@opencanvas/shared": "*",
38 | "@supabase/supabase-js": "^2.45.5",
39 | "date-fns": "^4.1.0",
40 | "dotenv": "^16.4.5",
41 | "exa-js": "^1.4.10",
42 | "framer-motion": "^11.11.9",
43 | "groq-sdk": "^0.13.0",
44 | "langchain": "^0.3.14",
45 | "langsmith": "^0.3.3",
46 | "lodash": "^4.17.21",
47 | "pdf-parse": "^1.1.1",
48 | "uuid": "^10.0.0",
49 | "zod": "^3.23.8"
50 | },
51 | "devDependencies": {
52 | "@eslint/js": "^9.12.0",
53 | "@types/eslint__js": "^8.42.3",
54 | "@types/lodash": "^4.17.12",
55 | "@types/node": "^20",
56 | "@types/pdf-parse": "^1.1.4",
57 | "@types/uuid": "^10.0.0",
58 | "@typescript-eslint/eslint-plugin": "^8.12.2",
59 | "@typescript-eslint/parser": "^8.8.1",
60 | "eslint": "^8",
61 | "eslint-plugin-unused-imports": "^4.1.4",
62 | "prettier": "^3.3.3",
63 | "tsx": "^4.19.1",
64 | "turbo": "latest",
65 | "typescript": "^5",
66 | "typescript-eslint": "^8.8.1",
67 | "vitest": "^3.0.4"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/rewrite-artifact/update-meta.ts:
--------------------------------------------------------------------------------
1 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
2 | import { OpenCanvasGraphAnnotation } from "../../state.js";
3 | import {
4 | formatArtifactContent,
5 | getModelFromConfig,
6 | isUsingO1MiniModel,
7 | } from "../../../utils.js";
8 | import { getArtifactContent } from "@opencanvas/shared/utils/artifacts";
9 | import { GET_TITLE_TYPE_REWRITE_ARTIFACT } from "../../prompts.js";
10 | import { OPTIONALLY_UPDATE_ARTIFACT_META_SCHEMA } from "./schemas.js";
11 | import { getFormattedReflections } from "../../../utils.js";
12 | import { z } from "zod";
13 |
14 | export async function optionallyUpdateArtifactMeta(
15 | state: typeof OpenCanvasGraphAnnotation.State,
16 | config: LangGraphRunnableConfig
17 | ): Promise> {
18 | const toolCallingModel = (
19 | await getModelFromConfig(config, {
20 | isToolCalling: true,
21 | })
22 | )
23 | .withStructuredOutput(
24 | OPTIONALLY_UPDATE_ARTIFACT_META_SCHEMA,
25 |
26 | {
27 | name: "optionallyUpdateArtifactMeta",
28 | }
29 | )
30 | .withConfig({ runName: "optionally_update_artifact_meta" });
31 |
32 | const memoriesAsString = await getFormattedReflections(config);
33 |
34 | const currentArtifactContent = state.artifact
35 | ? getArtifactContent(state.artifact)
36 | : undefined;
37 | if (!currentArtifactContent) {
38 | throw new Error("No artifact found");
39 | }
40 |
41 | const optionallyUpdateArtifactMetaPrompt =
42 | GET_TITLE_TYPE_REWRITE_ARTIFACT.replace(
43 | "{artifact}",
44 | formatArtifactContent(currentArtifactContent, true)
45 | ).replace("{reflections}", memoriesAsString);
46 |
47 | const recentHumanMessage = state._messages.findLast(
48 | (message) => message.getType() === "human"
49 | );
50 | if (!recentHumanMessage) {
51 | throw new Error("No recent human message found");
52 | }
53 |
54 | const isO1MiniModel = isUsingO1MiniModel(config);
55 | const optionallyUpdateArtifactResponse = await toolCallingModel.invoke([
56 | {
57 | role: isO1MiniModel ? "user" : "system",
58 | content: optionallyUpdateArtifactMetaPrompt,
59 | },
60 | recentHumanMessage,
61 | ]);
62 |
63 | return optionallyUpdateArtifactResponse;
64 | }
65 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/header/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReflectionsDialog } from "../../reflections-dialog/ReflectionsDialog";
2 | import { ArtifactTitle } from "./artifact-title";
3 | import { NavigateArtifactHistory } from "./navigate-artifact-history";
4 | import { ArtifactCodeV3, ArtifactMarkdownV3 } from "@opencanvas/shared/types";
5 | import { Assistant } from "@langchain/langgraph-sdk";
6 | import { PanelRightClose } from "lucide-react";
7 | import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button";
8 |
9 | interface ArtifactHeaderProps {
10 | isBackwardsDisabled: boolean;
11 | isForwardDisabled: boolean;
12 | setSelectedArtifact: (index: number) => void;
13 | currentArtifactContent: ArtifactCodeV3 | ArtifactMarkdownV3;
14 | isArtifactSaved: boolean;
15 | totalArtifactVersions: number;
16 | selectedAssistant: Assistant | undefined;
17 | artifactUpdateFailed: boolean;
18 | chatCollapsed: boolean;
19 | setChatCollapsed: (c: boolean) => void;
20 | }
21 |
22 | export function ArtifactHeader(props: ArtifactHeaderProps) {
23 | return (
24 |
25 |
26 | {props.chatCollapsed && (
27 |
props.setChatCollapsed(false)}
33 | >
34 |
35 |
36 | )}
37 |
42 |
43 |
44 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/reflect.ts:
--------------------------------------------------------------------------------
1 | import { Client } from "@langchain/langgraph-sdk";
2 | import { OpenCanvasGraphAnnotation } from "../state.js";
3 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
4 |
5 | export const reflectNode = async (
6 | state: typeof OpenCanvasGraphAnnotation.State,
7 | config: LangGraphRunnableConfig
8 | ) => {
9 | try {
10 | const langGraphClient = new Client({
11 | apiUrl: `http://localhost:${process.env.PORT}`,
12 | });
13 |
14 | const reflectionInput = {
15 | messages: state._messages,
16 | artifact: state.artifact,
17 | };
18 | const reflectionConfig = {
19 | configurable: {
20 | // Ensure we pass in the current graph's assistant ID as this is
21 | // how we fetch & store the memories.
22 | open_canvas_assistant_id: config.configurable?.assistant_id,
23 | },
24 | };
25 |
26 | const newThread = await langGraphClient.threads.create();
27 | // Create a new reflection run, but do not `wait` for it to finish.
28 | // Intended to be a background run.
29 | await langGraphClient.runs.create(
30 | // We enqueue the memory formation process on the same thread.
31 | // This means that IF this thread doesn't receive more messages before `afterSeconds`,
32 | // it will read from the shared state and extract memories for us.
33 | // If a new request comes in for this thread before the scheduled run is executed,
34 | // that run will be canceled, and a **new** one will be scheduled once
35 | // this node is executed again.
36 | newThread.thread_id,
37 | // Pass the name of the graph to run.
38 | "reflection",
39 | {
40 | input: reflectionInput,
41 | config: reflectionConfig,
42 | // This memory-formation run will be enqueued and run later
43 | // If a new run comes in before it is scheduled, it will be cancelled,
44 | // then when this node is executed again, a *new* run will be scheduled
45 | multitaskStrategy: "enqueue",
46 | // This lets us "debounce" repeated requests to the memory graph
47 | // if the user is actively engaging in a conversation. This saves us $$ and
48 | // can help reduce the occurrence of duplicate memories.
49 | afterSeconds: 5 * 60, // 5 minutes
50 | }
51 | );
52 | } catch (e) {
53 | console.error("Failed to start reflection");
54 | }
55 |
56 | return {};
57 | };
58 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SliderPrimitive from "@radix-ui/react-slider";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, orientation = "horizontal", ...props }, ref) => (
12 |
24 |
31 |
40 |
41 | {[1, 2, 3, 4, 5].map((tick) => (
42 |
60 | ))}
61 |
67 |
68 | ));
69 | Slider.displayName = SliderPrimitive.Root.displayName;
70 |
71 | export { Slider };
72 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/actions_toolbar/text/TranslateOptions.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | UsaFlag,
3 | ChinaFlag,
4 | IndiaFlag,
5 | SpanishFlag,
6 | FrenchFlag,
7 | } from "@/components/icons/flags";
8 | import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button";
9 | import { GraphInput } from "@opencanvas/shared/types";
10 | import { LanguageOptions } from "@opencanvas/shared/types";
11 |
12 | export interface TranslateOptionsProps {
13 | streamMessage: (params: GraphInput) => Promise;
14 | handleClose: () => void;
15 | }
16 |
17 | export function TranslateOptions(props: TranslateOptionsProps) {
18 | const { streamMessage } = props;
19 |
20 | const handleSubmit = async (language: LanguageOptions) => {
21 | props.handleClose();
22 | await streamMessage({
23 | language,
24 | });
25 | };
26 |
27 | return (
28 |
29 | await handleSubmit("english")}
35 | >
36 |
37 |
38 | await handleSubmit("mandarin")}
44 | >
45 |
46 |
47 | await handleSubmit("hindi")}
53 | >
54 |
55 |
56 | await handleSubmit("spanish")}
62 | >
63 |
64 |
65 | await handleSubmit("french")}
71 | >
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/runs/feedback/route.ts:
--------------------------------------------------------------------------------
1 | import { Client, Feedback } from "langsmith";
2 | import { NextRequest, NextResponse } from "next/server";
3 |
4 | export async function POST(req: NextRequest) {
5 | try {
6 | const body = await req.json();
7 | const { runId, feedbackKey, score, comment } = body;
8 |
9 | if (!runId || !feedbackKey) {
10 | return NextResponse.json(
11 | { error: "`runId` and `feedbackKey` are required." },
12 | { status: 400 }
13 | );
14 | }
15 |
16 | const lsClient = new Client({
17 | apiKey: process.env.LANGCHAIN_API_KEY,
18 | });
19 |
20 | const feedback = await lsClient.createFeedback(runId, feedbackKey, {
21 | score,
22 | comment,
23 | });
24 |
25 | return NextResponse.json(
26 | { success: true, feedback: feedback },
27 | { status: 200 }
28 | );
29 | } catch (error) {
30 | console.error("Failed to process feedback request:", error);
31 |
32 | return NextResponse.json(
33 | { error: "Failed to submit feedback." },
34 | { status: 500 }
35 | );
36 | }
37 | }
38 |
39 | export async function GET(req: NextRequest) {
40 | try {
41 | const searchParams = req.nextUrl.searchParams;
42 | const runId = searchParams.get("runId");
43 | const feedbackKey = searchParams.get("feedbackKey");
44 |
45 | if (!runId || !feedbackKey) {
46 | return new NextResponse(
47 | JSON.stringify({
48 | error: "`runId` and `feedbackKey` are required.",
49 | }),
50 | {
51 | status: 400,
52 | headers: { "Content-Type": "application/json" },
53 | }
54 | );
55 | }
56 |
57 | const lsClient = new Client({
58 | apiKey: process.env.LANGCHAIN_API_KEY,
59 | });
60 |
61 | const runFeedback: Feedback[] = [];
62 |
63 | const run_feedback = await lsClient.listFeedback({
64 | runIds: [runId],
65 | feedbackKeys: [feedbackKey],
66 | });
67 |
68 | for await (const feedback of run_feedback) {
69 | runFeedback.push(feedback);
70 | }
71 |
72 | return new NextResponse(
73 | JSON.stringify({
74 | feedback: runFeedback,
75 | }),
76 | {
77 | status: 200,
78 | headers: { "Content-Type": "application/json" },
79 | }
80 | );
81 | } catch (error) {
82 | console.error("Failed to fetch feedback:", error);
83 | return NextResponse.json(
84 | { error: "Failed to fetch feedback." },
85 | { status: 500 }
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/actions_toolbar/text/ReadingLevelOptions.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Baby,
3 | GraduationCap,
4 | PersonStanding,
5 | School,
6 | Swords,
7 | } from "lucide-react";
8 | import { ReadingLevelOptions as ReadingLevelOptionsType } from "@opencanvas/shared/types";
9 | import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button";
10 | import { GraphInput } from "@opencanvas/shared/types";
11 |
12 | export interface ReadingLevelOptionsProps {
13 | streamMessage: (params: GraphInput) => Promise;
14 | handleClose: () => void;
15 | }
16 |
17 | export function ReadingLevelOptions(props: ReadingLevelOptionsProps) {
18 | const { streamMessage } = props;
19 |
20 | const handleSubmit = async (readingLevel: ReadingLevelOptionsType) => {
21 | props.handleClose();
22 | await streamMessage({
23 | readingLevel,
24 | });
25 | };
26 |
27 | return (
28 |
29 |
await handleSubmit("phd")}
35 | >
36 |
37 |
38 |
await handleSubmit("college")}
44 | >
45 |
46 |
47 |
await handleSubmit("teenager")}
53 | >
54 |
55 |
56 |
await handleSubmit("child")}
62 | >
63 |
64 |
65 |
await handleSubmit("pirate")}
71 | >
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/whisper/audio/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import Groq from "groq-sdk";
3 | import { createClient } from "@supabase/supabase-js";
4 |
5 | export async function POST(req: NextRequest) {
6 | try {
7 | const body = await req.json();
8 | const { path } = body as { path: string };
9 |
10 | if (!path) {
11 | return NextResponse.json(
12 | { error: "`path` is required." },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | if (
18 | !process.env.NEXT_PUBLIC_SUPABASE_URL_DOCUMENTS ||
19 | !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY_DOCUMENTS
20 | ) {
21 | return NextResponse.json(
22 | {
23 | error:
24 | "Supabase credentials for uploading context documents are missing",
25 | },
26 | { status: 400 }
27 | );
28 | }
29 |
30 | const supabase = createClient(
31 | process.env.NEXT_PUBLIC_SUPABASE_URL_DOCUMENTS,
32 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY_DOCUMENTS
33 | );
34 |
35 | const supabaseFile = await supabase.storage
36 | .from("documents")
37 | .download(path);
38 |
39 | if (supabaseFile.error) {
40 | console.error(supabaseFile.error);
41 | return NextResponse.json(
42 | {
43 | error: `Failed to download context document: ${JSON.stringify(supabaseFile.error, null)}. File path: ${path}`,
44 | },
45 | { status: 400 }
46 | );
47 | }
48 |
49 | const groq = new Groq({
50 | apiKey: process.env.GROQ_API_KEY,
51 | });
52 |
53 | // supabaseFile.data is already a Blob, get its type
54 | const mimeType = supabaseFile.data.type;
55 | const fileExtension = mimeType.split("/")[1];
56 | const file = new File([supabaseFile.data], `audio.${fileExtension}`, {
57 | type: mimeType,
58 | });
59 |
60 | const transcription = await groq.audio.transcriptions.create({
61 | file,
62 | model: "distil-whisper-large-v3-en",
63 | language: "en",
64 | temperature: 0.0,
65 | });
66 |
67 | // Cleanup by deleting the file from supabase
68 | await supabase.storage.from("documents").remove([path]);
69 |
70 | return NextResponse.json(
71 | { success: true, text: transcription.text },
72 | { status: 200 }
73 | );
74 | } catch (error: any) {
75 | console.error("Failed to process feedback request:", error);
76 |
77 | return NextResponse.json(
78 | { error: "Failed to submit feedback." + error.message },
79 | { status: 500 }
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/generateFollowup.ts:
--------------------------------------------------------------------------------
1 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
2 | import { getModelFromConfig } from "../../utils.js";
3 | import {
4 | getArtifactContent,
5 | isArtifactMarkdownContent,
6 | } from "@opencanvas/shared/utils/artifacts";
7 | import { Reflections } from "@opencanvas/shared/types";
8 | import { ensureStoreInConfig, formatReflections } from "../../utils.js";
9 | import { FOLLOWUP_ARTIFACT_PROMPT } from "../prompts.js";
10 | import {
11 | OpenCanvasGraphAnnotation,
12 | OpenCanvasGraphReturnType,
13 | } from "../state.js";
14 |
15 | /**
16 | * Generate a followup message after generating or updating an artifact.
17 | */
18 | export const generateFollowup = async (
19 | state: typeof OpenCanvasGraphAnnotation.State,
20 | config: LangGraphRunnableConfig
21 | ): Promise => {
22 | const smallModel = await getModelFromConfig(config, {
23 | maxTokens: 250,
24 | // We say tool calling is true here because that'll cause it to use a small model
25 | isToolCalling: true,
26 | });
27 |
28 | const store = ensureStoreInConfig(config);
29 | const assistantId = config.configurable?.assistant_id;
30 | if (!assistantId) {
31 | throw new Error("`assistant_id` not found in configurable");
32 | }
33 | const memoryNamespace = ["memories", assistantId];
34 | const memoryKey = "reflection";
35 | const memories = await store.get(memoryNamespace, memoryKey);
36 | const memoriesAsString = memories?.value
37 | ? formatReflections(memories.value as Reflections, {
38 | onlyContent: true,
39 | })
40 | : "No reflections found.";
41 |
42 | const currentArtifactContent = state.artifact
43 | ? getArtifactContent(state.artifact)
44 | : undefined;
45 |
46 | const artifactContent = currentArtifactContent
47 | ? isArtifactMarkdownContent(currentArtifactContent)
48 | ? currentArtifactContent.fullMarkdown
49 | : currentArtifactContent.code
50 | : undefined;
51 |
52 | const formattedPrompt = FOLLOWUP_ARTIFACT_PROMPT.replace(
53 | "{artifactContent}",
54 | artifactContent || "No artifacts generated yet."
55 | )
56 | .replace("{reflections}", memoriesAsString)
57 | .replace(
58 | "{conversation}",
59 | state._messages
60 | .map((msg) => `<${msg.getType()}>\n${msg.content}\n${msg.getType()}>`)
61 | .join("\n\n")
62 | );
63 |
64 | // TODO: Include the chat history as well.
65 | const response = await smallModel.invoke([
66 | { role: "user", content: formattedPrompt },
67 | ]);
68 |
69 | return {
70 | messages: [response],
71 | _messages: [response],
72 | };
73 | };
74 |
--------------------------------------------------------------------------------
/apps/web/src/components/assistant-select/color-picker.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from "react";
2 | import React from "react";
3 | import { HexColorPicker } from "react-colorful";
4 | import { motion, AnimatePresence } from "framer-motion";
5 |
6 | interface ColorPickerProps {
7 | iconColor: string;
8 | setIconColor: Dispatch>;
9 | showColorPicker: boolean;
10 | setShowColorPicker: Dispatch>;
11 | hoverTimer: NodeJS.Timeout | null;
12 | setHoverTimer: Dispatch>;
13 | disabled: boolean;
14 | }
15 |
16 | export function ColorPicker(props: ColorPickerProps) {
17 | const {
18 | iconColor,
19 | setIconColor,
20 | showColorPicker,
21 | setShowColorPicker,
22 | hoverTimer,
23 | setHoverTimer,
24 | } = props;
25 |
26 | const handleMouseEnter = () => {
27 | if (props.disabled) return;
28 | const timer = setTimeout(() => {
29 | setShowColorPicker(true);
30 | }, 200);
31 | setHoverTimer(timer);
32 | };
33 |
34 | const handleMouseLeave = () => {
35 | if (hoverTimer) {
36 | clearTimeout(hoverTimer);
37 | }
38 | setShowColorPicker(false);
39 | };
40 |
41 | return (
42 |
43 |
{
48 | if (hoverTimer) {
49 | clearTimeout(hoverTimer);
50 | }
51 | }}
52 | />
53 |
54 | {showColorPicker && (
55 | setShowColorPicker(true)}
62 | onMouseLeave={handleMouseLeave}
63 | >
64 | {
68 | if (props.disabled) return;
69 |
70 | if (!e.startsWith("#")) {
71 | setIconColor("#" + e);
72 | } else {
73 | setIconColor(e);
74 | }
75 | }}
76 | />
77 |
78 | )}
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/apps/web/src/components/assistant-select/assistant-item.tsx:
--------------------------------------------------------------------------------
1 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
2 | import { Dispatch, MouseEventHandler, SetStateAction } from "react";
3 | import { Assistant } from "@langchain/langgraph-sdk";
4 | import { cn } from "@/lib/utils";
5 | import { getIcon } from "./utils";
6 | import { EditDeleteDropdown } from "./edit-delete-dropdown";
7 |
8 | interface AssistantItemProps {
9 | assistant: Assistant;
10 | allDisabled: boolean;
11 | selectedAssistantId: string | undefined;
12 | setAllDisabled: Dispatch
>;
13 | onClick: MouseEventHandler;
14 | setEditModalOpen: Dispatch>;
15 | deleteAssistant: (assistantId: string) => Promise;
16 | setAssistantDropdownOpen: Dispatch>;
17 | setEditingAssistant: Dispatch>;
18 | }
19 |
20 | export function AssistantItem({
21 | allDisabled,
22 | assistant,
23 | selectedAssistantId,
24 | setAllDisabled,
25 | onClick,
26 | setEditModalOpen,
27 | deleteAssistant,
28 | setAssistantDropdownOpen,
29 | setEditingAssistant,
30 | }: AssistantItemProps) {
31 | const isDefault = assistant.metadata?.is_default as boolean | undefined;
32 | const isSelected = assistant.assistant_id === selectedAssistantId;
33 | const metadata = assistant.metadata as Record;
34 |
35 | return (
36 |
37 |
45 |
49 | {getIcon(metadata?.iconData?.iconName as string | undefined)}
50 |
51 | {assistant.name}
52 | {isDefault && (
53 | {"(default)"}
54 | )}
55 | {isSelected && • }
56 |
57 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/generate-artifact/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContextDocumentMessages,
3 | getFormattedReflections,
4 | getModelConfig,
5 | getModelFromConfig,
6 | isUsingO1MiniModel,
7 | optionallyGetSystemPromptFromConfig,
8 | } from "../../../utils.js";
9 | import { ArtifactV3 } from "@opencanvas/shared/types";
10 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
11 | import {
12 | OpenCanvasGraphAnnotation,
13 | OpenCanvasGraphReturnType,
14 | } from "../../state.js";
15 | import { ARTIFACT_TOOL_SCHEMA } from "./schemas.js";
16 | import { createArtifactContent, formatNewArtifactPrompt } from "./utils.js";
17 | import { z } from "zod";
18 |
19 | /**
20 | * Generate a new artifact based on the user's query.
21 | */
22 | export const generateArtifact = async (
23 | state: typeof OpenCanvasGraphAnnotation.State,
24 | config: LangGraphRunnableConfig
25 | ): Promise => {
26 | const { modelName } = getModelConfig(config, {
27 | isToolCalling: true,
28 | });
29 | const smallModel = await getModelFromConfig(config, {
30 | temperature: 0.5,
31 | isToolCalling: true,
32 | });
33 |
34 | const modelWithArtifactTool = smallModel.bindTools(
35 | [
36 | {
37 | name: "generate_artifact",
38 | description: ARTIFACT_TOOL_SCHEMA.description,
39 | schema: ARTIFACT_TOOL_SCHEMA,
40 | },
41 | ],
42 | {
43 | tool_choice: "generate_artifact",
44 | }
45 | );
46 |
47 | const memoriesAsString = await getFormattedReflections(config);
48 | const formattedNewArtifactPrompt = formatNewArtifactPrompt(
49 | memoriesAsString,
50 | modelName
51 | );
52 |
53 | const userSystemPrompt = optionallyGetSystemPromptFromConfig(config);
54 | const fullSystemPrompt = userSystemPrompt
55 | ? `${userSystemPrompt}\n${formattedNewArtifactPrompt}`
56 | : formattedNewArtifactPrompt;
57 |
58 | const contextDocumentMessages = await createContextDocumentMessages(config);
59 | const isO1MiniModel = isUsingO1MiniModel(config);
60 | const response = await modelWithArtifactTool.invoke(
61 | [
62 | { role: isO1MiniModel ? "user" : "system", content: fullSystemPrompt },
63 | ...contextDocumentMessages,
64 | ...state._messages,
65 | ],
66 | { runName: "generate_artifact" }
67 | );
68 | const args = response.tool_calls?.[0].args as
69 | | z.infer
70 | | undefined;
71 | if (!args) {
72 | throw new Error("No args found in response");
73 | }
74 |
75 | const newArtifactContent = createArtifactContent(args);
76 | const newArtifact: ArtifactV3 = {
77 | currentIndex: 1,
78 | contents: [newArtifactContent],
79 | };
80 |
81 | return {
82 | artifact: newArtifact,
83 | };
84 | };
85 |
--------------------------------------------------------------------------------
/apps/web/src/components/artifacts/actions_toolbar/text/LengthOptions.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { useState } from "react";
3 | import { ArtifactLengthOptions } from "@opencanvas/shared/types";
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipProvider,
8 | TooltipTrigger,
9 | } from "@/components/ui/tooltip";
10 | import { Slider } from "@/components/ui/slider";
11 | import { GraphInput } from "@opencanvas/shared/types";
12 |
13 | export interface LengthOptionsProps {
14 | streamMessage: (params: GraphInput) => Promise;
15 | handleClose: () => void;
16 | }
17 |
18 | const lengthOptions = [
19 | { value: 1, label: "Shortest" },
20 | { value: 2, label: "Shorter" },
21 | { value: 3, label: "Current length" },
22 | { value: 4, label: "Long" },
23 | { value: 5, label: "Longest" },
24 | ];
25 |
26 | export function LengthOptions(props: LengthOptionsProps) {
27 | const { streamMessage } = props;
28 | const [open, setOpen] = useState(false);
29 | const [value, setValue] = useState([3]);
30 |
31 | const handleSubmit = async (artifactLength: ArtifactLengthOptions) => {
32 | props.handleClose();
33 | await streamMessage({
34 | artifactLength,
35 | });
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
43 | {
50 | setValue(newValue);
51 | setOpen(true);
52 | }}
53 | onValueCommit={async (v) => {
54 | setOpen(false);
55 | switch (v[0]) {
56 | case 1:
57 | await handleSubmit("shortest");
58 | break;
59 | case 2:
60 | await handleSubmit("short");
61 | break;
62 | case 3:
63 | // Same length, do nothing.
64 | break;
65 | case 4:
66 | await handleSubmit("long");
67 | break;
68 | case 5:
69 | await handleSubmit("longest");
70 | break;
71 | }
72 | }}
73 | orientation="vertical"
74 | color="black"
75 | className={cn("h-[180px] w-[26px]")}
76 | />
77 |
78 |
79 | {lengthOptions.find((option) => option.value === value[0])?.label}
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/useFeedback.ts:
--------------------------------------------------------------------------------
1 | import { Feedback } from "langsmith";
2 | import { useCallback, useState } from "react";
3 |
4 | export interface FeedbackResponse {
5 | success: boolean;
6 | feedback: Feedback;
7 | }
8 |
9 | interface UseFeedbackResult {
10 | isLoading: boolean;
11 | error: string | null;
12 | sendFeedback: (
13 | runId: string,
14 | feedbackKey: string,
15 | score: number,
16 | comment?: string
17 | ) => Promise;
18 | getFeedback: (
19 | runId: string,
20 | feedbackKey: string
21 | ) => Promise;
22 | }
23 |
24 | export function useFeedback(): UseFeedbackResult {
25 | const [isLoading, setIsLoading] = useState(false);
26 | const [error, setError] = useState(null);
27 |
28 | const sendFeedback = useCallback(
29 | async (
30 | runId: string,
31 | feedbackKey: string,
32 | score: number,
33 | comment?: string
34 | ): Promise => {
35 | setIsLoading(true);
36 | setError(null);
37 |
38 | try {
39 | const res = await fetch("/api/runs/feedback", {
40 | method: "POST",
41 | body: JSON.stringify({ runId, feedbackKey, score, comment }),
42 | headers: {
43 | "Content-Type": "application/json",
44 | },
45 | });
46 |
47 | if (!res.ok) {
48 | return;
49 | }
50 |
51 | return (await res.json()) as FeedbackResponse;
52 | } catch (error) {
53 | console.error("Error sending feedback:", error);
54 | setError(
55 | error instanceof Error ? error.message : "An unknown error occurred"
56 | );
57 | return;
58 | } finally {
59 | setIsLoading(false);
60 | }
61 | },
62 | []
63 | );
64 |
65 | const getFeedback = useCallback(
66 | async (
67 | runId: string,
68 | feedbackKey: string
69 | ): Promise => {
70 | setIsLoading(true);
71 | setError(null);
72 | try {
73 | const res = await fetch(
74 | `/api/runs/feedback?runId=${encodeURIComponent(runId)}&feedbackKey=${encodeURIComponent(feedbackKey)}`
75 | );
76 |
77 | if (!res.ok) {
78 | return;
79 | }
80 |
81 | return await res.json();
82 | } catch (error) {
83 | console.error("Error getting feedback:", error);
84 | setError(
85 | error instanceof Error ? error.message : "An unknown error occurred"
86 | );
87 | return;
88 | } finally {
89 | setIsLoading(false);
90 | }
91 | },
92 | []
93 | );
94 |
95 | return {
96 | isLoading,
97 | sendFeedback,
98 | getFeedback,
99 | error,
100 | };
101 | }
102 |
--------------------------------------------------------------------------------
/apps/agents/src/open-canvas/nodes/replyToGeneralInput.ts:
--------------------------------------------------------------------------------
1 | import { LangGraphRunnableConfig } from "@langchain/langgraph";
2 | import { getArtifactContent } from "@opencanvas/shared/utils/artifacts";
3 | import { Reflections } from "@opencanvas/shared/types";
4 | import {
5 | createContextDocumentMessages,
6 | ensureStoreInConfig,
7 | formatArtifactContentWithTemplate,
8 | formatReflections,
9 | getModelFromConfig,
10 | isUsingO1MiniModel,
11 | } from "../../utils.js";
12 | import { CURRENT_ARTIFACT_PROMPT, NO_ARTIFACT_PROMPT } from "../prompts.js";
13 | import {
14 | OpenCanvasGraphAnnotation,
15 | OpenCanvasGraphReturnType,
16 | } from "../state.js";
17 |
18 | /**
19 | * Generate responses to questions. Does not generate artifacts.
20 | */
21 | export const replyToGeneralInput = async (
22 | state: typeof OpenCanvasGraphAnnotation.State,
23 | config: LangGraphRunnableConfig
24 | ): Promise => {
25 | const smallModel = await getModelFromConfig(config);
26 |
27 | const prompt = `You are an AI assistant tasked with responding to the users question.
28 |
29 | The user has generated artifacts in the past. Use the following artifacts as context when responding to the users question.
30 |
31 | You also have the following reflections on style guidelines and general memories/facts about the user to use when generating your response.
32 |
33 | {reflections}
34 |
35 |
36 | {currentArtifactPrompt}`;
37 |
38 | const currentArtifactContent = state.artifact
39 | ? getArtifactContent(state.artifact)
40 | : undefined;
41 |
42 | const store = ensureStoreInConfig(config);
43 | const assistantId = config.configurable?.assistant_id;
44 | if (!assistantId) {
45 | throw new Error("`assistant_id` not found in configurable");
46 | }
47 | const memoryNamespace = ["memories", assistantId];
48 | const memoryKey = "reflection";
49 | const memories = await store.get(memoryNamespace, memoryKey);
50 | const memoriesAsString = memories?.value
51 | ? formatReflections(memories.value as Reflections)
52 | : "No reflections found.";
53 |
54 | const formattedPrompt = prompt
55 | .replace("{reflections}", memoriesAsString)
56 | .replace(
57 | "{currentArtifactPrompt}",
58 | currentArtifactContent
59 | ? formatArtifactContentWithTemplate(
60 | CURRENT_ARTIFACT_PROMPT,
61 | currentArtifactContent
62 | )
63 | : NO_ARTIFACT_PROMPT
64 | );
65 |
66 | const contextDocumentMessages = await createContextDocumentMessages(config);
67 | const isO1MiniModel = isUsingO1MiniModel(config);
68 | const response = await smallModel.invoke([
69 | { role: isO1MiniModel ? "user" : "system", content: formattedPrompt },
70 | ...contextDocumentMessages,
71 | ...state._messages,
72 | ]);
73 |
74 | return {
75 | messages: [response],
76 | _messages: [response],
77 | };
78 | };
79 |
--------------------------------------------------------------------------------
/apps/web/src/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from "@supabase/ssr";
2 | import { NextRequest, NextResponse } from "next/server";
3 |
4 | export async function updateSession(request: NextRequest) {
5 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) {
6 | throw new Error("NEXT_PUBLIC_SUPABASE_URL is not defined");
7 | }
8 | if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
9 | throw new Error("NEXT_PUBLIC_SUPABASE_ANON_KEY is not defined");
10 | }
11 |
12 | let supabaseResponse = NextResponse.next({
13 | request,
14 | });
15 |
16 | const supabase = createServerClient(
17 | process.env.NEXT_PUBLIC_SUPABASE_URL,
18 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
19 | {
20 | cookies: {
21 | getAll() {
22 | return request.cookies.getAll();
23 | },
24 | setAll(cookiesToSet) {
25 | cookiesToSet.forEach(({ name, value }) =>
26 | request.cookies.set(name, value)
27 | );
28 | supabaseResponse = NextResponse.next({
29 | request,
30 | });
31 | cookiesToSet.forEach(({ name, value, options }) =>
32 | supabaseResponse.cookies.set(name, value, options)
33 | );
34 | },
35 | },
36 | }
37 | );
38 |
39 | // IMPORTANT: Avoid writing any logic between createServerClient and
40 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug
41 | // issues with users being randomly logged out.
42 |
43 | const {
44 | data: { user },
45 | } = await supabase.auth.getUser();
46 |
47 | if (!user && !request.nextUrl.pathname.startsWith("/auth")) {
48 | // no user, respond by redirecting the user to the login page
49 | const url = request.nextUrl.clone();
50 | url.pathname = "/auth/login";
51 | return NextResponse.redirect(url);
52 | }
53 |
54 | if (user) {
55 | if (
56 | request.nextUrl.pathname.startsWith("/auth") &&
57 | !request.nextUrl.pathname.startsWith("/auth/signout")
58 | ) {
59 | // user is logged in, respond by redirecting the user to the home page
60 | const url = new URL("/", request.url);
61 | return NextResponse.redirect(url);
62 | }
63 | }
64 |
65 | // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
66 | // creating a new response object with NextResponse.next() make sure to:
67 | // 1. Pass the request in it, like so:
68 | // const myNewResponse = NextResponse.next({ request })
69 | // 2. Copy over the cookies, like so:
70 | // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
71 | // 3. Change the myNewResponse object to fit your needs, but avoid changing
72 | // the cookies!
73 | // 4. Finally:
74 | // return myNewResponse
75 | // If this is not done, you may be causing the browser and server to go out
76 | // of sync and terminate the user's session prematurely!
77 |
78 | return supabaseResponse;
79 | }
80 |
--------------------------------------------------------------------------------
/apps/web/src/components/assistant-select/context-documents/uploaded-file.tsx:
--------------------------------------------------------------------------------
1 | import PDFIcon from "@/components/icons/svg/PDFIcon.svg";
2 | import TXTIcon from "@/components/icons/svg/TXTIcon.svg";
3 | import MP4Icon from "@/components/icons/svg/MP4Icon.svg";
4 | import MP3Icon from "@/components/icons/svg/MP3Icon.svg";
5 | import { X } from "lucide-react";
6 | import NextImage from "next/image";
7 | import { Button } from "../../ui/button";
8 | import {
9 | ALLOWED_AUDIO_TYPE_ENDINGS,
10 | ALLOWED_VIDEO_TYPE_ENDINGS,
11 | } from "@/constants";
12 | import { ContextDocument } from "@opencanvas/shared/types";
13 | import { cn } from "@/lib/utils";
14 |
15 | export function UploadedFiles({
16 | files,
17 | handleRemoveFile,
18 | className,
19 | }: {
20 | files: FileList | ContextDocument[] | undefined;
21 | handleRemoveFile?: (index: number) => void;
22 | className?: string;
23 | }) {
24 | if (!files) return null;
25 |
26 | const filesArr = Array.isArray(files) ? files : Array.from(files);
27 |
28 | return (
29 |
30 | {filesArr.map((file, index) => (
31 |
35 | {file.type.includes("pdf") && (
36 |
37 | )}
38 | {file.type.startsWith("text/") &&
39 | !ALLOWED_VIDEO_TYPE_ENDINGS.some((ending) =>
40 | file.name.endsWith(ending)
41 | ) &&
42 | !ALLOWED_AUDIO_TYPE_ENDINGS.some((ending) =>
43 | file.name.endsWith(ending)
44 | ) && (
45 |
46 | )}
47 | {ALLOWED_VIDEO_TYPE_ENDINGS.some((ending) =>
48 | file.name.endsWith(ending)
49 | ) && (
50 |
51 | )}
52 | {ALLOWED_AUDIO_TYPE_ENDINGS.some((ending) =>
53 | file.name.endsWith(ending)
54 | ) && (
55 |
56 | )}
57 |
{file.name}
58 | {handleRemoveFile && (
59 |
{
64 | e.preventDefault();
65 | e.stopPropagation();
66 | handleRemoveFile(index);
67 | }}
68 | >
69 |
70 |
71 | )}
72 |
73 | ))}
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/packages/evals/src/agent.int.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "vitest";
2 | import * as ls from "langsmith/vitest";
3 | import { z } from "zod";
4 | import { ChatOpenAI } from "@langchain/openai";
5 |
6 | import { graph } from "@opencanvas/agents/dist/open-canvas/index";
7 | import { QUERY_ROUTING_DATA } from "./data/query_routing.js";
8 | import { CODEGEN_DATA } from "./data/codegen.js";
9 |
10 | ls.describe("query routing", () => {
11 | ls.test(
12 | "routes followups with questions to update artifact",
13 | {
14 | inputs: QUERY_ROUTING_DATA.inputs,
15 | referenceOutputs: QUERY_ROUTING_DATA.referenceOutputs,
16 | },
17 | async ({ inputs, referenceOutputs }) => {
18 | const generatePathNode = graph.nodes.generatePath;
19 | const res = await generatePathNode.invoke(inputs, {
20 | configurable: {
21 | customModelName: "gpt-4o-mini",
22 | },
23 | });
24 | ls.logOutputs(res);
25 | expect(res).toEqual(referenceOutputs);
26 | }
27 | );
28 | });
29 |
30 | const qualityEvaluator = async (params: {
31 | inputs: string;
32 | outputs: string;
33 | }) => {
34 | const judge = new ChatOpenAI({ model: "gpt-4o" }).withStructuredOutput(
35 | z.object({
36 | justification: z
37 | .string()
38 | .describe("reasoning for why you are assigning a given quality score"),
39 | quality_score: z
40 | .number()
41 | .describe(
42 | "quality score for how well the generated code answers the query."
43 | ),
44 | }),
45 | {
46 | name: "judge",
47 | }
48 | );
49 | const EVAL_PROMPT = [
50 | `Given the following user query and generated code, judge whether the`,
51 | `code satisfies the user's query. Return a quality score between 1 and 10,`,
52 | `where a 1 would be completely irrelevant to the user's input, and 10 would be a perfectly accurate code sample.`,
53 | `A 5 would be a code sample that is partially on target, but is missing some aspect of a user's request.`,
54 | `Justify your answer.\n`,
55 | `\n${params.inputs}\n \n`,
56 | `\n${params.outputs}\n `,
57 | ].join(" ");
58 | const res = await judge.invoke(EVAL_PROMPT);
59 | return {
60 | key: "quality",
61 | score: res.quality_score,
62 | comment: res.justification,
63 | };
64 | };
65 |
66 | ls.describe("codegen", () => {
67 | ls.test(
68 | "generate code with an LLM agent when asked",
69 | {
70 | inputs: CODEGEN_DATA.inputs,
71 | referenceOutputs: {},
72 | },
73 | async ({ inputs }) => {
74 | const generateArtifactNode = graph.nodes.generateArtifact;
75 | const res = await generateArtifactNode.invoke(inputs, {
76 | configurable: {
77 | customModelName: "gpt-4o-mini",
78 | },
79 | });
80 | ls.logOutputs(res);
81 | const generatedCode = (res.artifact?.contents[0] as any).code;
82 | expect(generatedCode).toBeDefined();
83 | const wrappedEvaluator = ls.wrapEvaluator(qualityEvaluator);
84 | await wrappedEvaluator({
85 | inputs: inputs.messages[0].content,
86 | outputs: generatedCode,
87 | });
88 | }
89 | );
90 | });
91 |
--------------------------------------------------------------------------------
/apps/agents/src/reflection/prompts.ts:
--------------------------------------------------------------------------------
1 | export const REFLECT_SYSTEM_PROMPT = `You are an expert assistant, and writer. You are tasked with reflecting on the following conversation between a user and an AI assistant.
2 | You are also provided with an 'artifact' the user and assistant worked together on to write. Artifacts can be code, creative writing, emails, or any other form of written content.
3 |
4 |
5 | {artifact}
6 |
7 |
8 | You have also previously generated the following reflections about the user. Your reflections are broken down into two categories:
9 | 1. Style Guidelines: These are the style guidelines you have generated for the user. Style guidelines can be anything from writing style, to code style, to design style.
10 | They should be general, and apply to the all the users work, including the conversation and artifact generated.
11 | 2. Content: These are general memories, facts, and insights you generate about the user. These can be anything from the users interests, to their goals, to their personality traits.
12 | Ensure you think carefully about what goes in here, as the assistant will use these when generating future responses or artifacts for the user.
13 |
14 |
15 | {reflections}
16 |
17 |
18 | Your job is to take all of the context and existing reflections and re-generate all. Use these guidelines when generating the new set of reflections:
19 |
20 |
21 | - Ensure your reflections are relevant to the conversation and artifact.
22 | - Remove duplicate reflections, or combine multiple reflections into one if they are duplicating content.
23 | - Do not remove reflections unless the conversation/artifact clearly demonstrates they should no longer be included.
24 | This does NOT mean remove reflections if you see no evidence of them in the conversation/artifact, but instead remove them if the user indicates they are no longer relevant.
25 | - Keep the rules you list high signal-to-noise - don't include unnecessary reflections, but make sure the ones you do add are descriptive.
26 | This is very important. We do NOT want to confuse the assistant in future interactions by having lots and lots of rules and memories.
27 | - Your reflections should be very descriptive and detailed, ensuring they are clear and will not be misinterpreted.
28 | - Keep the total number of style and user facts low. It's better to have individual rules be more detailed, than to have many rules that are vague.
29 | - Do NOT generate rules off of suspicions. Your rules should be based on cold hard facts from the conversation, and changes to the artifact the user has requested.
30 | You must be able to provide evidence and sources for each rule you generate if asked, so don't make assumptions.
31 | - Content reflections should be based on the user's messages, not the generated artifacts. Ensure you follow this rule closely to ensure you do not record things generated by the assistant as facts about the user.
32 |
33 |
34 | I'll reiterate one final time: ensure the reflections you generate are kept at a reasonable length, are descriptive, and are based on the conversation and artifact provided.
35 |
36 | Finally, use the 'generate_reflections' tool to generate the new, full list of reflections.`;
37 |
38 | export const REFLECT_USER_PROMPT = `Here is my conversation:
39 |
40 | {conversation}`;
41 |
--------------------------------------------------------------------------------