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.
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/PKG-INFO +1 -1
- chat_console-0.1.95/app/main.py +664 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/model_selector.py +8 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/PKG-INFO +1 -1
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/setup.py +1 -1
- chat_console-0.1.9.dev1/app/main.py +0 -661
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/LICENSE +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/README.md +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/__init__.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/__init__.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/anthropic.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/base.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/ollama.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/api/openai.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/config.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/database.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/models.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/__init__.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/chat_interface.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/chat_list.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/search.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/ui/styles.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/app/utils.py +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/SOURCES.txt +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/dependency_links.txt +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/entry_points.txt +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/requires.txt +0 -0
- {chat_console-0.1.9.dev1 → chat_console-0.1.95}/chat_console.egg-info/top_level.txt +0 -0
- {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.
|
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
|