Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 20 additions & 13 deletions e2e/conversation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ test.describe('Connecting', () => {
// Go to the app
await page.goto('/');

// Click the left panel toggle button to show conversations
await page.getByTestId('toggle-conversations-sidebar').click();
// Wait a moment for the page to fully load
await page.waitForLoadState('networkidle');

// Should show demo conversations initially
await expect(page.getByText('Introduction to gptme')).toBeVisible();
// The sidebar should be visible by default in the new layout
// Check if we can see demo conversations (they should be visible by default)
await expect(page.getByText('Introduction to gptme')).toBeVisible({ timeout: 10000 });

// Should show connection status
const connectionButton = page.getByRole('button', { name: /Connect/i });
Expand All @@ -30,9 +31,8 @@ test.describe('Connecting', () => {
// Wait for success toast to confirm API connection
await expect(page.getByText('Connected to gptme server')).toBeVisible();

// Wait for conversations to load
// Should show both demo conversations and connected conversations
await page.getByTestId('toggle-conversations-sidebar').click();
// In the new layout, conversations should be visible by default
// No need to toggle sidebar, but ensure we're on chat section
await expect(page.getByText('Introduction to gptme')).toBeVisible();

// Wait for loading state to finish
Expand Down Expand Up @@ -73,11 +73,12 @@ test.describe('Connecting', () => {
// Start with server unavailable
await page.goto('/');

// Click the left panel toggle button to show conversations
await page.getByTestId('toggle-conversations-sidebar').click();
// Wait a moment for the page to fully load
await page.waitForLoadState('networkidle');

// In the new layout, conversations should be visible by default
// Should still show demo conversations
await expect(page.getByText('Introduction to gptme')).toBeVisible();
await expect(page.getByText('Introduction to gptme')).toBeVisible({ timeout: 10000 });

// Click connect button and try to connect to non-existent server
const connectionButton = page.getByRole('button', { name: /Connect/i });
Expand Down Expand Up @@ -117,28 +118,34 @@ test.describe('Conversation Flow', () => {
test('should be able to create a new conversation and send a message', async ({ page }) => {
await page.goto('/');

// Wait for the page to load completely
await page.waitForLoadState('networkidle');

const message = 'Hello. We are testing, just say exactly "Hello world" without anything else.';

// Make sure we can see the chat input
await expect(page.getByTestId('chat-input')).toBeVisible({ timeout: 10000 });

// Type a message
await page.getByTestId('chat-input').fill(message);
await page.keyboard.press('Enter');

// Wait for the new conversation page to load
await expect(page).toHaveURL(/\/chat\/.+$/);
await expect(page).toHaveURL(/\/chat\/.+$/, { timeout: 15000 });

// Should show the message in the conversation
// Look specifically for the user's message in a user message container
await expect(
page.locator('.role-user', {
hasText: message,
})
).toBeVisible();
).toBeVisible({ timeout: 15000 });

// Should show the AI's response
await expect(
page.locator('.role-assistant', {
hasText: 'Hello world',
})
).toBeVisible();
).toBeVisible({ timeout: 30000 }); // AI response might take longer
});
});
2 changes: 1 addition & 1 deletion src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export const ChatInput: FC<Props> = ({

return (
<form onSubmit={handleSubmit} className="p-4">
<div className="mx-auto flex max-w-2xl flex-col">
<div className="flex flex-col">
<div className="flex">
<Computed>
{() => (
Expand Down
20 changes: 11 additions & 9 deletions src/components/ConversationContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,17 @@ export const ConversationContent: FC<Props> = ({ conversationId, isReadOnly }) =
</div>

<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-background via-background/80 to-transparent">
<ChatInput
conversationId={conversationId}
onSend={handleSendMessage}
onInterrupt={interruptGeneration}
isReadOnly={isReadOnly}
hasSession$={hasSession$}
defaultModel={defaultModel || undefined}
autoFocus$={shouldFocus$}
/>
<div className=" mx-auto max-w-2xl">
<ChatInput
conversationId={conversationId}
onSend={handleSendMessage}
onInterrupt={interruptGeneration}
isReadOnly={isReadOnly}
hasSession$={hasSession$}
defaultModel={defaultModel || undefined}
autoFocus$={shouldFocus$}
/>
</div>
</div>
</main>
);
Expand Down
39 changes: 29 additions & 10 deletions src/components/ConversationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,47 @@ export const ConversationList: FC<Props> = ({
useEffect(() => {
if (observer.current) observer.current.disconnect();

// Only set up observer if we have content and can scroll
const container = scrollContainerRef.current;
const sentinel = loadMoreSentinelRef.current;

if (!container || !sentinel || !hasNextPage) {
return;
}

observer.current = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetching) {
console.log('[ConversationList] Loading more conversations...');
fetchNextPage();
const entry = entries[0];
if (entry.isIntersecting && hasNextPage && !isFetching) {
// Additional check: ensure we actually have scrollable content or are near the bottom
const containerHeight = container.clientHeight;
const scrollHeight = container.scrollHeight;
const scrollTop = container.scrollTop;

// Load if we have scrollable content and are near the bottom, OR if content doesn't fill container yet
const hasScrollableContent = scrollHeight > containerHeight;
const nearBottom = scrollTop + containerHeight >= scrollHeight - 100;

if (!hasScrollableContent || nearBottom) {
console.log('[ConversationList] Loading more conversations...');
fetchNextPage();
}
}
},
{
root: scrollContainerRef.current,
root: container,
threshold: 0.1,
rootMargin: '0px 0px 100px 0px', // Trigger loading before reaching the end
rootMargin: '0px 0px 50px 0px',
}
);

if (loadMoreSentinelRef.current) {
observer.current.observe(loadMoreSentinelRef.current);
}
observer.current.observe(sentinel);

return () => {
if (observer.current) observer.current.disconnect();
};
}, [isFetching, hasNextPage, fetchNextPage]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasNextPage, fetchNextPage]); // isFetching intentionally excluded to prevent observer recreation

if (!conversations) {
return null;
Expand Down Expand Up @@ -255,7 +274,7 @@ export const ConversationList: FC<Props> = ({
<div
ref={scrollContainerRef}
data-testid="conversation-list"
className="h-full space-y-2 overflow-y-auto overflow-x-hidden p-3"
className="h-full space-y-2 overflow-y-auto overflow-x-hidden p-2"
>
{isLoading && (
<div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
Expand Down
Loading
Loading