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
20 changes: 11 additions & 9 deletions e2e/conversation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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();

// Should show demo conversations initially
await expect(page.getByText('Introduction to gptme')).toBeVisible();

Expand All @@ -19,9 +22,6 @@ test.describe('Connecting', () => {
await expect(page.getByText(/Hello! I'm gptme, your AI programming assistant/)).toBeVisible();
await page.goto('/');

// Should show demo conversations immediately
await expect(page.getByText('Introduction to gptme')).toBeVisible();

// Wait for successful connection
await expect(page.getByRole('button', { name: /Connect/i })).toHaveClass(/text-green-600/, {
timeout: 10000,
Expand All @@ -32,6 +32,7 @@ test.describe('Connecting', () => {

// Wait for conversations to load
// Should show both demo conversations and connected conversations
await page.getByTestId('toggle-conversations-sidebar').click();
await expect(page.getByText('Introduction to gptme')).toBeVisible();

// Wait for loading state to finish
Expand Down Expand Up @@ -72,6 +73,9 @@ 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();

// Should still show demo conversations
await expect(page.getByText('Introduction to gptme')).toBeVisible();

Expand Down Expand Up @@ -113,17 +117,15 @@ test.describe('Conversation Flow', () => {
test('should be able to create a new conversation and send a message', async ({ page }) => {
await page.goto('/');

// Click the "New Conversation" button to start a new conversation
await page.locator('[data-testid="new-conversation-button"]').click();

// Wait for the new conversation page to load
await expect(page).toHaveURL(/\?conversation=\d+$/);

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

// Type a message
await page.getByTestId('chat-input').fill(message);
await page.keyboard.press('Enter');
// await page.locator('[data-testid="new-conversation-button"]').click();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the commented-out new conversation button click if it's no longer needed.

Suggested change
// await page.locator('[data-testid="new-conversation-button"]').click();


// Wait for the new conversation page to load
await expect(page).toHaveURL(/\?conversation=.+$/);

// Should show the message in the conversation
// Look specifically for the user's message in a user message container
Expand Down
178 changes: 96 additions & 82 deletions src/components/ConversationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,101 +102,115 @@ export const ConversationList: FC<Props> = ({

return (
<div
className={`cursor-pointer rounded-lg p-3 transition-colors hover:bg-accent ${
className={`cursor-pointer rounded-lg py-2 pl-2 transition-colors hover:bg-accent ${
isSelected ? 'bg-accent' : ''
}`}
onClick={() => onSelect(conv.name)}
>
<div data-testid="conversation-title" className="mb-1 font-medium">
{stripDate(conv.name)}
</div>
<div className="flex items-center space-x-3 text-sm text-muted-foreground">
<Tooltip>
<TooltipTrigger>
<time className="flex items-center" dateTime={conv.lastUpdated.toISOString()}>
<Clock className="mr-1 h-4 w-4" />
{getRelativeTimeString(conv.lastUpdated)}
</time>
</TooltipTrigger>
<TooltipContent>{conv.lastUpdated.toLocaleString()}</TooltipContent>
</Tooltip>
<Computed>
{() => {
const storeConv = conversations$.get(conv.name)?.get();
const isLoaded = storeConv?.data?.log?.length > 0;

if (!isLoaded) {
<div>
<div
data-testid="conversation-title"
className="mb-1 whitespace-nowrap font-medium"
style={{
maskImage:
'linear-gradient(to right, black 0%, black calc(100% - 2rem), transparent 100%)',
WebkitMaskImage:
'linear-gradient(to right, black 0%, black calc(100% - 2rem), transparent 100%)',
}}
>
{stripDate(conv.name)}
</div>
<div className="flex items-center space-x-3 text-xs text-muted-foreground">
<Tooltip>
<TooltipTrigger>
<time
className="flex items-center whitespace-nowrap"
dateTime={conv.lastUpdated.toISOString()}
>
<Clock className="mr-1 h-3 w-3" />
{getRelativeTimeString(conv.lastUpdated)}
</time>
</TooltipTrigger>
<TooltipContent>{conv.lastUpdated.toLocaleString()}</TooltipContent>
</Tooltip>
<Computed>
{() => {
const storeConv = conversations$.get(conv.name)?.get();
const isLoaded = storeConv?.data?.log?.length > 0;

if (!isLoaded) {
return (
<span className="flex items-center">
<MessageSquare className="mr-1 h-3 w-3" />
{conv.messageCount}
</span>
);
}

const breakdown = getMessageBreakdown();
const totalCount = Object.values(breakdown).reduce((a, b) => a + b, 0);

return (
<span className="flex items-center">
<MessageSquare className="mr-1 h-4 w-4" />
{conv.messageCount}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center">
<MessageSquare className="mr-1 h-3 w-3" />
{totalCount}
</span>
</TooltipTrigger>
<TooltipContent>
<div className="whitespace-pre">{formatBreakdown(breakdown)}</div>
</TooltipContent>
</Tooltip>
);
}

const breakdown = getMessageBreakdown();
const totalCount = Object.values(breakdown).reduce((a, b) => a + b, 0);
}}
</Computed>

return (
{/* Show conversation state indicators */}
<div className="flex items-center space-x-2">
{convState?.isConnected && (
<Tooltip>
<TooltipTrigger asChild>
<TooltipTrigger>
<span className="flex items-center">
<MessageSquare className="mr-1 h-4 w-4" />
{totalCount}
<Signal className="h-3 w-3 text-primary" />
</span>
</TooltipTrigger>
<TooltipContent>Connected</TooltipContent>
</Tooltip>
)}
{convState?.isGenerating && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<Loader2 className="h-3 w-3 animate-spin text-primary" />
</span>
</TooltipTrigger>
<TooltipContent>Generating...</TooltipContent>
</Tooltip>
)}
{convState?.pendingTool && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<span className="text-lg">⚙️</span>
</span>
</TooltipTrigger>
<TooltipContent>
<div className="whitespace-pre">{formatBreakdown(breakdown)}</div>
Pending tool: {convState.pendingTool.tooluse.tool}
</TooltipContent>
</Tooltip>
);
}}
</Computed>

{/* Show conversation state indicators */}
<div className="flex items-center space-x-2">
{convState?.isConnected && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<Signal className="h-4 w-4 text-primary" />
</span>
</TooltipTrigger>
<TooltipContent>Connected</TooltipContent>
</Tooltip>
)}
{convState?.isGenerating && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
</span>
</TooltipTrigger>
<TooltipContent>Generating...</TooltipContent>
</Tooltip>
)}
{convState?.pendingTool && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<span className="text-lg">⚙️</span>
</span>
</TooltipTrigger>
<TooltipContent>
Pending tool: {convState.pendingTool.tooluse.tool}
</TooltipContent>
</Tooltip>
)}
{conv.readonly && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<Lock className="h-4 w-4" />
</span>
</TooltipTrigger>
<TooltipContent>This conversation is read-only</TooltipContent>
</Tooltip>
)}
)}
{conv.readonly && (
<Tooltip>
<TooltipTrigger>
<span className="flex items-center">
<Lock className="h-3 w-3" />
</span>
</TooltipTrigger>
<TooltipContent>This conversation is read-only</TooltipContent>
</Tooltip>
)}
</div>
</div>
</div>
</div>
Expand All @@ -207,7 +221,7 @@ export const ConversationList: FC<Props> = ({
};

return (
<div data-testid="conversation-list" className="h-full space-y-2 overflow-y-auto p-4">
<div data-testid="conversation-list" className="h-full space-y-2 overflow-y-auto p-3">
{isLoading && (
<div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Expand Down
57 changes: 54 additions & 3 deletions src/components/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo, useRef, type FC } from 'react';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import { useCallback, useEffect } from 'react';
import { WelcomeView } from '@/components/WelcomeView';
import { setDocumentTitle } from '@/utils/title';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { LeftSidebar } from '@/components/LeftSidebar';
Expand All @@ -17,6 +18,7 @@ import {
initializeConversations,
selectedConversation$,
initConversation,
conversations$,
} from '@/stores/conversations';
import {
leftSidebarVisible$,
Expand All @@ -35,6 +37,7 @@ const Conversations: FC<Props> = ({ route }) => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const conversationParam = searchParams.get('conversation');
const stepParam = searchParams.get('step');
const { api, isConnected$, connectionConfig } = useApi();
const queryClient = useQueryClient();
const isConnected = use$(isConnected$);
Expand All @@ -54,9 +57,44 @@ const Conversations: FC<Props> = ({ route }) => {
// Handle initial conversation selection
if (conversationParam) {
selectedConversation$.set(conversationParam);
} else {
// Need to use empty string instead of null due to type constraints
selectedConversation$.set('');
}
}, [conversationParam]);

// Handle step parameter for auto-generation
useEffect(() => {
if (stepParam === 'true' && conversationParam && isConnected) {
console.log(`[Conversations] Step parameter detected for ${conversationParam}`);

// Watch for conversation to be connected
const checkAndStart = () => {
const conversation = conversations$.get(conversationParam);
if (conversation?.isConnected.get()) {
console.log(
`[Conversations] Conversation ${conversationParam} is connected, starting generation`
);

// Start generation
api.step(conversationParam).catch((error) => {
console.error('[Conversations] Failed to start generation:', error);
});

// Remove step parameter from URL
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete('step');
navigate(`${route}?${newSearchParams.toString()}`, { replace: true });
} else {
// Check again in 100ms
setTimeout(checkAndStart, 100);
}
};

checkAndStart();
}
}, [stepParam, conversationParam, isConnected, api, navigate, route, searchParams]);

// Fetch conversations from API
const {
data: apiConversations = [],
Expand Down Expand Up @@ -150,8 +188,9 @@ const Conversations: FC<Props> = ({ route }) => {

// Update conversation$ when available conversations change
useEffect(() => {
const selected = selectedConversation$.get();
conversation$.set(allConversations.find((conv) => conv.name === selected));
const selectedId = selectedConversation$.get();
const selectedConversation = allConversations.find((conv) => conv.name === selectedId);
conversation$.set(selectedConversation);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allConversations]);

Expand All @@ -178,6 +217,14 @@ const Conversations: FC<Props> = ({ route }) => {
};
}, []);

// Hide sidebars by default when no conversation is selected
useEffect(() => {
if (!selectedConversation$.get()) {
leftPanelRef.current?.collapse();
rightPanelRef.current?.collapse();
}
}, []);

return (
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel
Expand Down Expand Up @@ -215,7 +262,11 @@ const Conversations: FC<Props> = ({ route }) => {
isReadOnly={conversation.readonly}
/>
</div>
) : null;
) : (
<div className="flex h-full flex-1 items-center justify-center p-4">
<WelcomeView onToggleHistory={() => leftPanelRef.current?.expand()} />
</div>
);
}}
</Memo>
</ResizablePanel>
Expand Down
Loading
Loading