diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx index 87fd5558..c4062102 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx @@ -27,7 +27,6 @@ export default function ThreadItem({ const { slug } = useParams(); const optionsContainer = useRef(null); const [showOptions, setShowOptions] = useState(false); - const [name, setName] = useState(thread.name); const linkTo = !thread.slug ? paths.workspace.chat(slug) : paths.workspace.thread(slug, thread.slug); @@ -97,7 +96,7 @@ export default function ThreadItem({ isActive ? "font-medium text-white" : "text-slate-400" }`} > - {truncate(name, 25)} + {truncate(thread.name, 25)}

)} @@ -133,7 +132,6 @@ export default function ThreadItem({ workspace={workspace} thread={thread} onRemove={onRemove} - onRename={setName} close={() => setShowOptions(false)} /> )} @@ -144,14 +142,7 @@ export default function ThreadItem({ ); } -function OptionsMenu({ - containerRef, - workspace, - thread, - onRename, - onRemove, - close, -}) { +function OptionsMenu({ containerRef, workspace, thread, onRemove, close }) { const menuRef = useRef(null); // Ref menu options @@ -208,7 +199,7 @@ function OptionsMenu({ return; } - onRename(name); + thread.name = name; close(); }; diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx index d1c0ba8c..f2d99cd8 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx @@ -12,6 +12,26 @@ export default function ThreadContainer({ workspace }) { const [loading, setLoading] = useState(true); const [ctrlPressed, setCtrlPressed] = useState(false); + useEffect(() => { + const chatHandler = (event) => { + const { threadSlug, newName } = event.detail; + setThreads((prevThreads) => + prevThreads.map((thread) => { + if (thread.slug === threadSlug) { + return { ...thread, name: newName }; + } + return thread; + }) + ); + }; + + window.addEventListener("renameThread", chatHandler); + + return () => { + window.removeEventListener("renameThread", chatHandler); + }; + }, []); + useEffect(() => { async function fetchThreads() { if (!workspace.slug) return; diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 28d87e0d..6e32d23f 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -12,6 +12,7 @@ import handleSocketResponse, { AGENT_SESSION_END, AGENT_SESSION_START, } from "@/utils/chat/agent"; +import truncate from "truncate"; export default function ChatContainer({ workspace, knownHistory = [] }) { const { threadSlug = null } = useParams(); @@ -39,6 +40,18 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { event.preventDefault(); if (!message || message === "") return false; + // If first message and it is a thread + // and message is not blank/whitespace, + // then send event to rename the thread + if (threadSlug && chatHistory.length === 0 && message.trim().length > 0) { + const truncatedName = truncate(message, 22); + window.dispatchEvent( + new CustomEvent("renameThread", { + detail: { threadSlug, newName: truncatedName }, + }) + ); + } + const prevChatHistory = [ ...chatHistory, { content: message, role: "user" }, diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index 7445c213..915acbf9 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -15,6 +15,8 @@ const { validWorkspaceSlug, } = require("../utils/middleware/validWorkspace"); const { writeResponseChunk } = require("../utils/helpers/chat/responses"); +const { WorkspaceThread } = require("../models/workspaceThread"); +const truncate = require("truncate"); function chatEndpoints(app) { if (!app) return; @@ -196,6 +198,14 @@ function chatEndpoints(app) { user, thread ); + + await WorkspaceThread.autoRenameThread({ + thread, + workspace, + user, + newName: truncate(message, 22), + }); + await Telemetry.sendTelemetry("sent_chat", { multiUserMode: multiUserMode(response), LLMSelection: process.env.LLM_PROVIDER || "openai", diff --git a/server/models/workspaceThread.js b/server/models/workspaceThread.js index a2a96f31..ffbc8aed 100644 --- a/server/models/workspaceThread.js +++ b/server/models/workspaceThread.js @@ -8,7 +8,7 @@ const WorkspaceThread = { try { const thread = await prisma.workspace_threads.create({ data: { - name: "New thread", + name: "Thread", slug: uuidv4(), user_id: userId ? Number(userId) : null, workspace_id: workspace.id, @@ -84,6 +84,25 @@ const WorkspaceThread = { return []; } }, + + // Will fire on first message (included or not) for a thread and rename the thread with the newName prop. + autoRenameThread: async function ({ + workspace = null, + thread = null, + user = null, + newName = null, + }) { + if (!workspace || !thread || !newName) return false; + const { WorkspaceChats } = require("./workspaceChats"); + const chatCount = await WorkspaceChats.count({ + workspaceId: workspace.id, + user_id: user?.id || null, + thread_id: thread.id, + }); + if (chatCount !== 1) return false; + await this.update(thread, { name: newName }); + return true; + }, }; module.exports = { WorkspaceThread }; diff --git a/server/package.json b/server/package.json index 1b0ba280..9cc27c8b 100644 --- a/server/package.json +++ b/server/package.json @@ -75,6 +75,7 @@ "sqlite3": "^5.1.6", "swagger-autogen": "^2.23.5", "swagger-ui-express": "^5.0.0", + "truncate": "^3.0.0", "url-pattern": "^1.0.3", "uuid": "^9.0.0", "uuid-apikey": "^1.5.3", diff --git a/server/yarn.lock b/server/yarn.lock index c6cf4c2c..a05c62fc 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -6211,6 +6211,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== +truncate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/truncate/-/truncate-3.0.0.tgz#7dbe19e2f72c614e36b79bab00fbfbeb1cbaf078" + integrity sha512-C+0Xojw7wZPl6MDq5UjMTuxZvBPK04mtdFet7k+GSZPINcvLZFCXg+15kWIL4wAqDB7CksIsKiRLbQ1wa7rKdw== + tslib@^2.2.0, tslib@^2.4.0, tslib@^2.5.3, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"