tzamuncode 0.1.0__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.
@@ -0,0 +1,323 @@
1
+ """
2
+ Interactive TUI Chat with dropdown menus like Claude Code
3
+ """
4
+
5
+ from textual.app import App, ComposeResult
6
+ from textual.containers import Container, Vertical
7
+ from textual.widgets import Header, Footer, Static, Input, RichLog, OptionList
8
+ from textual.widgets.option_list import Option
9
+ from textual.binding import Binding
10
+ from rich.text import Text
11
+ from typing import Optional
12
+
13
+ from ..models.ollama import OllamaClient
14
+
15
+
16
+ class InteractiveTUIChat(App):
17
+ """Interactive TUI Chat with dropdown menus"""
18
+
19
+ CSS = """
20
+ Screen {
21
+ background: #1e1e1e;
22
+ }
23
+
24
+ #messages {
25
+ height: 1fr;
26
+ background: #1e1e1e;
27
+ overflow-y: scroll;
28
+ border: none;
29
+ padding: 0 1;
30
+ }
31
+
32
+ #command-menu {
33
+ display: none;
34
+ layer: overlay;
35
+ width: 50;
36
+ height: auto;
37
+ max-height: 15;
38
+ background: #2d2d2d;
39
+ border: solid #3a3a3a;
40
+ offset: 2 2;
41
+ }
42
+
43
+ #shortcuts-menu {
44
+ display: none;
45
+ layer: overlay;
46
+ width: 40;
47
+ height: auto;
48
+ max-height: 10;
49
+ background: #2d2d2d;
50
+ border: solid #3a3a3a;
51
+ offset: 2 2;
52
+ }
53
+
54
+ #separator {
55
+ dock: bottom;
56
+ height: 1;
57
+ background: #1e1e1e;
58
+ color: #3a3a3a;
59
+ }
60
+
61
+ #input-container {
62
+ dock: bottom;
63
+ height: 1;
64
+ background: #1e1e1e;
65
+ padding: 0 1;
66
+ }
67
+
68
+ #input {
69
+ width: 100%;
70
+ background: #1e1e1e;
71
+ border: none;
72
+ }
73
+
74
+ #shortcuts-hint {
75
+ dock: bottom;
76
+ height: 1;
77
+ background: #1e1e1e;
78
+ color: #666;
79
+ text-align: right;
80
+ padding: 0 1;
81
+ }
82
+ """
83
+
84
+ BINDINGS = [
85
+ Binding("ctrl+c", "quit", "Quit"),
86
+ Binding("ctrl+l", "clear", "Clear"),
87
+ Binding("escape", "hide_menus", "Close menu", show=False),
88
+ ]
89
+
90
+ def __init__(self, model: str = "qwen2.5:32b", **kwargs):
91
+ super().__init__(**kwargs)
92
+ self.model = model
93
+ self.client = OllamaClient(model=model)
94
+ self.conversation_history = []
95
+ self.menu_visible = False
96
+
97
+ def compose(self) -> ComposeResult:
98
+ """Create child widgets"""
99
+ # Message history (no header, clean terminal look)
100
+ yield RichLog(id="messages", highlight=True, markup=True)
101
+
102
+ # Command menu (hidden by default)
103
+ command_options = [
104
+ Option("/help - Show available commands", id="help"),
105
+ Option("/models - List and switch models", id="models"),
106
+ Option("/settings - Show settings", id="settings"),
107
+ Option("/clear - Clear conversation", id="clear"),
108
+ Option("/exit - Exit chat", id="exit"),
109
+ ]
110
+ yield OptionList(*command_options, id="command-menu")
111
+
112
+ # Shortcuts menu (hidden by default)
113
+ shortcut_options = [
114
+ Option("Ctrl+C - Exit chat", id="ctrl_c"),
115
+ Option("Ctrl+L - Clear screen", id="ctrl_l"),
116
+ Option("/ - Show commands", id="slash"),
117
+ Option("? - Show shortcuts", id="question"),
118
+ ]
119
+ yield OptionList(*shortcut_options, id="shortcuts-menu")
120
+
121
+ # Separator line (like Claude Code)
122
+ yield Static("─" * 200, id="separator")
123
+
124
+ # Input container
125
+ with Container(id="input-container"):
126
+ yield Input(
127
+ placeholder="",
128
+ id="input"
129
+ )
130
+
131
+ # Shortcuts hint (like Claude Code)
132
+ yield Static("? for shortcuts", id="shortcuts-hint")
133
+
134
+ def on_mount(self) -> None:
135
+ """Called when app starts"""
136
+ messages = self.query_one("#messages", RichLog)
137
+
138
+ # Minimal welcome like Claude Code
139
+ messages.write("╔╦╗╔═╗╔═╗╔╦╗╦ ╦╔╗╔╔═╗╔═╗╔╦╗╔═╗")
140
+ messages.write(" ║ ╔═╝╠═╣║║║║ ║║║║║ ║ ║ ║║╣ ")
141
+ messages.write(" ╩ ╚═╝╩ ╩╩ ╩╚═╝╝╚╝╚═╝╚═╝═╩╝╚═╝")
142
+ messages.write("")
143
+ messages.write(f"[dim]AI Coding Assistant • Built in Saudi Arabia 🇸🇦[/dim]")
144
+ messages.write(f"[dim]Model: {self.model} • Type '/' for commands[/dim]")
145
+ messages.write("")
146
+ messages.write("[green]Welcome![/green]")
147
+ messages.write("")
148
+
149
+ # Focus input
150
+ self.query_one("#input", Input).focus()
151
+
152
+ def on_input_changed(self, event: Input.Changed) -> None:
153
+ """Handle input changes in real-time"""
154
+ current_value = event.value
155
+
156
+ # Show command menu when user types '/'
157
+ if current_value == '/':
158
+ self.show_command_menu()
159
+ return
160
+
161
+ # Show shortcuts menu when user types '?'
162
+ if current_value == '?':
163
+ self.show_shortcuts_menu()
164
+ return
165
+
166
+ # Hide menus if user continues typing
167
+ if len(current_value) > 1 and self.menu_visible:
168
+ self.hide_menus()
169
+
170
+ def show_command_menu(self) -> None:
171
+ """Show interactive command menu"""
172
+ menu = self.query_one("#command-menu", OptionList)
173
+ menu.styles.display = "block"
174
+ menu.focus()
175
+ self.menu_visible = True
176
+
177
+ def show_shortcuts_menu(self) -> None:
178
+ """Show interactive shortcuts menu"""
179
+ menu = self.query_one("#shortcuts-menu", OptionList)
180
+ menu.styles.display = "block"
181
+ menu.focus()
182
+ self.menu_visible = True
183
+
184
+ def hide_menus(self) -> None:
185
+ """Hide all menus"""
186
+ self.query_one("#command-menu").styles.display = "none"
187
+ self.query_one("#shortcuts-menu").styles.display = "none"
188
+ self.menu_visible = False
189
+ self.query_one("#input", Input).focus()
190
+
191
+ def action_hide_menus(self) -> None:
192
+ """Action to hide menus"""
193
+ self.hide_menus()
194
+ # Clear input
195
+ self.query_one("#input", Input).value = ""
196
+
197
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
198
+ """Handle menu option selection"""
199
+ option_id = event.option_id
200
+
201
+ # Hide menus
202
+ self.hide_menus()
203
+
204
+ # Clear input
205
+ input_widget = self.query_one("#input", Input)
206
+ input_widget.value = ""
207
+
208
+ # Execute command
209
+ if option_id == "help":
210
+ self.show_help_in_messages()
211
+ elif option_id == "models":
212
+ self.show_models_in_messages()
213
+ elif option_id == "settings":
214
+ self.show_settings_in_messages()
215
+ elif option_id == "clear":
216
+ self.action_clear()
217
+ elif option_id == "exit":
218
+ self.exit()
219
+
220
+ def show_help_in_messages(self) -> None:
221
+ """Show help in message area"""
222
+ messages = self.query_one("#messages", RichLog)
223
+ messages.write("[bold cyan]Available Commands:[/bold cyan]")
224
+ messages.write(" /help - Show available commands")
225
+ messages.write(" /models - List and switch models")
226
+ messages.write(" /settings - Show settings")
227
+ messages.write(" /clear - Clear conversation")
228
+ messages.write(" /exit - Exit chat")
229
+ messages.write("")
230
+
231
+ def show_models_in_messages(self) -> None:
232
+ """Show models in message area"""
233
+ messages = self.query_one("#messages", RichLog)
234
+ messages.write("[bold cyan]Available Models:[/bold cyan]")
235
+ try:
236
+ models = self.client.list_models()
237
+ for idx, model in enumerate(models, 1):
238
+ status = "✓ Active" if model == self.model else ""
239
+ messages.write(f" {idx}. {model} {status}")
240
+ except Exception as e:
241
+ messages.write(f"[bold red]Error:[/bold red] {e}")
242
+ messages.write("")
243
+
244
+ def show_settings_in_messages(self) -> None:
245
+ """Show settings in message area"""
246
+ messages = self.query_one("#messages", RichLog)
247
+ messages.write("[bold cyan]Current Settings:[/bold cyan]")
248
+ messages.write(f" Model: {self.model}")
249
+ messages.write(f" Streaming: Enabled")
250
+ messages.write("")
251
+
252
+ def on_input_submitted(self, event: Input.Submitted) -> None:
253
+ """Handle input submission"""
254
+ user_input = event.value.strip()
255
+
256
+ if not user_input:
257
+ return
258
+
259
+ # Clear input
260
+ event.input.value = ""
261
+
262
+ # Handle exit
263
+ if user_input.lower() in ['exit', 'quit']:
264
+ self.exit()
265
+ return
266
+
267
+ # Send to AI
268
+ self.send_message(user_input)
269
+
270
+ def send_message(self, message: str) -> None:
271
+ """Send message to AI"""
272
+ messages = self.query_one("#messages", RichLog)
273
+
274
+ # Show user message
275
+ messages.write(f"[bold green]You[/bold green] › {message}")
276
+
277
+ # Add to history
278
+ self.conversation_history.append({"role": "user", "content": message})
279
+
280
+ # Show thinking
281
+ messages.write("[dim]TzamunCode is thinking...[/dim]")
282
+
283
+ try:
284
+ # Get response
285
+ response = ""
286
+ for chunk in self.client.chat_stream(self.conversation_history):
287
+ response += chunk
288
+
289
+ # Clear and re-show conversation
290
+ messages.clear()
291
+ for msg in self.conversation_history:
292
+ if msg["role"] == "user":
293
+ messages.write(f"[bold green]You[/bold green] › {msg['content']}")
294
+ elif msg["role"] == "assistant":
295
+ messages.write(f"[bold blue]TzamunCode[/bold blue] › {msg['content']}")
296
+
297
+ # Show new response
298
+ messages.write(f"[bold blue]TzamunCode[/bold blue] › {response}")
299
+ messages.write("")
300
+
301
+ # Add to history
302
+ self.conversation_history.append({"role": "assistant", "content": response})
303
+
304
+ except Exception as e:
305
+ messages.write(f"[bold red]Error:[/bold red] {e}")
306
+
307
+ def action_clear(self) -> None:
308
+ """Clear conversation"""
309
+ self.conversation_history = []
310
+ messages = self.query_one("#messages", RichLog)
311
+ messages.clear()
312
+ messages.write("[bold green]Conversation cleared[/bold green]")
313
+ messages.write("")
314
+
315
+ def action_quit(self) -> None:
316
+ """Quit app"""
317
+ self.exit()
318
+
319
+
320
+ def run_interactive_chat(model: str = "qwen2.5:32b", system: Optional[str] = None):
321
+ """Run interactive chat"""
322
+ app = InteractiveTUIChat(model=model)
323
+ app.run()