├── .codespellignore ├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── apps ├── agents │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc │ ├── package.json │ ├── scripts │ │ └── check-dev-server.ts │ ├── src │ │ ├── open-canvas │ │ │ ├── index.ts │ │ │ ├── nodes │ │ │ │ ├── customAction.ts │ │ │ │ ├── generate-artifact │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schemas.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generate-path │ │ │ │ │ ├── documents.ts │ │ │ │ │ ├── dynamic-determine-path.ts │ │ │ │ │ ├── include-url-contents.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── generateFollowup.ts │ │ │ │ ├── generateTitle.ts │ │ │ │ ├── reflect.ts │ │ │ │ ├── replyToGeneralInput.ts │ │ │ │ ├── rewrite-artifact │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schemas.ts │ │ │ │ │ ├── update-meta.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── rewriteArtifactTheme.ts │ │ │ │ ├── rewriteCodeArtifactTheme.ts │ │ │ │ ├── summarizer.ts │ │ │ │ ├── updateArtifact.ts │ │ │ │ └── updateHighlightedText.ts │ │ │ ├── prompts.ts │ │ │ └── state.ts │ │ ├── reflection │ │ │ ├── index.ts │ │ │ ├── prompts.ts │ │ │ └── state.ts │ │ ├── summarizer │ │ │ ├── index.ts │ │ │ └── state.ts │ │ ├── thread-title │ │ │ ├── index.ts │ │ │ ├── prompts.ts │ │ │ └── state.ts │ │ ├── utils.ts │ │ └── web-search │ │ │ ├── index.ts │ │ │ ├── nodes │ │ │ ├── classify-message.ts │ │ │ ├── query-generator.ts │ │ │ └── search.ts │ │ │ └── state.ts │ ├── tsconfig.json │ └── turbo.json └── web │ ├── .env.example │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── LICENSE │ ├── README.md │ ├── case-study.txt │ ├── components.json │ ├── ls.vitest.config.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── lc_logo.jpg │ ├── lg_studio_graph_diagram.png │ └── screenshot.png │ ├── src │ ├── app │ │ ├── api │ │ │ ├── [..._path] │ │ │ │ └── route.ts │ │ │ ├── firecrawl │ │ │ │ └── scrape │ │ │ │ │ └── route.ts │ │ │ ├── runs │ │ │ │ ├── feedback │ │ │ │ │ └── route.ts │ │ │ │ └── share │ │ │ │ │ └── route.ts │ │ │ ├── store │ │ │ │ ├── delete │ │ │ │ │ ├── id │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── get │ │ │ │ │ └── route.ts │ │ │ │ └── put │ │ │ │ │ └── route.ts │ │ │ └── whisper │ │ │ │ └── audio │ │ │ │ └── route.ts │ │ ├── auth │ │ │ ├── callback │ │ │ │ └── route.ts │ │ │ ├── confirm │ │ │ │ └── route.ts │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ ├── signout │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ ├── page.tsx │ │ │ │ └── success │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components │ │ ├── NoSSRWrapper.tsx │ │ ├── artifacts │ │ │ ├── ArtifactLoading.tsx │ │ │ ├── ArtifactRenderer.tsx │ │ │ ├── CodeRenderer.module.css │ │ │ ├── CodeRenderer.tsx │ │ │ ├── TextRenderer.module.css │ │ │ ├── TextRenderer.tsx │ │ │ ├── actions_toolbar │ │ │ │ ├── code │ │ │ │ │ ├── PortToLanguage.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── custom │ │ │ │ │ ├── FullPrompt.tsx │ │ │ │ │ ├── NewCustomQuickActionDialog.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── text │ │ │ │ │ ├── LengthOptions.tsx │ │ │ │ │ ├── ReadingLevelOptions.tsx │ │ │ │ │ ├── TranslateOptions.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── components │ │ │ │ ├── AskOpenCanvas.tsx │ │ │ │ └── CopyText.tsx │ │ │ └── header │ │ │ │ ├── artifact-title.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── navigate-artifact-history.tsx │ │ ├── assistant-select │ │ │ ├── assistant-item.tsx │ │ │ ├── color-picker.tsx │ │ │ ├── context-documents │ │ │ │ ├── index.tsx │ │ │ │ └── uploaded-file.tsx │ │ │ ├── create-edit-assistant-dialog.tsx │ │ │ ├── edit-delete-dropdown.module.css │ │ │ ├── edit-delete-dropdown.tsx │ │ │ ├── icon-select.tsx │ │ │ ├── index.tsx │ │ │ └── utils.tsx │ │ ├── assistant-ui │ │ │ ├── attachment.tsx │ │ │ └── tooltip-icon-button.tsx │ │ ├── auth │ │ │ ├── login │ │ │ │ ├── Login.tsx │ │ │ │ ├── actions.ts │ │ │ │ └── user-auth-form-login.tsx │ │ │ └── signup │ │ │ │ ├── Signup.tsx │ │ │ │ ├── actions.ts │ │ │ │ ├── success │ │ │ │ └── index.tsx │ │ │ │ └── user-auth-form-signup.tsx │ │ ├── canvas │ │ │ ├── canavas-loading.tsx │ │ │ ├── canvas.tsx │ │ │ ├── content-composer.tsx │ │ │ └── index.ts │ │ ├── chat-interface │ │ │ ├── composer-actions-popout.tsx │ │ │ ├── composer.tsx │ │ │ ├── drag-drop-wrapper.tsx │ │ │ ├── feedback.tsx │ │ │ ├── index.tsx │ │ │ ├── messages.tsx │ │ │ ├── model-selector │ │ │ │ ├── index.tsx │ │ │ │ ├── model-config-pannel.tsx │ │ │ │ └── new-badge.tsx │ │ │ ├── thread-history.tsx │ │ │ ├── thread.tsx │ │ │ └── welcome.tsx │ │ ├── icons │ │ │ ├── flags.tsx │ │ │ ├── langsmith.tsx │ │ │ ├── magic_pencil.tsx │ │ │ └── svg │ │ │ │ ├── LLMIcon.svg │ │ │ │ ├── MP3Icon.svg │ │ │ │ ├── MP4Icon.svg │ │ │ │ ├── PDFIcon.svg │ │ │ │ └── TXTIcon.svg │ │ ├── reflections-dialog │ │ │ ├── ConfirmClearDialog.tsx │ │ │ └── ReflectionsDialog.tsx │ │ ├── tool-hooks │ │ │ ├── AttachmentsToolUI.tsx │ │ │ └── LangSmithLinkToolUI.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert.tsx │ │ │ ├── assistant-ui │ │ │ │ ├── attachment-adapters │ │ │ │ │ ├── audio.ts │ │ │ │ │ ├── pdf.ts │ │ │ │ │ └── video.ts │ │ │ │ ├── attachment-ui.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── markdown-text.tsx │ │ │ │ ├── syntax-highlighter.tsx │ │ │ │ ├── tooltip-icon-button.tsx │ │ │ │ └── utils │ │ │ │ │ └── withDefaults.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── header.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── icons.tsx │ │ │ ├── inline-context-tooltip.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── programming-lang-dropdown.tsx │ │ │ ├── progress.tsx │ │ │ ├── resizable.tsx │ │ │ ├── select.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ └── tooltip.tsx │ │ └── web-search-results │ │ │ ├── index.tsx │ │ │ └── loading-cards.tsx │ ├── constants.ts │ ├── contexts │ │ ├── AssistantContext.tsx │ │ ├── GraphContext.tsx │ │ ├── ThreadProvider.tsx │ │ ├── UserContext.tsx │ │ └── utils.ts │ ├── hooks │ │ ├── use-toast.ts │ │ ├── useContextDocuments.tsx │ │ ├── useFeedback.ts │ │ ├── useLocalStorage.tsx │ │ ├── useRuns.tsx │ │ ├── useStore.tsx │ │ └── utils.ts │ ├── lib │ │ ├── attachments.tsx │ │ ├── convert_messages.ts │ │ ├── cookies.ts │ │ ├── get_language_template.ts │ │ ├── normalize_string.ts │ │ ├── store.ts │ │ ├── supabase │ │ │ ├── client.ts │ │ │ ├── middleware.ts │ │ │ ├── server.ts │ │ │ └── verify_user_server.ts │ │ └── utils.ts │ ├── middleware.ts │ ├── types.ts │ └── workers │ │ └── graph-stream │ │ ├── stream.worker.ts │ │ ├── streamWorker.ts │ │ └── streamWorker.types.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── langgraph.json ├── package.json ├── packages ├── evals │ ├── .eslintrc.cjs │ ├── .prettierrc │ ├── package.json │ ├── src │ │ ├── agent.int.test.ts │ │ ├── data │ │ │ ├── codegen.ts │ │ │ └── query_routing.ts │ │ └── highlights.ts │ ├── tsconfig.json │ └── turbo.json └── shared │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc │ ├── package.json │ ├── src │ ├── constants.ts │ ├── models.ts │ ├── prompts │ │ └── quick-actions.ts │ ├── types.ts │ └── utils │ │ ├── artifacts.ts │ │ ├── thinking.ts │ │ └── urls.ts │ ├── tsconfig.json │ └── turbo.json ├── static └── screenshot.png ├── tsconfig.json ├── turbo.json └── yarn.lock /.codespellignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/open-canvas/be112c2017902daa8dc3dc0e9347b320d2fba636/.codespellignore -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "groq", 4 | "langchain", 5 | "langsmith", 6 | "opencanvas", 7 | "Signup", 8 | "sourounding", 9 | "Supabase" 10 | ] 11 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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": "yarn langgraphjs dev --port 54367 --config ../../langgraph.json --no-browser", 16 | "build": "yarn clean && tsc", 17 | "clean": "rm -rf ./dist .turbo || true", 18 | "format": "prettier --config .prettierrc --write \"src\" \"scripts\"", 19 | "format:check": "prettier --config .prettierrc --check \"src\" \"scripts\"", 20 | "lint": "eslint src", 21 | "lint:fix": "eslint src --fix", 22 | "postinstall": "yarn turbo build" 23 | }, 24 | "dependencies": { 25 | "@ffmpeg/ffmpeg": "^0.12.15", 26 | "@ffmpeg/util": "^0.12.2", 27 | "@langchain/anthropic": "^0.3.21", 28 | "@langchain/community": "^0.3.45", 29 | "@langchain/core": "^0.3.57", 30 | "@langchain/exa": "^0.1.0", 31 | "@langchain/google-genai": "^0.2.10", 32 | "@langchain/groq": "^0.2.2", 33 | "@langchain/langgraph": "^0.2.73", 34 | "@langchain/langgraph-sdk": "^0.0.78", 35 | "@langchain/ollama": "^0.2.0", 36 | "@langchain/openai": "^0.4.2", 37 | "@mendable/firecrawl-js": "1.10.1", 38 | "@opencanvas/shared": "*", 39 | "@supabase/supabase-js": "^2.45.5", 40 | "date-fns": "^4.1.0", 41 | "dotenv": "^16.4.5", 42 | "exa-js": "^1.4.10", 43 | "framer-motion": "^11.11.9", 44 | "groq-sdk": "^0.13.0", 45 | "langchain": "^0.3.27", 46 | "langsmith": "^0.3.29", 47 | "lodash": "^4.17.21", 48 | "pdf-parse": "^1.1.1", 49 | "uuid": "^10.0.0", 50 | "zod": "^3.23.8" 51 | }, 52 | "devDependencies": { 53 | "@eslint/js": "^9.12.0", 54 | "@langchain/langgraph-cli": "^0.0.27", 55 | "@types/eslint__js": "^8.42.3", 56 | "@types/lodash": "^4.17.12", 57 | "@types/node": "^20", 58 | "@types/pdf-parse": "^1.1.4", 59 | "@types/uuid": "^10.0.0", 60 | "@typescript-eslint/eslint-plugin": "^8.12.2", 61 | "@typescript-eslint/parser": "^8.8.1", 62 | "eslint": "^8", 63 | "eslint-plugin-unused-imports": "^4.1.4", 64 | "prettier": "^3.3.3", 65 | "tsx": "^4.19.1", 66 | "turbo": "latest", 67 | "typescript": "^5", 68 | "typescript-eslint": "^8.8.1", 69 | "vitest": "^3.0.4" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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/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/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/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`) 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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/agents/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["//"], 3 | "tasks": { 4 | "build": { 5 | "outputs": ["**/dist/**"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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/.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/.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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/public/lc_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/open-canvas/be112c2017902daa8dc3dc0e9347b320d2fba636/apps/web/public/lc_logo.jpg -------------------------------------------------------------------------------- /apps/web/public/lg_studio_graph_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/open-canvas/be112c2017902daa8dc3dc0e9347b320d2fba636/apps/web/public/lg_studio_graph_diagram.png -------------------------------------------------------------------------------- /apps/web/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/open-canvas/be112c2017902daa8dc3dc0e9347b320d2fba636/apps/web/public/screenshot.png -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/open-canvas/be112c2017902daa8dc3dc0e9347b320d2fba636/apps/web/src/app/favicon.ico -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/artifacts/actions_toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./text"; 2 | export * from "./code"; 3 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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 | 71 | )} 72 |
73 | ))} 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | 37 | 38 | {tooltip} 39 | 40 | 41 | ); 42 | }); 43 | 44 | TooltipIconButton.displayName = "TooltipIconButton"; 45 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/web/src/components/canvas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./canvas"; 2 | export * from "./canavas-loading"; 3 | -------------------------------------------------------------------------------- /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/chat-interface/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./thread"; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/web/src/components/icons/svg/MP3Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/web/src/components/icons/svg/MP4Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/web/src/components/icons/svg/PDFIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/web/src/components/icons/svg/TXTIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 | 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 | 45 |
46 | 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | 45 | 46 | {tooltip} 47 | 48 | 49 | ); 50 | } 51 | ); 52 | 53 | TooltipIconButton.displayName = "TooltipIconButton"; 54 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | 41 | 42 | {/* hides browsers password toggles */} 43 | 51 |
52 | ); 53 | } 54 | ); 55 | 56 | PasswordInput.displayName = "PasswordInput"; 57 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 |