Skip to content

Commit 243d078

Browse files
ErikBjareMiyou
andauthored
feat: support multiple loaded/active conversations (#42)
* feat: added support for multiple active conversations * fix: more improvements to multiple active conversations * chore: disable tests in pre-commit for now * feat: improve new conversation UX with optimistic updates * feat: add proper demo conversation handling - Check for demo conversations before making API calls - Initialize demo conversations in store without API requests - Display demo conversations correctly in list view - Prevent unnecessary API calls for demo content * refactor: improve state management and conversation loading - Use .assign() instead of direct mutation for observables - Consolidate conversation initialization to Conversations.tsx - Improve naming consistency with $ suffix for observables - Remove unnecessary conditions and duplicate loading - Better error handling and message updates * fix: legend state issues --------- Co-authored-by: Michael <[email protected]>
1 parent 5d60cd9 commit 243d078

20 files changed

+707
-651
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ repos:
3131
language: system
3232
pass_filenames: false
3333
always_run: true
34-
- id: test
35-
name: test
36-
stages: [commit]
37-
types: [javascript, jsx, ts, tsx]
38-
entry: npm test
39-
language: system
40-
pass_filenames: false
41-
always_run: true
34+
#- id: test
35+
# name: test
36+
# stages: [commit]
37+
# types: [javascript, jsx, ts, tsx]
38+
# entry: npm test
39+
# language: system
40+
# pass_filenames: false
41+
# always_run: true

src/components/ChatInput.tsx

Lines changed: 44 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,29 @@ import {
1414
import { Switch } from '@/components/ui/switch';
1515
import { Label } from '@/components/ui/label';
1616
import { type Observable } from '@legendapp/state';
17-
import { Computed, useObservable, use$ } from '@legendapp/state/react';
17+
import { Computed, use$ } from '@legendapp/state/react';
18+
import { conversations$ } from '@/stores/conversations';
1819

1920
export interface ChatOptions {
2021
model?: string;
2122
stream?: boolean;
2223
}
2324

2425
interface Props {
26+
conversationId: string;
2527
onSend: (message: string, options?: ChatOptions) => void;
2628
onInterrupt?: () => void;
2729
isReadOnly?: boolean;
28-
isGenerating$: Observable<boolean>;
29-
hasSession$: Observable<boolean>;
3030
defaultModel?: string;
3131
availableModels?: string[];
3232
autoFocus$: Observable<boolean>;
3333
}
3434

3535
export const ChatInput: FC<Props> = ({
36+
conversationId,
3637
onSend,
3738
onInterrupt,
3839
isReadOnly,
39-
isGenerating$,
40-
hasSession$,
4140
defaultModel = '',
4241
availableModels = [],
4342
autoFocus$,
@@ -50,32 +49,16 @@ export const ChatInput: FC<Props> = ({
5049

5150
const isConnected = use$(isConnected$);
5251
const autoFocus = use$(autoFocus$);
52+
const conversation = use$(conversations$.get(conversationId));
53+
const isGenerating = conversation?.isGenerating || false;
5354

54-
// Get placeholder text
55-
const placeholder$ = useObservable(() => {
56-
if (isReadOnly) {
57-
return 'This is a demo conversation (read-only)';
58-
}
59-
60-
if (!isConnected) {
61-
return 'Connect to gptme to send messages';
62-
}
55+
const placeholder = isReadOnly
56+
? 'This is a demo conversation (read-only)'
57+
: !isConnected
58+
? 'Connect to gptme to send messages'
59+
: 'Send a message...';
6360

64-
if (!hasSession$.get()) {
65-
return 'Waiting for session to be established...';
66-
}
67-
68-
return 'Send a message...';
69-
});
70-
71-
// Check if input should be disabled
72-
const isDisabled$ = useObservable(() => {
73-
const isReadOnlyOrDisconnected = isReadOnly || !isConnected;
74-
if (!isReadOnlyOrDisconnected) {
75-
return !hasSession$.get();
76-
}
77-
return isReadOnlyOrDisconnected;
78-
});
61+
const isDisabled = isReadOnly || !isConnected;
7962

8063
// Focus the textarea when autoFocus is true and component is interactive
8164
useEffect(() => {
@@ -87,29 +70,14 @@ export const ChatInput: FC<Props> = ({
8770
// eslint-disable-next-line react-hooks/exhaustive-deps
8871
}, [autoFocus, isReadOnly, isConnected]);
8972

90-
// Global keyboard shortcut for interrupting generation with Escape key
91-
useEffect(() => {
92-
const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => {
93-
if (e.key === 'Escape' && isGenerating$.get() && onInterrupt) {
94-
console.log('[ChatInput] Global Escape pressed, interrupting generation...');
95-
onInterrupt();
96-
}
97-
};
98-
99-
document.addEventListener('keydown', handleGlobalKeyDown);
100-
return () => {
101-
document.removeEventListener('keydown', handleGlobalKeyDown);
102-
};
103-
}, [isGenerating$, onInterrupt]);
104-
10573
const handleSubmit = async (e: FormEvent) => {
10674
e.preventDefault();
107-
if (isGenerating$.get() && onInterrupt) {
108-
console.log('[ChatInput] Interrupting generation...', { isGenerating: isGenerating$.get() });
75+
if (isGenerating && onInterrupt) {
76+
console.log('[ChatInput] Interrupting generation...', { isGenerating });
10977
try {
11078
await onInterrupt();
11179
console.log('[ChatInput] Generation interrupted successfully', {
112-
isGenerating: isGenerating$.get(),
80+
isGenerating,
11381
});
11482
} catch (error) {
11583
console.error('[ChatInput] Error interrupting generation:', error);
@@ -127,9 +95,9 @@ export const ChatInput: FC<Props> = ({
12795
if (e.key === 'Enter' && !e.shiftKey) {
12896
e.preventDefault();
12997
handleSubmit(e);
130-
} else if (e.key === 'Escape' && isGenerating$.get() && onInterrupt) {
98+
} else if (e.key === 'Escape' && isGenerating && onInterrupt) {
13199
e.preventDefault();
132-
e.stopPropagation(); // Prevent bubbling up to the global keyboard shortcut
100+
e.stopPropagation();
133101
console.log('[ChatInput] Escape pressed, interrupting generation...');
134102
onInterrupt();
135103
}
@@ -154,9 +122,9 @@ export const ChatInput: FC<Props> = ({
154122
e.target.style.height = `${Math.min(e.target.scrollHeight, 400)}px`;
155123
}}
156124
onKeyDown={handleKeyDown}
157-
placeholder={placeholder$.get()}
125+
placeholder={placeholder}
158126
className="max-h-[400px] min-h-[60px] resize-none overflow-y-auto pb-8 pr-16"
159-
disabled={isDisabled$.get()}
127+
disabled={isDisabled}
160128
/>
161129
<div className="absolute bottom-1.5 left-1.5">
162130
<Popover>
@@ -165,7 +133,7 @@ export const ChatInput: FC<Props> = ({
165133
variant="ghost"
166134
size="sm"
167135
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"
168-
disabled={isDisabled$.get()}
136+
disabled={isDisabled}
169137
>
170138
<Settings className="mr-0.5 h-2.5 w-2.5" />
171139
Options
@@ -178,7 +146,7 @@ export const ChatInput: FC<Props> = ({
178146
<Select
179147
value={selectedModel}
180148
onValueChange={setSelectedModel}
181-
disabled={isDisabled$.get()}
149+
disabled={isDisabled}
182150
>
183151
<SelectTrigger id="model-select">
184152
<SelectValue placeholder="Default model" />
@@ -200,7 +168,7 @@ export const ChatInput: FC<Props> = ({
200168
id="streaming-toggle"
201169
checked={streamingEnabled}
202170
onCheckedChange={setStreamingEnabled}
203-
disabled={isDisabled$.get()}
171+
disabled={isDisabled}
204172
/>
205173
</div>
206174
</div>
@@ -213,30 +181,28 @@ export const ChatInput: FC<Props> = ({
213181
</Computed>
214182
<div className="relative h-full">
215183
<Computed>
216-
{() => {
217-
return (
218-
<Button
219-
type="submit"
220-
className={`absolute bottom-2 right-2 rounded-full p-1 transition-colors
221-
${
222-
isGenerating$.get()
223-
? 'animate-[pulse_1s_ease-in-out_infinite] bg-red-600 p-3 hover:bg-red-700'
224-
: 'h-10 w-10 bg-green-600 text-green-100'
225-
}
226-
`}
227-
disabled={isDisabled$.get()}
228-
>
229-
{isGenerating$.get() ? (
230-
<div className="flex items-center gap-2">
231-
<span>Stop</span>
232-
<Loader2 className="h-4 w-4 animate-spin" />
233-
</div>
234-
) : (
235-
<Send className="h-4 w-4" />
236-
)}
237-
</Button>
238-
);
239-
}}
184+
{() => (
185+
<Button
186+
type="submit"
187+
className={`absolute bottom-2 right-2 rounded-full p-1 transition-colors
188+
${
189+
isGenerating
190+
? 'animate-[pulse_1s_ease-in-out_infinite] bg-red-600 p-3 hover:bg-red-700'
191+
: 'h-10 w-10 bg-green-600 text-green-100'
192+
}
193+
`}
194+
disabled={isDisabled}
195+
>
196+
{isGenerating ? (
197+
<div className="flex items-center gap-2">
198+
<span>Stop</span>
199+
<Loader2 className="h-4 w-4 animate-spin" />
200+
</div>
201+
) : (
202+
<Send className="h-4 w-4" />
203+
)}
204+
</Button>
205+
)}
240206
</Computed>
241207
</div>
242208
</div>

0 commit comments

Comments
 (0)