codegpt-ai 2.17.0 → 2.26.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 +839 -0
- package/package.json +3 -1
- package/tui.py +436 -0
package/desktop.py
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
"""CodeGPT Desktop — Claude + ChatGPT + OpenClaw style GUI."""
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import requests
|
|
5
|
+
import webview
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
# Config
|
|
11
|
+
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/chat")
|
|
12
|
+
MODEL = "llama3.2"
|
|
13
|
+
SYSTEM = "You are a helpful AI assistant. Be concise and technical."
|
|
14
|
+
|
|
15
|
+
saved_url = Path.home() / ".codegpt" / "ollama_url"
|
|
16
|
+
if saved_url.exists():
|
|
17
|
+
url = saved_url.read_text().strip()
|
|
18
|
+
if url:
|
|
19
|
+
OLLAMA_URL = url
|
|
20
|
+
if "/api/chat" not in OLLAMA_URL:
|
|
21
|
+
OLLAMA_URL = OLLAMA_URL.rstrip("/") + "/api/chat"
|
|
22
|
+
|
|
23
|
+
def try_connect(url):
|
|
24
|
+
try:
|
|
25
|
+
r = requests.get(url.replace("/api/chat", "/api/tags"), timeout=3)
|
|
26
|
+
return [m["name"] for m in r.json().get("models", [])]
|
|
27
|
+
except:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
if not try_connect(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
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
profile_file = Path.home() / ".codegpt" / "profiles" / "cli_profile.json"
|
|
37
|
+
USERNAME = "User"
|
|
38
|
+
PERSONA = "default"
|
|
39
|
+
if profile_file.exists():
|
|
40
|
+
try:
|
|
41
|
+
p = json.loads(profile_file.read_text())
|
|
42
|
+
USERNAME = p.get("name", "User")
|
|
43
|
+
MODEL = p.get("model", MODEL)
|
|
44
|
+
PERSONA = p.get("persona", "default")
|
|
45
|
+
except:
|
|
46
|
+
pass
|
|
47
|
+
|
|
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
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Api:
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.messages = []
|
|
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")
|
|
70
|
+
|
|
71
|
+
def check_status(self):
|
|
72
|
+
try:
|
|
73
|
+
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
74
|
+
models = [m["name"] for m in r.json().get("models", [])]
|
|
75
|
+
return json.dumps({"online": True, "models": models, "model": self.model, "persona": self.persona})
|
|
76
|
+
except:
|
|
77
|
+
return json.dumps({"online": False, "models": [], "model": self.model, "persona": self.persona})
|
|
78
|
+
|
|
79
|
+
def send_message(self, text):
|
|
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})
|
|
85
|
+
|
|
86
|
+
self.messages.append({"role": "user", "content": text})
|
|
87
|
+
try:
|
|
88
|
+
start = time.time()
|
|
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)
|
|
94
|
+
data = resp.json()
|
|
95
|
+
content = data.get("message", {}).get("content", "No response.")
|
|
96
|
+
elapsed = round(time.time() - start, 1)
|
|
97
|
+
tokens = data.get("eval_count", 0)
|
|
98
|
+
self.total_tokens += tokens
|
|
99
|
+
self.messages.append({"role": "assistant", "content": content})
|
|
100
|
+
self._auto_save()
|
|
101
|
+
return json.dumps({"content": content, "tokens": tokens, "elapsed": elapsed, "total_tokens": self.total_tokens})
|
|
102
|
+
except Exception as e:
|
|
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
|
|
311
|
+
|
|
312
|
+
def new_chat(self):
|
|
313
|
+
self._auto_save()
|
|
314
|
+
self.messages = []
|
|
315
|
+
self.chat_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
316
|
+
return "ok"
|
|
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
|
+
|
|
337
|
+
def get_username(self):
|
|
338
|
+
return USERNAME
|
|
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
|
+
|
|
376
|
+
|
|
377
|
+
HTML = """
|
|
378
|
+
<!DOCTYPE html>
|
|
379
|
+
<html>
|
|
380
|
+
<head>
|
|
381
|
+
<meta charset="utf-8">
|
|
382
|
+
<style>
|
|
383
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
384
|
+
:root {
|
|
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;
|
|
388
|
+
}
|
|
389
|
+
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', -apple-system, sans-serif; height: 100vh; display: flex; overflow: hidden; }
|
|
390
|
+
|
|
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;
|
|
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; }
|
|
422
|
+
|
|
423
|
+
/* Main */
|
|
424
|
+
.main { flex: 1; display: flex; flex-direction: column; }
|
|
425
|
+
.header {
|
|
426
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
427
|
+
padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
|
|
428
|
+
}
|
|
429
|
+
.header h1 { font-size: 16px; font-weight: 600; }
|
|
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; }
|
|
438
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
|
|
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; }
|
|
448
|
+
.msg .body pre code { background: none; padding: 0; }
|
|
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; }
|
|
453
|
+
.thinking { display: none; color: var(--dim); font-size: 13px; margin-bottom: 16px; }
|
|
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; }
|
|
468
|
+
.input-wrap textarea:focus { border-color: var(--accent); }
|
|
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;
|
|
479
|
+
}
|
|
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;
|
|
484
|
+
}
|
|
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;
|
|
497
|
+
}
|
|
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; }
|
|
509
|
+
</style>
|
|
510
|
+
</head>
|
|
511
|
+
<body>
|
|
512
|
+
|
|
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>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
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>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
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>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
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>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<script>
|
|
583
|
+
let busy = false;
|
|
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
|
+
|
|
612
|
+
async function init() {
|
|
613
|
+
const name = await pywebview.api.get_username();
|
|
614
|
+
const h = new Date().getHours();
|
|
615
|
+
const g = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
|
|
616
|
+
document.getElementById('greeting').textContent = g + ', ' + name;
|
|
617
|
+
|
|
618
|
+
await loadModels();
|
|
619
|
+
await loadPersonas();
|
|
620
|
+
await loadChats();
|
|
621
|
+
checkStatus();
|
|
622
|
+
setInterval(checkStatus, 30000);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function checkStatus() {
|
|
626
|
+
const r = JSON.parse(await pywebview.api.check_status());
|
|
627
|
+
document.getElementById('dot').className = 'dot ' + (r.online ? 'on' : 'off');
|
|
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();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function esc(t) {
|
|
667
|
+
t = t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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
|
+
});
|
|
672
|
+
t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
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');
|
|
677
|
+
return t;
|
|
678
|
+
}
|
|
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
|
+
|
|
694
|
+
function addMsg(role, content, stats) {
|
|
695
|
+
const w = document.getElementById('welcome');
|
|
696
|
+
if (w) w.style.display = 'none';
|
|
697
|
+
const d = document.createElement('div');
|
|
698
|
+
d.className = 'msg ' + role;
|
|
699
|
+
const icon = role === 'user' ? 'U' : 'AI';
|
|
700
|
+
const label = role === 'user' ? 'You' : 'CodeGPT';
|
|
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>' : '');
|
|
704
|
+
const c = document.getElementById('msgs');
|
|
705
|
+
c.insertBefore(d, document.getElementById('think'));
|
|
706
|
+
c.scrollTop = c.scrollHeight;
|
|
707
|
+
}
|
|
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
|
+
|
|
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
|
+
}
|
|
773
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
774
|
+
if (e.ctrlKey && e.key === 'n') { e.preventDefault(); newChat(); }
|
|
775
|
+
const el = e.target;
|
|
776
|
+
el.style.height = 'auto';
|
|
777
|
+
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function go(t) { document.getElementById('inp').value = t; send(); }
|
|
781
|
+
|
|
782
|
+
async function newChat() {
|
|
783
|
+
await pywebview.api.new_chat();
|
|
784
|
+
document.getElementById('msgs').innerHTML =
|
|
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();
|
|
789
|
+
}
|
|
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
|
+
|
|
794
|
+
async function send() {
|
|
795
|
+
const inp = document.getElementById('inp');
|
|
796
|
+
const text = inp.value.trim();
|
|
797
|
+
if (!text || busy) return;
|
|
798
|
+
inp.value = ''; inp.style.height = 'auto';
|
|
799
|
+
busy = true;
|
|
800
|
+
document.getElementById('btn').disabled = true;
|
|
801
|
+
addMsg('user', text);
|
|
802
|
+
document.getElementById('think').className = 'thinking on';
|
|
803
|
+
|
|
804
|
+
const r = JSON.parse(await pywebview.api.send_message(text));
|
|
805
|
+
document.getElementById('think').className = 'thinking';
|
|
806
|
+
addMsg('ai', r.content, r.tokens + ' tokens · ' + r.elapsed + 's');
|
|
807
|
+
document.getElementById('tc').textContent = r.total_tokens.toLocaleString() + ' tokens';
|
|
808
|
+
|
|
809
|
+
busy = false;
|
|
810
|
+
document.getElementById('btn').disabled = false;
|
|
811
|
+
inp.focus();
|
|
812
|
+
loadChats();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
window.addEventListener('pywebviewready', init);
|
|
816
|
+
const MODEL = '""" + MODEL + """';
|
|
817
|
+
</script>
|
|
818
|
+
</body>
|
|
819
|
+
</html>
|
|
820
|
+
"""
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def main():
|
|
824
|
+
api = Api()
|
|
825
|
+
window = webview.create_window(
|
|
826
|
+
"CodeGPT",
|
|
827
|
+
html=HTML,
|
|
828
|
+
js_api=api,
|
|
829
|
+
width=1000,
|
|
830
|
+
height=700,
|
|
831
|
+
min_size=(600, 400),
|
|
832
|
+
background_color="#0d1117",
|
|
833
|
+
text_select=True,
|
|
834
|
+
)
|
|
835
|
+
webview.start(debug=False)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
if __name__ == "__main__":
|
|
839
|
+
main()
|