chat-console 0.1.1__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 +6 -0
- app/api/__init__.py +1 -0
- app/api/anthropic.py +92 -0
- app/api/base.py +74 -0
- app/api/ollama.py +116 -0
- app/api/openai.py +78 -0
- app/config.py +127 -0
- app/database.py +285 -0
- app/main.py +599 -0
- app/models.py +83 -0
- app/ui/__init__.py +1 -0
- app/ui/chat_interface.py +345 -0
- app/ui/chat_list.py +336 -0
- app/ui/model_selector.py +296 -0
- app/ui/search.py +308 -0
- app/ui/styles.py +275 -0
- app/utils.py +202 -0
- chat_console-0.1.1.dist-info/LICENSE +21 -0
- chat_console-0.1.1.dist-info/METADATA +111 -0
- chat_console-0.1.1.dist-info/RECORD +23 -0
- chat_console-0.1.1.dist-info/WHEEL +5 -0
- chat_console-0.1.1.dist-info/entry_points.txt +3 -0
- chat_console-0.1.1.dist-info/top_level.txt +1 -0
app/ui/styles.py
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
from rich.style import Style
|
2
|
+
from rich.theme import Theme
|
3
|
+
from textual.widget import Widget
|
4
|
+
from textual.app import ComposeResult
|
5
|
+
from textual.containers import Container, Horizontal, Vertical
|
6
|
+
from textual.css.query import NoMatches
|
7
|
+
|
8
|
+
# Define color palette
|
9
|
+
COLORS = {
|
10
|
+
"dark": {
|
11
|
+
"background": "#0C0C0C",
|
12
|
+
"foreground": "#33FF33",
|
13
|
+
"user_msg": "#00FFFF",
|
14
|
+
"assistant_msg": "#33FF33",
|
15
|
+
"system_msg": "#FF8C00",
|
16
|
+
"highlight": "#FFD700",
|
17
|
+
"selection": "#1A1A1A",
|
18
|
+
"border": "#33FF33",
|
19
|
+
"error": "#FF0000",
|
20
|
+
"success": "#33FF33",
|
21
|
+
},
|
22
|
+
"light": {
|
23
|
+
"background": "#F0F0F0",
|
24
|
+
"foreground": "#000000",
|
25
|
+
"user_msg": "#0000FF",
|
26
|
+
"assistant_msg": "#008000",
|
27
|
+
"system_msg": "#800080",
|
28
|
+
"highlight": "#0078D7",
|
29
|
+
"selection": "#ADD6FF",
|
30
|
+
"border": "#D0D0D0",
|
31
|
+
"error": "#D32F2F",
|
32
|
+
"success": "#388E3C",
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
def get_theme(theme_name="dark"):
|
37
|
+
"""Get Rich theme based on theme name"""
|
38
|
+
colors = COLORS.get(theme_name, COLORS["dark"])
|
39
|
+
|
40
|
+
return Theme({
|
41
|
+
"user": Style(color=colors["user_msg"], bold=True),
|
42
|
+
"assistant": Style(color=colors["assistant_msg"]),
|
43
|
+
"system": Style(color=colors["system_msg"], italic=True),
|
44
|
+
"highlight": Style(color=colors["highlight"], bold=True),
|
45
|
+
"selection": Style(bgcolor=colors["selection"]),
|
46
|
+
"border": Style(color=colors["border"]),
|
47
|
+
"error": Style(color=colors["error"], bold=True),
|
48
|
+
"success": Style(color=colors["success"]),
|
49
|
+
"prompt": Style(color=colors["highlight"]),
|
50
|
+
"heading": Style(color=colors["highlight"], bold=True),
|
51
|
+
"dim": Style(color=colors["border"]),
|
52
|
+
"code": Style(bgcolor="#2D2D2D", color="#D4D4D4"),
|
53
|
+
"code.syntax": Style(color="#569CD6"),
|
54
|
+
"link": Style(color=colors["highlight"], underline=True),
|
55
|
+
})
|
56
|
+
|
57
|
+
# Textual CSS for the application
|
58
|
+
CSS = """
|
59
|
+
/* Base styles */
|
60
|
+
Screen {
|
61
|
+
background: $surface;
|
62
|
+
color: $text;
|
63
|
+
}
|
64
|
+
|
65
|
+
/* Chat message styles */
|
66
|
+
.message {
|
67
|
+
width: 100%;
|
68
|
+
padding: 0 1;
|
69
|
+
margin: 0;
|
70
|
+
}
|
71
|
+
|
72
|
+
.message-content {
|
73
|
+
width: 100%;
|
74
|
+
text-align: left;
|
75
|
+
padding: 0;
|
76
|
+
}
|
77
|
+
|
78
|
+
/* Code blocks */
|
79
|
+
.code-block {
|
80
|
+
background: $surface-darken-3;
|
81
|
+
color: $text-muted;
|
82
|
+
border: solid $primary-darken-3;
|
83
|
+
margin: 1 2;
|
84
|
+
padding: 1;
|
85
|
+
overflow: auto;
|
86
|
+
}
|
87
|
+
|
88
|
+
/* Input area */
|
89
|
+
#input-container {
|
90
|
+
height: auto;
|
91
|
+
background: $surface;
|
92
|
+
border-top: solid $primary-darken-2;
|
93
|
+
padding: 0;
|
94
|
+
}
|
95
|
+
|
96
|
+
#message-input {
|
97
|
+
background: $surface-darken-1;
|
98
|
+
color: $text;
|
99
|
+
border: solid $primary-darken-2;
|
100
|
+
min-height: 2;
|
101
|
+
padding: 0 1;
|
102
|
+
}
|
103
|
+
|
104
|
+
#message-input:focus {
|
105
|
+
border: tall $primary;
|
106
|
+
}
|
107
|
+
|
108
|
+
/* Action buttons */
|
109
|
+
.action-button {
|
110
|
+
background: $primary;
|
111
|
+
color: $text;
|
112
|
+
border: none;
|
113
|
+
min-width: 10;
|
114
|
+
margin-left: 1;
|
115
|
+
}
|
116
|
+
|
117
|
+
.action-button:hover {
|
118
|
+
background: $primary-lighten-1;
|
119
|
+
}
|
120
|
+
|
121
|
+
/* Sidebar */
|
122
|
+
#sidebar {
|
123
|
+
width: 25%;
|
124
|
+
min-width: 18;
|
125
|
+
background: $surface-darken-1;
|
126
|
+
border-right: solid $primary-darken-2 1;
|
127
|
+
}
|
128
|
+
|
129
|
+
/* Chat list */
|
130
|
+
.chat-item {
|
131
|
+
padding: 0 1;
|
132
|
+
height: 2;
|
133
|
+
border-bottom: solid $primary-darken-3 1;
|
134
|
+
}
|
135
|
+
|
136
|
+
.chat-item:hover {
|
137
|
+
background: $primary-darken-2;
|
138
|
+
}
|
139
|
+
|
140
|
+
.chat-item.selected {
|
141
|
+
background: $primary-darken-1;
|
142
|
+
border-left: wide $primary;
|
143
|
+
}
|
144
|
+
|
145
|
+
.chat-title {
|
146
|
+
width: 100%;
|
147
|
+
content-align: center middle;
|
148
|
+
text-align: left;
|
149
|
+
}
|
150
|
+
|
151
|
+
.chat-model {
|
152
|
+
color: $text-muted;
|
153
|
+
text-align: right;
|
154
|
+
}
|
155
|
+
|
156
|
+
.chat-date {
|
157
|
+
color: $text-muted;
|
158
|
+
text-align: right;
|
159
|
+
}
|
160
|
+
|
161
|
+
/* Search input */
|
162
|
+
#search-input {
|
163
|
+
width: 100%;
|
164
|
+
border: solid $primary-darken-2 1;
|
165
|
+
margin: 0 1;
|
166
|
+
height: 2;
|
167
|
+
}
|
168
|
+
|
169
|
+
#search-input:focus {
|
170
|
+
border: solid $primary;
|
171
|
+
}
|
172
|
+
|
173
|
+
/* Model selector */
|
174
|
+
#model-selector {
|
175
|
+
width: 100%;
|
176
|
+
height: 2;
|
177
|
+
margin: 0 1;
|
178
|
+
background: $surface-darken-1;
|
179
|
+
border: solid $primary-darken-2 1;
|
180
|
+
}
|
181
|
+
|
182
|
+
/* Style selector */
|
183
|
+
#style-selector {
|
184
|
+
width: 100%;
|
185
|
+
height: 2;
|
186
|
+
margin: 0 1;
|
187
|
+
background: $surface-darken-1;
|
188
|
+
border: solid $primary-darken-2 1;
|
189
|
+
}
|
190
|
+
|
191
|
+
/* Header */
|
192
|
+
#app-header {
|
193
|
+
width: 100%;
|
194
|
+
height: 2;
|
195
|
+
background: $surface-darken-2;
|
196
|
+
color: $text;
|
197
|
+
content-align: center middle;
|
198
|
+
text-align: center;
|
199
|
+
border-bottom: solid $primary-darken-2 1;
|
200
|
+
}
|
201
|
+
|
202
|
+
/* Loading indicator */
|
203
|
+
#loading-indicator {
|
204
|
+
background: $surface-darken-1;
|
205
|
+
color: $text;
|
206
|
+
padding: 0 1;
|
207
|
+
height: auto;
|
208
|
+
width: 100%;
|
209
|
+
border-top: solid $primary-darken-2 1;
|
210
|
+
display: none;
|
211
|
+
}
|
212
|
+
|
213
|
+
/* Settings modal */
|
214
|
+
.modal {
|
215
|
+
background: $surface;
|
216
|
+
border: solid $primary;
|
217
|
+
padding: 1;
|
218
|
+
height: auto;
|
219
|
+
min-width: 40;
|
220
|
+
max-width: 60;
|
221
|
+
}
|
222
|
+
|
223
|
+
.modal-title {
|
224
|
+
background: $primary;
|
225
|
+
color: $text;
|
226
|
+
width: 100%;
|
227
|
+
height: 3;
|
228
|
+
content-align: center middle;
|
229
|
+
text-align: center;
|
230
|
+
}
|
231
|
+
|
232
|
+
.form-label {
|
233
|
+
width: 100%;
|
234
|
+
padding: 1 0;
|
235
|
+
}
|
236
|
+
|
237
|
+
.form-input {
|
238
|
+
width: 100%;
|
239
|
+
background: $surface-darken-1;
|
240
|
+
border: solid $primary-darken-2;
|
241
|
+
height: 3;
|
242
|
+
margin-bottom: 1;
|
243
|
+
}
|
244
|
+
|
245
|
+
.form-input:focus {
|
246
|
+
border: solid $primary;
|
247
|
+
}
|
248
|
+
|
249
|
+
.button-container {
|
250
|
+
width: 100%;
|
251
|
+
height: 3;
|
252
|
+
align: right middle;
|
253
|
+
}
|
254
|
+
|
255
|
+
.button {
|
256
|
+
background: $primary-darken-1;
|
257
|
+
color: $text;
|
258
|
+
min-width: 6;
|
259
|
+
margin-left: 1;
|
260
|
+
border: solid $primary 1;
|
261
|
+
}
|
262
|
+
|
263
|
+
.button.cancel {
|
264
|
+
background: $error;
|
265
|
+
}
|
266
|
+
|
267
|
+
/* Tags */
|
268
|
+
.tag {
|
269
|
+
background: $primary-darken-1;
|
270
|
+
color: $text;
|
271
|
+
padding: 0 1;
|
272
|
+
margin: 0 1 0 0;
|
273
|
+
border: solid $border;
|
274
|
+
}
|
275
|
+
"""
|
app/utils.py
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
import re
|
3
|
+
import asyncio
|
4
|
+
import time
|
5
|
+
from typing import List, Dict, Any, Optional, Generator, Awaitable, Callable
|
6
|
+
import textwrap
|
7
|
+
import threading
|
8
|
+
from rich.text import Text
|
9
|
+
from rich.markdown import Markdown
|
10
|
+
from rich.syntax import Syntax
|
11
|
+
from rich.panel import Panel
|
12
|
+
from rich.console import Console
|
13
|
+
|
14
|
+
from .models import Message, Conversation
|
15
|
+
from .database import ChatDatabase
|
16
|
+
from .api.base import BaseModelClient
|
17
|
+
|
18
|
+
def generate_conversation_title(messages: List[Message], model: str) -> str:
|
19
|
+
"""Generate a title for a conversation based on its content"""
|
20
|
+
# Find the first user message
|
21
|
+
first_user_message = None
|
22
|
+
for msg in messages:
|
23
|
+
if msg.role == "user":
|
24
|
+
first_user_message = msg
|
25
|
+
break
|
26
|
+
|
27
|
+
if first_user_message is None:
|
28
|
+
return f"New conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
29
|
+
|
30
|
+
# Use first line of the first user message (up to 40 chars)
|
31
|
+
content = first_user_message.content.strip()
|
32
|
+
|
33
|
+
# Get first line
|
34
|
+
first_line = content.split('\n')[0]
|
35
|
+
|
36
|
+
# Truncate if needed
|
37
|
+
if len(first_line) > 40:
|
38
|
+
title = first_line[:37] + "..."
|
39
|
+
else:
|
40
|
+
title = first_line
|
41
|
+
|
42
|
+
return title
|
43
|
+
|
44
|
+
def format_code_blocks(text: str) -> str:
|
45
|
+
"""Ensure code blocks have proper formatting"""
|
46
|
+
# Make sure code blocks are properly formatted with triple backticks
|
47
|
+
pattern = r"```(\w*)\n(.*?)\n```"
|
48
|
+
|
49
|
+
def code_replace(match):
|
50
|
+
lang = match.group(1)
|
51
|
+
code = match.group(2)
|
52
|
+
# Ensure code has proper indentation
|
53
|
+
code_lines = code.split('\n')
|
54
|
+
code = '\n'.join([line.rstrip() for line in code_lines])
|
55
|
+
return f"```{lang}\n{code}\n```"
|
56
|
+
|
57
|
+
return re.sub(pattern, code_replace, text, flags=re.DOTALL)
|
58
|
+
|
59
|
+
def extract_code_blocks(text: str) -> List[Dict[str, str]]:
|
60
|
+
"""Extract code blocks from text content"""
|
61
|
+
blocks = []
|
62
|
+
pattern = r"```(\w*)\n(.*?)\n```"
|
63
|
+
matches = re.finditer(pattern, text, re.DOTALL)
|
64
|
+
|
65
|
+
for match in matches:
|
66
|
+
lang = match.group(1) or "text"
|
67
|
+
code = match.group(2).strip()
|
68
|
+
blocks.append({
|
69
|
+
"language": lang,
|
70
|
+
"code": code,
|
71
|
+
"start": match.start(),
|
72
|
+
"end": match.end()
|
73
|
+
})
|
74
|
+
|
75
|
+
return blocks
|
76
|
+
|
77
|
+
def format_text(text: str, highlight_code: bool = True) -> Text:
|
78
|
+
"""Format text with optional code highlighting"""
|
79
|
+
result = Text()
|
80
|
+
|
81
|
+
if not highlight_code:
|
82
|
+
return Text(text)
|
83
|
+
|
84
|
+
# Split by code blocks
|
85
|
+
parts = re.split(r'(```\w*\n.*?\n```)', text, flags=re.DOTALL)
|
86
|
+
|
87
|
+
for part in parts:
|
88
|
+
if part.startswith('```'):
|
89
|
+
# Handle code block
|
90
|
+
match = re.match(r'```(\w*)\n(.*?)\n```', part, re.DOTALL)
|
91
|
+
if match:
|
92
|
+
lang = match.group(1) or "text"
|
93
|
+
code = match.group(2).strip()
|
94
|
+
syntax = Syntax(
|
95
|
+
code,
|
96
|
+
lang,
|
97
|
+
theme="monokai",
|
98
|
+
line_numbers=True,
|
99
|
+
word_wrap=True,
|
100
|
+
indent_guides=True
|
101
|
+
)
|
102
|
+
result.append("\n")
|
103
|
+
result.append(syntax)
|
104
|
+
result.append("\n")
|
105
|
+
else:
|
106
|
+
# Handle regular text
|
107
|
+
if part.strip():
|
108
|
+
result.append(Text(part.strip()))
|
109
|
+
result.append("\n")
|
110
|
+
|
111
|
+
return result
|
112
|
+
|
113
|
+
def create_new_conversation(db: ChatDatabase, model: str, style: str = "default") -> Conversation:
|
114
|
+
"""Create a new conversation in the database"""
|
115
|
+
title = f"New conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
116
|
+
conversation_id = db.create_conversation(title, model, style)
|
117
|
+
|
118
|
+
# Get full conversation object
|
119
|
+
conversation_data = db.get_conversation(conversation_id)
|
120
|
+
return Conversation.from_dict(conversation_data)
|
121
|
+
|
122
|
+
def update_conversation_title(db: ChatDatabase, conversation: Conversation) -> None:
|
123
|
+
"""Update the title of a conversation based on its content"""
|
124
|
+
if not conversation.messages:
|
125
|
+
return
|
126
|
+
|
127
|
+
title = generate_conversation_title(conversation.messages, conversation.model)
|
128
|
+
db.update_conversation(conversation.id, title=title)
|
129
|
+
conversation.title = title
|
130
|
+
|
131
|
+
def add_message_to_conversation(
|
132
|
+
db: ChatDatabase,
|
133
|
+
conversation: Conversation,
|
134
|
+
role: str,
|
135
|
+
content: str
|
136
|
+
) -> Message:
|
137
|
+
"""Add a message to a conversation in the database"""
|
138
|
+
message_id = db.add_message(conversation.id, role, content)
|
139
|
+
|
140
|
+
# Create message object
|
141
|
+
message = Message(
|
142
|
+
id=message_id,
|
143
|
+
conversation_id=conversation.id,
|
144
|
+
role=role,
|
145
|
+
content=content,
|
146
|
+
timestamp=datetime.now().isoformat()
|
147
|
+
)
|
148
|
+
|
149
|
+
# Add to conversation
|
150
|
+
conversation.messages.append(message)
|
151
|
+
|
152
|
+
# Update conversation title if it's the default
|
153
|
+
if conversation.title.startswith("New conversation"):
|
154
|
+
update_conversation_title(db, conversation)
|
155
|
+
|
156
|
+
return message
|
157
|
+
|
158
|
+
def run_in_thread(func: Callable, *args, **kwargs) -> threading.Thread:
|
159
|
+
"""Run a function in a separate thread"""
|
160
|
+
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
|
161
|
+
thread.daemon = True
|
162
|
+
thread.start()
|
163
|
+
return thread
|
164
|
+
|
165
|
+
async def generate_streaming_response(
|
166
|
+
messages: List[Dict[str, str]],
|
167
|
+
model: str,
|
168
|
+
style: str,
|
169
|
+
client: BaseModelClient,
|
170
|
+
callback: Callable[[str], Awaitable[None]]
|
171
|
+
) -> str:
|
172
|
+
"""Generate a streaming response and call the callback for each chunk"""
|
173
|
+
full_response = ""
|
174
|
+
|
175
|
+
try:
|
176
|
+
# Get the async generator from the client
|
177
|
+
stream = client.generate_stream(messages, model, style)
|
178
|
+
# Iterate over the generator properly
|
179
|
+
async for chunk in stream:
|
180
|
+
if chunk: # Only process non-empty chunks
|
181
|
+
full_response += chunk
|
182
|
+
# Update UI and ensure event loop processes it
|
183
|
+
await callback(chunk)
|
184
|
+
# Small delay to prevent overwhelming the event loop
|
185
|
+
await asyncio.sleep(0.01)
|
186
|
+
except Exception as e:
|
187
|
+
error_msg = f"\n\nError generating response: {str(e)}"
|
188
|
+
full_response += error_msg
|
189
|
+
await callback(error_msg)
|
190
|
+
|
191
|
+
return full_response
|
192
|
+
|
193
|
+
def get_elapsed_time(start_time: float) -> str:
|
194
|
+
"""Get the elapsed time as a formatted string"""
|
195
|
+
elapsed = time.time() - start_time
|
196
|
+
|
197
|
+
if elapsed < 60:
|
198
|
+
return f"{elapsed:.1f}s"
|
199
|
+
else:
|
200
|
+
minutes = int(elapsed // 60)
|
201
|
+
seconds = elapsed % 60
|
202
|
+
return f"{minutes}m {seconds:.1f}s"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,111 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: chat-console
|
3
|
+
Version: 0.1.1
|
4
|
+
Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
|
5
|
+
Home-page: https://github.com/wazacraftrfid/chat-console
|
6
|
+
Author: Johnathan Greenaway
|
7
|
+
Author-email: john@fimbriata.dev
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENSE
|
14
|
+
Requires-Dist: textual>=0.11.1
|
15
|
+
Requires-Dist: typer>=0.7.0
|
16
|
+
Requires-Dist: requests>=2.28.1
|
17
|
+
Requires-Dist: anthropic>=0.5.0
|
18
|
+
Requires-Dist: openai>=0.27.0
|
19
|
+
Requires-Dist: python-dotenv>=0.21.0
|
20
|
+
Dynamic: author
|
21
|
+
Dynamic: author-email
|
22
|
+
Dynamic: classifier
|
23
|
+
Dynamic: description
|
24
|
+
Dynamic: description-content-type
|
25
|
+
Dynamic: home-page
|
26
|
+
Dynamic: requires-dist
|
27
|
+
Dynamic: requires-python
|
28
|
+
Dynamic: summary
|
29
|
+
|
30
|
+
# Chat CLI
|
31
|
+
|
32
|
+
A comprehensive command-line interface for chatting with various AI language models. This application allows you to interact with different LLM providers through an intuitive terminal-based interface.
|
33
|
+
|
34
|
+
## Features
|
35
|
+
|
36
|
+
- Interactive terminal UI with Textual library
|
37
|
+
- Support for multiple AI models:
|
38
|
+
- OpenAI models (GPT-3.5, GPT-4)
|
39
|
+
- Anthropic models (Claude 3 Opus, Sonnet, Haiku)
|
40
|
+
- Conversation history with search functionality
|
41
|
+
- Customizable response styles (concise, detailed, technical, friendly)
|
42
|
+
- Code syntax highlighting
|
43
|
+
- Markdown rendering
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
1. Clone this repository:
|
48
|
+
```
|
49
|
+
git clone https://github.com/yourusername/chat-cli.git
|
50
|
+
cd chat-cli
|
51
|
+
```
|
52
|
+
|
53
|
+
2. Install the required dependencies:
|
54
|
+
```
|
55
|
+
pip install -r requirements.txt
|
56
|
+
```
|
57
|
+
|
58
|
+
3. Set up your API keys:
|
59
|
+
|
60
|
+
Create a `.env` file in the project root directory with your API keys:
|
61
|
+
```
|
62
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
63
|
+
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
64
|
+
```
|
65
|
+
|
66
|
+
## Usage
|
67
|
+
|
68
|
+
Run the application:
|
69
|
+
```
|
70
|
+
chat-cli
|
71
|
+
```
|
72
|
+
|
73
|
+
### Keyboard Shortcuts
|
74
|
+
|
75
|
+
- `q` - Quit the application
|
76
|
+
- `n` - Start a new conversation
|
77
|
+
- `s` - Toggle sidebar
|
78
|
+
- `f` - Focus search box
|
79
|
+
- `Escape` - Cancel current generation
|
80
|
+
- `Ctrl+C` - Quit the application
|
81
|
+
|
82
|
+
### Configuration
|
83
|
+
|
84
|
+
The application creates a configuration file at `~/.chatcli/config.json` on first run. You can edit this file to:
|
85
|
+
|
86
|
+
- Change the default model
|
87
|
+
- Modify available models
|
88
|
+
- Add or edit response styles
|
89
|
+
- Change the theme
|
90
|
+
- Adjust other settings
|
91
|
+
|
92
|
+
## Data Storage
|
93
|
+
|
94
|
+
Conversation history is stored in a SQLite database at `~/.chatcli/chat_history.db`.
|
95
|
+
|
96
|
+
## Development
|
97
|
+
|
98
|
+
The application is structured as follows:
|
99
|
+
|
100
|
+
- `main.py` - Main application entry point
|
101
|
+
- `app/` - Application modules
|
102
|
+
- `api/` - LLM provider API client implementations
|
103
|
+
- `ui/` - User interface components
|
104
|
+
- `config.py` - Configuration management
|
105
|
+
- `database.py` - Database operations
|
106
|
+
- `models.py` - Data models
|
107
|
+
- `utils.py` - Utility functions
|
108
|
+
|
109
|
+
## License
|
110
|
+
|
111
|
+
MIT
|
@@ -0,0 +1,23 @@
|
|
1
|
+
app/__init__.py,sha256=u5X4kPcpqZ12ZLnhwwOCScNvftaknDTrb0DMXqR_iLc,130
|
2
|
+
app/config.py,sha256=PLEic_jwfWvWJxDfQMbKSbJ4ULrcmDhVe0apqegMO_g,3571
|
3
|
+
app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
|
4
|
+
app/main.py,sha256=B_E8ovyGX1xLXDJF_nkjQcbX_AP5hLDih3jqJXdHMMY,19848
|
5
|
+
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
+
app/utils.py,sha256=oUpQpqrxvvQn0S0lMCSwDC1Rx0PHpoAIRDySohYV5Oo,6586
|
7
|
+
app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
|
8
|
+
app/api/anthropic.py,sha256=leWSnCfqKnxHB5k3l_oVty4km3q18dodJkPAxwvhEt0,4211
|
9
|
+
app/api/base.py,sha256=-Lx6nfgvEPjrAnQXuCgG-zr8soD1AibTtP15gVD3O48,3138
|
10
|
+
app/api/ollama.py,sha256=naD5-WVCthZ-0s4iBo_bYV1hRMcuczly-lghmB2_loQ,5033
|
11
|
+
app/api/openai.py,sha256=70NITI4upGld_xpaCZLoMd0ObSeVdhtiyUfY9hYHlhE,3420
|
12
|
+
app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
|
13
|
+
app/ui/chat_interface.py,sha256=5gSOa7zT9bWujkPYctB8gVm4yypnkmKHcY1VtaKcEQs,11126
|
14
|
+
app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
|
15
|
+
app/ui/model_selector.py,sha256=Rv0i2VjLL2-cp4Pn_uMnAnAIV7Zk9gBX1XoWKBzkxHg,10367
|
16
|
+
app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
|
17
|
+
app/ui/styles.py,sha256=eVDBTpBGnQ-mg5SeLi6i74ZjhCpItxAwWh1IelD09GY,5445
|
18
|
+
chat_console-0.1.1.dist-info/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
19
|
+
chat_console-0.1.1.dist-info/METADATA,sha256=Nhshut-tQIN9whzOfxVBNYVAjx6wG90KoyzLgpNeFlg,2899
|
20
|
+
chat_console-0.1.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
21
|
+
chat_console-0.1.1.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
22
|
+
chat_console-0.1.1.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
23
|
+
chat_console-0.1.1.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
app
|