chat-console 0.2.9__py3-none-any.whl → 0.2.99__py3-none-any.whl
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.
- app/__init__.py +1 -1
- app/api/anthropic.py +163 -26
- app/api/base.py +45 -2
- app/api/ollama.py +202 -43
- app/api/openai.py +53 -4
- app/config.py +53 -7
- app/main.py +512 -103
- app/ui/chat_interface.py +40 -20
- app/ui/model_browser.py +405 -45
- app/ui/model_selector.py +77 -19
- app/utils.py +359 -85
- {chat_console-0.2.9.dist-info → chat_console-0.2.99.dist-info}/METADATA +1 -1
- chat_console-0.2.99.dist-info/RECORD +24 -0
- chat_console-0.2.9.dist-info/RECORD +0 -24
- {chat_console-0.2.9.dist-info → chat_console-0.2.99.dist-info}/WHEEL +0 -0
- {chat_console-0.2.9.dist-info → chat_console-0.2.99.dist-info}/entry_points.txt +0 -0
- {chat_console-0.2.9.dist-info → chat_console-0.2.99.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.2.9.dist-info → chat_console-0.2.99.dist-info}/top_level.txt +0 -0
app/ui/chat_interface.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
from typing import List, Dict, Any, Optional, Callable, Awaitable
|
2
|
-
import time
|
3
2
|
import asyncio
|
4
3
|
from datetime import datetime
|
5
4
|
import re
|
@@ -8,9 +7,9 @@ import logging
|
|
8
7
|
from textual.app import ComposeResult
|
9
8
|
from textual.containers import Container, ScrollableContainer, Vertical
|
10
9
|
from textual.reactive import reactive
|
11
|
-
from textual.widgets import Button, Input, Label, Static
|
10
|
+
from textual.widgets import Button, Input, Label, Static # Removed RichLog from here
|
12
11
|
from textual.widget import Widget
|
13
|
-
|
12
|
+
# Removed RichLog import as MessageDisplay will inherit from Static
|
14
13
|
from textual.message import Message
|
15
14
|
from textual.binding import Binding
|
16
15
|
|
@@ -53,16 +52,22 @@ class SendButton(Button):
|
|
53
52
|
self.styles.text_opacity = 100
|
54
53
|
self.styles.text_style = "bold"
|
55
54
|
|
56
|
-
class MessageDisplay(
|
57
|
-
"""Widget to display a single message"""
|
55
|
+
class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
56
|
+
"""Widget to display a single message using Static"""
|
58
57
|
|
59
58
|
DEFAULT_CSS = """
|
60
59
|
MessageDisplay {
|
61
60
|
width: 100%;
|
62
|
-
height: auto;
|
61
|
+
height: auto; /* Let height adjust automatically */
|
62
|
+
min-height: 1; /* Ensure minimum height */
|
63
|
+
min-width: 60; /* Set a reasonable minimum width to avoid constant width adjustment */
|
63
64
|
margin: 1 0;
|
64
|
-
overflow: auto;
|
65
65
|
padding: 1;
|
66
|
+
text-wrap: wrap; /* Explicitly enable text wrapping via CSS */
|
67
|
+
content-align: left top; /* Anchor content to top-left */
|
68
|
+
overflow-y: auto; /* Changed from 'visible' to valid 'auto' value */
|
69
|
+
box-sizing: border-box; /* Include padding in size calculations */
|
70
|
+
transition: none; /* Fixed property name from 'transitions' to 'transition' */
|
66
71
|
}
|
67
72
|
|
68
73
|
MessageDisplay.user-message {
|
@@ -90,14 +95,16 @@ class MessageDisplay(RichLog):
|
|
90
95
|
highlight_code: bool = True,
|
91
96
|
name: Optional[str] = None
|
92
97
|
):
|
98
|
+
# Initialize Static with empty content and necessary parameters
|
99
|
+
# Static supports markup but handles wrap differently via styles
|
93
100
|
super().__init__(
|
94
|
-
|
101
|
+
"", # Initialize with empty content initially
|
95
102
|
markup=True,
|
96
|
-
wrap=True,
|
97
103
|
name=name
|
98
104
|
)
|
105
|
+
# Enable text wrapping via CSS (already set in DEFAULT_CSS)
|
99
106
|
self.message = message
|
100
|
-
self.highlight_code = highlight_code
|
107
|
+
self.highlight_code = highlight_code # Keep this for potential future use or logic
|
101
108
|
|
102
109
|
def on_mount(self) -> None:
|
103
110
|
"""Handle mount event"""
|
@@ -109,18 +116,28 @@ class MessageDisplay(RichLog):
|
|
109
116
|
elif self.message.role == "system":
|
110
117
|
self.add_class("system-message")
|
111
118
|
|
112
|
-
# Initial content
|
113
|
-
self.
|
119
|
+
# Initial content using Static's update method
|
120
|
+
self.update(self._format_content(self.message.content))
|
114
121
|
|
115
122
|
async def update_content(self, content: str) -> None:
|
116
|
-
"""Update the message content"""
|
123
|
+
"""Update the message content using Static.update() with optimizations for streaming"""
|
124
|
+
# Quick unchanged content check to avoid unnecessary updates
|
125
|
+
if self.message.content == content:
|
126
|
+
return
|
127
|
+
|
128
|
+
# Update the stored message object content first
|
117
129
|
self.message.content = content
|
118
|
-
|
119
|
-
|
120
|
-
#
|
121
|
-
self.
|
122
|
-
|
123
|
-
|
130
|
+
|
131
|
+
# Format with fixed-width placeholder to minimize layout shifts
|
132
|
+
# This avoids text reflowing as new tokens arrive
|
133
|
+
formatted_content = self._format_content(content)
|
134
|
+
|
135
|
+
# Use minimal update that doesn't trigger a refresh
|
136
|
+
# This allows parent to control refresh timing and avoid flickering
|
137
|
+
self.update(formatted_content, refresh=False)
|
138
|
+
|
139
|
+
# No refresh or layout recalculation is performed here
|
140
|
+
# The parent container will handle refresh timing for better stability
|
124
141
|
|
125
142
|
def _format_content(self, content: str) -> str:
|
126
143
|
"""Format message content with timestamp"""
|
@@ -164,6 +181,9 @@ class ChatInterface(Container):
|
|
164
181
|
border-bottom: solid $primary-darken-2;
|
165
182
|
overflow: auto;
|
166
183
|
padding: 0 1;
|
184
|
+
content-align: left top; /* Keep content anchored at top */
|
185
|
+
box-sizing: border-box;
|
186
|
+
scrollbar-gutter: stable; /* Better than scrollbar-size which isn't valid */
|
167
187
|
}
|
168
188
|
|
169
189
|
#input-area {
|
@@ -243,7 +263,7 @@ class ChatInterface(Container):
|
|
243
263
|
yield MessageDisplay(message, highlight_code=CONFIG["highlight_code"])
|
244
264
|
with Container(id="input-area"):
|
245
265
|
yield Container(
|
246
|
-
Label("▪▪▪ Generating response...", id="loading-text", markup=
|
266
|
+
Label("▪▪▪ Generating response...", id="loading-text", markup=False),
|
247
267
|
id="loading-indicator"
|
248
268
|
)
|
249
269
|
with Container(id="controls"):
|
app/ui/model_browser.py
CHANGED
@@ -684,23 +684,124 @@ class ModelBrowser(Container):
|
|
684
684
|
|
685
685
|
async def _run_selected_model(self) -> None:
|
686
686
|
"""Set the selected model as the active model in the main app"""
|
687
|
-
#
|
688
|
-
|
687
|
+
# Import debug_log function from main
|
688
|
+
from app.main import debug_log
|
689
|
+
|
690
|
+
debug_log(f"Entering _run_selected_model for tab {self.current_tab}")
|
689
691
|
|
692
|
+
# Try several ways to get a valid model ID
|
693
|
+
model_id = ""
|
694
|
+
|
695
|
+
# First try the selected_model_id property
|
696
|
+
if self.selected_model_id and self.selected_model_id.strip():
|
697
|
+
debug_log(f"Using model_id from selected_model_id: {self.selected_model_id}")
|
698
|
+
logger.info(f"Using model_id from selected_model_id: {self.selected_model_id}")
|
699
|
+
model_id = self.selected_model_id
|
700
|
+
|
701
|
+
# If that didn't work, try getting it through the getter method
|
690
702
|
if not model_id:
|
691
|
-
|
692
|
-
|
703
|
+
debug_log("Trying to get model_id from _get_selected_model_id method")
|
704
|
+
logger.info("Trying to get model_id from _get_selected_model_id method")
|
705
|
+
model_id = self._get_selected_model_id()
|
706
|
+
debug_log(f"_get_selected_model_id returned: '{model_id}'")
|
707
|
+
|
708
|
+
# As a last resort, if we're in local tab, try to just get the first model
|
709
|
+
if not model_id and self.current_tab == "local":
|
710
|
+
debug_log("Trying fallback to first local model")
|
693
711
|
|
712
|
+
# Extra defensive check for local_models
|
713
|
+
if not self.local_models:
|
714
|
+
debug_log("local_models list is empty or None")
|
715
|
+
else:
|
716
|
+
debug_log(f"local_models has {len(self.local_models)} items")
|
717
|
+
try:
|
718
|
+
# Check if the list is valid and not empty
|
719
|
+
if len(self.local_models) > 0:
|
720
|
+
first_model = self.local_models[0]
|
721
|
+
debug_log(f"First local model: {repr(first_model)}")
|
722
|
+
|
723
|
+
# Very defensive checks
|
724
|
+
if first_model is None:
|
725
|
+
debug_log("First model is None")
|
726
|
+
elif not isinstance(first_model, dict):
|
727
|
+
debug_log(f"First model is not a dict: {type(first_model)}")
|
728
|
+
elif "id" not in first_model:
|
729
|
+
debug_log(f"First model has no 'id' key: {first_model}")
|
730
|
+
else:
|
731
|
+
model_id = first_model.get("id", "")
|
732
|
+
debug_log(f"Falling back to first model in list: {model_id}")
|
733
|
+
logger.info(f"Falling back to first model in list: {model_id}")
|
734
|
+
else:
|
735
|
+
debug_log("local_models list is empty")
|
736
|
+
except Exception as e:
|
737
|
+
debug_log(f"Error getting first local model: {str(e)}")
|
738
|
+
logger.error(f"Error getting first local model: {str(e)}")
|
739
|
+
|
740
|
+
# Or if we're in available tab, try to get name of first available model
|
741
|
+
if not model_id and self.current_tab == "available":
|
742
|
+
debug_log("Trying fallback to first available model")
|
743
|
+
|
744
|
+
# Extra defensive check for available_models
|
745
|
+
if not self.available_models:
|
746
|
+
debug_log("available_models list is empty or None")
|
747
|
+
else:
|
748
|
+
debug_log(f"available_models has {len(self.available_models)} items")
|
749
|
+
try:
|
750
|
+
# Check if the list is valid and not empty
|
751
|
+
if len(self.available_models) > 0:
|
752
|
+
first_model = self.available_models[0]
|
753
|
+
debug_log(f"First available model: {repr(first_model)}")
|
754
|
+
|
755
|
+
# Very defensive checks
|
756
|
+
if first_model is None:
|
757
|
+
debug_log("First available model is None")
|
758
|
+
elif not isinstance(first_model, dict):
|
759
|
+
debug_log(f"First available model is not a dict: {type(first_model)}")
|
760
|
+
elif "name" not in first_model:
|
761
|
+
debug_log(f"First available model has no 'name' key: {first_model}")
|
762
|
+
else:
|
763
|
+
model_id = first_model.get("name", "")
|
764
|
+
debug_log(f"Falling back to first available model: {model_id}")
|
765
|
+
logger.info(f"Falling back to first available model: {model_id}")
|
766
|
+
else:
|
767
|
+
debug_log("available_models list is empty")
|
768
|
+
except Exception as e:
|
769
|
+
debug_log(f"Error getting first available model: {str(e)}")
|
770
|
+
logger.error(f"Error getting first available model: {str(e)}")
|
771
|
+
|
772
|
+
# Last resort - hardcoded fallback to a common model
|
773
|
+
if not model_id:
|
774
|
+
debug_log("All attempts to get model_id failed, checking if we should use default")
|
775
|
+
# Only use default if we're in the available models tab, otherwise notify user
|
776
|
+
if self.current_tab == "available":
|
777
|
+
debug_log("Using 'llama3' as last-resort default")
|
778
|
+
model_id = "llama3"
|
779
|
+
self.notify("No model selected, using llama3 as default", severity="warning")
|
780
|
+
else:
|
781
|
+
debug_log("No model_id found and no default for local tab")
|
782
|
+
self.notify("No model selected", severity="warning")
|
783
|
+
return
|
784
|
+
|
785
|
+
debug_log(f"Final model_id: {model_id}")
|
786
|
+
logger.info(f"Setting model to: {model_id}")
|
787
|
+
|
694
788
|
try:
|
695
789
|
# Set the model in the app
|
696
790
|
if hasattr(self.app, "selected_model"):
|
791
|
+
debug_log("Setting model in the app")
|
697
792
|
self.app.selected_model = model_id
|
793
|
+
debug_log("Updating app info")
|
698
794
|
self.app.update_app_info() # Update app info to show new model
|
795
|
+
debug_log("Model set successfully")
|
699
796
|
self.notify(f"Model set to: {model_id}", severity="success")
|
797
|
+
debug_log("Closing model browser screen")
|
700
798
|
self.app.pop_screen() # Close the model browser screen
|
701
799
|
else:
|
800
|
+
debug_log("app does not have selected_model attribute")
|
702
801
|
self.notify("Cannot set model: app interface not available", severity="error")
|
703
802
|
except Exception as e:
|
803
|
+
debug_log(f"Error setting model: {str(e)}")
|
804
|
+
logger.error(f"Error setting model: {str(e)}")
|
704
805
|
self.notify(f"Error setting model: {str(e)}", severity="error")
|
705
806
|
|
706
807
|
async def _pull_selected_model(self) -> None:
|
@@ -1063,72 +1164,331 @@ class ModelBrowser(Container):
|
|
1063
1164
|
|
1064
1165
|
def _get_selected_model_id(self) -> str:
|
1065
1166
|
"""Get the ID of the currently selected model"""
|
1167
|
+
# Import debug_log function from main
|
1168
|
+
from app.main import debug_log
|
1169
|
+
|
1170
|
+
debug_log(f"Entering _get_selected_model_id for tab {self.current_tab}")
|
1171
|
+
|
1066
1172
|
try:
|
1067
1173
|
if self.current_tab == "local":
|
1174
|
+
debug_log("Processing local models tab")
|
1068
1175
|
table = self.query_one("#local-models-table", DataTable)
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1176
|
+
|
1177
|
+
# Log table state
|
1178
|
+
debug_log(f"Table cursor_row: {table.cursor_row}, row_count: {table.row_count}")
|
1179
|
+
|
1180
|
+
if table.cursor_row is not None and table.row_count > 0:
|
1181
|
+
# Safety checks on cursor row
|
1182
|
+
if table.cursor_row < 0 or table.cursor_row >= table.row_count:
|
1183
|
+
debug_log(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
|
1184
|
+
logger.error(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
|
1185
|
+
return ""
|
1186
|
+
|
1072
1187
|
try:
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1188
|
+
debug_log(f"Getting row at cursor position {table.cursor_row}")
|
1189
|
+
row = table.get_row_at(table.cursor_row)
|
1190
|
+
debug_log(f"Row data: {repr(row)}")
|
1191
|
+
|
1192
|
+
# Use a more permissive approach that works with any indexable type
|
1193
|
+
if row and hasattr(row, '__getitem__') and len(row) > 0:
|
1194
|
+
try:
|
1195
|
+
row_value = row[0]
|
1196
|
+
row_name = str(row_value) if row_value is not None else ""
|
1197
|
+
debug_log(f"Row name extracted: '{row_name}'")
|
1198
|
+
|
1199
|
+
# Check if local_models is valid
|
1200
|
+
if not self.local_models:
|
1201
|
+
debug_log("local_models list is empty")
|
1202
|
+
return ""
|
1203
|
+
|
1204
|
+
debug_log(f"Searching through {len(self.local_models)} local models")
|
1205
|
+
for i, model in enumerate(self.local_models):
|
1206
|
+
try:
|
1207
|
+
# Defensively check if model is a dict and has required keys
|
1208
|
+
if not isinstance(model, dict):
|
1209
|
+
debug_log(f"Model at index {i} is not a dict: {repr(model)}")
|
1210
|
+
continue
|
1211
|
+
|
1212
|
+
model_name = model.get("name")
|
1213
|
+
model_id = model.get("id")
|
1214
|
+
|
1215
|
+
if model_name is None:
|
1216
|
+
debug_log(f"Model at index {i} has no name: {repr(model)}")
|
1217
|
+
continue
|
1218
|
+
|
1219
|
+
debug_log(f"Comparing '{model_name}' with '{row_name}'")
|
1220
|
+
if model_name == row_name:
|
1221
|
+
debug_log(f"Found matching model: {model_id}")
|
1222
|
+
return model_id
|
1223
|
+
except Exception as model_err:
|
1224
|
+
debug_log(f"Error processing model at index {i}: {str(model_err)}")
|
1225
|
+
logger.error(f"Error processing model: {model_err}")
|
1226
|
+
continue
|
1227
|
+
|
1228
|
+
# If we got here, no match was found
|
1229
|
+
debug_log(f"No matching model found for '{row_name}'")
|
1230
|
+
except Exception as extract_err:
|
1231
|
+
debug_log(f"Error extracting row name: {str(extract_err)}")
|
1232
|
+
else:
|
1233
|
+
debug_log(f"Invalid row data: Row doesn't support indexing, is empty, or is None: {repr(row)}")
|
1234
|
+
return ""
|
1235
|
+
|
1236
|
+
except (IndexError, TypeError, AttributeError) as e:
|
1237
|
+
debug_log(f"Error processing row data in _get_selected_model_id: {str(e)}")
|
1238
|
+
logger.error(f"Error processing row data in _get_selected_model_id: {str(e)}")
|
1080
1239
|
else:
|
1240
|
+
debug_log("Processing available models tab")
|
1081
1241
|
table = self.query_one("#available-models-table", DataTable)
|
1082
|
-
|
1242
|
+
|
1243
|
+
# Log table state
|
1244
|
+
debug_log(f"Available table cursor_row: {table.cursor_row}, row_count: {table.row_count}")
|
1245
|
+
|
1246
|
+
if table.cursor_row is not None and table.row_count > 0:
|
1247
|
+
# Safety checks on cursor row
|
1248
|
+
if table.cursor_row < 0 or table.cursor_row >= table.row_count:
|
1249
|
+
debug_log(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
|
1250
|
+
logger.error(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
|
1251
|
+
# Try to select first row instead of returning empty
|
1252
|
+
table.cursor_row = 0
|
1253
|
+
debug_log("Reset cursor_row to 0")
|
1254
|
+
|
1083
1255
|
try:
|
1256
|
+
debug_log(f"Getting row at cursor position {table.cursor_row}")
|
1084
1257
|
row = table.get_row_at(table.cursor_row)
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1258
|
+
debug_log(f"Row data: {repr(row)}")
|
1259
|
+
|
1260
|
+
# Use a more permissive approach that works with any indexable type
|
1261
|
+
if row and hasattr(row, '__getitem__') and len(row) > 0:
|
1262
|
+
try:
|
1263
|
+
row_value = row[0]
|
1264
|
+
model_name = str(row_value) if row_value is not None else ""
|
1265
|
+
debug_log(f"Available model selected: '{model_name}'")
|
1266
|
+
logger.info(f"Selected available model: {model_name}")
|
1267
|
+
return model_name
|
1268
|
+
except Exception as extract_err:
|
1269
|
+
debug_log(f"Error extracting row name: {str(extract_err)}")
|
1270
|
+
return ""
|
1271
|
+
else:
|
1272
|
+
debug_log(f"Invalid row data: Row doesn't support indexing, is empty, or is None: {repr(row)}")
|
1273
|
+
return ""
|
1274
|
+
|
1275
|
+
except (IndexError, TypeError, AttributeError) as e:
|
1276
|
+
debug_log(f"Error getting row at cursor: {str(e)}")
|
1089
1277
|
logger.error(f"Error getting row at cursor: {str(e)}")
|
1090
1278
|
|
1091
1279
|
# If we couldn't get a valid row, check if there are any rows and select the first one
|
1092
|
-
|
1280
|
+
debug_log("Trying fallback to first row")
|
1281
|
+
if hasattr(table, 'row_count') and table.row_count > 0:
|
1093
1282
|
try:
|
1094
1283
|
# Select the first row and get its ID
|
1284
|
+
debug_log(f"Setting cursor_row to 0 and getting first row")
|
1095
1285
|
table.cursor_row = 0
|
1096
1286
|
row = table.get_row_at(0)
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1287
|
+
debug_log(f"First row data: {repr(row)}")
|
1288
|
+
|
1289
|
+
# Use a more permissive approach that works with any indexable type
|
1290
|
+
if row and hasattr(row, '__getitem__') and len(row) > 0:
|
1291
|
+
try:
|
1292
|
+
row_value = row[0]
|
1293
|
+
model_name = str(row_value) if row_value is not None else ""
|
1294
|
+
debug_log(f"First available model selected: '{model_name}'")
|
1295
|
+
logger.info(f"Selected first available model: {model_name}")
|
1296
|
+
return model_name
|
1297
|
+
except Exception as extract_err:
|
1298
|
+
debug_log(f"Error extracting first row name: {str(extract_err)}")
|
1299
|
+
return ""
|
1300
|
+
else:
|
1301
|
+
debug_log(f"Invalid first row data: Row doesn't support indexing, is empty, or is None: {repr(row)}")
|
1302
|
+
return ""
|
1303
|
+
|
1304
|
+
except (IndexError, TypeError, AttributeError) as e:
|
1305
|
+
debug_log(f"Error selecting first row: {str(e)}")
|
1100
1306
|
logger.error(f"Error selecting first row: {str(e)}")
|
1307
|
+
else:
|
1308
|
+
debug_log("Table has no rows or row_count attribute missing")
|
1101
1309
|
except Exception as e:
|
1310
|
+
debug_log(f"Unexpected error in _get_selected_model_id: {str(e)}")
|
1102
1311
|
logger.error(f"Error in _get_selected_model_id: {str(e)}")
|
1103
1312
|
|
1313
|
+
debug_log("No model ID found, returning empty string")
|
1104
1314
|
return ""
|
1105
1315
|
|
1106
1316
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
1107
1317
|
"""Handle row selection in data tables"""
|
1318
|
+
# Import debug_log function from main
|
1319
|
+
from app.main import debug_log
|
1320
|
+
|
1321
|
+
debug_log(f"Entering on_data_table_row_selected")
|
1322
|
+
|
1108
1323
|
# Set selected model ID based on the selected row
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1324
|
+
try:
|
1325
|
+
# Check if event has valid structure
|
1326
|
+
if not hasattr(event, 'data_table') or not event.data_table:
|
1327
|
+
debug_log("Event has no data_table attribute or it is None")
|
1328
|
+
logger.error("Invalid event in on_data_table_row_selected")
|
1329
|
+
return
|
1330
|
+
|
1331
|
+
if not hasattr(event, 'cursor_row'):
|
1332
|
+
debug_log("Event has no cursor_row attribute")
|
1333
|
+
logger.error("Event has no cursor_row attribute")
|
1334
|
+
return
|
1335
|
+
|
1336
|
+
debug_log(f"Event cursor_row: {event.cursor_row}")
|
1337
|
+
|
1338
|
+
if not hasattr(event.data_table, 'id') or not event.data_table.id:
|
1339
|
+
debug_log("Data table has no ID attribute or it is empty")
|
1340
|
+
logger.error("Data table has no ID in on_data_table_row_selected")
|
1341
|
+
return
|
1342
|
+
|
1343
|
+
debug_log(f"Data table ID: {event.data_table.id}")
|
1344
|
+
|
1345
|
+
if event.data_table.id == "local-models-table":
|
1346
|
+
debug_log("Processing local models table")
|
1347
|
+
try:
|
1348
|
+
# Carefully get the row data
|
1349
|
+
if event.cursor_row is None or event.cursor_row < 0:
|
1350
|
+
debug_log(f"Invalid cursor row: {event.cursor_row}")
|
1351
|
+
logger.error(f"Invalid cursor row: {event.cursor_row}")
|
1352
|
+
return
|
1353
|
+
|
1354
|
+
# Check row count to avoid index errors
|
1355
|
+
if not hasattr(event.data_table, 'row_count'):
|
1356
|
+
debug_log("Data table has no row_count attribute")
|
1357
|
+
return
|
1358
|
+
|
1359
|
+
debug_log(f"Table row_count: {event.data_table.row_count}")
|
1360
|
+
|
1361
|
+
if event.data_table.row_count <= event.cursor_row:
|
1362
|
+
debug_log(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
|
1363
|
+
logger.error(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
|
1364
|
+
return
|
1365
|
+
|
1366
|
+
debug_log(f"Getting row at cursor position {event.cursor_row}")
|
1367
|
+
row = event.data_table.get_row_at(event.cursor_row)
|
1368
|
+
debug_log(f"Row data: {repr(row)}")
|
1369
|
+
|
1370
|
+
# Find the model ID from the display name with more permissive checks
|
1371
|
+
try:
|
1372
|
+
# Check if row is valid and can be indexed
|
1373
|
+
if row is None:
|
1374
|
+
debug_log("Row is None")
|
1375
|
+
return
|
1376
|
+
|
1377
|
+
if not hasattr(row, '__getitem__') or not hasattr(row, '__len__'):
|
1378
|
+
debug_log(f"Row doesn't support indexing or length: {type(row)}")
|
1379
|
+
return
|
1380
|
+
|
1381
|
+
if len(row) <= 0:
|
1382
|
+
debug_log("Row has no items")
|
1383
|
+
return
|
1384
|
+
|
1385
|
+
# Extract the first item (model name)
|
1386
|
+
try:
|
1387
|
+
row_value = row[0]
|
1388
|
+
row_name = str(row_value) if row_value is not None else ""
|
1389
|
+
debug_log(f"DataTable row selected with name: '{row_name}'")
|
1390
|
+
logger.info(f"DataTable row selected with name: {row_name}")
|
1391
|
+
except Exception as idx_err:
|
1392
|
+
debug_log(f"Error accessing row[0]: {str(idx_err)}")
|
1393
|
+
return
|
1394
|
+
|
1395
|
+
# Check if local_models is a valid list
|
1396
|
+
if not self.local_models:
|
1397
|
+
debug_log("local_models list is empty or None")
|
1398
|
+
return
|
1399
|
+
|
1400
|
+
debug_log(f"Searching through {len(self.local_models)} local models")
|
1401
|
+
for i, model in enumerate(self.local_models):
|
1402
|
+
try:
|
1403
|
+
# Use .get() for safer dictionary access
|
1404
|
+
model_name = model.get("name", None)
|
1405
|
+
if model_name is None:
|
1406
|
+
debug_log(f"Model at index {i} has no name")
|
1407
|
+
continue
|
1408
|
+
|
1409
|
+
debug_log(f"Comparing '{model_name}' with '{row_name}'")
|
1410
|
+
if model_name == row_name:
|
1411
|
+
model_id = model.get("id", "")
|
1412
|
+
debug_log(f"Found match. Setting selected_model_id to: '{model_id}'")
|
1413
|
+
logger.info(f"Setting selected_model_id to: {model_id}")
|
1414
|
+
self.selected_model_id = model_id
|
1415
|
+
break
|
1416
|
+
except Exception as model_err:
|
1417
|
+
debug_log(f"Error processing model at index {i}: {str(model_err)}")
|
1418
|
+
logger.error(f"Error matching local model: {str(model_err)}")
|
1419
|
+
continue
|
1420
|
+
except (IndexError, TypeError) as idx_err:
|
1421
|
+
debug_log(f"Error accessing row data: {str(idx_err)}")
|
1422
|
+
logger.error(f"Error accessing row data: {str(idx_err)}")
|
1423
|
+
except (IndexError, TypeError, AttributeError) as e:
|
1424
|
+
debug_log(f"Error processing local table row data: {str(e)}")
|
1425
|
+
logger.error(f"Error processing local table row data: {str(e)}")
|
1426
|
+
elif event.data_table.id == "available-models-table":
|
1427
|
+
debug_log("Processing available models table")
|
1428
|
+
try:
|
1429
|
+
# Similar safety checks for available models
|
1430
|
+
if event.cursor_row is None or event.cursor_row < 0:
|
1431
|
+
debug_log(f"Invalid cursor row: {event.cursor_row}")
|
1432
|
+
logger.error(f"Invalid cursor row: {event.cursor_row}")
|
1433
|
+
return
|
1434
|
+
|
1435
|
+
# Check row count to avoid index errors
|
1436
|
+
if not hasattr(event.data_table, 'row_count'):
|
1437
|
+
debug_log("Data table has no row_count attribute")
|
1438
|
+
return
|
1439
|
+
|
1440
|
+
debug_log(f"Available table row_count: {event.data_table.row_count}")
|
1441
|
+
|
1442
|
+
if event.data_table.row_count <= event.cursor_row:
|
1443
|
+
debug_log(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
|
1444
|
+
logger.error(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
|
1445
|
+
return
|
1446
|
+
|
1447
|
+
debug_log(f"Getting row at cursor position {event.cursor_row}")
|
1448
|
+
row = event.data_table.get_row_at(event.cursor_row)
|
1449
|
+
debug_log(f"Row data: {repr(row)}")
|
1450
|
+
|
1451
|
+
# Model name is used as ID, with more permissive checks
|
1452
|
+
try:
|
1453
|
+
# Check if row is valid and can be indexed
|
1454
|
+
if row is None:
|
1455
|
+
debug_log("Row is None")
|
1456
|
+
return
|
1457
|
+
|
1458
|
+
if not hasattr(row, '__getitem__') or not hasattr(row, '__len__'):
|
1459
|
+
debug_log(f"Row doesn't support indexing or length: {type(row)}")
|
1460
|
+
return
|
1461
|
+
|
1462
|
+
if len(row) <= 0:
|
1463
|
+
debug_log("Row has no items")
|
1464
|
+
return
|
1465
|
+
|
1466
|
+
# Extract the first item (model name)
|
1467
|
+
try:
|
1468
|
+
row_value = row[0]
|
1469
|
+
model_name = str(row_value) if row_value is not None else ""
|
1470
|
+
debug_log(f"Available model selected: '{model_name}'")
|
1471
|
+
logger.info(f"Selected available model: {model_name}")
|
1472
|
+
self.selected_model_id = model_name
|
1473
|
+
except Exception as access_err:
|
1474
|
+
debug_log(f"Error accessing row[0]: {str(access_err)}")
|
1475
|
+
logger.error(f"Error accessing row[0]: {str(access_err)}")
|
1476
|
+
self.selected_model_id = ""
|
1477
|
+
except (IndexError, TypeError) as idx_err:
|
1478
|
+
debug_log(f"Error accessing available table row data: {str(idx_err)}")
|
1479
|
+
logger.error(f"Error accessing available table row data: {str(idx_err)}")
|
1480
|
+
self.selected_model_id = ""
|
1481
|
+
except (IndexError, TypeError, AttributeError) as e:
|
1482
|
+
debug_log(f"Error getting model ID from available table row: {str(e)}")
|
1483
|
+
logger.error(f"Error getting model ID from available table row: {str(e)}")
|
1128
1484
|
self.selected_model_id = ""
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1485
|
+
else:
|
1486
|
+
debug_log(f"Unknown table ID: {event.data_table.id}")
|
1487
|
+
except Exception as e:
|
1488
|
+
# Catch-all for any other errors
|
1489
|
+
debug_log(f"Unexpected error in on_data_table_row_selected: {str(e)}")
|
1490
|
+
logger.error(f"Unexpected error in on_data_table_row_selected: {str(e)}")
|
1491
|
+
self.selected_model_id = ""
|
1132
1492
|
|
1133
1493
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
1134
1494
|
"""Handle input submission (Enter key in search input)"""
|