chat-console 0.1.9.dev1__tar.gz → 0.1.95__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/PKG-INFO +1 -1
  2. chat_console-0.1.95/app/main.py +664 -0
  3. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/model_selector.py +8 -0
  4. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/PKG-INFO +1 -1
  5. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/setup.py +1 -1
  6. chat_console-0.1.9.dev1/app/main.py +0 -661
  7. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/LICENSE +0 -0
  8. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/README.md +0 -0
  9. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/__init__.py +0 -0
  10. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/__init__.py +0 -0
  11. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/anthropic.py +0 -0
  12. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/base.py +0 -0
  13. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/ollama.py +0 -0
  14. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/openai.py +0 -0
  15. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/config.py +0 -0
  16. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/database.py +0 -0
  17. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/models.py +0 -0
  18. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/__init__.py +0 -0
  19. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/chat_interface.py +0 -0
  20. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/chat_list.py +0 -0
  21. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/search.py +0 -0
  22. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/styles.py +0 -0
  23. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/utils.py +0 -0
  24. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/SOURCES.txt +0 -0
  25. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/dependency_links.txt +0 -0
  26. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/entry_points.txt +0 -0
  27. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/requires.txt +0 -0
  28. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/top_level.txt +0 -0
  29. {chat_console-0.1.9.dev1 → chat_console-0.1.95}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.1.9.dev1
3
+ Version: 0.1.95
4
4
  Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
5
5
  Home-page: https://github.com/wazacraftrfid/chat-console
6
6
  Author: Johnathan Greenaway
@@ -0,0 +1,664 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simplified version of Chat CLI with AI functionality
4
+ """
5
+ import os
6
+ import asyncio
7
+ import typer
8
+ from typing import List, Optional, Callable, Awaitable
9
+ from datetime import datetime
10
+
11
+ from textual.app import App, ComposeResult
12
+ from textual.containers import Container, Horizontal, Vertical, ScrollableContainer, Center
13
+ from textual.reactive import reactive
14
+ from textual.widgets import Button, Input, Label, Static, Header, Footer, ListView, ListItem
15
+ from textual.binding import Binding
16
+ from textual import work
17
+ from textual.screen import Screen
18
+ from openai import OpenAI
19
+ from app.models import Message, Conversation
20
+ from app.database import ChatDatabase
21
+ from app.config import CONFIG, OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_BASE_URL
22
+ from app.ui.chat_interface import MessageDisplay
23
+ from app.ui.model_selector import ModelSelector, StyleSelector
24
+ from app.ui.chat_list import ChatList
25
+ from app.api.base import BaseModelClient
26
+ from app.utils import generate_streaming_response, save_settings_to_config # Import save function
27
+
28
+ # --- Remove SettingsScreen class entirely ---
29
+
30
+ class HistoryScreen(Screen):
31
+ """Screen for viewing chat history."""
32
+
33
+ BINDINGS = [
34
+ Binding("escape", "pop_screen", "Close"),
35
+ ]
36
+
37
+ CSS = """
38
+ #history-container {
39
+ width: 80; # Keep HistoryScreen CSS
40
+ height: 40;
41
+ background: $surface;
42
+ border: round $primary;
43
+ padding: 1; # Keep HistoryScreen CSS
44
+ }
45
+
46
+ #title { # Keep HistoryScreen CSS
47
+ width: 100%; # Keep HistoryScreen CSS
48
+ content-align: center middle;
49
+ text-align: center;
50
+ padding-bottom: 1;
51
+ }
52
+
53
+ ListView { # Keep HistoryScreen CSS
54
+ width: 100%; # Keep HistoryScreen CSS
55
+ height: 1fr;
56
+ border: solid $primary;
57
+ }
58
+
59
+ ListItem { # Keep HistoryScreen CSS
60
+ padding: 1; # Keep HistoryScreen CSS
61
+ border-bottom: solid $primary-darken-2;
62
+ }
63
+
64
+ ListItem:hover { # Keep HistoryScreen CSS
65
+ background: $primary-darken-1; # Keep HistoryScreen CSS
66
+ }
67
+
68
+ #button-row { # Keep HistoryScreen CSS
69
+ width: 100%; # Keep HistoryScreen CSS
70
+ height: 3;
71
+ align-horizontal: center;
72
+ margin-top: 1; # Keep HistoryScreen CSS
73
+ }
74
+ """
75
+
76
+ def __init__(self, conversations: List[dict], callback: Callable[[int], Awaitable[None]]): # Keep HistoryScreen __init__
77
+ super().__init__() # Keep HistoryScreen __init__
78
+ self.conversations = conversations # Keep HistoryScreen __init__
79
+ self.callback = callback # Keep HistoryScreen __init__
80
+
81
+ def compose(self) -> ComposeResult: # Keep HistoryScreen compose
82
+ """Create the history screen layout."""
83
+ with Center():
84
+ with Container(id="history-container"):
85
+ yield Static("Chat History", id="title")
86
+ yield ListView(id="history-list")
87
+ with Horizontal(id="button-row"):
88
+ yield Button("Cancel", variant="primary")
89
+
90
+ async def on_mount(self) -> None: # Keep HistoryScreen on_mount
91
+ """Initialize the history list after mount."""
92
+ list_view = self.query_one("#history-list", ListView)
93
+ for conv in self.conversations:
94
+ title = conv["title"]
95
+ model = conv["model"]
96
+ if model in CONFIG["available_models"]:
97
+ model = CONFIG["available_models"][model]["display_name"]
98
+ item = ListItem(Label(f"{title} ({model})"))
99
+ # Prefix numeric IDs with 'conv-' to make them valid identifiers
100
+ item.id = f"conv-{conv['id']}"
101
+ await list_view.mount(item)
102
+
103
+ async def on_list_view_selected(self, event: ListView.Selected) -> None: # Keep HistoryScreen on_list_view_selected
104
+ """Handle conversation selection."""
105
+ # Remove 'conv-' prefix to get the numeric ID
106
+ conv_id = int(event.item.id.replace('conv-', ''))
107
+ self.app.pop_screen()
108
+ await self.callback(conv_id)
109
+
110
+ def on_button_pressed(self, event: Button.Pressed) -> None: # Keep HistoryScreen on_button_pressed
111
+ if event.button.label == "Cancel":
112
+ self.app.pop_screen()
113
+
114
+ class SimpleChatApp(App): # Keep SimpleChatApp class definition
115
+ """Simplified Chat CLI application.""" # Keep SimpleChatApp docstring
116
+
117
+ TITLE = "Chat CLI" # Keep SimpleChatApp TITLE
118
+ SUB_TITLE = "AI Chat Interface" # Keep SimpleChatApp SUB_TITLE
119
+ DARK = True # Keep SimpleChatApp DARK
120
+
121
+ CSS = """ # Keep SimpleChatApp CSS start
122
+ #main-content { # Keep SimpleChatApp CSS
123
+ width: 100%;
124
+ height: 100%;
125
+ padding: 0 1;
126
+ }
127
+
128
+ #conversation-title { # Keep SimpleChatApp CSS
129
+ width: 100%; # Keep SimpleChatApp CSS
130
+ height: 2;
131
+ background: $surface-darken-2;
132
+ color: $text;
133
+ content-align: center middle;
134
+ text-align: center;
135
+ border-bottom: solid $primary-darken-2;
136
+ }
137
+
138
+ #messages-container { # Keep SimpleChatApp CSS
139
+ width: 100%; # Keep SimpleChatApp CSS
140
+ height: 1fr;
141
+ min-height: 10;
142
+ border-bottom: solid $primary-darken-2;
143
+ overflow: auto;
144
+ padding: 0 1;
145
+ }
146
+
147
+ #loading-indicator { # Keep SimpleChatApp CSS
148
+ width: 100%; # Keep SimpleChatApp CSS
149
+ height: 1;
150
+ background: $primary-darken-1;
151
+ color: $text;
152
+ content-align: center middle;
153
+ text-align: center;
154
+ }
155
+
156
+ #loading-indicator.hidden { # Keep SimpleChatApp CSS
157
+ display: none;
158
+ }
159
+
160
+ #input-area { # Keep SimpleChatApp CSS
161
+ width: 100%; # Keep SimpleChatApp CSS
162
+ height: auto;
163
+ min-height: 4;
164
+ max-height: 10;
165
+ padding: 1;
166
+ }
167
+
168
+ #message-input { # Keep SimpleChatApp CSS
169
+ width: 1fr; # Keep SimpleChatApp CSS
170
+ min-height: 2;
171
+ height: auto;
172
+ margin-right: 1;
173
+ border: solid $primary-darken-2;
174
+ }
175
+
176
+ #message-input:focus { # Keep SimpleChatApp CSS
177
+ border: solid $primary;
178
+ }
179
+
180
+ /* Removed CSS for #send-button, #new-chat-button, #view-history-button, #settings-button */ # Keep SimpleChatApp CSS comment
181
+ /* Removed CSS for #button-row */ # Keep SimpleChatApp CSS comment
182
+
183
+ #settings-panel { /* Add CSS for the new settings panel */
184
+ display: none; /* Hidden by default */
185
+ align: center middle;
186
+ width: 60;
187
+ height: auto;
188
+ background: $surface;
189
+ border: thick $primary;
190
+ padding: 1 2;
191
+ layer: settings; /* Ensure it's above other elements */
192
+ }
193
+
194
+ #settings-panel.visible { /* Class to show the panel */
195
+ display: block;
196
+ }
197
+
198
+ #settings-title {
199
+ width: 100%;
200
+ content-align: center middle;
201
+ padding-bottom: 1;
202
+ border-bottom: thick $primary-darken-2; /* Correct syntax for bottom border */
203
+ }
204
+
205
+ #settings-buttons {
206
+ width: 100%;
207
+ height: auto;
208
+ align: center middle;
209
+ padding-top: 1;
210
+ }
211
+
212
+ """
213
+
214
+ BINDINGS = [ # Keep SimpleChatApp BINDINGS, ensure Enter is not globally bound for settings
215
+ Binding("q", "quit", "Quit", show=True, key_display="q"),
216
+ Binding("n", "action_new_conversation", "New Chat", show=True, key_display="n"),
217
+ Binding("c", "action_new_conversation", "New Chat", show=False, key_display="c"),
218
+ Binding("escape", "escape", "Cancel / Stop", show=True, key_display="esc"), # Escape might close settings panel too
219
+ Binding("ctrl+c", "quit", "Quit", show=False),
220
+ Binding("h", "view_history", "History", show=True, key_display="h"),
221
+ Binding("s", "settings", "Settings", show=True, key_display="s"),
222
+ ] # Keep SimpleChatApp BINDINGS end
223
+
224
+ current_conversation = reactive(None) # Keep SimpleChatApp reactive var
225
+ is_generating = reactive(False) # Keep SimpleChatApp reactive var
226
+
227
+ def __init__(self, initial_text: Optional[str] = None): # Keep SimpleChatApp __init__
228
+ super().__init__() # Keep SimpleChatApp __init__
229
+ self.db = ChatDatabase() # Keep SimpleChatApp __init__
230
+ self.messages = [] # Keep SimpleChatApp __init__
231
+ self.selected_model = CONFIG["default_model"] # Keep SimpleChatApp __init__
232
+ self.selected_style = CONFIG["default_style"] # Keep SimpleChatApp __init__
233
+ self.initial_text = initial_text # Keep SimpleChatApp __init__
234
+
235
+ def compose(self) -> ComposeResult: # Modify SimpleChatApp compose
236
+ """Create the simplified application layout."""
237
+ yield Header()
238
+
239
+ with Vertical(id="main-content"):
240
+ # Conversation title
241
+ yield Static("New Conversation", id="conversation-title")
242
+
243
+ # Messages area
244
+ with ScrollableContainer(id="messages-container"):
245
+ # Will be populated with messages
246
+ pass
247
+
248
+ # Loading indicator
249
+ yield Static("Generating response...", id="loading-indicator", classes="hidden")
250
+
251
+ # Input area
252
+ with Container(id="input-area"):
253
+ yield Input(placeholder="Type your message here...", id="message-input")
254
+ # Removed Static widgets previously used for diagnosis
255
+
256
+ # --- Add Settings Panel (hidden initially) ---
257
+ with Container(id="settings-panel"):
258
+ yield Static("Settings", id="settings-title")
259
+ yield ModelSelector(self.selected_model)
260
+ yield StyleSelector(self.selected_style)
261
+ with Horizontal(id="settings-buttons"):
262
+ yield Button("Save", id="settings-save-button", variant="success")
263
+ yield Button("Cancel", id="settings-cancel-button", variant="error")
264
+
265
+ yield Footer()
266
+
267
+ async def on_mount(self) -> None: # Keep SimpleChatApp on_mount
268
+ """Initialize the application on mount.""" # Keep SimpleChatApp on_mount docstring
269
+ # Check API keys and services # Keep SimpleChatApp on_mount
270
+ api_issues = [] # Keep SimpleChatApp on_mount
271
+ if not OPENAI_API_KEY: # Keep SimpleChatApp on_mount
272
+ api_issues.append("- OPENAI_API_KEY is not set") # Keep SimpleChatApp on_mount
273
+ if not ANTHROPIC_API_KEY: # Keep SimpleChatApp on_mount
274
+ api_issues.append("- ANTHROPIC_API_KEY is not set") # Keep SimpleChatApp on_mount
275
+
276
+ # Check Ollama availability and try to start if not running # Keep SimpleChatApp on_mount
277
+ from app.utils import ensure_ollama_running # Keep SimpleChatApp on_mount
278
+ if not ensure_ollama_running(): # Keep SimpleChatApp on_mount
279
+ api_issues.append("- Ollama server not running and could not be started") # Keep SimpleChatApp on_mount
280
+ else: # Keep SimpleChatApp on_mount
281
+ # Check for available models # Keep SimpleChatApp on_mount
282
+ from app.api.ollama import OllamaClient # Keep SimpleChatApp on_mount
283
+ try: # Keep SimpleChatApp on_mount
284
+ ollama = OllamaClient() # Keep SimpleChatApp on_mount
285
+ models = await ollama.get_available_models() # Keep SimpleChatApp on_mount
286
+ if not models: # Keep SimpleChatApp on_mount
287
+ api_issues.append("- No Ollama models found") # Keep SimpleChatApp on_mount
288
+ except Exception: # Keep SimpleChatApp on_mount
289
+ api_issues.append("- Error connecting to Ollama server") # Keep SimpleChatApp on_mount
290
+
291
+ if api_issues: # Keep SimpleChatApp on_mount
292
+ self.notify( # Keep SimpleChatApp on_mount
293
+ "Service issues detected:\n" + "\n".join(api_issues) + # Keep SimpleChatApp on_mount
294
+ "\n\nEnsure services are configured and running.", # Keep SimpleChatApp on_mount
295
+ title="Service Warning", # Keep SimpleChatApp on_mount
296
+ severity="warning", # Keep SimpleChatApp on_mount
297
+ timeout=10 # Keep SimpleChatApp on_mount
298
+ ) # Keep SimpleChatApp on_mount
299
+
300
+ # Create a new conversation # Keep SimpleChatApp on_mount
301
+ await self.create_new_conversation() # Keep SimpleChatApp on_mount
302
+
303
+ # If initial text was provided, send it # Keep SimpleChatApp on_mount
304
+ if self.initial_text: # Keep SimpleChatApp on_mount
305
+ input_widget = self.query_one("#message-input", Input) # Keep SimpleChatApp on_mount
306
+ input_widget.value = self.initial_text # Keep SimpleChatApp on_mount
307
+ await self.action_send_message() # Keep SimpleChatApp on_mount
308
+ else: # Keep SimpleChatApp on_mount
309
+ # Focus the input if no initial text # Keep SimpleChatApp on_mount
310
+ self.query_one("#message-input").focus() # Keep SimpleChatApp on_mount
311
+
312
+ async def create_new_conversation(self) -> None: # Keep SimpleChatApp create_new_conversation
313
+ """Create a new chat conversation.""" # Keep SimpleChatApp create_new_conversation docstring
314
+ # Create new conversation in database using selected model and style # Keep SimpleChatApp create_new_conversation
315
+ model = self.selected_model # Keep SimpleChatApp create_new_conversation
316
+ style = self.selected_style # Keep SimpleChatApp create_new_conversation
317
+
318
+ # Create a title for the new conversation # Keep SimpleChatApp create_new_conversation
319
+ title = f"New conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})" # Keep SimpleChatApp create_new_conversation
320
+
321
+ # Create conversation in database using the correct method # Keep SimpleChatApp create_new_conversation
322
+ conversation_id = self.db.create_conversation(title, model, style) # Keep SimpleChatApp create_new_conversation
323
+
324
+ # Get the full conversation data # Keep SimpleChatApp create_new_conversation
325
+ conversation_data = self.db.get_conversation(conversation_id) # Keep SimpleChatApp create_new_conversation
326
+
327
+ # Set as current conversation # Keep SimpleChatApp create_new_conversation
328
+ self.current_conversation = Conversation.from_dict(conversation_data) # Keep SimpleChatApp create_new_conversation
329
+
330
+ # Update UI # Keep SimpleChatApp create_new_conversation
331
+ title = self.query_one("#conversation-title", Static) # Keep SimpleChatApp create_new_conversation
332
+ title.update(self.current_conversation.title) # Keep SimpleChatApp create_new_conversation
333
+
334
+ # Clear messages and update UI # Keep SimpleChatApp create_new_conversation
335
+ self.messages = [] # Keep SimpleChatApp create_new_conversation
336
+ await self.update_messages_ui() # Keep SimpleChatApp create_new_conversation
337
+
338
+ async def action_new_conversation(self) -> None: # Keep SimpleChatApp action_new_conversation
339
+ """Handle the new conversation action.""" # Keep SimpleChatApp action_new_conversation docstring
340
+ await self.create_new_conversation() # Keep SimpleChatApp action_new_conversation
341
+
342
+ def action_escape(self) -> None: # Modify SimpleChatApp action_escape
343
+ """Handle escape key globally."""
344
+ settings_panel = self.query_one("#settings-panel")
345
+ if settings_panel.has_class("visible"):
346
+ # If settings panel is visible, hide it
347
+ settings_panel.remove_class("visible")
348
+ self.query_one("#message-input").focus() # Focus input after closing settings
349
+ elif self.is_generating:
350
+ # Otherwise, stop generation if running
351
+ self.is_generating = False # Keep SimpleChatApp action_escape
352
+ self.notify("Generation stopped", severity="warning") # Keep SimpleChatApp action_escape
353
+ loading = self.query_one("#loading-indicator") # Keep SimpleChatApp action_escape
354
+ loading.add_class("hidden") # Keep SimpleChatApp action_escape
355
+ # else: # Optional: Add other escape behavior for the main screen if desired # Keep SimpleChatApp action_escape comment
356
+ # pass # Keep SimpleChatApp action_escape comment
357
+
358
+ # Removed action_confirm_or_send - Enter is handled by Input submission # Keep SimpleChatApp comment
359
+
360
+ async def update_messages_ui(self) -> None: # Keep SimpleChatApp update_messages_ui
361
+ """Update the messages UI.""" # Keep SimpleChatApp update_messages_ui docstring
362
+ # Clear existing messages # Keep SimpleChatApp update_messages_ui
363
+ messages_container = self.query_one("#messages-container") # Keep SimpleChatApp update_messages_ui
364
+ messages_container.remove_children() # Keep SimpleChatApp update_messages_ui
365
+
366
+ # Add messages with a small delay between each # Keep SimpleChatApp update_messages_ui
367
+ for message in self.messages: # Keep SimpleChatApp update_messages_ui
368
+ display = MessageDisplay(message, highlight_code=CONFIG["highlight_code"]) # Keep SimpleChatApp update_messages_ui
369
+ messages_container.mount(display) # Keep SimpleChatApp update_messages_ui
370
+ messages_container.scroll_end(animate=False) # Keep SimpleChatApp update_messages_ui
371
+ await asyncio.sleep(0.01) # Small delay to prevent UI freezing # Keep SimpleChatApp update_messages_ui
372
+
373
+ # Final scroll to bottom # Keep SimpleChatApp update_messages_ui
374
+ messages_container.scroll_end(animate=False) # Keep SimpleChatApp update_messages_ui
375
+
376
+ async def on_input_submitted(self, event: Input.Submitted) -> None: # Keep SimpleChatApp on_input_submitted
377
+ """Handle input submission (Enter key in the main input).""" # Keep SimpleChatApp on_input_submitted docstring
378
+ await self.action_send_message() # Restore direct call # Keep SimpleChatApp on_input_submitted
379
+
380
+ async def action_send_message(self) -> None: # Keep SimpleChatApp action_send_message
381
+ """Initiate message sending.""" # Keep SimpleChatApp action_send_message docstring
382
+ input_widget = self.query_one("#message-input", Input) # Keep SimpleChatApp action_send_message
383
+ content = input_widget.value.strip() # Keep SimpleChatApp action_send_message
384
+
385
+ if not content or not self.current_conversation: # Keep SimpleChatApp action_send_message
386
+ return # Keep SimpleChatApp action_send_message
387
+
388
+ # Clear input # Keep SimpleChatApp action_send_message
389
+ input_widget.value = "" # Keep SimpleChatApp action_send_message
390
+
391
+ # Create user message # Keep SimpleChatApp action_send_message
392
+ user_message = Message(role="user", content=content) # Keep SimpleChatApp action_send_message
393
+ self.messages.append(user_message) # Keep SimpleChatApp action_send_message
394
+
395
+ # Save to database # Keep SimpleChatApp action_send_message
396
+ self.db.add_message( # Keep SimpleChatApp action_send_message
397
+ self.current_conversation.id, # Keep SimpleChatApp action_send_message
398
+ "user", # Keep SimpleChatApp action_send_message
399
+ content # Keep SimpleChatApp action_send_message
400
+ ) # Keep SimpleChatApp action_send_message
401
+
402
+ # Update UI # Keep SimpleChatApp action_send_message
403
+ await self.update_messages_ui() # Keep SimpleChatApp action_send_message
404
+
405
+ # Generate AI response # Keep SimpleChatApp action_send_message
406
+ await self.generate_response() # Keep SimpleChatApp action_send_message
407
+
408
+ # Focus back on input # Keep SimpleChatApp action_send_message
409
+ input_widget.focus() # Keep SimpleChatApp action_send_message
410
+
411
+ async def generate_response(self) -> None: # Keep SimpleChatApp generate_response
412
+ """Generate an AI response.""" # Keep SimpleChatApp generate_response docstring
413
+ if not self.current_conversation or not self.messages: # Keep SimpleChatApp generate_response
414
+ return # Keep SimpleChatApp generate_response
415
+
416
+ self.is_generating = True # Keep SimpleChatApp generate_response
417
+ loading = self.query_one("#loading-indicator") # Keep SimpleChatApp generate_response
418
+ loading.remove_class("hidden") # Keep SimpleChatApp generate_response
419
+
420
+ try: # Keep SimpleChatApp generate_response
421
+ # Get conversation parameters # Keep SimpleChatApp generate_response
422
+ model = self.selected_model # Keep SimpleChatApp generate_response
423
+ style = self.selected_style # Keep SimpleChatApp generate_response
424
+
425
+ # Convert messages to API format # Keep SimpleChatApp generate_response
426
+ api_messages = [] # Keep SimpleChatApp generate_response
427
+ for msg in self.messages: # Keep SimpleChatApp generate_response
428
+ api_messages.append({ # Keep SimpleChatApp generate_response
429
+ "role": msg.role, # Keep SimpleChatApp generate_response
430
+ "content": msg.content # Keep SimpleChatApp generate_response
431
+ }) # Keep SimpleChatApp generate_response
432
+
433
+ # Get appropriate client # Keep SimpleChatApp generate_response
434
+ try: # Keep SimpleChatApp generate_response
435
+ client = BaseModelClient.get_client_for_model(model) # Keep SimpleChatApp generate_response
436
+ if client is None: # Keep SimpleChatApp generate_response
437
+ raise Exception(f"No client available for model: {model}") # Keep SimpleChatApp generate_response
438
+ except Exception as e: # Keep SimpleChatApp generate_response
439
+ self.notify(f"Failed to initialize model client: {str(e)}", severity="error") # Keep SimpleChatApp generate_response
440
+ return # Keep SimpleChatApp generate_response
441
+
442
+ # Start streaming response # Keep SimpleChatApp generate_response
443
+ assistant_message = Message(role="assistant", content="Thinking...") # Keep SimpleChatApp generate_response
444
+ self.messages.append(assistant_message) # Keep SimpleChatApp generate_response
445
+ messages_container = self.query_one("#messages-container") # Keep SimpleChatApp generate_response
446
+ message_display = MessageDisplay(assistant_message, highlight_code=CONFIG["highlight_code"]) # Keep SimpleChatApp generate_response
447
+ messages_container.mount(message_display) # Keep SimpleChatApp generate_response
448
+ messages_container.scroll_end(animate=False) # Keep SimpleChatApp generate_response
449
+
450
+ # Add small delay to show thinking state # Keep SimpleChatApp generate_response
451
+ await asyncio.sleep(0.5) # Keep SimpleChatApp generate_response
452
+
453
+ # Stream chunks to the UI with synchronization # Keep SimpleChatApp generate_response
454
+ update_lock = asyncio.Lock() # Keep SimpleChatApp generate_response
455
+
456
+ async def update_ui(content: str): # Keep SimpleChatApp generate_response
457
+ if not self.is_generating: # Keep SimpleChatApp generate_response
458
+ return # Keep SimpleChatApp generate_response
459
+
460
+ async with update_lock: # Keep SimpleChatApp generate_response
461
+ try: # Keep SimpleChatApp generate_response
462
+ # Clear thinking indicator on first content # Keep SimpleChatApp generate_response
463
+ if assistant_message.content == "Thinking...": # Keep SimpleChatApp generate_response
464
+ assistant_message.content = "" # Keep SimpleChatApp generate_response
465
+
466
+ # Update message with full content so far # Keep SimpleChatApp generate_response
467
+ assistant_message.content = content # Keep SimpleChatApp generate_response
468
+ # Update UI with full content # Keep SimpleChatApp generate_response
469
+ await message_display.update_content(content) # Keep SimpleChatApp generate_response
470
+ # Force a refresh and scroll # Keep SimpleChatApp generate_response
471
+ self.refresh(layout=True) # Keep SimpleChatApp generate_response
472
+ await asyncio.sleep(0.05) # Longer delay for UI stability # Keep SimpleChatApp generate_response
473
+ messages_container.scroll_end(animate=False) # Keep SimpleChatApp generate_response
474
+ # Force another refresh to ensure content is visible # Keep SimpleChatApp generate_response
475
+ self.refresh(layout=True) # Keep SimpleChatApp generate_response
476
+ except Exception as e: # Keep SimpleChatApp generate_response
477
+ logger.error(f"Error updating UI: {str(e)}") # Keep SimpleChatApp generate_response
478
+
479
+ # Generate the response with timeout and cleanup # Keep SimpleChatApp generate_response
480
+ generation_task = None # Keep SimpleChatApp generate_response
481
+ try: # Keep SimpleChatApp generate_response
482
+ # Create a task for the response generation # Keep SimpleChatApp generate_response
483
+ generation_task = asyncio.create_task( # Keep SimpleChatApp generate_response
484
+ generate_streaming_response( # Keep SimpleChatApp generate_response
485
+ api_messages, # Keep SimpleChatApp generate_response
486
+ model, # Keep SimpleChatApp generate_response
487
+ style, # Keep SimpleChatApp generate_response
488
+ client, # Keep SimpleChatApp generate_response
489
+ update_ui # Keep SimpleChatApp generate_response
490
+ ) # Keep SimpleChatApp generate_response
491
+ ) # Keep SimpleChatApp generate_response
492
+
493
+ # Wait for response with timeout # Keep SimpleChatApp generate_response
494
+ full_response = await asyncio.wait_for(generation_task, timeout=60) # Longer timeout # Keep SimpleChatApp generate_response
495
+
496
+ # Save to database only if we got a complete response # Keep SimpleChatApp generate_response
497
+ if self.is_generating and full_response: # Keep SimpleChatApp generate_response
498
+ self.db.add_message( # Keep SimpleChatApp generate_response
499
+ self.current_conversation.id, # Keep SimpleChatApp generate_response
500
+ "assistant", # Keep SimpleChatApp generate_response
501
+ full_response # Keep SimpleChatApp generate_response
502
+ ) # Keep SimpleChatApp generate_response
503
+ # Force a final refresh # Keep SimpleChatApp generate_response
504
+ self.refresh(layout=True) # Keep SimpleChatApp generate_response
505
+ await asyncio.sleep(0.1) # Wait for UI to update # Keep SimpleChatApp generate_response
506
+
507
+ except asyncio.TimeoutError: # Keep SimpleChatApp generate_response
508
+ logger.error("Response generation timed out") # Keep SimpleChatApp generate_response
509
+ error_msg = "Response generation timed out. The model may be busy or unresponsive. Please try again." # Keep SimpleChatApp generate_response
510
+ self.notify(error_msg, severity="error") # Keep SimpleChatApp generate_response
511
+
512
+ # Remove the incomplete message # Keep SimpleChatApp generate_response
513
+ if self.messages and self.messages[-1].role == "assistant": # Keep SimpleChatApp generate_response
514
+ self.messages.pop() # Keep SimpleChatApp generate_response
515
+
516
+ # Update UI to remove the incomplete message # Keep SimpleChatApp generate_response
517
+ await self.update_messages_ui() # Keep SimpleChatApp generate_response
518
+
519
+ finally: # Keep SimpleChatApp generate_response
520
+ # Ensure task is properly cancelled and cleaned up # Keep SimpleChatApp generate_response
521
+ if generation_task: # Keep SimpleChatApp generate_response
522
+ if not generation_task.done(): # Keep SimpleChatApp generate_response
523
+ generation_task.cancel() # Keep SimpleChatApp generate_response
524
+ try: # Keep SimpleChatApp generate_response
525
+ await generation_task # Keep SimpleChatApp generate_response
526
+ except (asyncio.CancelledError, Exception) as e: # Keep SimpleChatApp generate_response
527
+ logger.error(f"Error cleaning up generation task: {str(e)}") # Keep SimpleChatApp generate_response
528
+
529
+ # Force a final UI refresh # Keep SimpleChatApp generate_response
530
+ self.refresh(layout=True) # Keep SimpleChatApp generate_response
531
+
532
+ except Exception as e: # Keep SimpleChatApp generate_response
533
+ self.notify(f"Error generating response: {str(e)}", severity="error") # Keep SimpleChatApp generate_response
534
+ # Add error message # Keep SimpleChatApp generate_response
535
+ error_msg = f"Error generating response: {str(e)}" # Keep SimpleChatApp generate_response
536
+ self.messages.append(Message(role="assistant", content=error_msg)) # Keep SimpleChatApp generate_response
537
+ await self.update_messages_ui() # Keep SimpleChatApp generate_response
538
+ finally: # Keep SimpleChatApp generate_response
539
+ self.is_generating = False # Keep SimpleChatApp generate_response
540
+ loading = self.query_one("#loading-indicator") # Keep SimpleChatApp generate_response
541
+ loading.add_class("hidden") # Keep SimpleChatApp generate_response
542
+
543
+ def on_model_selector_model_selected(self, event: ModelSelector.ModelSelected) -> None: # Keep SimpleChatApp on_model_selector_model_selected
544
+ """Handle model selection""" # Keep SimpleChatApp on_model_selector_model_selected docstring
545
+ self.selected_model = event.model_id # Keep SimpleChatApp on_model_selector_model_selected
546
+
547
+ def on_style_selector_style_selected(self, event: StyleSelector.StyleSelected) -> None: # Keep SimpleChatApp on_style_selector_style_selected
548
+ """Handle style selection""" # Keep SimpleChatApp on_style_selector_style_selected docstring
549
+ self.selected_style = event.style_id # Keep SimpleChatApp on_style_selector_style_selected
550
+
551
+ async def on_button_pressed(self, event: Button.Pressed) -> None: # Modify SimpleChatApp on_button_pressed
552
+ """Handle button presses."""
553
+ button_id = event.button.id
554
+
555
+ # --- Handle Settings Panel Buttons ---
556
+ if button_id == "settings-cancel-button":
557
+ settings_panel = self.query_one("#settings-panel")
558
+ settings_panel.remove_class("visible")
559
+ self.query_one("#message-input").focus() # Focus input after closing
560
+ elif button_id == "settings-save-button":
561
+ # --- Save Logic ---
562
+ try:
563
+ # Get selected values (assuming selectors update self.selected_model/style directly via events)
564
+ model_to_save = self.selected_model
565
+ style_to_save = self.selected_style
566
+
567
+ # Save globally
568
+ save_settings_to_config(model_to_save, style_to_save)
569
+
570
+ # Update current conversation if one exists
571
+ if self.current_conversation:
572
+ self.db.update_conversation(
573
+ self.current_conversation.id,
574
+ model=model_to_save,
575
+ style=style_to_save
576
+ )
577
+ self.current_conversation.model = model_to_save
578
+ self.current_conversation.style = style_to_save
579
+ self.notify("Settings saved.", severity="information")
580
+ except Exception as e:
581
+ self.notify(f"Error saving settings: {str(e)}", severity="error")
582
+ finally:
583
+ # Hide panel regardless of save success/failure
584
+ settings_panel = self.query_one("#settings-panel")
585
+ settings_panel.remove_class("visible")
586
+ self.query_one("#message-input").focus() # Focus input after closing
587
+
588
+ # --- Keep other button logic if needed (currently none) ---
589
+ # elif button_id == "send-button": # Example if send button existed
590
+ # await self.action_send_message()
591
+
592
+ async def view_chat_history(self) -> None: # Keep SimpleChatApp view_chat_history
593
+ """Show chat history in a popup.""" # Keep SimpleChatApp view_chat_history docstring
594
+ # Get recent conversations # Keep SimpleChatApp view_chat_history
595
+ conversations = self.db.get_all_conversations(limit=CONFIG["max_history_items"]) # Keep SimpleChatApp view_chat_history
596
+ if not conversations: # Keep SimpleChatApp view_chat_history
597
+ self.notify("No chat history found", severity="warning") # Keep SimpleChatApp view_chat_history
598
+ return # Keep SimpleChatApp view_chat_history
599
+
600
+ async def handle_selection(selected_id: int) -> None: # Keep SimpleChatApp view_chat_history
601
+ if not selected_id: # Keep SimpleChatApp view_chat_history
602
+ return # Keep SimpleChatApp view_chat_history
603
+
604
+ # Get full conversation # Keep SimpleChatApp view_chat_history
605
+ conversation_data = self.db.get_conversation(selected_id) # Keep SimpleChatApp view_chat_history
606
+ if not conversation_data: # Keep SimpleChatApp view_chat_history
607
+ self.notify("Could not load conversation", severity="error") # Keep SimpleChatApp view_chat_history
608
+ return # Keep SimpleChatApp view_chat_history
609
+
610
+ # Update current conversation # Keep SimpleChatApp view_chat_history
611
+ self.current_conversation = Conversation.from_dict(conversation_data) # Keep SimpleChatApp view_chat_history
612
+
613
+ # Update title # Keep SimpleChatApp view_chat_history
614
+ title = self.query_one("#conversation-title", Static) # Keep SimpleChatApp view_chat_history
615
+ title.update(self.current_conversation.title) # Keep SimpleChatApp view_chat_history
616
+
617
+ # Load messages # Keep SimpleChatApp view_chat_history
618
+ self.messages = [Message(**msg) for msg in self.current_conversation.messages] # Keep SimpleChatApp view_chat_history
619
+ await self.update_messages_ui() # Keep SimpleChatApp view_chat_history
620
+
621
+ # Update model and style selectors # Keep SimpleChatApp view_chat_history
622
+ self.selected_model = self.current_conversation.model # Keep SimpleChatApp view_chat_history
623
+ self.selected_style = self.current_conversation.style # Keep SimpleChatApp view_chat_history
624
+
625
+ self.push_screen(HistoryScreen(conversations, handle_selection)) # Keep SimpleChatApp view_chat_history
626
+
627
+ async def action_view_history(self) -> None: # Keep SimpleChatApp action_view_history
628
+ """Action to view chat history via key binding.""" # Keep SimpleChatApp action_view_history docstring
629
+ # Only trigger if message input is not focused # Keep SimpleChatApp action_view_history
630
+ input_widget = self.query_one("#message-input", Input) # Keep SimpleChatApp action_view_history
631
+ if not input_widget.has_focus: # Keep SimpleChatApp action_view_history
632
+ await self.view_chat_history() # Keep SimpleChatApp action_view_history
633
+
634
+ def action_settings(self) -> None: # Modify SimpleChatApp action_settings
635
+ """Action to open/close settings panel via key binding."""
636
+ # Only trigger if message input is not focused
637
+ input_widget = self.query_one("#message-input", Input)
638
+ if not input_widget.has_focus:
639
+ settings_panel = self.query_one("#settings-panel")
640
+ settings_panel.toggle_class("visible") # Toggle visibility class
641
+ if settings_panel.has_class("visible"):
642
+ # Try focusing the first element in the panel (e.g., ModelSelector)
643
+ try:
644
+ model_selector = settings_panel.query_one(ModelSelector)
645
+ model_selector.focus()
646
+ except Exception:
647
+ pass # Ignore if focus fails
648
+ else:
649
+ input_widget.focus() # Focus input when closing
650
+
651
+ def main(initial_text: Optional[str] = typer.Argument(None, help="Initial text to start the chat with")): # Keep main function
652
+ """Entry point for the chat-cli application""" # Keep main function docstring
653
+ # When no argument is provided, typer passes the ArgumentInfo object # Keep main function
654
+ # When an argument is provided, typer passes the actual value # Keep main function
655
+ if isinstance(initial_text, typer.models.ArgumentInfo): # Keep main function
656
+ initial_value = None # No argument provided # Keep main function
657
+ else: # Keep main function
658
+ initial_value = str(initial_text) if initial_text is not None else None # Keep main function
659
+
660
+ app = SimpleChatApp(initial_text=initial_value) # Keep main function
661
+ app.run() # Keep main function
662
+
663
+ if __name__ == "__main__": # Keep main function entry point
664
+ typer.run(main) # Keep main function entry point