@@ -14,30 +14,29 @@ import {
14
14
import { Switch } from '@/components/ui/switch' ;
15
15
import { Label } from '@/components/ui/label' ;
16
16
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' ;
18
19
19
20
export interface ChatOptions {
20
21
model ?: string ;
21
22
stream ?: boolean ;
22
23
}
23
24
24
25
interface Props {
26
+ conversationId : string ;
25
27
onSend : ( message : string , options ?: ChatOptions ) => void ;
26
28
onInterrupt ?: ( ) => void ;
27
29
isReadOnly ?: boolean ;
28
- isGenerating$ : Observable < boolean > ;
29
- hasSession$ : Observable < boolean > ;
30
30
defaultModel ?: string ;
31
31
availableModels ?: string [ ] ;
32
32
autoFocus$ : Observable < boolean > ;
33
33
}
34
34
35
35
export const ChatInput : FC < Props > = ( {
36
+ conversationId,
36
37
onSend,
37
38
onInterrupt,
38
39
isReadOnly,
39
- isGenerating$,
40
- hasSession$,
41
40
defaultModel = '' ,
42
41
availableModels = [ ] ,
43
42
autoFocus$,
@@ -50,32 +49,16 @@ export const ChatInput: FC<Props> = ({
50
49
51
50
const isConnected = use$ ( isConnected$ ) ;
52
51
const autoFocus = use$ ( autoFocus$ ) ;
52
+ const conversation = use$ ( conversations$ . get ( conversationId ) ) ;
53
+ const isGenerating = conversation ?. isGenerating || false ;
53
54
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...' ;
63
60
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 ;
79
62
80
63
// Focus the textarea when autoFocus is true and component is interactive
81
64
useEffect ( ( ) => {
@@ -87,29 +70,14 @@ export const ChatInput: FC<Props> = ({
87
70
// eslint-disable-next-line react-hooks/exhaustive-deps
88
71
} , [ autoFocus , isReadOnly , isConnected ] ) ;
89
72
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
-
105
73
const handleSubmit = async ( e : FormEvent ) => {
106
74
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 } ) ;
109
77
try {
110
78
await onInterrupt ( ) ;
111
79
console . log ( '[ChatInput] Generation interrupted successfully' , {
112
- isGenerating : isGenerating$ . get ( ) ,
80
+ isGenerating,
113
81
} ) ;
114
82
} catch ( error ) {
115
83
console . error ( '[ChatInput] Error interrupting generation:' , error ) ;
@@ -127,9 +95,9 @@ export const ChatInput: FC<Props> = ({
127
95
if ( e . key === 'Enter' && ! e . shiftKey ) {
128
96
e . preventDefault ( ) ;
129
97
handleSubmit ( e ) ;
130
- } else if ( e . key === 'Escape' && isGenerating$ . get ( ) && onInterrupt ) {
98
+ } else if ( e . key === 'Escape' && isGenerating && onInterrupt ) {
131
99
e . preventDefault ( ) ;
132
- e . stopPropagation ( ) ; // Prevent bubbling up to the global keyboard shortcut
100
+ e . stopPropagation ( ) ;
133
101
console . log ( '[ChatInput] Escape pressed, interrupting generation...' ) ;
134
102
onInterrupt ( ) ;
135
103
}
@@ -154,9 +122,9 @@ export const ChatInput: FC<Props> = ({
154
122
e . target . style . height = `${ Math . min ( e . target . scrollHeight , 400 ) } px` ;
155
123
} }
156
124
onKeyDown = { handleKeyDown }
157
- placeholder = { placeholder$ . get ( ) }
125
+ placeholder = { placeholder }
158
126
className = "max-h-[400px] min-h-[60px] resize-none overflow-y-auto pb-8 pr-16"
159
- disabled = { isDisabled$ . get ( ) }
127
+ disabled = { isDisabled }
160
128
/>
161
129
< div className = "absolute bottom-1.5 left-1.5" >
162
130
< Popover >
@@ -165,7 +133,7 @@ export const ChatInput: FC<Props> = ({
165
133
variant = "ghost"
166
134
size = "sm"
167
135
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 }
169
137
>
170
138
< Settings className = "mr-0.5 h-2.5 w-2.5" />
171
139
Options
@@ -178,7 +146,7 @@ export const ChatInput: FC<Props> = ({
178
146
< Select
179
147
value = { selectedModel }
180
148
onValueChange = { setSelectedModel }
181
- disabled = { isDisabled$ . get ( ) }
149
+ disabled = { isDisabled }
182
150
>
183
151
< SelectTrigger id = "model-select" >
184
152
< SelectValue placeholder = "Default model" />
@@ -200,7 +168,7 @@ export const ChatInput: FC<Props> = ({
200
168
id = "streaming-toggle"
201
169
checked = { streamingEnabled }
202
170
onCheckedChange = { setStreamingEnabled }
203
- disabled = { isDisabled$ . get ( ) }
171
+ disabled = { isDisabled }
204
172
/>
205
173
</ div >
206
174
</ div >
@@ -213,30 +181,28 @@ export const ChatInput: FC<Props> = ({
213
181
</ Computed >
214
182
< div className = "relative h-full" >
215
183
< 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
+ ) }
240
206
</ Computed >
241
207
</ div >
242
208
</ div >
0 commit comments