44import os
55import datetime
66
7- from textual import on , work
8- from textual .app import App , ComposeResult
7+ from textual import work , Logger
8+ from textual .app import App
99from textual .widget import Widget
10- from textual .widgets import Header , Input , Static , Markdown , Label
10+ from textual .widgets import Header , Static , Markdown , Label , TextArea
1111from textual .containers import Vertical , VerticalScroll
12- from textual .events import Key
1312from textual .reactive import reactive
14- from rich .style import Style
15- from rich .text import Text
16- from rich .panel import Panel
13+ from textual .message import Message
1714
1815from ..utils import cumcat
1916from ..chat import Chat
3431##
3532
3633class Sidebar (Widget ):
37- DEFAULT_CSS = """
38- Sidebar {
39- width: 30;
40- layer: sidebar;
41- dock: left;
42- offset-x: -100%;
43-
44- border-right: #cccccc;
45-
46- transition: offset 100ms;
47-
48- &.-visible {
49- offset-x: 0;
50- }
51-
52- & > Vertical {
53- margin: 1 2;
54- }
55- }
56-
57- #history_title {
58- width: 100%;
59- text-align: center;
60- border: round #d576f6;
61- }
62-
63- Sidebar > Vertical > Label {
64- margin-bottom: 1;
65- text-wrap: wrap;
66- }
67- """
68-
6934 def __init__ (self , convo , ** kwargs ):
7035 super ().__init__ (** kwargs )
7136 self .convo = convo
@@ -82,13 +47,6 @@ def compose(self):
8247##
8348
8449class ChatMessage (Markdown ):
85- DEFAULT_CSS = """
86- ChatMessage {
87- padding: 0 1;
88- margin: 0 0;
89- }
90- """
91-
9250 def __init__ (self , title , text , ** kwargs ):
9351 super ().__init__ (text , ** kwargs )
9452 self .border_title = title
@@ -103,17 +61,13 @@ def on_click(self, event):
10361 pass
10462
10563 def update (self , text ):
64+ if len (text .strip ()) == 0 :
65+ text = '...'
10666 self ._text = text
10767 return super ().update (text )
10868
10969# chat history widget
11070class ChatHistory (VerticalScroll ):
111- DEFAULT_CSS = """
112- ChatHistory {
113- scrollbar-size-vertical: 0;
114- }
115- """
116-
11771 def __init__ (self , system = None , ** kwargs ):
11872 super ().__init__ (** kwargs )
11973 self .system = system
@@ -122,32 +76,25 @@ def compose(self):
12276 if self .system is not None :
12377 yield ChatMessage ('system' , self .system )
12478
125- class BareQuery (Input ):
126- DEFAULT_CSS = """
127- BareQuery {
128- background: transparent;
129- padding: 0 1;
130- }
131- """
132-
133- def __init__ (self , height , ** kwargs ):
134- super ().__init__ (** kwargs )
135- self .styles .border = ('none' , None )
136- self .styles .height = height
137-
138- class ChatInput (Static ):
139- DEFAULT_CSS = """
140- ChatInput {
141- border: round white;
142- }
143- """
79+ class ChatInput (TextArea ):
80+ class Submitted (Message ):
81+ def __init__ (self , text : str ) -> None :
82+ self .text = text
83+ super ().__init__ ()
14484
14585 def __init__ (self , ** kwargs ):
146- super ().__init__ (** kwargs )
147- self .border_title = 'user'
86+ super ().__init__ (highlight_cursor_line = False , ** kwargs )
14887
149- def compose (self ):
150- yield BareQuery (height = 3 , placeholder = 'Type a message...' )
88+ def on_key (self , event ):
89+ if event .key == 'ctrl+enter' :
90+ self .insert ('\n ' )
91+ event .prevent_default ()
92+ elif event .key == 'enter' :
93+ if len (query := self .text .strip ()) > 0 :
94+ message = self .Submitted (query )
95+ self .post_message (message )
96+ self .clear ()
97+ event .prevent_default ()
15198
15299# textualize chat app
153100class ChatWindow (Static ):
@@ -162,24 +109,19 @@ def compose(self):
162109
163110 def on_key (self , event ):
164111 history = self .query_one ('ChatHistory' )
165- if event .key == 'PageUp ' :
112+ if event .key == 'pageup ' :
166113 history .scroll_up (animate = False )
167- elif event .key == 'PageDown ' :
114+ elif event .key == 'pagedown ' :
168115 history .scroll_down (animate = False )
169116
170- @on (Input .Submitted )
171- async def on_input (self , event ):
172- query = self .query_one ('BareQuery' )
173- history = self .query_one ('ChatHistory' )
174-
175- # ignore empty messages
176- if len (message := query .value ) == 0 :
177- return
178- query .clear ()
117+ async def on_chat_input_submitted (self , message ):
118+ await self .submit_query (message .text )
179119
120+ async def submit_query (self , query ):
180121 # mount user query and start response
122+ history = self .query_one ('ChatHistory' )
181123 response = ChatMessage ('assistant' , '...' )
182- await history .mount (ChatMessage ('user' , message ))
124+ await history .mount (ChatMessage ('user' , query ))
183125 await history .mount (response )
184126
185127 # make update method
@@ -188,13 +130,15 @@ def update(reply):
188130 history .scroll_end (animate = False )
189131
190132 # send message
191- generate = self .stream (message )
133+ generate = self .stream (query )
134+ self .log .debug (f'STARTING STREAM: { query } ' )
192135 self .pipe_stream (generate , update )
193136
194137 @work (thread = True )
195138 async def pipe_stream (self , generate , setter ):
196139 async for reply in cumcat (generate ):
197140 self .app .call_from_thread (setter , reply )
141+ self .log .debug ('STREAM DONE' )
198142
199143class ConvoStore :
200144 def __init__ (self , store ):
@@ -242,6 +186,63 @@ def load_store(self):
242186 self .convo [file ] = self .load_convo (path )
243187
244188class TextualChat (App ):
189+ CSS = """
190+ ChatMessage {
191+ background: transparent;
192+ padding: 0 1;
193+ margin: 0 0;
194+ }
195+
196+ ChatHistory {
197+ scrollbar-size-vertical: 0;
198+ }
199+
200+ ChatInput {
201+ background: transparent;
202+ border: round white;
203+ padding: 0 1;
204+ height: 6;
205+ }
206+
207+ ChatInput > TextArea {
208+ height: 100%;
209+ }
210+
211+ ChatWindow {
212+ background: transparent;
213+ }
214+
215+ Sidebar {
216+ width: 30;
217+ layer: sidebar;
218+ dock: left;
219+ offset-x: -100%;
220+
221+ border-right: #cccccc;
222+
223+ transition: offset 100ms;
224+
225+ &.-visible {
226+ offset-x: 0;
227+ }
228+
229+ & > Vertical {
230+ margin: 1 2;
231+ }
232+ }
233+
234+ #history_title {
235+ width: 100%;
236+ text-align: center;
237+ border: round #d576f6;
238+ }
239+
240+ Sidebar > Vertical > Label {
241+ margin-bottom: 1;
242+ text-wrap: wrap;
243+ }
244+ """
245+
245246 show_sidebar = reactive (False )
246247
247248 def __init__ (self , chat , store = None , ** kwargs ):
@@ -262,26 +263,19 @@ def compose(self):
262263 yield ChatWindow (self .chat .stream_async , system = self .chat .system )
263264
264265 def on_mount (self ):
265- query = self .query_one ('BareQuery ' )
266+ query = self .query_one ('ChatInput ' )
266267 history = self .query_one ('ChatHistory' )
267268 self .set_focus (query )
268269 history .scroll_end (animate = False )
269270
270271 def on_key (self , event ):
271- if event .key in ('up' , 'down' , 'pageup' , 'pagedown' ):
272- history = self .query_one ('ChatHistory' )
273- if event .key == 'up' :
274- history .scroll_up (animate = False )
275- elif event .key == 'down' :
276- history .scroll_down (animate = False )
277- elif event .key == 'pageup' :
278- history .scroll_page_up (animate = False )
279- elif event .key == 'pagedown' :
280- history .scroll_page_down (animate = False )
281- elif event .key == 'ctrl+s' :
272+ if event .key == 'ctrl+s' :
282273 if self .store is not None :
283274 self .show_sidebar = not self .show_sidebar
284275 event .prevent_default ()
276+ elif event .key == 'ctrl+c' :
277+ self .exit ()
278+ event .prevent_default ()
285279
286280 def watch_show_sidebar (self , show_sidebar ):
287281 if self .store is not None :
0 commit comments