Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function ThreadItem({
toggleMarkForDeletion,
hasNext,
ctrlPressed = false,
wasBumped = false,
}) {
const { slug, threadSlug = null } = useParams();
const optionsContainer = useRef(null);
Expand All @@ -32,7 +33,7 @@ export default function ThreadItem({

return (
<div
className="w-full relative flex h-[38px] items-center border-none rounded-lg"
className={`w-full relative flex h-[38px] items-center border-none rounded-lg ${wasBumped ? "thread-bump-animation" : ""}`}
role="listitem"
>
{/* Curved line Element and leader if required */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ import Workspace from "@/models/workspace";
import paths from "@/utils/paths";
import showToast from "@/utils/toast";
import { Plus, CircleNotch, Trash } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import ThreadItem from "./ThreadItem";
import { useParams } from "react-router-dom";
export const THREAD_RENAME_EVENT = "renameThread";
export const THREAD_ACTIVITY_EVENT = "threadActivity";

export default function ThreadContainer({ workspace }) {
const { threadSlug = null } = useParams();
const [threads, setThreads] = useState([]);
const [loading, setLoading] = useState(true);
const [ctrlPressed, setCtrlPressed] = useState(false);
const [bumpedThreadSlug, setBumpedThreadSlug] = useState(null);
const timeoutRef = useRef(null);

useEffect(() => {
const chatHandler = (event) => {
Expand All @@ -33,6 +36,50 @@ export default function ThreadContainer({ workspace }) {
};
}, []);

useEffect(() => {
const activityHandler = (event) => {
const { threadSlug: activeSlug } = event.detail;
if (!activeSlug) return;

setThreads((prevThreads) => {
const idx = prevThreads.findIndex((t) => t.slug === activeSlug);
if (idx <= 0) return prevThreads;

// Move thread to top
const thread = prevThreads[idx];
const reordered = [
thread,
...prevThreads.slice(0, idx),
...prevThreads.slice(idx + 1),
];

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

setBumpedThreadSlug(activeSlug);

// Wait for animation before resetting
timeoutRef.current = setTimeout(() => {
setBumpedThreadSlug(null);
timeoutRef.current = null;
}, 800);

return reordered;
});
};

window.addEventListener(THREAD_ACTIVITY_EVENT, activityHandler);
return () => {
window.removeEventListener(THREAD_ACTIVITY_EVENT, activityHandler);

// Cleanup timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

useEffect(() => {
async function fetchThreads() {
if (!workspace.slug) return;
Expand Down Expand Up @@ -144,6 +191,7 @@ export default function ThreadContainer({ workspace }) {
onRemove={removeThread}
thread={thread}
hasNext={i !== threads.length - 1}
wasBumped={thread.slug === bumpedThreadSlug}
/>
))}
<DeleteAllThreadButton
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import SpeechRecognition, {
} from "react-speech-recognition";
import { ChatTooltips } from "./ChatTooltips";
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
import { THREAD_ACTIVITY_EVENT } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer";

export default function ChatContainer({ workspace, knownHistory = [] }) {
const { threadSlug = null } = useParams();
Expand Down Expand Up @@ -217,7 +218,8 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
setChatHistory,
remHistory,
_chatHistory,
setSocketId
setSocketId,
threadSlug
),
attachments,
});
Expand Down Expand Up @@ -275,6 +277,16 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
});
setWebsocket(socket);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));

// Move thread to top
if (threadSlug) {
window.dispatchEvent(
new CustomEvent(THREAD_ACTIVITY_EVENT, {
detail: { threadSlug },
})
);
}

window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
} catch (e) {
setChatHistory((prev) => [
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,26 @@ does not extend the close button beyond the viewport. */
animation: thoughtTransition 0.5s ease-out forwards;
}

/* Thread bump animation when thread moves to top of list */
@keyframes threadBump {
0% {
opacity: 0.75;
transform: translateY(-3px);
}
70% {
opacity: 0.95;
transform: translateY(0px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}

.thread-bump-animation {
animation: threadBump 0.6s ease-out forwards;
}

.checklist-completed {
-webkit-animation: fadein 0.3s linear forwards;
animation: fadein 0.3s linear forwards;
Expand Down
35 changes: 33 additions & 2 deletions frontend/src/utils/chat/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { THREAD_RENAME_EVENT } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer";
import {
THREAD_RENAME_EVENT,
THREAD_ACTIVITY_EVENT,
} from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer";
import { emitAssistantMessageCompleteEvent } from "@/components/contexts/TTSProvider";
export const ABORT_STREAM_EVENT = "abort-chat-stream";

Expand All @@ -9,7 +12,8 @@ export default function handleChat(
setChatHistory,
remHistory,
_chatHistory,
setWebsocket
setWebsocket,
threadSlug = null
) {
const {
uuid,
Expand Down Expand Up @@ -83,6 +87,15 @@ export default function handleChat(
metrics,
});
emitAssistantMessageCompleteEvent(chatId);

// Move thread to top
if (threadSlug) {
window.dispatchEvent(
new CustomEvent(THREAD_ACTIVITY_EVENT, {
detail: { threadSlug },
})
);
}
} else if (
type === "textResponseChunk" ||
type === "finalizeResponseStream"
Expand All @@ -108,6 +121,15 @@ export default function handleChat(

emitAssistantMessageCompleteEvent(chatId);
setLoadingResponse(false);

// Move thread to top
if (threadSlug) {
window.dispatchEvent(
new CustomEvent(THREAD_ACTIVITY_EVENT, {
detail: { threadSlug },
})
);
}
} else {
updatedHistory = {
...existingHistory,
Expand Down Expand Up @@ -161,6 +183,15 @@ export default function handleChat(
if (action === "reset_chat") {
// Chat was reset, keep reset message and clear everything else.
setChatHistory([_chatHistory.pop()]);

// Move thread to top
if (threadSlug) {
window.dispatchEvent(
new CustomEvent(THREAD_ACTIVITY_EVENT, {
detail: { threadSlug },
})
);
}
}

// If thread was updated automatically based on chat prompt
Expand Down
12 changes: 8 additions & 4 deletions server/endpoints/workspaceThreads.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ function workspaceThreadEndpoints(app) {
try {
const user = await userFromSession(request, response);
const workspace = response.locals.workspace;
const threads = await WorkspaceThread.where({
workspace_id: workspace.id,
user_id: user?.id || null,
});
const threads = await WorkspaceThread.where(
{
workspace_id: workspace.id,
user_id: user?.id || null,
},
null,
{ lastUpdatedAt: "desc" }
);
response.status(200).json({ threads });
} catch (e) {
console.error(e.message, e);
Expand Down
9 changes: 9 additions & 0 deletions server/models/workspaceChats.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ const WorkspaceChats = {
include,
},
});

// Update thread timestamp for ordering
if (threadId) {
await prisma.workspace_threads.update({
where: { id: threadId },
data: { lastUpdatedAt: new Date() },
});
}

return { chat, message: null };
} catch (error) {
console.error(error.message);
Expand Down
13 changes: 13 additions & 0 deletions server/models/workspaceThread.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ const WorkspaceThread = {
}
},

// Updates timestamp for thread when activity occurs
touchActivity: async function (threadId) {
if (!threadId) return;
try {
await prisma.workspace_threads.update({
where: { id: threadId },
data: { lastUpdatedAt: new Date() },
});
} catch (error) {
console.error(error.message);
}
},

// Will fire on first message (included or not) for a thread and rename the thread with the newName prop.
autoRenameThread: async function ({
workspace = null,
Expand Down
3 changes: 3 additions & 0 deletions server/utils/chats/commands/reset.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { WorkspaceChats } = require("../../../models/workspaceChats");
const { WorkspaceThread } = require("../../../models/workspaceThread");

async function resetMemory(
workspace,
Expand All @@ -16,6 +17,8 @@ async function resetMemory(
)
: await WorkspaceChats.markHistoryInvalid(workspace.id, user);

if (thread?.id) await WorkspaceThread.touchActivity(thread.id);

return {
uuid: msgUUID,
type: "textResponse",
Expand Down