chat-console 0.4.3__py3-none-any.whl → 0.4.7__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/ui/borders.py ADDED
@@ -0,0 +1,154 @@
1
+ """
2
+ ASCII Border System for Clean, Functional Design
3
+ Following Dieter Rams' principle of "Less but better"
4
+ """
5
+
6
+ # Clean ASCII border system with different weights for hierarchy
7
+ CLEAN_BORDERS = {
8
+ 'light': {
9
+ 'top_left': '┌', 'top_right': '┐', 'bottom_left': '└', 'bottom_right': '┘',
10
+ 'horizontal': '─', 'vertical': '│', 'cross': '┼', 'tee_down': '┬',
11
+ 'tee_up': '┴', 'tee_right': '├', 'tee_left': '┤'
12
+ },
13
+ 'heavy': {
14
+ 'top_left': '┏', 'top_right': '┓', 'bottom_left': '┗', 'bottom_right': '┛',
15
+ 'horizontal': '━', 'vertical': '┃', 'cross': '╋', 'tee_down': '┳',
16
+ 'tee_up': '┻', 'tee_right': '┣', 'tee_left': '┫'
17
+ },
18
+ 'double': {
19
+ 'top_left': '╔', 'top_right': '╗', 'bottom_left': '╚', 'bottom_right': '╝',
20
+ 'horizontal': '═', 'vertical': '║', 'cross': '╬', 'tee_down': '╦',
21
+ 'tee_up': '╩', 'tee_right': '╠', 'tee_left': '╣'
22
+ }
23
+ }
24
+
25
+ def create_border_line(width: int, border_type: str = 'light', position: str = 'top') -> str:
26
+ """
27
+ Create a border line of specified width and type
28
+
29
+ Args:
30
+ width: Width of the border line
31
+ border_type: 'light', 'heavy', or 'double'
32
+ position: 'top', 'bottom', 'middle'
33
+ """
34
+ borders = CLEAN_BORDERS[border_type]
35
+
36
+ if position == 'top':
37
+ return borders['top_left'] + borders['horizontal'] * (width - 2) + borders['top_right']
38
+ elif position == 'bottom':
39
+ return borders['bottom_left'] + borders['horizontal'] * (width - 2) + borders['bottom_right']
40
+ elif position == 'middle':
41
+ return borders['tee_right'] + borders['horizontal'] * (width - 2) + borders['tee_left']
42
+ else:
43
+ return borders['horizontal'] * width
44
+
45
+ def create_bordered_content(content: str, width: int, border_type: str = 'light', title: str = None) -> str:
46
+ """
47
+ Create content with clean ASCII borders
48
+
49
+ Args:
50
+ content: Text content to border
51
+ width: Total width including borders
52
+ border_type: Border style
53
+ title: Optional title for the border
54
+ """
55
+ borders = CLEAN_BORDERS[border_type]
56
+ inner_width = width - 2
57
+
58
+ lines = []
59
+
60
+ # Top border with optional title
61
+ if title:
62
+ title_line = f" {title} "
63
+ padding = (inner_width - len(title_line)) // 2
64
+ top_line = borders['top_left'] + borders['horizontal'] * padding + title_line
65
+ top_line += borders['horizontal'] * (inner_width - len(top_line) + 1) + borders['top_right']
66
+ lines.append(top_line)
67
+ else:
68
+ lines.append(create_border_line(width, border_type, 'top'))
69
+
70
+ # Content lines
71
+ content_lines = content.split('\n') if content else ['']
72
+ for line in content_lines:
73
+ # Truncate or pad line to fit
74
+ if len(line) > inner_width:
75
+ line = line[:inner_width-3] + '...'
76
+ else:
77
+ line = line.ljust(inner_width)
78
+ lines.append(borders['vertical'] + line + borders['vertical'])
79
+
80
+ # Bottom border
81
+ lines.append(create_border_line(width, border_type, 'bottom'))
82
+
83
+ return '\n'.join(lines)
84
+
85
+ def create_app_header(title: str, model: str, width: int) -> str:
86
+ """
87
+ Create a clean application header following Rams design principles
88
+ """
89
+ # Use light borders for the main frame
90
+ borders = CLEAN_BORDERS['light']
91
+ inner_width = width - 2
92
+
93
+ # Title and model info with intentional spacing
94
+ title_text = f" {title} "
95
+ model_text = f" Model: {model} "
96
+
97
+ # Calculate spacing
98
+ used_space = len(title_text) + len(model_text)
99
+ remaining = inner_width - used_space
100
+ spacing = '─' * max(0, remaining)
101
+
102
+ header_content = title_text + spacing + model_text
103
+
104
+ # Ensure it fits exactly
105
+ if len(header_content) > inner_width:
106
+ header_content = header_content[:inner_width]
107
+ elif len(header_content) < inner_width:
108
+ header_content = header_content.ljust(inner_width, '─')
109
+
110
+ return borders['top_left'] + header_content + borders['top_right']
111
+
112
+ def create_section_border(title: str, width: int, border_type: str = 'light') -> str:
113
+ """
114
+ Create a section divider with title
115
+ """
116
+ borders = CLEAN_BORDERS[border_type]
117
+
118
+ if title:
119
+ title_with_spaces = f" {title} "
120
+ remaining = width - len(title_with_spaces) - 2
121
+ left_dash = remaining // 2
122
+ right_dash = remaining - left_dash
123
+
124
+ return (borders['tee_right'] +
125
+ borders['horizontal'] * left_dash +
126
+ title_with_spaces +
127
+ borders['horizontal'] * right_dash +
128
+ borders['tee_left'])
129
+ else:
130
+ return borders['tee_right'] + borders['horizontal'] * (width - 2) + borders['tee_left']
131
+
132
+ # Minimal loading indicators following "less but better" principle
133
+ MINIMAL_LOADING_FRAMES = [
134
+ " ▪▫▫ ",
135
+ " ▫▪▫ ",
136
+ " ▫▫▪ ",
137
+ " ▫▪▫ ",
138
+ ]
139
+
140
+ DOTS_LOADING = [
141
+ "●○○○",
142
+ "○●○○",
143
+ "○○●○",
144
+ "○○○●",
145
+ "○○●○",
146
+ "○●○○",
147
+ ]
148
+
149
+ # Simple progress indicators
150
+ PROGRESS_BARS = {
151
+ 'simple': ['▱', '▰'],
152
+ 'blocks': ['░', '▓'],
153
+ 'dots': ['○', '●']
154
+ }
app/ui/chat_interface.py CHANGED
@@ -22,70 +22,72 @@ from ..config import CONFIG
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
24
  class SendButton(Button):
25
- """Custom send button implementation"""
25
+ """Minimal send button following Rams design principles"""
26
26
 
27
27
  DEFAULT_CSS = """
28
- /* Drastically simplified SendButton CSS */
29
28
  SendButton {
30
- color: white; /* Basic text color */
31
- /* Removed most properties */
32
- margin: 0 1; /* Keep margin for spacing */
29
+ background: transparent;
30
+ color: #E8E8E8;
31
+ border: solid #333333 1;
32
+ margin: 0 1;
33
+ padding: 1 2;
33
34
  }
34
35
 
35
- SendButton > .button--label {
36
- color: white; /* Basic label color */
37
- width: auto; /* Ensure label width isn't constrained */
38
- height: auto; /* Ensure label height isn't constrained */
39
- /* Removed most properties */
36
+ SendButton:hover {
37
+ background: #1A1A1A;
38
+ border: solid #33FF33 1;
39
+ color: #E8E8E8;
40
+ }
41
+
42
+ SendButton:focus {
43
+ border: solid #33FF33 1;
44
+ outline: none;
40
45
  }
41
46
  """
42
47
 
43
48
  def __init__(self, name: Optional[str] = None):
44
49
  super().__init__(
45
- " SEND ⬆",
46
- name=name,
47
- variant="success"
50
+ "→", # Simple arrow - functional and clear
51
+ name=name
48
52
  )
49
53
 
50
- def on_mount(self) -> None:
51
- """Handle mount event"""
52
- self.styles.text_opacity = 100
53
- self.styles.text_style = "bold"
54
-
55
54
  class MessageDisplay(Static): # Inherit from Static instead of RichLog
56
55
  """Widget to display a single message using Static"""
57
56
 
58
57
  DEFAULT_CSS = """
58
+ /* Rams-inspired message styling - "Less but better" */
59
59
  MessageDisplay {
60
60
  width: 100%;
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 */
64
- margin: 1 0;
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' */
61
+ height: auto;
62
+ margin: 2 0; /* Generous vertical spacing */
63
+ padding: 2 3; /* Intentional padding for readability */
64
+ text-wrap: wrap;
65
+ content-align: left top;
66
+ overflow-y: auto;
67
+ box-sizing: border-box;
68
+ background: transparent; /* Clean default */
69
+ border: none; /* Remove unnecessary borders */
71
70
  }
72
71
 
73
72
  MessageDisplay.user-message {
74
- background: $primary-darken-2;
75
- border-right: wide $primary;
76
- margin-right: 4;
73
+ background: #1A1A1A; /* Subtle distinction */
74
+ border-left: solid #33FF33 2; /* Minimal accent line */
75
+ margin-left: 2; /* Slight indent */
76
+ margin-right: 8; /* Asymmetric layout for hierarchy */
77
77
  }
78
78
 
79
79
  MessageDisplay.assistant-message {
80
- background: $surface;
81
- border-left: wide $secondary;
82
- margin-left: 4;
80
+ background: transparent; /* Clean background */
81
+ border-left: solid #666666 1; /* Subtle indicator */
82
+ margin-right: 2; /* Opposite indent */
83
+ margin-left: 8; /* Asymmetric layout */
83
84
  }
84
85
 
85
86
  MessageDisplay.system-message {
86
- background: $surface-darken-1;
87
- border: dashed $primary-background;
87
+ background: transparent;
88
+ border: solid #333333 1;
88
89
  margin: 1 4;
90
+ color: #666666; /* Muted for less important messages */
89
91
  }
90
92
  """
91
93
 
@@ -168,31 +170,31 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
168
170
  logger.error(f"Error refreshing app: {str(e)}")
169
171
 
170
172
  def _format_content(self, content: str) -> str:
171
- """Format message content with timestamp and handle markdown links"""
173
+ """Format message content following Rams principles - clean and functional"""
172
174
  timestamp = datetime.now().strftime("%H:%M")
173
175
 
174
- # Special handling for "Thinking..." to make it visually distinct
176
+ # Special handling for "Thinking..." - minimal styling
175
177
  if content == "Thinking...":
176
- # Use italic style for the thinking indicator
177
- return f"[dim]{timestamp}[/dim] [italic]{content}[/italic]"
178
+ return f" {timestamp} [dim]{content}[/dim]"
178
179
 
179
- # Fix markdown-style links that cause markup errors
180
- # Convert [text](url) to a safe format for Textual markup
180
+ # Clean up markdown-style links for better readability
181
181
  content = re.sub(
182
182
  r'\[([^\]]+)\]\(([^)]+)\)',
183
183
  lambda m: f"{m.group(1)} ({m.group(2)})",
184
184
  content
185
185
  )
186
186
 
187
- # Escape any other potential markup characters
187
+ # Escape markup characters but keep content clean
188
188
  content = content.replace("[", "\\[").replace("]", "\\]")
189
- # But keep our timestamp markup
190
- timestamp_markup = f"[dim]{timestamp}[/dim]"
191
189
 
192
- # Use proper logging instead of print
193
- logger.debug(f"Formatting content: {len(content)} chars")
190
+ # Rams principle: "As little design as possible"
191
+ # Simple timestamp with generous spacing for readability
192
+ return f" {timestamp} {content}"
194
193
 
195
- return f"{timestamp_markup} {content}"
194
+ def _get_clean_border(self, width: int = 60) -> str:
195
+ """Create a clean ASCII border following the design spec"""
196
+ from .borders import create_border_line
197
+ return create_border_line(width, 'light', 'top')
196
198
 
197
199
  class InputWithFocus(Input):
198
200
  """Enhanced Input that better handles focus and maintains cursor position"""
@@ -218,66 +220,74 @@ class ChatInterface(Container):
218
220
  """Main chat interface container"""
219
221
 
220
222
  DEFAULT_CSS = """
223
+ /* Clean chat interface following Rams principles */
221
224
  ChatInterface {
222
225
  width: 100%;
223
226
  height: 100%;
224
- background: $surface;
227
+ background: #0C0C0C;
225
228
  }
226
229
 
227
230
  #messages-container {
228
231
  width: 100%;
229
232
  height: 1fr;
230
- min-height: 10;
231
- border-bottom: solid $primary-darken-2;
233
+ min-height: 15; /* More breathing room */
234
+ border-bottom: solid #333333 1;
232
235
  overflow: auto;
233
- padding: 0 1;
234
- content-align: left top; /* Keep content anchored at top */
236
+ padding: 2; /* Generous padding */
237
+ content-align: left top;
235
238
  box-sizing: border-box;
236
- scrollbar-gutter: stable; /* Better than scrollbar-size which isn't valid */
239
+ background: #0C0C0C;
237
240
  }
238
241
 
239
242
  #input-area {
240
243
  width: 100%;
241
244
  height: auto;
242
- min-height: 4;
243
- max-height: 10;
244
- padding: 1;
245
+ min-height: 5; /* Comfortable minimum */
246
+ max-height: 12;
247
+ padding: 2; /* Consistent padding */
248
+ background: #0C0C0C;
249
+ border-top: solid #333333 1;
245
250
  }
246
251
 
247
252
  #message-input {
248
253
  width: 1fr;
249
- min-height: 2;
254
+ min-height: 3; /* Comfortable input height */
250
255
  height: auto;
251
256
  margin-right: 1;
252
- border: solid $primary-darken-2;
257
+ border: solid #333333 1;
258
+ background: #0C0C0C;
259
+ color: #E8E8E8;
260
+ padding: 1;
253
261
  }
254
262
 
255
263
  #message-input:focus {
256
- border: solid $primary;
264
+ border: solid #33FF33 1;
265
+ outline: none;
257
266
  }
258
267
 
259
268
  #version-label {
260
269
  width: 100%;
261
- height: 1;
262
- background: $warning;
263
- color: black;
270
+ height: auto;
271
+ background: #0C0C0C;
272
+ color: #666666; /* Muted version info */
264
273
  text-align: right;
265
- padding: 0 1;
266
- text-style: bold;
274
+ padding: 1;
275
+ border-bottom: solid #333333 1;
267
276
  }
268
277
 
269
278
  #loading-indicator {
270
279
  width: 100%;
271
- height: 1;
272
- background: $primary-darken-1;
273
- color: $text;
280
+ height: 2;
281
+ background: #0C0C0C;
282
+ color: #666666; /* Subtle loading indicator */
274
283
  display: none;
275
- padding: 0 1;
284
+ padding: 0 2;
285
+ text-align: center;
286
+ border-bottom: solid #333333 1;
276
287
  }
277
288
 
278
289
  #loading-indicator.model-loading {
279
- background: $warning;
280
- color: $text;
290
+ color: #33FF33; /* Accent for model loading */
281
291
  }
282
292
  """
283
293
 
@@ -404,21 +414,17 @@ class ChatInterface(Container):
404
414
  input_widget.focus()
405
415
 
406
416
  def start_loading(self, model_loading: bool = False) -> None:
407
- """Show loading indicator
408
-
409
- Args:
410
- model_loading: If True, indicates Ollama is loading a model
411
- """
417
+ """Show minimal loading indicator following Rams principles"""
412
418
  self.is_loading = True
413
419
  loading = self.query_one("#loading-indicator")
414
420
  loading_text = self.query_one("#loading-text")
415
421
 
416
422
  if model_loading:
417
423
  loading.add_class("model-loading")
418
- loading_text.update("⚙️ Loading Ollama model...")
424
+ loading_text.update(" Preparing model")
419
425
  else:
420
426
  loading.remove_class("model-loading")
421
- loading_text.update("▪▪▪ Generating response...")
427
+ loading_text.update(" Generating")
422
428
 
423
429
  loading.display = True
424
430
 
app/ui/model_selector.py CHANGED
@@ -17,18 +17,20 @@ logger = logging.getLogger(__name__)
17
17
  class ModelSelector(Container):
18
18
  """Widget for selecting the AI model to use"""
19
19
 
20
+ # Rams-inspired selector styling
20
21
  DEFAULT_CSS = """
21
22
  ModelSelector {
22
23
  width: 100%;
23
24
  height: auto;
24
- padding: 0;
25
- background: $surface-darken-1;
25
+ padding: 1;
26
+ background: #0C0C0C;
27
+ border: solid #333333 1;
26
28
  }
27
29
 
28
30
  #selector-container {
29
31
  width: 100%;
30
32
  layout: horizontal;
31
- height: 3;
33
+ height: auto;
32
34
  padding: 0;
33
35
  }
34
36
 
@@ -36,11 +38,17 @@ class ModelSelector(Container):
36
38
  width: 30%;
37
39
  height: 3;
38
40
  margin-right: 1;
41
+ background: #0C0C0C;
42
+ color: #E8E8E8;
43
+ border: solid #333333 1;
39
44
  }
40
45
 
41
46
  #model-select, #custom-model-input {
42
47
  width: 1fr;
43
48
  height: 3;
49
+ background: #0C0C0C;
50
+ color: #E8E8E8;
51
+ border: solid #333333 1;
44
52
  }
45
53
 
46
54
  #custom-model-input {