Skip to content

Commit 59ef067

Browse files
authored
refactor: refactored to allow using as submodule (#39)
* refactor: move conversations to separate component * fix: explicitly set route on navigate * fix: disable unexpected any in smd.d.ts * fix: ChatInput disabled state not updating
1 parent cc85450 commit 59ef067

File tree

8 files changed

+190
-162
lines changed

8 files changed

+190
-162
lines changed

src/components/ChatInput.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { Switch } from '@/components/ui/switch';
1515
import { Label } from '@/components/ui/label';
1616
import { type Observable } from '@legendapp/state';
17-
import { Memo, useObserveEffect } from '@legendapp/state/react';
17+
import { Computed, useObserveEffect } from '@legendapp/state/react';
1818

1919
export interface ChatOptions {
2020
model?: string;
@@ -46,6 +46,8 @@ export const ChatInput: FC<Props> = ({
4646
const api = useApi();
4747
const textareaRef = useRef<HTMLTextAreaElement>(null);
4848

49+
const isDisabled = isReadOnly || !api.isConnected;
50+
4951
// Focus the textarea when autoFocus is true and component is interactive
5052
useEffect(() => {
5153
if (autoFocus && textareaRef.current && !isReadOnly && api.isConnected) {
@@ -124,7 +126,7 @@ export const ChatInput: FC<Props> = ({
124126
onKeyDown={handleKeyDown}
125127
placeholder={placeholder}
126128
className="max-h-[400px] min-h-[60px] resize-none overflow-y-auto pb-8 pr-16"
127-
disabled={!api.isConnected || isReadOnly}
129+
disabled={isDisabled}
128130
/>
129131
<div className="absolute bottom-1.5 left-1.5">
130132
<Popover>
@@ -133,7 +135,7 @@ export const ChatInput: FC<Props> = ({
133135
variant="ghost"
134136
size="sm"
135137
className="h-5 rounded-sm px-1.5 text-[10px] text-muted-foreground transition-all hover:bg-accent hover:text-muted-foreground hover:opacity-100"
136-
disabled={!api.isConnected || isReadOnly}
138+
disabled={isDisabled}
137139
>
138140
<Settings className="mr-0.5 h-2.5 w-2.5" />
139141
Options
@@ -146,7 +148,7 @@ export const ChatInput: FC<Props> = ({
146148
<Select
147149
value={selectedModel}
148150
onValueChange={setSelectedModel}
149-
disabled={!api.isConnected || isReadOnly}
151+
disabled={isDisabled}
150152
>
151153
<SelectTrigger id="model-select">
152154
<SelectValue placeholder="Default model" />
@@ -168,7 +170,7 @@ export const ChatInput: FC<Props> = ({
168170
id="streaming-toggle"
169171
checked={streamingEnabled}
170172
onCheckedChange={setStreamingEnabled}
171-
disabled={!api.isConnected || isReadOnly}
173+
disabled={isDisabled}
172174
/>
173175
</div>
174176
</div>
@@ -177,7 +179,7 @@ export const ChatInput: FC<Props> = ({
177179
</div>
178180
</div>
179181
<div className="relative h-full">
180-
<Memo>
182+
<Computed>
181183
{() => {
182184
return (
183185
<Button
@@ -189,7 +191,7 @@ export const ChatInput: FC<Props> = ({
189191
: 'h-10 w-10 bg-green-600 text-green-100'
190192
}
191193
`}
192-
disabled={!api.isConnected || isReadOnly}
194+
disabled={isDisabled}
193195
>
194196
{isGenerating$.get() ? (
195197
<div className="flex items-center gap-2">
@@ -202,7 +204,7 @@ export const ChatInput: FC<Props> = ({
202204
</Button>
203205
);
204206
}}
205-
</Memo>
207+
</Computed>
206208
</div>
207209
</div>
208210
</div>

src/components/ChatMessage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const ChatMessage: FC<Props> = ({
5454
// Handle content changes
5555
useObserveEffect(message$.content, () => {
5656
const message = message$.peek();
57+
if (!message) return;
5758
const parser = parser$.peek();
5859
if (!parser) return;
5960

src/components/Conversations.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { type FC } from 'react';
2+
import { useState, useCallback, useEffect } from 'react';
3+
import { setDocumentTitle } from '@/utils/title';
4+
import { useQuery, useQueryClient } from '@tanstack/react-query';
5+
import { LeftSidebar } from '@/components/LeftSidebar';
6+
import { RightSidebar } from '@/components/RightSidebar';
7+
import { ConversationContent } from '@/components/ConversationContent';
8+
import { useApi } from '@/contexts/ApiContext';
9+
import type { ConversationItem } from '@/components/ConversationList';
10+
import { toConversationItems } from '@/utils/conversation';
11+
import { demoConversations, type DemoConversation } from '@/democonversations';
12+
import { useSearchParams, useNavigate } from 'react-router-dom';
13+
14+
interface Props {
15+
className?: string;
16+
route: string;
17+
}
18+
19+
const Conversations: FC<Props> = ({ route }) => {
20+
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
21+
const [rightSidebarOpen, setRightSidebarOpen] = useState(false);
22+
const [searchParams] = useSearchParams();
23+
const navigate = useNavigate();
24+
const conversationParam = searchParams.get('conversation');
25+
const [selectedConversation, setSelectedConversation] = useState<string>(
26+
conversationParam || demoConversations[0].name
27+
);
28+
const { api, isConnected, connectionConfig } = useApi();
29+
const queryClient = useQueryClient();
30+
31+
// Update selected conversation when URL param changes
32+
useEffect(() => {
33+
if (conversationParam) {
34+
setSelectedConversation(conversationParam);
35+
}
36+
}, [conversationParam]);
37+
38+
// Fetch conversations from API with proper caching
39+
const {
40+
data: apiConversations = [],
41+
isError,
42+
error,
43+
isLoading,
44+
refetch,
45+
} = useQuery({
46+
queryKey: ['conversations', connectionConfig.baseUrl, isConnected],
47+
queryFn: async () => {
48+
console.log('Fetching conversations, connection state:', isConnected);
49+
if (!isConnected) {
50+
console.warn('Attempting to fetch conversations while disconnected');
51+
return [];
52+
}
53+
try {
54+
const conversations = await api.getConversations();
55+
console.log('Fetched conversations:', conversations);
56+
return conversations;
57+
} catch (err) {
58+
console.error('Failed to fetch conversations:', err);
59+
throw err;
60+
}
61+
},
62+
enabled: isConnected,
63+
staleTime: 0, // Always refetch when query is invalidated
64+
gcTime: 5 * 60 * 1000,
65+
});
66+
67+
// Log any query errors
68+
if (isError) {
69+
console.error('Conversation query error:', error);
70+
}
71+
72+
// Combine demo and API conversations
73+
const allConversations: ConversationItem[] = [
74+
// Convert demo conversations to ConversationItems
75+
...demoConversations.map((conv: DemoConversation) => ({
76+
name: conv.name,
77+
lastUpdated: conv.lastUpdated,
78+
messageCount: conv.messages.length,
79+
readonly: true,
80+
})),
81+
// Convert API conversations to ConversationItems
82+
...toConversationItems(apiConversations),
83+
];
84+
85+
const handleSelectConversation = useCallback(
86+
(id: string) => {
87+
if (id === selectedConversation) {
88+
return;
89+
}
90+
// Cancel any pending queries for the previous conversation
91+
queryClient.cancelQueries({
92+
queryKey: ['conversation', selectedConversation],
93+
});
94+
setSelectedConversation(id);
95+
// Update URL with the new conversation ID
96+
console.log(`[Conversations] [handleSelectConversation] id: ${id}`);
97+
navigate(`${route}?conversation=${id}`);
98+
},
99+
[selectedConversation, queryClient, navigate, route]
100+
);
101+
102+
const conversation = allConversations.find((conv) => conv.name === selectedConversation);
103+
104+
// Update document title when selected conversation changes
105+
useEffect(() => {
106+
if (conversation) {
107+
setDocumentTitle(conversation.name);
108+
} else {
109+
setDocumentTitle();
110+
}
111+
return () => setDocumentTitle(); // Reset title on unmount
112+
}, [conversation]);
113+
114+
return (
115+
<div className="flex flex-1 overflow-hidden">
116+
<LeftSidebar
117+
isOpen={leftSidebarOpen}
118+
onToggle={() => setLeftSidebarOpen(!leftSidebarOpen)}
119+
conversations={allConversations}
120+
selectedConversationId={selectedConversation}
121+
onSelectConversation={handleSelectConversation}
122+
isLoading={isLoading}
123+
isError={isError}
124+
error={error as Error}
125+
onRetry={() => refetch()}
126+
route={route}
127+
/>
128+
{conversation ? <ConversationContent conversation={conversation} /> : null}
129+
<RightSidebar
130+
isOpen={rightSidebarOpen}
131+
onToggle={() => setRightSidebarOpen(!rightSidebarOpen)}
132+
/>
133+
</div>
134+
);
135+
};
136+
137+
export default Conversations;

src/components/LeftSidebar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Props {
2020
isError?: boolean;
2121
error?: Error;
2222
onRetry?: () => void;
23+
route: string;
2324
}
2425

2526
export const LeftSidebar: FC<Props> = ({
@@ -32,6 +33,7 @@ export const LeftSidebar: FC<Props> = ({
3233
isError = false,
3334
error,
3435
onRetry,
36+
route,
3537
}) => {
3638
const { api, isConnected } = useApi();
3739
const { toast } = useToast();
@@ -47,7 +49,8 @@ export const LeftSidebar: FC<Props> = ({
4749
title: 'New conversation created',
4850
description: 'Starting a fresh conversation',
4951
});
50-
navigate(`/?conversation=${newId}`);
52+
console.log('newId', newId);
53+
navigate(`${route}?conversation=${newId}`);
5154
} catch {
5255
toast({
5356
variant: 'destructive',

src/contexts/ApiContext.tsx

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,38 @@ interface ApiContextType {
3030

3131
const ApiContext = createContext<ApiContextType | null>(null);
3232

33+
export function connectionConfigFromHash(hash: string) {
34+
const params = new URLSearchParams(hash);
35+
36+
// Get values from fragment
37+
const fragmentBaseUrl = params.get('baseUrl');
38+
const fragmentUserToken = params.get('userToken');
39+
40+
// Save fragment values to localStorage if present
41+
if (fragmentBaseUrl) {
42+
localStorage.setItem('gptme_baseUrl', fragmentBaseUrl);
43+
}
44+
if (fragmentUserToken) {
45+
localStorage.setItem('gptme_userToken', fragmentUserToken);
46+
}
47+
48+
// Clean fragment from URL if parameters were found
49+
if (fragmentBaseUrl || fragmentUserToken) {
50+
window.history.replaceState(null, '', window.location.pathname + window.location.search);
51+
}
52+
53+
// Get stored values
54+
const storedBaseUrl = localStorage.getItem('gptme_baseUrl');
55+
const storedUserToken = localStorage.getItem('gptme_userToken');
56+
57+
return {
58+
baseUrl:
59+
fragmentBaseUrl || storedBaseUrl || import.meta.env.VITE_API_URL || 'http://127.0.0.1:5700',
60+
authToken: fragmentUserToken || storedUserToken || null,
61+
useAuthToken: Boolean(fragmentUserToken || storedUserToken),
62+
};
63+
}
64+
3365
export function ApiProvider({
3466
children,
3567
queryClient,
@@ -41,35 +73,7 @@ export function ApiProvider({
4173
const [connectionConfig, setConnectionConfig] = useState<ConnectionConfig>(() => {
4274
// Get URL fragment parameters if they exist
4375
const hash = window.location.hash.substring(1);
44-
const params = new URLSearchParams(hash);
45-
46-
// Get values from fragment
47-
const fragmentBaseUrl = params.get('baseUrl');
48-
const fragmentUserToken = params.get('userToken');
49-
50-
// Save fragment values to localStorage if present
51-
if (fragmentBaseUrl) {
52-
localStorage.setItem('gptme_baseUrl', fragmentBaseUrl);
53-
}
54-
if (fragmentUserToken) {
55-
localStorage.setItem('gptme_userToken', fragmentUserToken);
56-
}
57-
58-
// Clean fragment from URL if parameters were found
59-
if (fragmentBaseUrl || fragmentUserToken) {
60-
window.history.replaceState(null, '', window.location.pathname + window.location.search);
61-
}
62-
63-
// Get stored values
64-
const storedBaseUrl = localStorage.getItem('gptme_baseUrl');
65-
const storedUserToken = localStorage.getItem('gptme_userToken');
66-
67-
return {
68-
baseUrl:
69-
fragmentBaseUrl || storedBaseUrl || import.meta.env.VITE_API_URL || 'http://127.0.0.1:5700',
70-
authToken: fragmentUserToken || storedUserToken || null,
71-
useAuthToken: Boolean(fragmentUserToken || storedUserToken),
72-
};
76+
return connectionConfigFromHash(hash);
7377
});
7478

7579
const [api, setApi] = useState(() =>

0 commit comments

Comments
 (0)