Skip to content

Commit

Permalink
Update thread list on first interaction (#923)
Browse files Browse the repository at this point in the history
- re-fetch the thread list on first interaction
- navigate to `/thread/:id` url when creating a new conversation
- update the `/thread/:id` page to allow for displaying the current chat
- add `threadId` to `useChatMessages` to get the current conversation thread id
- update "back to conversation" links
- clear current conversation when deleting the current thread or changing chat profile
  • Loading branch information
tpatel committed Apr 23, 2024
1 parent fa5f5d5 commit a538c32
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 39 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

Nothing unreleased!

## [1.0.505] - 2024-04-23

### Added

- The user's browser language configuration is available in `cl.user_session.get("languages")`
- Allow html in text elements - @jdb78
- Allow for setting a ChatProfile default - @kevinwmerritt

### Changed

- The thread history refreshes right after a new thread is created.
- The thread auto-tagging feature is now opt-in using `auto_tag_thread` in the config.toml file

### Fixed

- Fixed incorrect step ancestor in the OpenAI instrumentation
- Enabled having a `storage_provider` set to `None` in SQLAlchemyDataLayer - @mohamedalani
- Correctly serialize `generation` in SQLAlchemyDataLayer - @mohamedalani

## [1.0.504] - 2024-04-16

### Changed
Expand Down
8 changes: 7 additions & 1 deletion backend/chainlit/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,13 @@ async def flush_thread_queues(self, interaction: str):

async def init_thread(self, interaction: str):
await self.flush_thread_queues(interaction)
await self.emit("first_interaction", interaction)
await self.emit(
"first_interaction",
{
"interaction": interaction,
"thread_id": self.session.thread_id,
},
)

async def process_user_message(self, payload: UIMessagePayload):
step_dict = payload["message"]
Expand Down
5 changes: 4 additions & 1 deletion backend/chainlit/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ async def connection_successful(sid):
thread = await resume_thread(context.session)
if thread:
context.session.has_first_interaction = True
await context.emitter.emit("first_interaction", "resume")
await context.emitter.emit(
"first_interaction",
{"interaction": "resume", "thread_id": thread.get("id")},
)
await context.emitter.resume_thread(thread)
await config.code.on_chat_resume(thread)
return
Expand Down
21 changes: 20 additions & 1 deletion cypress/e2e/data_layer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,39 @@ async def update_thread(
metadata: Optional[Dict] = None,
tags: Optional[List[str]] = None,
):
thread = next((t for t in thread_history if t["id"] == "test2"), None)
thread = next((t for t in thread_history if t["id"] == thread_id), None)
if thread:
if name:
thread["name"] = name
if metadata:
thread["metadata"] = metadata
if tags:
thread["tags"] = tags
else:
thread_history.append(
{
"id": thread_id,
"name": name,
"metadata": metadata,
"tags": tags,
"createdAt": utc_now(),
"userId": user_id,
"userIdentifier": "admin",
"steps": [],
}
)

@cl_data.queue_until_user_message()
async def create_step(self, step_dict: StepDict):
global create_step_counter
create_step_counter += 1

thread = next(
(t for t in thread_history if t["id"] == step_dict.get("threadId")), None
)
if thread:
thread["steps"].append(step_dict)

async def get_thread_author(self, thread_id: str):
return "admin"

Expand Down
35 changes: 28 additions & 7 deletions cypress/e2e/data_layer/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ function login() {
}

function feedback() {
cy.location('pathname').should((loc) => {
expect(loc).to.eq('/');
});
submitMessage('Hello');
cy.location('pathname').should((loc) => {
// starts with /thread/
expect(loc).to.match(/^\/thread\//);
});
cy.get('.negative-feedback-off').should('have.length', 1);
cy.get('.positive-feedback-off').should('have.length', 1).click();
cy.get('#feedbackSubmit').click();
Expand Down Expand Up @@ -37,19 +44,33 @@ function threadList() {
}

function resumeThread() {
// Go to the "thread 2" thread and resume it
cy.get('#thread-test2').click();
let initialUrl;
cy.url().then((url) => {
initialUrl = url;
});
cy.get(`#chat-input`).should('not.exist');
cy.get('#resumeThread').click();
cy.get(`#chat-input`).should('exist');
// Make sure the url stays the same after resuming
cy.url().then((newUrl) => {
expect(newUrl).to.equal(initialUrl);
});

// back to the "hello" thread
cy.get('a').contains('Hello').click();
cy.get(`#chat-input`).should('not.exist');
cy.get('#resumeThread').click();
cy.get(`#chat-input`).should('exist');

cy.get('.step').should('have.length', 4);
cy.get('.step').should('have.length', 8);

cy.get('.step').eq(0).should('contain', 'Message 3');
cy.get('.step').eq(1).should('contain', 'Message 4');
// Thread name should be renamed with first interaction
cy.get('.step').eq(2).should('contain', 'Welcome back to Hello');
cy.get('.step').eq(3).should('contain', 'metadata');
cy.get('.step').eq(3).should('contain', 'chat_profile');
cy.get('.step').eq(0).should('contain', 'Hello');
cy.get('.step').eq(5).should('contain', 'Welcome back to Hello');
// Because the Thread was closed, the metadata should have been updated automatically
cy.get('.step').eq(6).should('contain', 'metadata');
cy.get('.step').eq(6).should('contain', 'chat_profile');
}

describe('Data Layer', () => {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/molecules/chatProfiles.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import size from 'lodash/size';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { grey } from 'theme';

Expand Down Expand Up @@ -30,10 +31,12 @@ export default function ChatProfiles() {
const [newChatProfile, setNewChatProfile] = useState<string | null>(null);
const [openDialog, setOpenDialog] = useState(false);
const isDarkMode = useIsDarkMode();
const navigate = useNavigate();

const handleClose = () => {
setOpenDialog(false);
setNewChatProfile(null);
navigate('/');
};

const handleConfirm = (newChatProfileWithoutConfirm?: string) => {
Expand Down
22 changes: 18 additions & 4 deletions frontend/src/components/organisms/chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useUpload } from 'hooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { toast } from 'sonner';
import { v4 as uuidv4 } from 'uuid';
Expand All @@ -10,6 +11,7 @@ import {
threadHistoryState,
useChatData,
useChatInteract,
useChatMessages,
useChatSession
} from '@chainlit/react-client';
import { sideViewState } from '@chainlit/react-client';
Expand Down Expand Up @@ -42,6 +44,7 @@ const Chat = () => {
const { error, disabled } = useChatData();
const { uploadFile } = useChatInteract();
const uploadFileRef = useRef(uploadFile);
const navigate = useNavigate();

const fileSpec = useMemo(
() => ({
Expand Down Expand Up @@ -151,11 +154,22 @@ const Chat = () => {
options: { noClick: true }
});

const { threadId } = useChatMessages();

useEffect(() => {
setThreads((prev) => ({
...prev,
currentThreadId: undefined
}));
const currentPage = new URL(window.location.href);
if (
projectSettings?.dataPersistence &&
threadId &&
currentPage.pathname === '/'
) {
navigate(`/thread/${threadId}`);
} else {
setThreads((prev) => ({
...prev,
currentThreadId: threadId
}));
}
}, []);

const enableMultiModalUpload =
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/organisms/threadHistory/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
IStep,
IThread,
accessTokenState,
nestMessages
nestMessages,
useChatMessages
} from '@chainlit/react-client';

import SideView from 'components/atoms/element/sideView';
Expand All @@ -33,6 +34,7 @@ const Thread = ({ thread, error, isLoading }: Props) => {
const [steps, setSteps] = useState<IStep[]>([]);
const apiClient = useRecoilValue(apiClientState);
const { t } = useTranslation();
const { threadId } = useChatMessages();

useEffect(() => {
if (!thread) return;
Expand Down Expand Up @@ -164,7 +166,12 @@ const Thread = ({ thread, error, isLoading }: Props) => {
id="thread-info"
severity="info"
action={
<Button component={Link} color="inherit" size="small" to="/">
<Button
component={Link}
color="inherit"
size="small"
to={threadId ? `/thread/${threadId}` : '/'}
>
<Translator path="components.organisms.threadHistory.Thread.backToChat" />
</Button>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Typography from '@mui/material/Typography';
import {
ThreadHistory,
useChatInteract,
useChatMessages,
useChatSession
} from '@chainlit/react-client';

Expand All @@ -41,6 +42,7 @@ const ThreadList = ({
}: Props) => {
const { idToResume } = useChatSession();
const { clear } = useChatInteract();
const { threadId: currentThreadId } = useChatMessages();
const navigate = useNavigate();
if (isFetching || (!threadHistory?.timeGroupedThreads && isLoadingMore)) {
return (
Expand Down Expand Up @@ -89,7 +91,7 @@ const ThreadList = ({
}

const handleDeleteThread = (threadId: string) => {
if (threadId === idToResume) {
if (threadId === idToResume || threadId === currentThreadId) {
clear();
}
if (threadId === threadHistory.currentThreadId) {
Expand Down
36 changes: 33 additions & 3 deletions frontend/src/components/organisms/threadHistory/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useAuth } from 'api/auth';
import isEqual from 'lodash/isEqual';
import uniqBy from 'lodash/uniqBy';
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';

import Box from '@mui/material/Box';
Expand All @@ -13,7 +14,8 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import {
IThreadFilters,
accessTokenState,
threadHistoryState
threadHistoryState,
useChatMessages
} from '@chainlit/react-client';

import { Translator } from 'components/i18n';
Expand Down Expand Up @@ -46,6 +48,8 @@ const _ThreadHistorySideBar = () => {
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const apiClient = useRecoilValue(apiClientState);
const { firstInteraction, messages, threadId } = useChatMessages();
const navigate = useNavigate();

const ref = useRef<HTMLDivElement>(null);
const filtersHasChanged = !isEqual(prevFilters, filters);
Expand All @@ -62,9 +66,12 @@ const _ThreadHistorySideBar = () => {
setShouldLoadMore(atBottom);
};

const fetchThreads = async (cursor?: string | number) => {
const fetchThreads = async (
cursor?: string | number,
isLoadingMore?: boolean
) => {
try {
if (cursor) {
if (cursor || isLoadingMore) {
setIsLoadingMore(true);
} else {
setIsFetching(true);
Expand Down Expand Up @@ -129,6 +136,29 @@ const _ThreadHistorySideBar = () => {
}
}, []);

useEffect(() => {
if (!firstInteraction) {
return;
}

// distinguish between the first interaction containing the word "resume"
// and the actual resume message
const isActualResume =
firstInteraction === 'resume' &&
messages.at(0)?.output.toLowerCase() !== 'resume';

if (isActualResume) {
return;
}

fetchThreads(undefined, true).then(() => {
const currentPage = new URL(window.location.href);
if (threadId && currentPage.pathname === '/') {
navigate(`/thread/${threadId}`);
}
});
}, [firstInteraction]);

return (
<Box display="flex" position="relative">
<Drawer
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/ResumeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export default function ResumeButton({ threadId }: Props) {
clear();
setIdToResume(threadId!);
toast.success('Chat resumed!');
navigate('/');
if (!pSettings?.dataPersistence) {
navigate('/');
}
};

return (
Expand Down

0 comments on commit a538c32

Please sign in to comment.