From d29292ebd297b1e2006a5d58943a0b2588efe442 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Thu, 6 Jun 2024 12:43:34 -0700 Subject: [PATCH 1/6] [FEAT] Add LiteLLM embedding provider support (#1579) * add liteLLM embedding provider support * update tooltip id --------- Co-authored-by: timothycarambat --- docker/.env.example | 6 + .../LiteLLMOptions/index.jsx | 186 ++++++++++++++++++ .../EmbeddingPreference/index.jsx | 9 + .../Steps/DataHandling/index.jsx | 7 + server/.env.example | 6 + .../utils/EmbeddingEngines/liteLLM/index.js | 93 +++++++++ server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 1 + 8 files changed, 311 insertions(+) create mode 100644 frontend/src/components/EmbeddingSelection/LiteLLMOptions/index.jsx create mode 100644 server/utils/EmbeddingEngines/liteLLM/index.js diff --git a/docker/.env.example b/docker/.env.example index 6368a190..174a9d69 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -128,6 +128,12 @@ GID='1000' # VOYAGEAI_API_KEY= # EMBEDDING_MODEL_PREF='voyage-large-2-instruct' +# EMBEDDING_ENGINE='litellm' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' +# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# LITE_LLM_BASE_PATH='http://127.0.0.1:4000' +# LITE_LLM_API_KEY='sk-123abc' + ########################################### ######## Vector Database Selection ######## ########################################### diff --git a/frontend/src/components/EmbeddingSelection/LiteLLMOptions/index.jsx b/frontend/src/components/EmbeddingSelection/LiteLLMOptions/index.jsx new file mode 100644 index 00000000..d5586c88 --- /dev/null +++ b/frontend/src/components/EmbeddingSelection/LiteLLMOptions/index.jsx @@ -0,0 +1,186 @@ +import { useEffect, useState } from "react"; +import System from "@/models/system"; +import { Warning } from "@phosphor-icons/react"; +import { Tooltip } from "react-tooltip"; + +export default function LiteLLMOptions({ settings }) { + const [basePathValue, setBasePathValue] = useState(settings?.LiteLLMBasePath); + const [basePath, setBasePath] = useState(settings?.LiteLLMBasePath); + const [apiKeyValue, setApiKeyValue] = useState(settings?.LiteLLMAPIKey); + const [apiKey, setApiKey] = useState(settings?.LiteLLMAPIKey); + + return ( +
+
+
+ + setBasePathValue(e.target.value)} + onBlur={() => setBasePath(basePathValue)} + /> +
+ +
+ + e.target.blur()} + defaultValue={settings?.EmbeddingModelMaxChunkLength} + required={false} + autoComplete="off" + /> +
+
+
+
+
+ +
+ setApiKeyValue(e.target.value)} + onBlur={() => setApiKey(apiKeyValue)} + /> +
+
+
+ ); +} + +function LiteLLMModelSelection({ settings, basePath = null, apiKey = null }) { + const [customModels, setCustomModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + if (!basePath) { + setCustomModels([]); + setLoading(false); + return; + } + setLoading(true); + const { models } = await System.customModels( + "litellm", + typeof apiKey === "boolean" ? null : apiKey, + basePath + ); + setCustomModels(models || []); + setLoading(false); + } + findCustomModels(); + }, [basePath, apiKey]); + + if (loading || customModels.length == 0) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ + +
+ +
+ ); +} + +function EmbeddingModelTooltip() { + return ( +
+ + +

+ Be sure to select a valid embedding model. Chat models are not + embedding models. See{" "} + + this page + {" "} + for more information. +

+
+
+ ); +} diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index 5a0f51c1..4d032dc0 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -11,6 +11,7 @@ import OllamaLogo from "@/media/llmprovider/ollama.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import CohereLogo from "@/media/llmprovider/cohere.png"; import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png"; +import LiteLLMLogo from "@/media/llmprovider/litellm.png"; import PreLoader from "@/components/Preloader"; import ChangeWarningModal from "@/components/ChangeWarning"; @@ -22,6 +23,7 @@ import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOption import LMStudioEmbeddingOptions from "@/components/EmbeddingSelection/LMStudioOptions"; import CohereEmbeddingOptions from "@/components/EmbeddingSelection/CohereOptions"; import VoyageAiOptions from "@/components/EmbeddingSelection/VoyageAiOptions"; +import LiteLLMOptions from "@/components/EmbeddingSelection/LiteLLMOptions"; import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; @@ -88,6 +90,13 @@ const EMBEDDERS = [ options: (settings) => , description: "Run powerful embedding models from Voyage AI.", }, + { + name: "LiteLLM", + value: "litellm", + logo: LiteLLMLogo, + options: (settings) => , + description: "Run powerful embedding models from LiteLLM.", + }, ]; export default function GeneralEmbeddingPreference() { diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 35358636..b4fa666f 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -301,6 +301,13 @@ export const EMBEDDING_ENGINE_PRIVACY = { ], logo: VoyageAiLogo, }, + litellm: { + name: "LiteLLM", + description: [ + "Your document text is only accessible on the server running LiteLLM and to the providers you configured in LiteLLM.", + ], + logo: LiteLLMLogo, + }, }; export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) { diff --git a/server/.env.example b/server/.env.example index f51d6177..6148d594 100644 --- a/server/.env.example +++ b/server/.env.example @@ -125,6 +125,12 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # VOYAGEAI_API_KEY= # EMBEDDING_MODEL_PREF='voyage-large-2-instruct' +# EMBEDDING_ENGINE='litellm' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' +# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# LITE_LLM_BASE_PATH='http://127.0.0.1:4000' +# LITE_LLM_API_KEY='sk-123abc' + ########################################### ######## Vector Database Selection ######## ########################################### diff --git a/server/utils/EmbeddingEngines/liteLLM/index.js b/server/utils/EmbeddingEngines/liteLLM/index.js new file mode 100644 index 00000000..cd22480b --- /dev/null +++ b/server/utils/EmbeddingEngines/liteLLM/index.js @@ -0,0 +1,93 @@ +const { toChunks, maximumChunkLength } = require("../../helpers"); + +class LiteLLMEmbedder { + constructor() { + const { OpenAI: OpenAIApi } = require("openai"); + if (!process.env.LITE_LLM_BASE_PATH) + throw new Error( + "LiteLLM must have a valid base path to use for the api." + ); + this.basePath = process.env.LITE_LLM_BASE_PATH; + this.openai = new OpenAIApi({ + baseURL: this.basePath, + apiKey: process.env.LITE_LLM_API_KEY ?? null, + }); + this.model = process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002"; + + // Limit of how many strings we can process in a single pass to stay with resource or network limits + this.maxConcurrentChunks = 500; + this.embeddingMaxChunkLength = maximumChunkLength(); + } + + async embedTextInput(textInput) { + const result = await this.embedChunks( + Array.isArray(textInput) ? textInput : [textInput] + ); + return result?.[0] || []; + } + + async embedChunks(textChunks = []) { + // Because there is a hard POST limit on how many chunks can be sent at once to LiteLLM (~8mb) + // we concurrently execute each max batch of text chunks possible. + // Refer to constructor maxConcurrentChunks for more info. + const embeddingRequests = []; + for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) { + embeddingRequests.push( + new Promise((resolve) => { + this.openai.embeddings + .create({ + model: this.model, + input: chunk, + }) + .then((result) => { + resolve({ data: result?.data, error: null }); + }) + .catch((e) => { + e.type = + e?.response?.data?.error?.code || + e?.response?.status || + "failed_to_embed"; + e.message = e?.response?.data?.error?.message || e.message; + resolve({ data: [], error: e }); + }); + }) + ); + } + + const { data = [], error = null } = await Promise.all( + embeddingRequests + ).then((results) => { + // If any errors were returned from LiteLLM abort the entire sequence because the embeddings + // will be incomplete. + const errors = results + .filter((res) => !!res.error) + .map((res) => res.error) + .flat(); + if (errors.length > 0) { + let uniqueErrors = new Set(); + errors.map((error) => + uniqueErrors.add(`[${error.type}]: ${error.message}`) + ); + + return { + data: [], + error: Array.from(uniqueErrors).join(", "), + }; + } + return { + data: results.map((res) => res?.data || []).flat(), + error: null, + }; + }); + + if (!!error) throw new Error(`LiteLLM Failed to embed: ${error}`); + return data.length > 0 && + data.every((embd) => embd.hasOwnProperty("embedding")) + ? data.map((embd) => embd.embedding) + : null; + } +} + +module.exports = { + LiteLLMEmbedder, +}; diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index e60202a6..8f0df126 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -128,6 +128,9 @@ function getEmbeddingEngineSelection() { case "voyageai": const { VoyageAiEmbedder } = require("../EmbeddingEngines/voyageAi"); return new VoyageAiEmbedder(); + case "litellm": + const { LiteLLMEmbedder } = require("../EmbeddingEngines/liteLLM"); + return new LiteLLMEmbedder(); default: return new NativeEmbedder(); } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index d5cdc68f..1a0e710a 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -577,6 +577,7 @@ function supportedEmbeddingModel(input = "") { "lmstudio", "cohere", "voyageai", + "litellm", ]; return supported.includes(input) ? null From 98cef508a6c879d290f1a29aaafb13e873e6b557 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Fri, 7 Jun 2024 03:50:42 +0800 Subject: [PATCH 2/6] Feature/devcontv2 (#1622) * Updated apt-packages source for devcontainer Switched the devcontainer's package source to a different repository to align with updated dependencies and package availability. The previous source from 'rocker-org' is replaced with 'devcontainers-contrib', which may offer more recent or relevant development tools. * Subject: Centralize prettier ignores and refine config Body: Centralized all prettier ignore rules by removing individual `.prettierignore` files in subprojects and updating the root `.prettierignore` to include previously ignored patterns, ensuring consistency across the workspace. Additionally, the prettier configuration was refined by making the file pattern for `.config.js` files consistent and adjusting quote styles for better readability. All lint scripts across the project were updated to respect the centralized ignore path, enhancing maintainability. The consolidation simplifies the process of managing ignore rules as the project scales, ensuring developers can focus on writing code without worrying about divergent formatting standards. These changes also align with introducing comprehensive linting across multiple environments to keep the codebase clean and consistent. This adjustment is a foundational step towards a more streamlined and unified code base, making it easier for new contributors to adhere to established coding standards and reducing the cognitive load associated with managing multiple configuration files across the project. * unset package json changes --------- Co-authored-by: Francisco Bischoff Co-authored-by: Francisco Bischoff <984592+franzbischoff@users.noreply.github.com> --- .devcontainer/devcontainer.json | 2 +- .prettierignore | 4 ++++ .prettierrc | 2 +- collector/package.json | 2 +- embed/.prettierignore | 9 --------- embed/jsconfig.json | 10 ++++------ embed/package.json | 3 ++- embed/vite.config.js | 6 +++--- frontend/jsconfig.json | 6 ++---- frontend/package.json | 2 +- frontend/vite.config.js | 2 +- server/package.json | 2 +- 12 files changed, 21 insertions(+), 29 deletions(-) delete mode 100644 embed/.prettierignore diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83792da7..58c42b62 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,7 @@ // Terraform support "ghcr.io/devcontainers/features/terraform:1": {}, // Just a wrap to install needed packages - "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { + "ghcr.io/devcontainers-contrib/features/apt-packages:1": { // Dependencies copied from ../docker/Dockerfile plus some dev stuff "packages": [ "build-essential", diff --git a/.prettierignore b/.prettierignore index faedf325..e3b0c14e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,7 @@ frontend/bundleinspector.html #server server/swagger/openapi.json + +#embed +**/static/** +embed/src/utils/chat/hljs.js diff --git a/.prettierrc b/.prettierrc index 3574c1df..5e2bccfe 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,7 +17,7 @@ } }, { - "files": "*.config.js", + "files": ["*.config.js"], "options": { "semi": false, "parser": "flow", diff --git a/collector/package.json b/collector/package.json index 785604e3..938d65e1 100644 --- a/collector/package.json +++ b/collector/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "NODE_ENV=development nodemon --ignore hotdir --ignore storage --trace-warnings index.js", "start": "NODE_ENV=production node index.js", - "lint": "yarn prettier --write ./processSingleFile ./processLink ./utils index.js" + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./processSingleFile ./processLink ./utils index.js" }, "dependencies": { "@googleapis/youtube": "^9.0.0", diff --git a/embed/.prettierignore b/embed/.prettierignore deleted file mode 100644 index d90a3c08..00000000 --- a/embed/.prettierignore +++ /dev/null @@ -1,9 +0,0 @@ -# defaults -**/.git -**/.svn -**/.hg -**/node_modules - -**/dist -**/static/** -src/utils/chat/hljs.js diff --git a/embed/jsconfig.json b/embed/jsconfig.json index c8cc81fd..20cd368c 100644 --- a/embed/jsconfig.json +++ b/embed/jsconfig.json @@ -4,9 +4,7 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": [ - "./src/*" - ], - } - } -} \ No newline at end of file + "@/*": ["./src/*"], + }, + }, +} diff --git a/embed/package.json b/embed/package.json index eb399930..712af8e6 100644 --- a/embed/package.json +++ b/embed/package.json @@ -1,6 +1,7 @@ { "name": "anythingllm-embedded-chat", "private": false, + "license": "MIT", "type": "module", "scripts": { "dev": "nodemon -e js,jsx,css --watch src --exec \"yarn run dev:preview\"", @@ -8,7 +9,7 @@ "dev:build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js", "build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js && npx terser --compress -o dist/anythingllm-chat-widget.min.js -- dist/anythingllm-chat-widget.js", "build:publish": "yarn build && mkdir -p ../frontend/public/embed && cp -r dist/anythingllm-chat-widget.min.js ../frontend/public/embed/anythingllm-chat-widget.min.js", - "lint": "yarn prettier --write ./src" + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./src" }, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", diff --git a/embed/vite.config.js b/embed/vite.config.js index 21506422..9e23c70d 100644 --- a/embed/vite.config.js +++ b/embed/vite.config.js @@ -38,7 +38,7 @@ export default defineConfig({ rollupOptions: { external: [ // Reduces transformation time by 50% and we don't even use this variant, so we can ignore. - /@phosphor-icons\/react\/dist\/ssr/, + /@phosphor-icons\/react\/dist\/ssr/ ] }, commonjsOptions: { @@ -51,7 +51,7 @@ export default defineConfig({ emptyOutDir: true, inlineDynamicImports: true, assetsDir: "", - sourcemap: 'inline', + sourcemap: "inline" }, optimizeDeps: { esbuildOptions: { @@ -60,5 +60,5 @@ export default defineConfig({ }, plugins: [] } - }, + } }) diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json index c8cc81fd..e21fc376 100644 --- a/frontend/jsconfig.json +++ b/frontend/jsconfig.json @@ -4,9 +4,7 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": [ - "./src/*" - ], + "@/*": ["./src/*"] } } -} \ No newline at end of file +} diff --git a/frontend/package.json b/frontend/package.json index 11e612fc..2b669731 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "start": "vite --open", "dev": "NODE_ENV=development vite --debug --host=0.0.0.0", "build": "vite build", - "lint": "yarn prettier --write ./src", + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./src", "preview": "vite preview" }, "dependencies": { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 3785b947..ff96bdcd 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -51,7 +51,7 @@ export default defineConfig({ rollupOptions: { external: [ // Reduces transformation time by 50% and we don't even use this variant, so we can ignore. - /@phosphor-icons\/react\/dist\/ssr/, + /@phosphor-icons\/react\/dist\/ssr/ ] }, commonjsOptions: { diff --git a/server/package.json b/server/package.json index 4f995470..b107695c 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js", "start": "NODE_ENV=production node index.js", - "lint": "yarn prettier --write ./endpoints ./models ./utils index.js", + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./endpoints ./models ./utils index.js", "swagger": "node ./swagger/init.js", "sqlite:migrate": "cd ./utils/prisma && node migrateFromSqlite.js" }, From 26c220503cbf22ec2b55fa588e1f795914beb89a Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Thu, 6 Jun 2024 12:56:11 -0700 Subject: [PATCH 3/6] [FEAT] Edit message button (#1392) * WIP edit message feature * WIP edit message * WIP editing messages feature * Fix PFPs TODO: Fix default user profile image Add User and Assistant workspace response * unset PFP changes for later PR --------- Co-authored-by: timothycarambat --- frontend/package.json | 2 +- frontend/src/components/ChatBubble/index.jsx | 5 +- frontend/src/components/DefaultChat/index.jsx | 23 ++-- frontend/src/components/UserIcon/index.jsx | 2 +- .../src/components/UserIcon/workspace.png | Bin 0 -> 1486 bytes .../Actions/EditMessage/index.jsx | 126 ++++++++++++++++++ .../HistoricalMessage/Actions/index.jsx | 13 +- .../ChatHistory/HistoricalMessage/index.jsx | 88 ++++++++---- .../ChatHistory/PromptReply/index.jsx | 4 +- .../ChatContainer/ChatHistory/index.jsx | 45 +++++++ .../WorkspaceChat/ChatContainer/index.jsx | 1 + .../src/components/WorkspaceChat/index.jsx | 1 + frontend/src/models/workspace.js | 53 +++++++- frontend/src/models/workspaceThread.js | 45 +++++++ frontend/src/utils/chat/index.js | 9 +- server/endpoints/workspaceThreads.js | 78 ++++++++++- server/endpoints/workspaces.js | 62 ++++++++- server/models/workspaceChats.js | 18 +++ server/utils/helpers/chat/responses.js | 1 + 19 files changed, 513 insertions(+), 63 deletions(-) create mode 100644 frontend/src/components/UserIcon/workspace.png create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx diff --git a/frontend/package.json b/frontend/package.json index 2b669731..8aa4dcfa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,4 +63,4 @@ "tailwindcss": "^3.3.1", "vite": "^4.3.0" } -} +} \ No newline at end of file diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx index 8d311883..c5a1f190 100644 --- a/frontend/src/components/ChatBubble/index.jsx +++ b/frontend/src/components/ChatBubble/index.jsx @@ -1,5 +1,5 @@ import React from "react"; -import Jazzicon from "../UserIcon"; +import UserIcon from "../UserIcon"; import { userFromStorage } from "@/utils/request"; import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; @@ -11,8 +11,7 @@ export default function ChatBubble({ message, type, popMsg }) {
- diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 43ae6e7a..ae52a0d2 100644 --- a/frontend/src/components/DefaultChat/index.jsx +++ b/frontend/src/components/DefaultChat/index.jsx @@ -13,7 +13,7 @@ import { isMobile } from "react-device-detect"; import { SidebarMobileHeader } from "../Sidebar"; import ChatBubble from "../ChatBubble"; import System from "@/models/system"; -import Jazzicon from "../UserIcon"; +import UserIcon from "../UserIcon"; import { userFromStorage } from "@/utils/request"; import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; import useUser from "@/hooks/useUser"; @@ -46,7 +46,7 @@ export default function DefaultChatContainer() { className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
- +
- +
- @@ -151,7 +150,7 @@ export default function DefaultChatContainer() { className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
- @@ -213,7 +211,7 @@ export default function DefaultChatContainer() { className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
- @@ -275,7 +272,7 @@ export default function DefaultChatContainer() { className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
K~#7F?N)tE zl~ou&@8#YLmwUN_@*#LZmsoj`Vv#eGH3*rTTXSV)*-A@Gt)LfMUS9sU1C?Y< zZEbD7+wHl6b~Ax?Cr+GrNDV5AEKoEyo9(^;rOeOs3IaT~r@^y7hK3qIZzX0Di2)(B z)15qdGS}z#yFp)8bOXG)M$m3#)cynL-35pyAxbD*O=+*x)^AzAew__FEltf$g21~t zO_Y?Fl$f+;O?DoRf8W`8yr>}meQ4SN^4|UXGY;(A->B2+EgrYa(b3-4uGQ-)2N|@a zXRKJ6x$1tuf6|_@Y}sRiAp8b4UNYYr&CSi#Rh~cp__))}5HPeIumO+*c(|##^@82$ zVt?AlebCPW;C94czd2YlZBwvuW0_W~EyCphYtNoL_YRKV93B~ie!kgkUK}bha~4}y zR~HcknWKV8s371FE_4HGp%1>lm%sJqyW`{IKYe@on+oVlfSAsouN{mr#1^6crHdEO zT&=5X6GgEb=6cX}N%B%B&QXCPktmPH6VkVqgb^VS;BXO46GY4#jYdBZ@k2LIUsKcl zK=zt#JkO(pN!!IsTbCpz??wH_hWe_~oll*H@d3aD)Elj+sE}lilawGuiNSgV2y+V} z1=~j`lrA645(?93G%`^R7=!gW9}o&aU-!nLBi5|!?Dvq6%i$Q@x&6sgNXTNbObiVT z@jyzc8A*^B6@e5Lkr0cEiz8mIcmAYs`y}9lk)dEzPN!3e3<#kU+%j(W?%ioyw-wt& zSvLFJo{86AEjy~$>-#CCV@OCnk!V9iNhHBcDS^&;$ii(OqreIyQ_sfi6T&4VB*d0H zz3XF{5eqnDue`A5aBFj87tD>q9mi*7WpQc%1g;Gd2v80)FNhMBL&z+l%vtF9`T0bm zl2YU$B11dF=eDm7>Z1)sJQYY?P4(H1wpJuGcIHCW=IuLoSt~0m4aLR9ijR?z5h4)S zKFAa=DBCxe>$yom6dWZ|Fx>!%jDsVr0kUWled?^O#2mj^DeTskrmB4}?)d^74Z?V# zC4I%|2lMjI0YQWM!asEyQo^1-nNqc;7}-)%k{xX{MkSb+Osy*@ zNVF_pejMgFm&-9&_S~~r*!}RdIo)2D7q*ClOZMJpo`31>r5PE=Qqt1g{>e!l)YnQu zEp%KIf@Gntg>W~z?zQqGMH>nWFV9eCL7QNCO-=e79~?e(puM@V3A%%snVEcFUthwp zPd+PKwc7gf?XH4$unPlEY~Hv7n!f-%WLCX~dl0#nhVQ%^9xY5a7)@C+RI?<@oFq%| zWdv@6sZ$?)c(k#ux&;XVd=7_0f@?th+V*8n&f0Zx=JSA)n1Eo)-K$rxN=!~ok!3k*+HP!Q*xBFPd&6Wh4f}jPXDCwS4y2+~VkriLAw4E0 zW;wJG=|~o^2;WFDBRKWf1 !prev); + } + + useEffect(() => { + function listenForEdits() { + if (!chatId || !role) return; + window.addEventListener(EDIT_EVENT, onEditEvent); + } + listenForEdits(); + return () => { + window.removeEventListener(EDIT_EVENT, onEditEvent); + }; + }, [chatId, role]); + + return { isEditing, setIsEditing }; +} + +export function EditMessageAction({ chatId = null, role, isEditing }) { + function handleEditClick() { + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) + ); + } + + if (!chatId || isEditing) return null; + return ( +
+ + +
+ ); +} + +export function EditMessageForm({ + role, + chatId, + message, + adjustTextArea, + saveChanges, +}) { + const formRef = useRef(null); + function handleSaveMessage(e) { + e.preventDefault(); + const form = new FormData(e.target); + const editedMessage = form.get("editedMessage"); + saveChanges({ editedMessage, chatId, role }); + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) + ); + } + + function cancelEdits() { + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) + ); + return false; + } + + useEffect(() => { + if (!formRef || !formRef.current) return; + formRef.current.focus(); + adjustTextArea({ target: formRef.current }); + }, [formRef]); + + return ( +
+