codegpt-ai 2.18.0 → 2.27.0
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.
- package/chat.py +50 -5
- package/desktop.py +642 -177
- package/package.json +2 -1
- package/tui.py +469 -0
package/desktop.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
"""CodeGPT Desktop — Claude
|
|
1
|
+
"""CodeGPT Desktop — Claude + ChatGPT + OpenClaw style GUI."""
|
|
2
2
|
import json
|
|
3
|
-
import
|
|
3
|
+
import time
|
|
4
4
|
import requests
|
|
5
5
|
import webview
|
|
6
6
|
import os
|
|
7
|
-
import time
|
|
8
7
|
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
9
|
|
|
10
10
|
# Config
|
|
11
11
|
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/chat")
|
|
12
12
|
MODEL = "llama3.2"
|
|
13
13
|
SYSTEM = "You are a helpful AI assistant. Be concise and technical."
|
|
14
14
|
|
|
15
|
-
# Load saved URL
|
|
16
15
|
saved_url = Path.home() / ".codegpt" / "ollama_url"
|
|
17
16
|
if saved_url.exists():
|
|
18
17
|
url = saved_url.read_text().strip()
|
|
@@ -21,93 +20,359 @@ if saved_url.exists():
|
|
|
21
20
|
if "/api/chat" not in OLLAMA_URL:
|
|
22
21
|
OLLAMA_URL = OLLAMA_URL.rstrip("/") + "/api/chat"
|
|
23
22
|
|
|
24
|
-
# Try connect
|
|
25
23
|
def try_connect(url):
|
|
26
24
|
try:
|
|
27
|
-
|
|
28
|
-
r = requests.get(base, timeout=3)
|
|
25
|
+
r = requests.get(url.replace("/api/chat", "/api/tags"), timeout=3)
|
|
29
26
|
return [m["name"] for m in r.json().get("models", [])]
|
|
30
27
|
except:
|
|
31
28
|
return []
|
|
32
29
|
|
|
33
|
-
# Auto-detect Ollama
|
|
34
30
|
if not try_connect(OLLAMA_URL):
|
|
35
|
-
for
|
|
36
|
-
if try_connect(
|
|
37
|
-
OLLAMA_URL =
|
|
31
|
+
for fb in ["http://localhost:11434/api/chat", "http://127.0.0.1:11434/api/chat"]:
|
|
32
|
+
if try_connect(fb):
|
|
33
|
+
OLLAMA_URL = fb
|
|
38
34
|
break
|
|
39
35
|
|
|
40
|
-
# Load profile
|
|
41
36
|
profile_file = Path.home() / ".codegpt" / "profiles" / "cli_profile.json"
|
|
42
37
|
USERNAME = "User"
|
|
38
|
+
PERSONA = "default"
|
|
43
39
|
if profile_file.exists():
|
|
44
40
|
try:
|
|
45
41
|
p = json.loads(profile_file.read_text())
|
|
46
42
|
USERNAME = p.get("name", "User")
|
|
47
43
|
MODEL = p.get("model", MODEL)
|
|
44
|
+
PERSONA = p.get("persona", "default")
|
|
48
45
|
except:
|
|
49
46
|
pass
|
|
50
47
|
|
|
51
48
|
OLLAMA_BASE = OLLAMA_URL.replace("/api/chat", "")
|
|
49
|
+
CHATS_DIR = Path.home() / ".codegpt" / "desktop_chats"
|
|
50
|
+
CHATS_DIR.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
PERSONAS = {
|
|
53
|
+
"default": "You are a helpful AI assistant. Be concise and technical.",
|
|
54
|
+
"hacker": "You are a cybersecurity expert. Technical jargon, CVEs, defensive security. Dark humor.",
|
|
55
|
+
"teacher": "You are a patient programming teacher. Step by step, analogies, examples.",
|
|
56
|
+
"roast": "You are a brutally sarcastic code reviewer. Roast bad code but always give the fix.",
|
|
57
|
+
"architect": "You are a senior system architect. Scalability, distributed systems, ASCII diagrams.",
|
|
58
|
+
"minimal": "Shortest possible answer. One line if possible. Code only.",
|
|
59
|
+
}
|
|
52
60
|
|
|
53
61
|
|
|
54
62
|
class Api:
|
|
55
|
-
"""Python bridge — handles Ollama calls for the JS frontend."""
|
|
56
|
-
|
|
57
63
|
def __init__(self):
|
|
58
64
|
self.messages = []
|
|
59
65
|
self.total_tokens = 0
|
|
66
|
+
self.model = MODEL
|
|
67
|
+
self.persona = PERSONA
|
|
68
|
+
self.system = PERSONAS.get(PERSONA, SYSTEM)
|
|
69
|
+
self.chat_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
60
70
|
|
|
61
71
|
def check_status(self):
|
|
62
72
|
try:
|
|
63
73
|
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
64
74
|
models = [m["name"] for m in r.json().get("models", [])]
|
|
65
|
-
return json.dumps({"online": True, "models": models, "model":
|
|
75
|
+
return json.dumps({"online": True, "models": models, "model": self.model, "persona": self.persona})
|
|
66
76
|
except:
|
|
67
|
-
return json.dumps({"online": False, "models": [], "model":
|
|
77
|
+
return json.dumps({"online": False, "models": [], "model": self.model, "persona": self.persona})
|
|
68
78
|
|
|
69
79
|
def send_message(self, text):
|
|
70
|
-
|
|
80
|
+
# Handle slash commands
|
|
81
|
+
if text.startswith("/"):
|
|
82
|
+
result = self._handle_command(text)
|
|
83
|
+
if result:
|
|
84
|
+
return json.dumps({"content": result, "tokens": 0, "elapsed": 0, "total_tokens": self.total_tokens, "is_system": True})
|
|
71
85
|
|
|
86
|
+
self.messages.append({"role": "user", "content": text})
|
|
72
87
|
try:
|
|
73
88
|
start = time.time()
|
|
74
|
-
resp = requests.post(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"stream": False,
|
|
80
|
-
},
|
|
81
|
-
timeout=120,
|
|
82
|
-
)
|
|
89
|
+
resp = requests.post(OLLAMA_URL, json={
|
|
90
|
+
"model": self.model,
|
|
91
|
+
"messages": [{"role": "system", "content": self.system}] + self.messages,
|
|
92
|
+
"stream": False,
|
|
93
|
+
}, timeout=120)
|
|
83
94
|
data = resp.json()
|
|
84
95
|
content = data.get("message", {}).get("content", "No response.")
|
|
85
96
|
elapsed = round(time.time() - start, 1)
|
|
86
97
|
tokens = data.get("eval_count", 0)
|
|
87
98
|
self.total_tokens += tokens
|
|
88
99
|
self.messages.append({"role": "assistant", "content": content})
|
|
89
|
-
|
|
90
|
-
return json.dumps({
|
|
91
|
-
"content": content,
|
|
92
|
-
"tokens": tokens,
|
|
93
|
-
"elapsed": elapsed,
|
|
94
|
-
"total_tokens": self.total_tokens,
|
|
95
|
-
})
|
|
100
|
+
self._auto_save()
|
|
101
|
+
return json.dumps({"content": content, "tokens": tokens, "elapsed": elapsed, "total_tokens": self.total_tokens})
|
|
96
102
|
except Exception as e:
|
|
97
|
-
return json.dumps({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
return json.dumps({"content": f"Error: {e}", "tokens": 0, "elapsed": 0, "total_tokens": self.total_tokens})
|
|
104
|
+
|
|
105
|
+
def _handle_command(self, text):
|
|
106
|
+
"""Handle slash commands in the desktop app."""
|
|
107
|
+
global OLLAMA_URL, OLLAMA_BASE
|
|
108
|
+
cmd = text.split()[0].lower()
|
|
109
|
+
args = text[len(cmd):].strip()
|
|
110
|
+
|
|
111
|
+
if cmd == "/help":
|
|
112
|
+
return (
|
|
113
|
+
"**Commands:**\n"
|
|
114
|
+
"`/new` — New conversation\n"
|
|
115
|
+
"`/model <name>` — Switch model\n"
|
|
116
|
+
"`/models` — List models\n"
|
|
117
|
+
"`/persona <name>` — Switch persona (default, hacker, teacher, roast, architect, minimal)\n"
|
|
118
|
+
"`/clear` — Clear chat\n"
|
|
119
|
+
"`/think` — Toggle deep thinking\n"
|
|
120
|
+
"`/temp <0-2>` — Set temperature\n"
|
|
121
|
+
"`/system <prompt>` — Set system prompt\n"
|
|
122
|
+
"`/tokens` — Show token count\n"
|
|
123
|
+
"`/history` — Show message count\n"
|
|
124
|
+
"`/server` — Show server info\n"
|
|
125
|
+
"`/connect <ip>` — Connect to remote Ollama\n"
|
|
126
|
+
"`/export` — Export chat as text\n"
|
|
127
|
+
"`/weather <city>` — Get weather\n"
|
|
128
|
+
"`/open <url>` — Open URL in browser\n"
|
|
129
|
+
"`/agent <name> <task>` — Run an AI agent\n"
|
|
130
|
+
"`/browse <url>` — Fetch and summarize a URL\n"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
elif cmd == "/new":
|
|
134
|
+
self.messages = []
|
|
135
|
+
self.chat_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
136
|
+
return "New conversation started."
|
|
137
|
+
|
|
138
|
+
elif cmd == "/model":
|
|
139
|
+
if args:
|
|
140
|
+
self.model = args
|
|
141
|
+
return f"Model switched to **{self.model}**"
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
145
|
+
models = [m["name"] for m in r.json().get("models", [])]
|
|
146
|
+
return "**Models:**\n" + "\n".join(f"- {'**'+m+'**' if m == self.model else m}" for m in models)
|
|
147
|
+
except:
|
|
148
|
+
return "Cannot reach Ollama."
|
|
149
|
+
|
|
150
|
+
elif cmd == "/models":
|
|
151
|
+
try:
|
|
152
|
+
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
153
|
+
models = [m["name"] for m in r.json().get("models", [])]
|
|
154
|
+
return "**Available models:**\n" + "\n".join(f"- {'**'+m+'** (active)' if m == self.model else m}" for m in models)
|
|
155
|
+
except:
|
|
156
|
+
return "Cannot reach Ollama."
|
|
157
|
+
|
|
158
|
+
elif cmd == "/persona":
|
|
159
|
+
if args and args in PERSONAS:
|
|
160
|
+
self.persona = args
|
|
161
|
+
self.system = PERSONAS[args]
|
|
162
|
+
return f"Persona switched to **{args}**"
|
|
163
|
+
elif args:
|
|
164
|
+
return f"Unknown persona. Available: {', '.join(PERSONAS.keys())}"
|
|
165
|
+
else:
|
|
166
|
+
return f"Current: **{self.persona}**\nAvailable: {', '.join(PERSONAS.keys())}"
|
|
167
|
+
|
|
168
|
+
elif cmd == "/think":
|
|
169
|
+
if "think step-by-step" in self.system:
|
|
170
|
+
self.system = PERSONAS.get(self.persona, SYSTEM)
|
|
171
|
+
return "Deep thinking **OFF**"
|
|
172
|
+
else:
|
|
173
|
+
self.system += "\n\nIMPORTANT: Think through this step-by-step. Show your reasoning."
|
|
174
|
+
return "Deep thinking **ON** — AI will show reasoning."
|
|
175
|
+
|
|
176
|
+
elif cmd == "/temp":
|
|
177
|
+
if args:
|
|
178
|
+
try:
|
|
179
|
+
t = float(args)
|
|
180
|
+
if 0 <= t <= 2:
|
|
181
|
+
return f"Temperature set to **{t}** (note: applied via system prompt guidance)"
|
|
182
|
+
except:
|
|
183
|
+
pass
|
|
184
|
+
return "Usage: /temp 0.7 (range 0.0 to 2.0)"
|
|
185
|
+
return "Usage: /temp 0.7"
|
|
186
|
+
|
|
187
|
+
elif cmd == "/system":
|
|
188
|
+
if args:
|
|
189
|
+
self.system = args
|
|
190
|
+
return f"System prompt updated."
|
|
191
|
+
return f"Current: {self.system[:100]}..."
|
|
192
|
+
|
|
193
|
+
elif cmd == "/tokens":
|
|
194
|
+
return f"**Session:** {self.total_tokens:,} tokens\n**Messages:** {len(self.messages)}"
|
|
195
|
+
|
|
196
|
+
elif cmd == "/history":
|
|
197
|
+
return f"**{len(self.messages)}** messages in current chat."
|
|
198
|
+
|
|
199
|
+
elif cmd == "/server":
|
|
200
|
+
try:
|
|
201
|
+
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
202
|
+
models = r.json().get("models", [])
|
|
203
|
+
return f"**Server:** {OLLAMA_BASE}\n**Status:** online\n**Models:** {len(models)}\n**Active:** {self.model}"
|
|
204
|
+
except:
|
|
205
|
+
return f"**Server:** {OLLAMA_BASE}\n**Status:** offline"
|
|
206
|
+
|
|
207
|
+
elif cmd == "/connect":
|
|
208
|
+
if args:
|
|
209
|
+
url = args if args.startswith("http") else "http://" + args
|
|
210
|
+
if ":" not in url.split("//")[1]:
|
|
211
|
+
url += ":11434"
|
|
212
|
+
test_url = url.rstrip("/") + "/api/chat"
|
|
213
|
+
if try_connect(test_url):
|
|
214
|
+
OLLAMA_URL = test_url
|
|
215
|
+
OLLAMA_BASE = test_url.replace("/api/chat", "")
|
|
216
|
+
return f"**Connected** to {OLLAMA_BASE}"
|
|
217
|
+
return f"Cannot reach {url}"
|
|
218
|
+
return "Usage: /connect 192.168.1.100"
|
|
219
|
+
|
|
220
|
+
elif cmd == "/export":
|
|
221
|
+
lines = []
|
|
222
|
+
for m in self.messages:
|
|
223
|
+
role = "You" if m["role"] == "user" else "AI"
|
|
224
|
+
lines.append(f"**{role}:** {m['content']}\n")
|
|
225
|
+
return "**Chat Export:**\n\n" + "\n".join(lines) if lines else "Nothing to export."
|
|
226
|
+
|
|
227
|
+
elif cmd == "/clear":
|
|
228
|
+
self.messages = []
|
|
229
|
+
return "Chat cleared."
|
|
230
|
+
|
|
231
|
+
elif cmd == "/weather":
|
|
232
|
+
city = args or "London"
|
|
233
|
+
try:
|
|
234
|
+
r = requests.get(f"https://wttr.in/{city}?format=j1", timeout=10)
|
|
235
|
+
d = r.json()
|
|
236
|
+
c = d["current_condition"][0]
|
|
237
|
+
return (
|
|
238
|
+
f"**{city}**\n"
|
|
239
|
+
f"- {c['weatherDesc'][0]['value']}\n"
|
|
240
|
+
f"- {c['temp_C']}°C (feels {c['FeelsLikeC']}°C)\n"
|
|
241
|
+
f"- Humidity: {c['humidity']}%\n"
|
|
242
|
+
f"- Wind: {c['windspeedMiles']} mph {c['winddir16Point']}"
|
|
243
|
+
)
|
|
244
|
+
except:
|
|
245
|
+
return f"Cannot get weather for {city}."
|
|
246
|
+
|
|
247
|
+
elif cmd == "/open":
|
|
248
|
+
if args:
|
|
249
|
+
import webbrowser
|
|
250
|
+
url = args if args.startswith("http") else "https://" + args
|
|
251
|
+
webbrowser.open(url)
|
|
252
|
+
return f"Opened: {url}"
|
|
253
|
+
return "Usage: /open google.com"
|
|
254
|
+
|
|
255
|
+
elif cmd == "/browse":
|
|
256
|
+
if args:
|
|
257
|
+
url = args if args.startswith("http") else "https://" + args
|
|
258
|
+
try:
|
|
259
|
+
import re as _re
|
|
260
|
+
r = requests.get(url, timeout=15, headers={"User-Agent": "CodeGPT/2.0"})
|
|
261
|
+
text = _re.sub(r'<script[^>]*>.*?</script>', '', r.text, flags=_re.DOTALL)
|
|
262
|
+
text = _re.sub(r'<style[^>]*>.*?</style>', '', text, flags=_re.DOTALL)
|
|
263
|
+
text = _re.sub(r'<[^>]+>', ' ', text)
|
|
264
|
+
text = _re.sub(r'\s+', ' ', text).strip()[:3000]
|
|
265
|
+
|
|
266
|
+
resp = requests.post(OLLAMA_URL, json={
|
|
267
|
+
"model": self.model,
|
|
268
|
+
"messages": [
|
|
269
|
+
{"role": "system", "content": "Summarize in 3-5 bullet points."},
|
|
270
|
+
{"role": "user", "content": f"URL: {url}\n\n{text}"},
|
|
271
|
+
], "stream": False,
|
|
272
|
+
}, timeout=60)
|
|
273
|
+
return resp.json().get("message", {}).get("content", text[:500])
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return f"Cannot fetch: {e}"
|
|
276
|
+
return "Usage: /browse github.com"
|
|
277
|
+
|
|
278
|
+
elif cmd == "/agent":
|
|
279
|
+
parts = args.split(maxsplit=1)
|
|
280
|
+
if len(parts) >= 2:
|
|
281
|
+
agent_name, task = parts
|
|
282
|
+
agents = {
|
|
283
|
+
"coder": "You are an expert programmer. Write clean, working code.",
|
|
284
|
+
"debugger": "You are a debugging expert. Find and fix bugs.",
|
|
285
|
+
"reviewer": "You are a code reviewer. Check for bugs, security, performance.",
|
|
286
|
+
"architect": "You are a system architect. Design with ASCII diagrams.",
|
|
287
|
+
"pentester": "You are an ethical pentester. Find vulnerabilities. Defensive only.",
|
|
288
|
+
"explainer": "You are a teacher. Explain simply with analogies.",
|
|
289
|
+
"optimizer": "You are a performance engineer. Optimize code.",
|
|
290
|
+
"researcher": "You are a research analyst. Deep-dive into topics.",
|
|
291
|
+
}
|
|
292
|
+
if agent_name in agents:
|
|
293
|
+
try:
|
|
294
|
+
resp = requests.post(OLLAMA_URL, json={
|
|
295
|
+
"model": self.model,
|
|
296
|
+
"messages": [
|
|
297
|
+
{"role": "system", "content": agents[agent_name]},
|
|
298
|
+
{"role": "user", "content": task},
|
|
299
|
+
], "stream": False,
|
|
300
|
+
}, timeout=90)
|
|
301
|
+
content = resp.json().get("message", {}).get("content", "")
|
|
302
|
+
self.messages.append({"role": "user", "content": f"[agent:{agent_name}] {task}"})
|
|
303
|
+
self.messages.append({"role": "assistant", "content": content})
|
|
304
|
+
return f"**Agent: {agent_name}**\n\n{content}"
|
|
305
|
+
except Exception as e:
|
|
306
|
+
return f"Agent error: {e}"
|
|
307
|
+
return f"Unknown agent. Available: {', '.join(agents.keys())}"
|
|
308
|
+
return "Usage: /agent coder build a flask API"
|
|
309
|
+
|
|
310
|
+
return None # Not a command — send as regular message
|
|
103
311
|
|
|
104
312
|
def new_chat(self):
|
|
313
|
+
self._auto_save()
|
|
105
314
|
self.messages = []
|
|
315
|
+
self.chat_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
106
316
|
return "ok"
|
|
107
317
|
|
|
318
|
+
def set_model(self, model):
|
|
319
|
+
self.model = model
|
|
320
|
+
return json.dumps({"model": self.model})
|
|
321
|
+
|
|
322
|
+
def set_persona(self, persona):
|
|
323
|
+
self.persona = persona
|
|
324
|
+
self.system = PERSONAS.get(persona, SYSTEM)
|
|
325
|
+
return json.dumps({"persona": self.persona})
|
|
326
|
+
|
|
327
|
+
def get_models(self):
|
|
328
|
+
try:
|
|
329
|
+
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
330
|
+
return json.dumps([m["name"] for m in r.json().get("models", [])])
|
|
331
|
+
except:
|
|
332
|
+
return json.dumps([])
|
|
333
|
+
|
|
334
|
+
def get_personas(self):
|
|
335
|
+
return json.dumps(list(PERSONAS.keys()))
|
|
336
|
+
|
|
108
337
|
def get_username(self):
|
|
109
338
|
return USERNAME
|
|
110
339
|
|
|
340
|
+
def get_chat_history(self):
|
|
341
|
+
chats = []
|
|
342
|
+
for f in sorted(CHATS_DIR.glob("*.json"), reverse=True)[:20]:
|
|
343
|
+
try:
|
|
344
|
+
data = json.loads(f.read_text())
|
|
345
|
+
first_msg = ""
|
|
346
|
+
for m in data.get("messages", []):
|
|
347
|
+
if m["role"] == "user":
|
|
348
|
+
first_msg = m["content"][:40]
|
|
349
|
+
break
|
|
350
|
+
chats.append({"id": f.stem, "title": first_msg or "Empty chat", "date": f.stem[:8]})
|
|
351
|
+
except:
|
|
352
|
+
pass
|
|
353
|
+
return json.dumps(chats)
|
|
354
|
+
|
|
355
|
+
def load_chat(self, chat_id):
|
|
356
|
+
f = CHATS_DIR / f"{chat_id}.json"
|
|
357
|
+
if f.exists():
|
|
358
|
+
data = json.loads(f.read_text())
|
|
359
|
+
self.messages = data.get("messages", [])
|
|
360
|
+
self.chat_id = chat_id
|
|
361
|
+
self.model = data.get("model", self.model)
|
|
362
|
+
return json.dumps({"messages": self.messages, "model": self.model})
|
|
363
|
+
return json.dumps({"messages": [], "model": self.model})
|
|
364
|
+
|
|
365
|
+
def delete_chat(self, chat_id):
|
|
366
|
+
f = CHATS_DIR / f"{chat_id}.json"
|
|
367
|
+
if f.exists():
|
|
368
|
+
f.unlink()
|
|
369
|
+
return "ok"
|
|
370
|
+
|
|
371
|
+
def _auto_save(self):
|
|
372
|
+
if self.messages:
|
|
373
|
+
data = {"messages": self.messages, "model": self.model, "saved": datetime.now().isoformat()}
|
|
374
|
+
(CHATS_DIR / f"{self.chat_id}.json").write_text(json.dumps(data))
|
|
375
|
+
|
|
111
376
|
|
|
112
377
|
HTML = """
|
|
113
378
|
<!DOCTYPE html>
|
|
@@ -116,168 +381,243 @@ HTML = """
|
|
|
116
381
|
<meta charset="utf-8">
|
|
117
382
|
<style>
|
|
118
383
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
119
|
-
|
|
120
384
|
:root {
|
|
121
|
-
--bg: #0d1117;
|
|
122
|
-
--
|
|
123
|
-
--
|
|
124
|
-
--text: #e6edf3;
|
|
125
|
-
--dim: #7d8590;
|
|
126
|
-
--accent: #58a6ff;
|
|
127
|
-
--red: #f85149;
|
|
128
|
-
--green: #3fb950;
|
|
129
|
-
--user-bg: #1c2128;
|
|
385
|
+
--bg: #0d1117; --surface: #161b22; --border: #30363d; --text: #e6edf3;
|
|
386
|
+
--dim: #7d8590; --accent: #58a6ff; --red: #f85149; --green: #3fb950;
|
|
387
|
+
--user-bg: #1c2128; --sidebar: #0d1117; --hover: #1c2128;
|
|
130
388
|
}
|
|
389
|
+
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', -apple-system, sans-serif; height: 100vh; display: flex; overflow: hidden; }
|
|
131
390
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
391
|
+
/* Sidebar */
|
|
392
|
+
.sidebar {
|
|
393
|
+
width: 260px; background: var(--sidebar); border-right: 1px solid var(--border);
|
|
394
|
+
display: flex; flex-direction: column; flex-shrink: 0;
|
|
395
|
+
}
|
|
396
|
+
.sidebar-header { padding: 16px; border-bottom: 1px solid var(--border); }
|
|
397
|
+
.sidebar-header h2 { font-size: 14px; color: var(--dim); margin-bottom: 10px; }
|
|
398
|
+
.new-btn {
|
|
399
|
+
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
|
400
|
+
border-radius: 8px; color: var(--text); padding: 10px; font-size: 13px;
|
|
401
|
+
cursor: pointer; text-align: left; display: flex; align-items: center; gap: 8px;
|
|
402
|
+
}
|
|
403
|
+
.new-btn:hover { border-color: var(--accent); }
|
|
404
|
+
.chat-list { flex: 1; overflow-y: auto; padding: 8px; }
|
|
405
|
+
.chat-item {
|
|
406
|
+
padding: 10px 12px; border-radius: 8px; cursor: pointer; margin-bottom: 2px;
|
|
407
|
+
font-size: 13px; color: var(--dim); display: flex; justify-content: space-between; align-items: center;
|
|
139
408
|
}
|
|
409
|
+
.chat-item:hover { background: var(--hover); color: var(--text); }
|
|
410
|
+
.chat-item.active { background: var(--hover); color: var(--text); }
|
|
411
|
+
.chat-item .del { opacity: 0; color: var(--red); cursor: pointer; font-size: 11px; }
|
|
412
|
+
.chat-item:hover .del { opacity: 1; }
|
|
413
|
+
|
|
414
|
+
/* Bottom controls */
|
|
415
|
+
.sidebar-bottom { padding: 12px; border-top: 1px solid var(--border); }
|
|
416
|
+
.select-row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
417
|
+
.select-row select {
|
|
418
|
+
flex: 1; background: var(--surface); border: 1px solid var(--border);
|
|
419
|
+
border-radius: 6px; color: var(--text); padding: 6px 8px; font-size: 12px; outline: none;
|
|
420
|
+
}
|
|
421
|
+
.sidebar-info { font-size: 11px; color: var(--dim); text-align: center; }
|
|
140
422
|
|
|
423
|
+
/* Main */
|
|
424
|
+
.main { flex: 1; display: flex; flex-direction: column; }
|
|
141
425
|
.header {
|
|
142
|
-
background: var(--surface);
|
|
143
|
-
|
|
144
|
-
padding: 12px 20px;
|
|
145
|
-
display: flex;
|
|
146
|
-
align-items: center;
|
|
147
|
-
justify-content: space-between;
|
|
426
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
427
|
+
padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
|
|
148
428
|
}
|
|
149
|
-
|
|
150
429
|
.header h1 { font-size: 16px; font-weight: 600; }
|
|
151
|
-
.header
|
|
152
|
-
.header
|
|
153
|
-
.header-info { font-size: 12px; color: var(--dim); }
|
|
154
|
-
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block;
|
|
155
|
-
.dot.on { background: var(--green); }
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.
|
|
159
|
-
flex: 1;
|
|
160
|
-
overflow-y: auto;
|
|
161
|
-
padding: 16px 20px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.msg { margin-bottom: 16px; animation: fadeIn 0.2s ease; }
|
|
430
|
+
.header .code { color: var(--red); }
|
|
431
|
+
.header .gpt { color: var(--accent); }
|
|
432
|
+
.header-info { font-size: 12px; color: var(--dim); display: flex; align-items: center; gap: 8px; }
|
|
433
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
434
|
+
.dot.on { background: var(--green); } .dot.off { background: var(--red); }
|
|
435
|
+
|
|
436
|
+
.messages { flex: 1; overflow-y: auto; padding: 20px; }
|
|
437
|
+
.msg { margin-bottom: 20px; animation: fadeIn 0.2s ease; max-width: 800px; }
|
|
165
438
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
|
|
166
|
-
|
|
167
|
-
.msg .
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
.msg .
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
.msg.user .role { color: var(--accent); }
|
|
176
|
-
.msg.user .icon { background: var(--accent); color: var(--bg); }
|
|
177
|
-
.msg.ai .role { color: var(--green); }
|
|
178
|
-
.msg.ai .icon { background: var(--green); color: var(--bg); }
|
|
179
|
-
|
|
180
|
-
.msg .body {
|
|
181
|
-
font-size: 14px; line-height: 1.6; padding: 10px 14px;
|
|
182
|
-
white-space: pre-wrap; word-wrap: break-word;
|
|
183
|
-
}
|
|
184
|
-
.msg.user .body { background: var(--user-bg); border: 1px solid var(--border); border-radius: 8px; }
|
|
185
|
-
.msg.ai .body { border-left: 2px solid var(--green); padding-left: 14px; }
|
|
186
|
-
|
|
187
|
-
.msg .body code { background: var(--surface); padding: 2px 6px; border-radius: 4px; font-family: 'Cascadia Code', monospace; font-size: 13px; }
|
|
188
|
-
.msg .body pre { background: var(--surface); padding: 12px; border-radius: 8px; margin: 8px 0; border: 1px solid var(--border); overflow-x: auto; }
|
|
439
|
+
.msg .role { font-size: 12px; font-weight: 600; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
|
|
440
|
+
.msg .icon { width: 20px; height: 20px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; }
|
|
441
|
+
.msg.user .role { color: var(--accent); } .msg.user .icon { background: var(--accent); color: var(--bg); }
|
|
442
|
+
.msg.ai .role { color: var(--green); } .msg.ai .icon { background: var(--green); color: var(--bg); }
|
|
443
|
+
.msg .body { font-size: 14px; line-height: 1.7; padding: 12px 16px; white-space: pre-wrap; word-wrap: break-word; }
|
|
444
|
+
.msg.user .body { background: var(--user-bg); border: 1px solid var(--border); border-radius: 10px; }
|
|
445
|
+
.msg.ai .body { border-left: 2px solid var(--green); padding-left: 16px; }
|
|
446
|
+
.msg .body code { background: var(--surface); padding: 2px 6px; border-radius: 4px; font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 13px; }
|
|
447
|
+
.msg .body pre { background: var(--surface); padding: 12px 16px; border-radius: 8px; margin: 10px 0; border: 1px solid var(--border); overflow-x: auto; position: relative; }
|
|
189
448
|
.msg .body pre code { background: none; padding: 0; }
|
|
190
|
-
.
|
|
191
|
-
|
|
449
|
+
.copy-btn { position: absolute; top: 8px; right: 8px; background: var(--border); border: none; border-radius: 4px; color: var(--dim); padding: 4px 8px; font-size: 11px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
|
|
450
|
+
pre:hover .copy-btn { opacity: 1; }
|
|
451
|
+
.copy-btn:hover { color: var(--text); }
|
|
452
|
+
.msg .stats { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
|
192
453
|
.thinking { display: none; color: var(--dim); font-size: 13px; margin-bottom: 16px; }
|
|
193
|
-
.thinking.on { display:
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
.
|
|
200
|
-
.
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
454
|
+
.thinking.on { display: flex; align-items: center; gap: 8px; }
|
|
455
|
+
.spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
456
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
457
|
+
|
|
458
|
+
.welcome { text-align: center; padding: 80px 20px; }
|
|
459
|
+
.welcome h2 { font-size: 28px; margin-bottom: 8px; }
|
|
460
|
+
.welcome p { color: var(--dim); margin-bottom: 24px; }
|
|
461
|
+
.chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; max-width: 500px; margin: 0 auto; }
|
|
462
|
+
.chips button { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; color: var(--text); padding: 10px 16px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
|
|
463
|
+
.chips button:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
|
|
464
|
+
|
|
465
|
+
.input-area { background: var(--surface); border-top: 1px solid var(--border); padding: 16px 20px; }
|
|
466
|
+
.input-wrap { display: flex; gap: 8px; align-items: flex-end; max-width: 800px; }
|
|
467
|
+
.input-wrap textarea { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; color: var(--text); padding: 12px 16px; font-size: 14px; font-family: inherit; resize: none; outline: none; min-height: 44px; max-height: 150px; transition: border-color 0.2s; }
|
|
206
468
|
.input-wrap textarea:focus { border-color: var(--accent); }
|
|
207
|
-
.input-wrap button {
|
|
208
|
-
|
|
209
|
-
|
|
469
|
+
.input-wrap button { background: var(--accent); border: none; border-radius: 10px; color: white; padding: 12px 18px; font-size: 14px; cursor: pointer; font-weight: 600; transition: opacity 0.2s; }
|
|
470
|
+
.input-wrap button:hover { opacity: 0.9; } .input-wrap button:disabled { opacity: 0.3; }
|
|
471
|
+
.footer { padding: 6px 20px; font-size: 11px; color: var(--dim); display: flex; justify-content: space-between; }
|
|
472
|
+
|
|
473
|
+
/* Welcome modal */
|
|
474
|
+
.modal-overlay {
|
|
475
|
+
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
476
|
+
background: rgba(0,0,0,0.7); z-index: 200;
|
|
477
|
+
display: flex; align-items: center; justify-content: center;
|
|
478
|
+
animation: fadeIn 0.3s ease;
|
|
210
479
|
}
|
|
211
|
-
.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
.
|
|
215
|
-
background: var(--surface); border-top: 1px solid var(--border);
|
|
216
|
-
padding: 6px 20px; font-size: 11px; color: var(--dim);
|
|
217
|
-
display: flex; justify-content: space-between;
|
|
480
|
+
.modal {
|
|
481
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 16px;
|
|
482
|
+
padding: 32px 40px; max-width: 480px; width: 90%; text-align: center;
|
|
483
|
+
animation: modalIn 0.3s ease;
|
|
218
484
|
}
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
.
|
|
222
|
-
.
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
485
|
+
@keyframes modalIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
|
486
|
+
.modal h2 { font-size: 22px; margin-bottom: 4px; }
|
|
487
|
+
.modal h2 .code { color: var(--red); } .modal h2 .gpt { color: var(--accent); }
|
|
488
|
+
.modal .ver { color: var(--dim); font-size: 13px; margin-bottom: 20px; }
|
|
489
|
+
.modal .features { text-align: left; margin: 16px 0; }
|
|
490
|
+
.modal .feat { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 14px; }
|
|
491
|
+
.modal .feat .num { color: var(--accent); font-weight: 700; min-width: 30px; }
|
|
492
|
+
.modal .feat .label { color: var(--dim); }
|
|
493
|
+
.modal .start-btn {
|
|
494
|
+
background: var(--accent); border: none; border-radius: 10px; color: white;
|
|
495
|
+
padding: 12px 32px; font-size: 15px; cursor: pointer; font-weight: 600;
|
|
496
|
+
margin-top: 20px; transition: opacity 0.2s;
|
|
226
497
|
}
|
|
227
|
-
.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
.
|
|
498
|
+
.modal .start-btn:hover { opacity: 0.9; }
|
|
499
|
+
.modal .tip { color: var(--dim); font-size: 12px; margin-top: 12px; }
|
|
500
|
+
|
|
501
|
+
/* Command autocomplete */
|
|
502
|
+
.cmd-menu { display: none; position: absolute; bottom: 100%; left: 0; right: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; max-height: 250px; overflow-y: auto; margin-bottom: 4px; z-index: 100; }
|
|
503
|
+
.cmd-menu.show { display: block; }
|
|
504
|
+
.cmd-item { padding: 8px 14px; cursor: pointer; display: flex; justify-content: space-between; font-size: 13px; }
|
|
505
|
+
.cmd-item:hover, .cmd-item.sel { background: var(--hover); }
|
|
506
|
+
.cmd-item .name { color: var(--accent); }
|
|
507
|
+
.cmd-item .desc { color: var(--dim); font-size: 12px; }
|
|
508
|
+
.kbd { background: var(--surface); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px; font-size: 10px; }
|
|
234
509
|
</style>
|
|
235
510
|
</head>
|
|
236
511
|
<body>
|
|
237
512
|
|
|
238
|
-
<div class="
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
513
|
+
<div class="sidebar">
|
|
514
|
+
<div class="sidebar-header">
|
|
515
|
+
<h2>CHATS</h2>
|
|
516
|
+
<button class="new-btn" onclick="newChat()">+ New Chat</button>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="chat-list" id="chatList"></div>
|
|
519
|
+
<div class="sidebar-bottom">
|
|
520
|
+
<div class="select-row">
|
|
521
|
+
<select id="modelSelect" onchange="setModel(this.value)"></select>
|
|
522
|
+
</div>
|
|
523
|
+
<div class="select-row">
|
|
524
|
+
<select id="personaSelect" onchange="setPersona(this.value)"></select>
|
|
525
|
+
</div>
|
|
526
|
+
<div class="sidebar-info" id="sidebarInfo">CodeGPT v2.0</div>
|
|
244
527
|
</div>
|
|
245
528
|
</div>
|
|
246
529
|
|
|
247
|
-
<div class="
|
|
248
|
-
<div class="
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
<
|
|
253
|
-
<button onclick="go('Write a Python function to find prime numbers')">Prime numbers</button>
|
|
254
|
-
<button onclick="go('What are the OWASP top 10?')">OWASP Top 10</button>
|
|
255
|
-
<button onclick="go('Design a login system with JWT')">JWT Auth</button>
|
|
530
|
+
<div class="main">
|
|
531
|
+
<div class="header">
|
|
532
|
+
<h1><span class="code">Code</span><span class="gpt">GPT</span></h1>
|
|
533
|
+
<div class="header-info">
|
|
534
|
+
<span class="dot" id="dot"></span>
|
|
535
|
+
<span id="statusText"></span>
|
|
256
536
|
</div>
|
|
257
537
|
</div>
|
|
258
|
-
<div class="thinking" id="think"><span class="d">Thinking<span>.</span><span>.</span><span>.</span></span></div>
|
|
259
|
-
</div>
|
|
260
538
|
|
|
261
|
-
<div class="
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
539
|
+
<div class="messages" id="msgs">
|
|
540
|
+
<div class="welcome" id="welcome">
|
|
541
|
+
<h2 id="greeting"></h2>
|
|
542
|
+
<p>How can I help you today?</p>
|
|
543
|
+
<div class="chips">
|
|
544
|
+
<button onclick="go('Explain how REST APIs work')">Explain REST APIs</button>
|
|
545
|
+
<button onclick="go('Write a Python function to find primes')">Prime numbers</button>
|
|
546
|
+
<button onclick="go('What are the OWASP top 10?')">OWASP Top 10</button>
|
|
547
|
+
<button onclick="go('Design a login system with JWT')">JWT Auth</button>
|
|
548
|
+
<button onclick="go('Compare Flask vs FastAPI')">Flask vs FastAPI</button>
|
|
549
|
+
<button onclick="go('Write a bash script to backup files')">Backup script</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
<div class="thinking" id="think"><div class="spinner"></div> Thinking...</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div class="input-area" style="position:relative">
|
|
556
|
+
<div class="cmd-menu" id="cmdMenu"></div>
|
|
557
|
+
<div class="input-wrap">
|
|
558
|
+
<textarea id="inp" placeholder="Message CodeGPT... (type / for commands)" rows="1" onkeydown="key(event)" oninput="onInput(event)" autofocus></textarea>
|
|
559
|
+
<button onclick="send()" id="btn">Send</button>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="footer">
|
|
563
|
+
<span id="tc">0 tokens</span>
|
|
564
|
+
<span><span class="kbd">Enter</span> send · <span class="kbd">Shift+Enter</span> new line · <span class="kbd">Ctrl+N</span> new chat</span>
|
|
265
565
|
</div>
|
|
266
566
|
</div>
|
|
267
567
|
|
|
268
|
-
<div
|
|
269
|
-
<
|
|
270
|
-
|
|
568
|
+
<div id="welcomeModal" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:200;display:flex;align-items:center;justify-content:center">
|
|
569
|
+
<div style="background:#161b22;border:1px solid #30363d;border-radius:16px;padding:32px 40px;max-width:480px;width:90%;text-align:center">
|
|
570
|
+
<h2 style="font-size:22px;margin-bottom:4px"><span style="color:#f85149">Code</span><span style="color:#58a6ff">GPT</span> Desktop</h2>
|
|
571
|
+
<p style="color:#7d8590;font-size:13px;margin-bottom:20px">v2.0 · Local AI · Powered by Ollama</p>
|
|
572
|
+
<div style="text-align:left;margin:16px 0">
|
|
573
|
+
<p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">123</span> commands <span style="color:#7d8590">type / to see all</span></p>
|
|
574
|
+
<p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">8</span> AI agents <span style="color:#7d8590">coder, reviewer, architect</span></p>
|
|
575
|
+
<p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">26</span> tools <span style="color:#7d8590">Claude, Codex, Gemini</span></p>
|
|
576
|
+
<p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">6</span> personas <span style="color:#7d8590">hacker, teacher, minimal</span></p>
|
|
577
|
+
</div>
|
|
578
|
+
<p style="color:#7d8590;font-size:12px;margin-top:16px">Click anywhere or wait to start</p>
|
|
579
|
+
</div>
|
|
271
580
|
</div>
|
|
272
581
|
|
|
273
582
|
<script>
|
|
274
583
|
let busy = false;
|
|
275
584
|
|
|
585
|
+
// Welcome — auto-dismiss after 3 seconds, or click/key
|
|
586
|
+
var _welcomed = false;
|
|
587
|
+
function _dismissWelcome() {
|
|
588
|
+
if (_welcomed) return;
|
|
589
|
+
_welcomed = true;
|
|
590
|
+
var m = document.getElementById('welcomeModal');
|
|
591
|
+
if (m) {
|
|
592
|
+
m.style.display = 'none';
|
|
593
|
+
m.style.visibility = 'hidden';
|
|
594
|
+
m.style.pointerEvents = 'none';
|
|
595
|
+
m.style.opacity = '0';
|
|
596
|
+
try { m.parentNode.removeChild(m); } catch(e) {}
|
|
597
|
+
}
|
|
598
|
+
// Clean up listeners
|
|
599
|
+
document.removeEventListener('mousedown', _dismissWelcome);
|
|
600
|
+
document.removeEventListener('keydown', _dismissWelcome);
|
|
601
|
+
if (_dismissTimer) clearTimeout(_dismissTimer);
|
|
602
|
+
// Focus input
|
|
603
|
+
setTimeout(function() {
|
|
604
|
+
var inp = document.getElementById('inp');
|
|
605
|
+
if (inp) inp.focus();
|
|
606
|
+
}, 100);
|
|
607
|
+
}
|
|
608
|
+
var _dismissTimer = setTimeout(_dismissWelcome, 3000);
|
|
609
|
+
document.addEventListener('mousedown', _dismissWelcome);
|
|
610
|
+
document.addEventListener('keydown', _dismissWelcome);
|
|
611
|
+
|
|
276
612
|
async function init() {
|
|
277
613
|
const name = await pywebview.api.get_username();
|
|
278
614
|
const h = new Date().getHours();
|
|
279
615
|
const g = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
|
|
280
|
-
document.getElementById('greeting').textContent = g + ', ' + name
|
|
616
|
+
document.getElementById('greeting').textContent = g + ', ' + name;
|
|
617
|
+
|
|
618
|
+
await loadModels();
|
|
619
|
+
await loadPersonas();
|
|
620
|
+
await loadChats();
|
|
281
621
|
checkStatus();
|
|
282
622
|
setInterval(checkStatus, 30000);
|
|
283
623
|
}
|
|
@@ -285,18 +625,72 @@ async function init() {
|
|
|
285
625
|
async function checkStatus() {
|
|
286
626
|
const r = JSON.parse(await pywebview.api.check_status());
|
|
287
627
|
document.getElementById('dot').className = 'dot ' + (r.online ? 'on' : 'off');
|
|
288
|
-
document.getElementById('
|
|
289
|
-
|
|
628
|
+
document.getElementById('statusText').textContent = r.online ? r.model : 'offline';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function loadModels() {
|
|
632
|
+
const models = JSON.parse(await pywebview.api.get_models());
|
|
633
|
+
const sel = document.getElementById('modelSelect');
|
|
634
|
+
sel.innerHTML = models.map(m => '<option value="'+m+'"'+(m===MODEL?' selected':'')+'>'+m+'</option>').join('');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function loadPersonas() {
|
|
638
|
+
const personas = JSON.parse(await pywebview.api.get_personas());
|
|
639
|
+
const sel = document.getElementById('personaSelect');
|
|
640
|
+
sel.innerHTML = personas.map(p => '<option value="'+p+'">'+p+'</option>').join('');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function loadChats() {
|
|
644
|
+
const chats = JSON.parse(await pywebview.api.get_chat_history());
|
|
645
|
+
const list = document.getElementById('chatList');
|
|
646
|
+
list.innerHTML = chats.map(c =>
|
|
647
|
+
'<div class="chat-item" onclick="loadChat(\''+c.id+'\')">' +
|
|
648
|
+
'<span>'+c.title+'</span>' +
|
|
649
|
+
'<span class="del" onclick="event.stopPropagation();deleteChat(\''+c.id+'\')">x</span>' +
|
|
650
|
+
'</div>'
|
|
651
|
+
).join('') || '<div style="padding:20px;text-align:center;color:var(--dim);font-size:12px">No chats yet</div>';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function loadChat(id) {
|
|
655
|
+
const r = JSON.parse(await pywebview.api.load_chat(id));
|
|
656
|
+
clearMessages();
|
|
657
|
+
r.messages.forEach(m => addMsg(m.role === 'user' ? 'user' : 'ai', m.content));
|
|
658
|
+
document.getElementById('modelSelect').value = r.model;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function deleteChat(id) {
|
|
662
|
+
await pywebview.api.delete_chat(id);
|
|
663
|
+
loadChats();
|
|
290
664
|
}
|
|
291
665
|
|
|
292
666
|
function esc(t) {
|
|
293
667
|
t = t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
294
|
-
|
|
668
|
+
// Code blocks with copy button
|
|
669
|
+
t = t.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, function(m, lang, code) {
|
|
670
|
+
return '<pre><code>'+code+'</code><button class="copy-btn" onclick="copyCode(this)">Copy</button></pre>';
|
|
671
|
+
});
|
|
295
672
|
t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
296
673
|
t = t.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
674
|
+
t = t.replace(/^### (.+)$/gm, '<h3 style="margin:8px 0 4px;font-size:15px">$1</h3>');
|
|
675
|
+
t = t.replace(/^## (.+)$/gm, '<h3 style="margin:8px 0 4px;font-size:16px">$1</h3>');
|
|
676
|
+
t = t.replace(/^- (.+)$/gm, '• $1');
|
|
297
677
|
return t;
|
|
298
678
|
}
|
|
299
679
|
|
|
680
|
+
function copyCode(btn) {
|
|
681
|
+
const code = btn.previousElementSibling.textContent;
|
|
682
|
+
navigator.clipboard.writeText(code);
|
|
683
|
+
btn.textContent = 'Copied!';
|
|
684
|
+
setTimeout(() => btn.textContent = 'Copy', 1500);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function clearMessages() {
|
|
688
|
+
const w = document.getElementById('welcome');
|
|
689
|
+
if (w) w.style.display = 'none';
|
|
690
|
+
const msgs = document.getElementById('msgs');
|
|
691
|
+
msgs.querySelectorAll('.msg').forEach(m => m.remove());
|
|
692
|
+
}
|
|
693
|
+
|
|
300
694
|
function addMsg(role, content, stats) {
|
|
301
695
|
const w = document.getElementById('welcome');
|
|
302
696
|
if (w) w.style.display = 'none';
|
|
@@ -304,19 +698,83 @@ function addMsg(role, content, stats) {
|
|
|
304
698
|
d.className = 'msg ' + role;
|
|
305
699
|
const icon = role === 'user' ? 'U' : 'AI';
|
|
306
700
|
const label = role === 'user' ? 'You' : 'CodeGPT';
|
|
307
|
-
d.innerHTML = '<div class="role"><span class="icon">'
|
|
308
|
-
+ '<div class="body">'
|
|
309
|
-
+ (stats ? '<div class="stats">'
|
|
701
|
+
d.innerHTML = '<div class="role"><span class="icon">'+icon+'</span>'+label+'</div>'
|
|
702
|
+
+ '<div class="body">'+esc(content)+'</div>'
|
|
703
|
+
+ (stats ? '<div class="stats">'+stats+'</div>' : '');
|
|
310
704
|
const c = document.getElementById('msgs');
|
|
311
705
|
c.insertBefore(d, document.getElementById('think'));
|
|
312
706
|
c.scrollTop = c.scrollHeight;
|
|
313
707
|
}
|
|
314
708
|
|
|
709
|
+
const CMDS = [
|
|
710
|
+
{name: '/help', desc: 'Show all commands'},
|
|
711
|
+
{name: '/new', desc: 'New conversation'},
|
|
712
|
+
{name: '/model', desc: 'Switch model'},
|
|
713
|
+
{name: '/models', desc: 'List all models'},
|
|
714
|
+
{name: '/persona', desc: 'Switch persona'},
|
|
715
|
+
{name: '/think', desc: 'Toggle deep thinking'},
|
|
716
|
+
{name: '/temp', desc: 'Set temperature (0-2)'},
|
|
717
|
+
{name: '/system', desc: 'Set system prompt'},
|
|
718
|
+
{name: '/tokens', desc: 'Show token count'},
|
|
719
|
+
{name: '/clear', desc: 'Clear chat'},
|
|
720
|
+
{name: '/server', desc: 'Server info'},
|
|
721
|
+
{name: '/connect', desc: 'Connect to remote Ollama'},
|
|
722
|
+
{name: '/export', desc: 'Export chat'},
|
|
723
|
+
{name: '/agent', desc: 'Run an AI agent'},
|
|
724
|
+
{name: '/browse', desc: 'Fetch and summarize URL'},
|
|
725
|
+
{name: '/open', desc: 'Open URL in browser'},
|
|
726
|
+
{name: '/weather', desc: 'Get weather'},
|
|
727
|
+
{name: '/history', desc: 'Message count'},
|
|
728
|
+
];
|
|
729
|
+
let cmdIdx = -1;
|
|
730
|
+
|
|
731
|
+
function onInput(e) {
|
|
732
|
+
const val = e.target.value;
|
|
733
|
+
const menu = document.getElementById('cmdMenu');
|
|
734
|
+
|
|
735
|
+
// Show commands when typing / (before a space)
|
|
736
|
+
if (val.startsWith('/') && val.indexOf(' ') === -1) {
|
|
737
|
+
const typed = val.toLowerCase();
|
|
738
|
+
// Just "/" shows all, otherwise filter
|
|
739
|
+
const matches = typed === '/' ? CMDS : CMDS.filter(c => c.name.startsWith(typed));
|
|
740
|
+
if (matches.length > 0) {
|
|
741
|
+
cmdIdx = 0;
|
|
742
|
+
menu.innerHTML = matches.map((c, i) =>
|
|
743
|
+
'<div class="cmd-item'+(i===0?' sel':'')+'" onclick="pickCmd(\\''+c.name+'\\')"><span class="name">'+c.name+'</span><span class="desc">'+c.desc+'</span></div>'
|
|
744
|
+
).join('');
|
|
745
|
+
menu.className = 'cmd-menu show';
|
|
746
|
+
} else {
|
|
747
|
+
menu.className = 'cmd-menu';
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
menu.className = 'cmd-menu';
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function pickCmd(cmd) {
|
|
755
|
+
document.getElementById('inp').value = cmd + ' ';
|
|
756
|
+
document.getElementById('cmdMenu').className = 'cmd-menu';
|
|
757
|
+
document.getElementById('inp').focus();
|
|
758
|
+
}
|
|
759
|
+
|
|
315
760
|
function key(e) {
|
|
761
|
+
const menu = document.getElementById('cmdMenu');
|
|
762
|
+
if (menu.classList.contains('show')) {
|
|
763
|
+
const items = menu.querySelectorAll('.cmd-item');
|
|
764
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); cmdIdx = Math.min(cmdIdx+1, items.length-1); items.forEach((it,i) => it.classList.toggle('sel', i===cmdIdx)); }
|
|
765
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); cmdIdx = Math.max(cmdIdx-1, 0); items.forEach((it,i) => it.classList.toggle('sel', i===cmdIdx)); }
|
|
766
|
+
else if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) {
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
if (items[cmdIdx]) { pickCmd(items[cmdIdx].querySelector('.name').textContent); }
|
|
769
|
+
}
|
|
770
|
+
else if (e.key === 'Escape') { menu.className = 'cmd-menu'; }
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
316
773
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
774
|
+
if (e.ctrlKey && e.key === 'n') { e.preventDefault(); newChat(); }
|
|
317
775
|
const el = e.target;
|
|
318
776
|
el.style.height = 'auto';
|
|
319
|
-
el.style.height = Math.min(el.scrollHeight,
|
|
777
|
+
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
|
320
778
|
}
|
|
321
779
|
|
|
322
780
|
function go(t) { document.getElementById('inp').value = t; send(); }
|
|
@@ -324,10 +782,15 @@ function go(t) { document.getElementById('inp').value = t; send(); }
|
|
|
324
782
|
async function newChat() {
|
|
325
783
|
await pywebview.api.new_chat();
|
|
326
784
|
document.getElementById('msgs').innerHTML =
|
|
327
|
-
'<div class="welcome" id="welcome"><h2
|
|
328
|
-
+ '<div class="thinking" id="think"><
|
|
785
|
+
'<div class="welcome" id="welcome"><h2>New conversation</h2><p>How can I help?</p></div>'
|
|
786
|
+
+ '<div class="thinking" id="think"><div class="spinner"></div> Thinking...</div>';
|
|
787
|
+
loadChats();
|
|
788
|
+
document.getElementById('inp').focus();
|
|
329
789
|
}
|
|
330
790
|
|
|
791
|
+
async function setModel(m) { await pywebview.api.set_model(m); checkStatus(); }
|
|
792
|
+
async function setPersona(p) { await pywebview.api.set_persona(p); }
|
|
793
|
+
|
|
331
794
|
async function send() {
|
|
332
795
|
const inp = document.getElementById('inp');
|
|
333
796
|
const text = inp.value.trim();
|
|
@@ -340,15 +803,17 @@ async function send() {
|
|
|
340
803
|
|
|
341
804
|
const r = JSON.parse(await pywebview.api.send_message(text));
|
|
342
805
|
document.getElementById('think').className = 'thinking';
|
|
343
|
-
addMsg('ai', r.content, r.tokens + ' tokens
|
|
806
|
+
addMsg('ai', r.content, r.tokens + ' tokens · ' + r.elapsed + 's');
|
|
344
807
|
document.getElementById('tc').textContent = r.total_tokens.toLocaleString() + ' tokens';
|
|
345
808
|
|
|
346
809
|
busy = false;
|
|
347
810
|
document.getElementById('btn').disabled = false;
|
|
348
811
|
inp.focus();
|
|
812
|
+
loadChats();
|
|
349
813
|
}
|
|
350
814
|
|
|
351
815
|
window.addEventListener('pywebviewready', init);
|
|
816
|
+
const MODEL = '""" + MODEL + """';
|
|
352
817
|
</script>
|
|
353
818
|
</body>
|
|
354
819
|
</html>
|
|
@@ -361,9 +826,9 @@ def main():
|
|
|
361
826
|
"CodeGPT",
|
|
362
827
|
html=HTML,
|
|
363
828
|
js_api=api,
|
|
364
|
-
width=
|
|
365
|
-
height=
|
|
366
|
-
min_size=(
|
|
829
|
+
width=1000,
|
|
830
|
+
height=700,
|
|
831
|
+
min_size=(600, 400),
|
|
367
832
|
background_color="#0d1117",
|
|
368
833
|
text_select=True,
|
|
369
834
|
)
|