codegpt-ai 1.0.0 → 1.2.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/README.md +210 -0
- package/ai_cli/__pycache__/__init__.cpython-313.pyc +0 -0
- package/ai_cli/__pycache__/__main__.cpython-313.pyc +0 -0
- package/ai_cli/__pycache__/doctor.cpython-313.pyc +0 -0
- package/ai_cli/__pycache__/updater.cpython-313.pyc +0 -0
- package/ai_cli/updater.py +48 -2
- package/bin/ai.js +19 -18
- package/bin/chat.js +361 -0
- package/chat.py +36 -6
- package/package.json +8 -4
- package/.claude/settings.local.json +0 -7
- package/.github/workflows/release.yml +0 -40
- package/ai.spec +0 -81
- package/app.py +0 -958
- package/bot.py +0 -1453
- package/build.ps1 +0 -22
- package/codex-instructions.md +0 -11
- package/install-termux.sh +0 -158
- package/install.ps1 +0 -89
- package/mobile.py +0 -422
- package/pyproject.toml +0 -33
- package/requirements.txt +0 -12
- package/run.py +0 -157
- package/server.py +0 -335
- package/uninstall.ps1 +0 -30
- package/web.py +0 -728
package/web.py
DELETED
|
@@ -1,728 +0,0 @@
|
|
|
1
|
-
"""CodeGPT Web App — Flask + HTML/JS. Works on phone via Chrome."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import time
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from flask import Flask, request, jsonify, Response, stream_with_context, render_template_string
|
|
8
|
-
|
|
9
|
-
import requests as http_requests
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
from groq import Groq
|
|
13
|
-
HAS_GROQ = True
|
|
14
|
-
except ImportError:
|
|
15
|
-
HAS_GROQ = False
|
|
16
|
-
|
|
17
|
-
app = Flask(__name__)
|
|
18
|
-
|
|
19
|
-
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
|
20
|
-
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
|
21
|
-
DEFAULT_MODEL = "llama-3.2-3b-preview"
|
|
22
|
-
OLLAMA_MODEL = "llama3.2"
|
|
23
|
-
PROVIDER = "groq" if (GROQ_API_KEY and HAS_GROQ) else "ollama"
|
|
24
|
-
|
|
25
|
-
SYSTEM_PROMPT = """You are an AI modeled after a highly technical, system-focused developer mindset.
|
|
26
|
-
Be direct, concise, and dense with information. No fluff, no filler, no emojis.
|
|
27
|
-
Give conclusions first, then minimal necessary explanation.
|
|
28
|
-
Blunt but intelligent. Slightly dark tone is acceptable.
|
|
29
|
-
Keep responses concise."""
|
|
30
|
-
|
|
31
|
-
PERSONAS = {
|
|
32
|
-
"Default": SYSTEM_PROMPT,
|
|
33
|
-
"Hacker": "You are a cybersecurity expert. Technical jargon, CVEs, attack vectors. Defensive security only. Concise.",
|
|
34
|
-
"Teacher": "You are a patient programming teacher. Step by step, analogies, examples. Concise.",
|
|
35
|
-
"Roast": "You are a brutally sarcastic code reviewer. Roast then fix. Dark humor. Concise.",
|
|
36
|
-
"Minimal": "Shortest possible answer. One line if possible. Code only.",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
HTML = """<!DOCTYPE html>
|
|
40
|
-
<html lang="en">
|
|
41
|
-
<head>
|
|
42
|
-
<meta charset="UTF-8">
|
|
43
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
44
|
-
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
45
|
-
<meta name="mobile-web-app-capable" content="yes">
|
|
46
|
-
<meta name="theme-color" content="#0d1117">
|
|
47
|
-
<title>CodeGPT</title>
|
|
48
|
-
<link rel="manifest" href="/manifest.json">
|
|
49
|
-
<style>
|
|
50
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
51
|
-
body {
|
|
52
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
53
|
-
background: #0d1117;
|
|
54
|
-
color: #c9d1d9;
|
|
55
|
-
height: 100vh;
|
|
56
|
-
display: flex;
|
|
57
|
-
flex-direction: column;
|
|
58
|
-
overflow: hidden;
|
|
59
|
-
}
|
|
60
|
-
.header {
|
|
61
|
-
background: #161b22;
|
|
62
|
-
padding: 12px 16px;
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
justify-content: space-between;
|
|
66
|
-
border-bottom: 1px solid #30363d;
|
|
67
|
-
flex-shrink: 0;
|
|
68
|
-
}
|
|
69
|
-
.header h1 { font-size: 18px; color: #58a6ff; font-weight: 700; }
|
|
70
|
-
.header-btns { display: flex; gap: 8px; }
|
|
71
|
-
.header-btn {
|
|
72
|
-
background: none; border: 1px solid #30363d; color: #8b949e;
|
|
73
|
-
padding: 6px 10px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
|
74
|
-
}
|
|
75
|
-
.header-btn:hover { border-color: #58a6ff; color: #58a6ff; }
|
|
76
|
-
.chat {
|
|
77
|
-
flex: 1;
|
|
78
|
-
overflow-y: auto;
|
|
79
|
-
padding: 16px;
|
|
80
|
-
display: flex;
|
|
81
|
-
flex-direction: column;
|
|
82
|
-
gap: 12px;
|
|
83
|
-
scroll-behavior: smooth;
|
|
84
|
-
}
|
|
85
|
-
.msg { max-width: 85%; padding: 10px 14px; border-radius: 16px; line-height: 1.5; font-size: 14px; word-wrap: break-word; }
|
|
86
|
-
.msg.user {
|
|
87
|
-
align-self: flex-end;
|
|
88
|
-
background: rgba(88,166,255,0.15);
|
|
89
|
-
border: 1px solid rgba(88,166,255,0.2);
|
|
90
|
-
border-bottom-right-radius: 4px;
|
|
91
|
-
}
|
|
92
|
-
.msg.ai {
|
|
93
|
-
align-self: flex-start;
|
|
94
|
-
background: rgba(35,134,54,0.1);
|
|
95
|
-
border: 1px solid rgba(35,134,54,0.15);
|
|
96
|
-
border-bottom-left-radius: 4px;
|
|
97
|
-
}
|
|
98
|
-
.msg .role { font-size: 11px; font-weight: 700; margin-bottom: 4px; }
|
|
99
|
-
.msg.user .role { color: #58a6ff; }
|
|
100
|
-
.msg.ai .role { color: #238636; }
|
|
101
|
-
.msg .stats { font-size: 10px; color: #484f58; margin-top: 6px; }
|
|
102
|
-
.msg pre {
|
|
103
|
-
background: #161b22; padding: 8px 10px; border-radius: 8px;
|
|
104
|
-
overflow-x: auto; margin: 6px 0; font-size: 13px; border: 1px solid #30363d;
|
|
105
|
-
}
|
|
106
|
-
.msg code { font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 13px; }
|
|
107
|
-
.msg p { margin: 4px 0; }
|
|
108
|
-
.msg ul, .msg ol { padding-left: 20px; margin: 4px 0; }
|
|
109
|
-
.thinking {
|
|
110
|
-
align-self: flex-start;
|
|
111
|
-
color: #8b949e;
|
|
112
|
-
font-style: italic;
|
|
113
|
-
font-size: 13px;
|
|
114
|
-
display: flex;
|
|
115
|
-
align-items: center;
|
|
116
|
-
gap: 8px;
|
|
117
|
-
padding: 8px 14px;
|
|
118
|
-
}
|
|
119
|
-
.dot-pulse { display: flex; gap: 4px; }
|
|
120
|
-
.dot-pulse span {
|
|
121
|
-
width: 6px; height: 6px; background: #238636; border-radius: 50%;
|
|
122
|
-
animation: pulse 1.4s infinite;
|
|
123
|
-
}
|
|
124
|
-
.dot-pulse span:nth-child(2) { animation-delay: 0.2s; }
|
|
125
|
-
.dot-pulse span:nth-child(3) { animation-delay: 0.4s; }
|
|
126
|
-
@keyframes pulse { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } }
|
|
127
|
-
.input-bar {
|
|
128
|
-
display: flex;
|
|
129
|
-
gap: 8px;
|
|
130
|
-
padding: 10px 16px;
|
|
131
|
-
background: #161b22;
|
|
132
|
-
border-top: 1px solid #30363d;
|
|
133
|
-
flex-shrink: 0;
|
|
134
|
-
}
|
|
135
|
-
#msg-input {
|
|
136
|
-
flex: 1;
|
|
137
|
-
background: #0d1117;
|
|
138
|
-
border: 1px solid #30363d;
|
|
139
|
-
color: #c9d1d9;
|
|
140
|
-
padding: 10px 14px;
|
|
141
|
-
border-radius: 20px;
|
|
142
|
-
font-size: 14px;
|
|
143
|
-
outline: none;
|
|
144
|
-
}
|
|
145
|
-
#msg-input:focus { border-color: #58a6ff; }
|
|
146
|
-
#send-btn {
|
|
147
|
-
background: #238636;
|
|
148
|
-
border: none;
|
|
149
|
-
color: white;
|
|
150
|
-
width: 40px; height: 40px;
|
|
151
|
-
border-radius: 50%;
|
|
152
|
-
cursor: pointer;
|
|
153
|
-
font-size: 18px;
|
|
154
|
-
display: flex;
|
|
155
|
-
align-items: center;
|
|
156
|
-
justify-content: center;
|
|
157
|
-
flex-shrink: 0;
|
|
158
|
-
}
|
|
159
|
-
#send-btn:hover { background: #2ea043; }
|
|
160
|
-
#send-btn:disabled { background: #21262d; color: #484f58; cursor: not-allowed; }
|
|
161
|
-
.status {
|
|
162
|
-
font-size: 11px; color: #484f58; padding: 4px 16px;
|
|
163
|
-
background: #161b22; flex-shrink: 0;
|
|
164
|
-
}
|
|
165
|
-
.welcome {
|
|
166
|
-
display: flex; flex-direction: column; align-items: center;
|
|
167
|
-
justify-content: center; flex: 1; gap: 16px; padding: 32px;
|
|
168
|
-
}
|
|
169
|
-
.welcome h2 { color: #58a6ff; font-size: 24px; }
|
|
170
|
-
.welcome p { color: #8b949e; font-size: 14px; }
|
|
171
|
-
.suggestions { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 320px; }
|
|
172
|
-
.suggestion {
|
|
173
|
-
background: #161b22; border: 1px solid #30363d; color: #c9d1d9;
|
|
174
|
-
padding: 10px 14px; border-radius: 12px; cursor: pointer;
|
|
175
|
-
text-align: left; font-size: 13px;
|
|
176
|
-
}
|
|
177
|
-
.suggestion:hover { border-color: #58a6ff; }
|
|
178
|
-
.modal-overlay {
|
|
179
|
-
display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
180
|
-
background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center;
|
|
181
|
-
}
|
|
182
|
-
.modal-overlay.active { display: flex; }
|
|
183
|
-
.modal {
|
|
184
|
-
background: #161b22; border: 1px solid #30363d; border-radius: 16px;
|
|
185
|
-
padding: 24px; width: 90%; max-width: 360px;
|
|
186
|
-
}
|
|
187
|
-
.modal h3 { color: #58a6ff; margin-bottom: 16px; }
|
|
188
|
-
.modal select, .modal input[type=text] {
|
|
189
|
-
width: 100%; background: #0d1117; border: 1px solid #30363d;
|
|
190
|
-
color: #c9d1d9; padding: 8px 12px; border-radius: 8px; margin: 8px 0; font-size: 14px;
|
|
191
|
-
}
|
|
192
|
-
.modal-btns { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
|
193
|
-
.modal-btns button {
|
|
194
|
-
padding: 8px 16px; border-radius: 8px; border: none; cursor: pointer; font-size: 13px;
|
|
195
|
-
}
|
|
196
|
-
.btn-cancel { background: #21262d; color: #c9d1d9; }
|
|
197
|
-
.btn-save { background: #238636; color: white; }
|
|
198
|
-
</style>
|
|
199
|
-
</head>
|
|
200
|
-
<body>
|
|
201
|
-
|
|
202
|
-
<div class="header">
|
|
203
|
-
<h1>CodeGPT</h1>
|
|
204
|
-
<div class="header-btns">
|
|
205
|
-
<button class="header-btn" onclick="newChat()">New</button>
|
|
206
|
-
<button class="header-btn" onclick="openSettings()">Settings</button>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<div class="chat" id="chat"></div>
|
|
211
|
-
|
|
212
|
-
<div class="status" id="status">Ready</div>
|
|
213
|
-
|
|
214
|
-
<div class="input-bar">
|
|
215
|
-
<input type="text" id="msg-input" placeholder="Message CodeGPT..." autocomplete="off">
|
|
216
|
-
<button id="send-btn" onclick="send()">▶</button>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
|
-
<div class="modal-overlay" id="settings-modal">
|
|
220
|
-
<div class="modal">
|
|
221
|
-
<h3>Settings</h3>
|
|
222
|
-
<label style="color:#8b949e;font-size:12px">Persona</label>
|
|
223
|
-
<select id="persona-select">
|
|
224
|
-
<option>Default</option><option>Hacker</option><option>Teacher</option>
|
|
225
|
-
<option>Roast</option><option>Minimal</option>
|
|
226
|
-
</select>
|
|
227
|
-
<div style="border-top:1px solid #30363d;margin-top:16px;padding-top:12px">
|
|
228
|
-
<button onclick="showInstallGuide()" style="width:100%;background:#238636;border:none;color:white;padding:10px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:700" id="install-btn-settings">Install App</button>
|
|
229
|
-
</div>
|
|
230
|
-
<div class="modal-btns">
|
|
231
|
-
<button class="btn-cancel" onclick="closeSettings()">Close</button>
|
|
232
|
-
<button class="btn-save" onclick="saveSettings()">Save</button>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
<script>
|
|
238
|
-
const chat = document.getElementById('chat');
|
|
239
|
-
const input = document.getElementById('msg-input');
|
|
240
|
-
const sendBtn = document.getElementById('send-btn');
|
|
241
|
-
const status = document.getElementById('status');
|
|
242
|
-
|
|
243
|
-
let messages = [];
|
|
244
|
-
let persona = localStorage.getItem('persona') || 'Default';
|
|
245
|
-
let msgCount = 0;
|
|
246
|
-
let streaming = false;
|
|
247
|
-
|
|
248
|
-
document.getElementById('persona-select').value = persona;
|
|
249
|
-
|
|
250
|
-
// Welcome
|
|
251
|
-
showWelcome();
|
|
252
|
-
|
|
253
|
-
input.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }});
|
|
254
|
-
|
|
255
|
-
function showWelcome() {
|
|
256
|
-
const suggestions = [
|
|
257
|
-
'Explain how TCP/IP works',
|
|
258
|
-
'Write a Python CPU monitor',
|
|
259
|
-
'OWASP top 10 explained',
|
|
260
|
-
'Design a REST API',
|
|
261
|
-
];
|
|
262
|
-
chat.innerHTML = `
|
|
263
|
-
<div class="welcome">
|
|
264
|
-
<h2>CodeGPT</h2>
|
|
265
|
-
<p>Local AI assistant</p>
|
|
266
|
-
<div class="suggestions">
|
|
267
|
-
${suggestions.map(s => `<button class="suggestion" onclick="sendText('${s}')">${s}</button>`).join('')}
|
|
268
|
-
</div>
|
|
269
|
-
</div>
|
|
270
|
-
`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function sendText(text) { input.value = text; send(); }
|
|
274
|
-
|
|
275
|
-
function addMsg(role, content, stats) {
|
|
276
|
-
const div = document.createElement('div');
|
|
277
|
-
div.className = 'msg ' + role;
|
|
278
|
-
let html = `<div class="role">${role === 'user' ? 'You' : 'AI'}</div>`;
|
|
279
|
-
if (role === 'ai') {
|
|
280
|
-
html += formatMarkdown(content);
|
|
281
|
-
} else {
|
|
282
|
-
html += `<div>${escapeHtml(content)}</div>`;
|
|
283
|
-
}
|
|
284
|
-
if (stats) html += `<div class="stats">${stats}</div>`;
|
|
285
|
-
div.innerHTML = html;
|
|
286
|
-
chat.appendChild(div);
|
|
287
|
-
chat.scrollTop = chat.scrollHeight;
|
|
288
|
-
return div;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function addThinking() {
|
|
292
|
-
const div = document.createElement('div');
|
|
293
|
-
div.className = 'thinking';
|
|
294
|
-
div.id = 'thinking';
|
|
295
|
-
div.innerHTML = '<div class="dot-pulse"><span></span><span></span><span></span></div> Thinking...';
|
|
296
|
-
chat.appendChild(div);
|
|
297
|
-
chat.scrollTop = chat.scrollHeight;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function removeThinking() {
|
|
301
|
-
const el = document.getElementById('thinking');
|
|
302
|
-
if (el) el.remove();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function send() {
|
|
306
|
-
const text = input.value.trim();
|
|
307
|
-
if (!text || streaming) return;
|
|
308
|
-
|
|
309
|
-
// Clear welcome
|
|
310
|
-
const welcome = chat.querySelector('.welcome');
|
|
311
|
-
if (welcome) welcome.remove();
|
|
312
|
-
|
|
313
|
-
input.value = '';
|
|
314
|
-
addMsg('user', text);
|
|
315
|
-
addThinking();
|
|
316
|
-
messages.push({role: 'user', content: text});
|
|
317
|
-
msgCount++;
|
|
318
|
-
streaming = true;
|
|
319
|
-
sendBtn.disabled = true;
|
|
320
|
-
status.textContent = 'Streaming...';
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
const resp = await fetch('/chat', {
|
|
324
|
-
method: 'POST',
|
|
325
|
-
headers: {'Content-Type': 'application/json'},
|
|
326
|
-
body: JSON.stringify({messages, persona}),
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
const reader = resp.body.getReader();
|
|
330
|
-
const decoder = new TextDecoder();
|
|
331
|
-
let full = '';
|
|
332
|
-
let stats = '';
|
|
333
|
-
let aiDiv = null;
|
|
334
|
-
|
|
335
|
-
removeThinking();
|
|
336
|
-
|
|
337
|
-
while (true) {
|
|
338
|
-
const {done, value} = await reader.read();
|
|
339
|
-
if (done) break;
|
|
340
|
-
|
|
341
|
-
const lines = decoder.decode(value, {stream: true}).split('\\n');
|
|
342
|
-
for (const line of lines) {
|
|
343
|
-
if (!line.trim()) continue;
|
|
344
|
-
try {
|
|
345
|
-
const chunk = JSON.parse(line);
|
|
346
|
-
if (chunk.content) full += chunk.content;
|
|
347
|
-
if (chunk.done) {
|
|
348
|
-
stats = chunk.stats || `${chunk.provider || ''}`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (aiDiv) {
|
|
352
|
-
aiDiv.querySelector('.ai-content').innerHTML = formatMarkdown(full);
|
|
353
|
-
} else {
|
|
354
|
-
aiDiv = document.createElement('div');
|
|
355
|
-
aiDiv.className = 'msg ai';
|
|
356
|
-
aiDiv.innerHTML = `<div class="role">AI</div><div class="ai-content">${formatMarkdown(full)}</div><div class="stats"></div>`;
|
|
357
|
-
chat.appendChild(aiDiv);
|
|
358
|
-
}
|
|
359
|
-
chat.scrollTop = chat.scrollHeight;
|
|
360
|
-
} catch(e) {}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (aiDiv && stats) {
|
|
365
|
-
aiDiv.querySelector('.stats').textContent = stats;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (full) {
|
|
369
|
-
messages.push({role: 'assistant', content: full});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
} catch(e) {
|
|
373
|
-
removeThinking();
|
|
374
|
-
addMsg('ai', 'Error: ' + e.message);
|
|
375
|
-
if (messages.length && messages[messages.length-1].role === 'user') messages.pop();
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
streaming = false;
|
|
379
|
-
sendBtn.disabled = false;
|
|
380
|
-
status.textContent = `${persona} | ${msgCount} msgs`;
|
|
381
|
-
input.focus();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function newChat() {
|
|
385
|
-
messages = [];
|
|
386
|
-
msgCount = 0;
|
|
387
|
-
showWelcome();
|
|
388
|
-
status.textContent = 'Ready';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function openSettings() { document.getElementById('settings-modal').classList.add('active'); }
|
|
392
|
-
function closeSettings() { document.getElementById('settings-modal').classList.remove('active'); }
|
|
393
|
-
function saveSettings() {
|
|
394
|
-
persona = document.getElementById('persona-select').value;
|
|
395
|
-
localStorage.setItem('persona', persona);
|
|
396
|
-
status.textContent = `Persona: ${persona}`;
|
|
397
|
-
closeSettings();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function escapeHtml(s) {
|
|
401
|
-
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function formatMarkdown(text) {
|
|
405
|
-
// Sanitize HTML to prevent XSS from AI output
|
|
406
|
-
text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
407
|
-
// Code blocks
|
|
408
|
-
text = text.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
409
|
-
// Inline code
|
|
410
|
-
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
411
|
-
// Bold
|
|
412
|
-
text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
|
413
|
-
// Italic
|
|
414
|
-
text = text.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
|
415
|
-
// Lists
|
|
416
|
-
text = text.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
417
|
-
text = text.replace(/^\\d+\\. (.+)$/gm, '<li>$1</li>');
|
|
418
|
-
// Paragraphs
|
|
419
|
-
text = text.replace(/\\n\\n/g, '</p><p>');
|
|
420
|
-
text = text.replace(/\\n/g, '<br>');
|
|
421
|
-
return '<p>' + text + '</p>';
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// --- PWA Install ---
|
|
425
|
-
let deferredPrompt = null;
|
|
426
|
-
|
|
427
|
-
window.addEventListener('beforeinstallprompt', e => {
|
|
428
|
-
e.preventDefault();
|
|
429
|
-
deferredPrompt = e;
|
|
430
|
-
showInstallBanner();
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
function showInstallBanner() {
|
|
434
|
-
if (document.getElementById('install-banner')) return;
|
|
435
|
-
const banner = document.createElement('div');
|
|
436
|
-
banner.id = 'install-banner';
|
|
437
|
-
banner.style.cssText = `
|
|
438
|
-
position: fixed; bottom: 70px; left: 16px; right: 16px;
|
|
439
|
-
background: linear-gradient(135deg, #238636, #1a7f2b);
|
|
440
|
-
color: white; padding: 14px 18px; border-radius: 14px;
|
|
441
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
442
|
-
box-shadow: 0 4px 20px rgba(35,134,54,0.4);
|
|
443
|
-
z-index: 200; animation: slideUp 0.3s ease;
|
|
444
|
-
font-size: 14px;
|
|
445
|
-
`;
|
|
446
|
-
banner.innerHTML = `
|
|
447
|
-
<div>
|
|
448
|
-
<div style="font-weight:700">Install CodeGPT</div>
|
|
449
|
-
<div style="font-size:12px;opacity:0.8">Add to home screen for the full app experience</div>
|
|
450
|
-
</div>
|
|
451
|
-
<div style="display:flex;gap:8px">
|
|
452
|
-
<button onclick="dismissInstall()" style="background:rgba(255,255,255,0.2);border:none;color:white;padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px">Later</button>
|
|
453
|
-
<button onclick="installApp()" style="background:white;border:none;color:#238636;padding:8px 16px;border-radius:8px;cursor:pointer;font-weight:700;font-size:13px">Install</button>
|
|
454
|
-
</div>
|
|
455
|
-
`;
|
|
456
|
-
document.body.appendChild(banner);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function installApp() {
|
|
460
|
-
if (!deferredPrompt) return;
|
|
461
|
-
deferredPrompt.prompt();
|
|
462
|
-
const result = await deferredPrompt.userChoice;
|
|
463
|
-
if (result.outcome === 'accepted') {
|
|
464
|
-
document.getElementById('install-banner')?.remove();
|
|
465
|
-
}
|
|
466
|
-
deferredPrompt = null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function dismissInstall() {
|
|
470
|
-
document.getElementById('install-banner')?.remove();
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function showInstallGuide() {
|
|
474
|
-
if (deferredPrompt) {
|
|
475
|
-
installApp();
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
// Manual install instructions
|
|
479
|
-
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
480
|
-
const isAndroid = /Android/.test(navigator.userAgent);
|
|
481
|
-
let steps = '';
|
|
482
|
-
if (isIOS) {
|
|
483
|
-
steps = `
|
|
484
|
-
<div style="font-size:14px;line-height:1.8">
|
|
485
|
-
<b>iOS Install:</b><br>
|
|
486
|
-
1. Tap the <b>Share</b> button (box with arrow)<br>
|
|
487
|
-
2. Scroll down and tap <b>"Add to Home Screen"</b><br>
|
|
488
|
-
3. Tap <b>"Add"</b>
|
|
489
|
-
</div>`;
|
|
490
|
-
} else if (isAndroid) {
|
|
491
|
-
steps = `
|
|
492
|
-
<div style="font-size:14px;line-height:1.8">
|
|
493
|
-
<b>Android Install:</b><br>
|
|
494
|
-
1. Tap the <b>3-dot menu</b> (top right)<br>
|
|
495
|
-
2. Tap <b>"Add to Home Screen"</b><br>
|
|
496
|
-
or <b>"Install app"</b><br>
|
|
497
|
-
3. Tap <b>"Install"</b>
|
|
498
|
-
</div>`;
|
|
499
|
-
} else {
|
|
500
|
-
steps = `
|
|
501
|
-
<div style="font-size:14px;line-height:1.8">
|
|
502
|
-
<b>Desktop Install:</b><br>
|
|
503
|
-
1. Click the <b>install icon</b> in the URL bar<br>
|
|
504
|
-
(right side, looks like a monitor with arrow)<br>
|
|
505
|
-
2. Click <b>"Install"</b>
|
|
506
|
-
</div>`;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const overlay = document.createElement('div');
|
|
510
|
-
overlay.className = 'modal-overlay active';
|
|
511
|
-
overlay.id = 'install-guide';
|
|
512
|
-
overlay.innerHTML = `
|
|
513
|
-
<div class="modal">
|
|
514
|
-
<h3>Install CodeGPT</h3>
|
|
515
|
-
${steps}
|
|
516
|
-
<div class="modal-btns">
|
|
517
|
-
<button class="btn-save" onclick="document.getElementById('install-guide').remove()">Got it</button>
|
|
518
|
-
</div>
|
|
519
|
-
</div>
|
|
520
|
-
`;
|
|
521
|
-
document.body.appendChild(overlay);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
window.addEventListener('appinstalled', () => {
|
|
525
|
-
document.getElementById('install-banner')?.remove();
|
|
526
|
-
deferredPrompt = null;
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Hide install button if already installed
|
|
530
|
-
if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone) {
|
|
531
|
-
const ib = document.getElementById('install-btn-settings');
|
|
532
|
-
if (ib) ib.style.display = 'none';
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Register service worker
|
|
536
|
-
if ('serviceWorker' in navigator) {
|
|
537
|
-
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
538
|
-
}
|
|
539
|
-
</script>
|
|
540
|
-
|
|
541
|
-
<style>
|
|
542
|
-
@keyframes slideUp {
|
|
543
|
-
from { transform: translateY(100px); opacity: 0; }
|
|
544
|
-
to { transform: translateY(0); opacity: 1; }
|
|
545
|
-
}
|
|
546
|
-
</style>
|
|
547
|
-
</body>
|
|
548
|
-
</html>"""
|
|
549
|
-
|
|
550
|
-
SW_JS = """self.addEventListener('install', e => self.skipWaiting());
|
|
551
|
-
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
|
|
552
|
-
self.addEventListener('fetch', e => e.respondWith(fetch(e.request).catch(() => new Response('Offline'))));"""
|
|
553
|
-
|
|
554
|
-
MANIFEST = """{
|
|
555
|
-
"name": "CodeGPT",
|
|
556
|
-
"short_name": "CodeGPT",
|
|
557
|
-
"description": "Local AI coding assistant",
|
|
558
|
-
"start_url": "/",
|
|
559
|
-
"display": "standalone",
|
|
560
|
-
"orientation": "portrait",
|
|
561
|
-
"background_color": "#0d1117",
|
|
562
|
-
"theme_color": "#0d1117",
|
|
563
|
-
"categories": ["productivity", "utilities"],
|
|
564
|
-
"icons": [
|
|
565
|
-
{"src": "/icon", "sizes": "192x192", "type": "image/svg+xml", "purpose": "any maskable"},
|
|
566
|
-
{"src": "/icon-512", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any maskable"}
|
|
567
|
-
]
|
|
568
|
-
}"""
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
# --- Routes ---
|
|
572
|
-
|
|
573
|
-
@app.route("/")
|
|
574
|
-
def index():
|
|
575
|
-
return render_template_string(HTML)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
@app.route("/sw.js")
|
|
579
|
-
def service_worker():
|
|
580
|
-
return Response(SW_JS, content_type="application/javascript")
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
@app.route("/manifest.json")
|
|
584
|
-
def manifest():
|
|
585
|
-
return Response(MANIFEST, content_type="application/json")
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
@app.route("/icon")
|
|
589
|
-
def icon():
|
|
590
|
-
svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
|
591
|
-
<rect width="192" height="192" rx="32" fill="#0d1117"/>
|
|
592
|
-
<text x="96" y="120" text-anchor="middle" font-family="monospace" font-size="72" font-weight="bold" fill="#58a6ff">G</text>
|
|
593
|
-
</svg>'''
|
|
594
|
-
return Response(svg, content_type="image/svg+xml")
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
@app.route("/icon-512")
|
|
598
|
-
def icon_512():
|
|
599
|
-
svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
|
600
|
-
<rect width="512" height="512" rx="64" fill="#0d1117"/>
|
|
601
|
-
<text x="256" y="320" text-anchor="middle" font-family="monospace" font-size="192" font-weight="bold" fill="#58a6ff">G</text>
|
|
602
|
-
</svg>'''
|
|
603
|
-
return Response(svg, content_type="image/svg+xml")
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
@app.route("/chat", methods=["POST"])
|
|
607
|
-
def chat_stream():
|
|
608
|
-
data = request.get_json()
|
|
609
|
-
msgs = data.get("messages", [])
|
|
610
|
-
persona = data.get("persona", "Default")
|
|
611
|
-
system = PERSONAS.get(persona, SYSTEM_PROMPT)
|
|
612
|
-
|
|
613
|
-
if not msgs:
|
|
614
|
-
return jsonify({"error": "No messages"}), 400
|
|
615
|
-
|
|
616
|
-
try:
|
|
617
|
-
if PROVIDER == "groq":
|
|
618
|
-
gen = _stream_groq(msgs, system)
|
|
619
|
-
else:
|
|
620
|
-
gen = _stream_ollama(msgs, system)
|
|
621
|
-
return Response(stream_with_context(gen), content_type="application/x-ndjson")
|
|
622
|
-
except Exception as e:
|
|
623
|
-
return jsonify({"error": str(e)}), 500
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def _stream_groq(messages, system):
|
|
627
|
-
client = Groq(api_key=GROQ_API_KEY)
|
|
628
|
-
full_messages = [{"role": "system", "content": system}] + messages
|
|
629
|
-
completion = client.chat.completions.create(
|
|
630
|
-
model=DEFAULT_MODEL, messages=full_messages, stream=True, max_tokens=4096,
|
|
631
|
-
)
|
|
632
|
-
for chunk in completion:
|
|
633
|
-
delta = chunk.choices[0].delta
|
|
634
|
-
if delta and delta.content:
|
|
635
|
-
yield json.dumps({"content": delta.content, "done": False}) + "\n"
|
|
636
|
-
yield json.dumps({"content": "", "done": True, "provider": "groq", "stats": "groq cloud"}) + "\n"
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def _stream_ollama(messages, system):
|
|
640
|
-
full_messages = [{"role": "system", "content": system}] + messages
|
|
641
|
-
response = http_requests.post(
|
|
642
|
-
f"{OLLAMA_URL}/api/chat",
|
|
643
|
-
json={"model": OLLAMA_MODEL, "messages": full_messages, "stream": True},
|
|
644
|
-
stream=True, timeout=120,
|
|
645
|
-
)
|
|
646
|
-
response.raise_for_status()
|
|
647
|
-
for line in response.iter_lines():
|
|
648
|
-
if not line:
|
|
649
|
-
continue
|
|
650
|
-
try:
|
|
651
|
-
chunk = json.loads(line)
|
|
652
|
-
except json.JSONDecodeError:
|
|
653
|
-
continue
|
|
654
|
-
content = chunk.get("message", {}).get("content", "")
|
|
655
|
-
done = chunk.get("done", False)
|
|
656
|
-
out = {"content": content, "done": done}
|
|
657
|
-
if done:
|
|
658
|
-
ec = chunk.get("eval_count", 0)
|
|
659
|
-
td = chunk.get("total_duration", 0)
|
|
660
|
-
ds = td / 1e9 if td else 0
|
|
661
|
-
tps = ec / ds if ds > 0 else 0
|
|
662
|
-
out["stats"] = f"{ec} tok | {ds:.1f}s | {tps:.0f} tok/s"
|
|
663
|
-
out["provider"] = "ollama"
|
|
664
|
-
yield json.dumps(out) + "\n"
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
@app.route("/health")
|
|
668
|
-
def health():
|
|
669
|
-
return jsonify({"status": "ok", "provider": PROVIDER})
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def generate_ssl_cert():
|
|
673
|
-
"""Generate self-signed SSL cert for HTTPS (required for PWA install)."""
|
|
674
|
-
cert_dir = Path.home() / ".codegpt" / "ssl"
|
|
675
|
-
cert_file = cert_dir / "cert.pem"
|
|
676
|
-
key_file = cert_dir / "key.pem"
|
|
677
|
-
|
|
678
|
-
if cert_file.exists() and key_file.exists():
|
|
679
|
-
return str(cert_file), str(key_file)
|
|
680
|
-
|
|
681
|
-
cert_dir.mkdir(parents=True, exist_ok=True)
|
|
682
|
-
|
|
683
|
-
try:
|
|
684
|
-
from OpenSSL import crypto
|
|
685
|
-
k = crypto.PKey()
|
|
686
|
-
k.generate_key(crypto.TYPE_RSA, 2048)
|
|
687
|
-
|
|
688
|
-
cert = crypto.X509()
|
|
689
|
-
cert.get_subject().CN = "CodeGPT"
|
|
690
|
-
cert.set_serial_number(1000)
|
|
691
|
-
cert.gmtime_adj_notBefore(0)
|
|
692
|
-
cert.gmtime_adj_notAfter(365 * 24 * 60 * 60) # 1 year
|
|
693
|
-
cert.set_issuer(cert.get_subject())
|
|
694
|
-
cert.set_pubkey(k)
|
|
695
|
-
cert.sign(k, "sha256")
|
|
696
|
-
|
|
697
|
-
cert_file.write_bytes(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
|
698
|
-
key_file.write_bytes(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
|
|
699
|
-
|
|
700
|
-
return str(cert_file), str(key_file)
|
|
701
|
-
except ImportError:
|
|
702
|
-
print(" pyopenssl not installed. Running HTTP only (PWA install won't work).")
|
|
703
|
-
return None, None
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if __name__ == "__main__":
|
|
707
|
-
import ssl
|
|
708
|
-
PORT = int(os.environ.get("PORT", 5050))
|
|
709
|
-
|
|
710
|
-
cert, key = generate_ssl_cert()
|
|
711
|
-
|
|
712
|
-
print(f"\n CodeGPT Web App")
|
|
713
|
-
print(f" Provider: {PROVIDER}")
|
|
714
|
-
if cert:
|
|
715
|
-
print(f" https://localhost:{PORT}")
|
|
716
|
-
print(f" https://192.168.1.237:{PORT} (open on phone)")
|
|
717
|
-
print(f" HTTPS enabled - PWA install ready")
|
|
718
|
-
else:
|
|
719
|
-
print(f" http://localhost:{PORT}")
|
|
720
|
-
print(f" http://192.168.1.237:{PORT} (open on phone)")
|
|
721
|
-
print()
|
|
722
|
-
|
|
723
|
-
if cert:
|
|
724
|
-
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
725
|
-
context.load_cert_chain(cert, key)
|
|
726
|
-
app.run(host="0.0.0.0", port=PORT, debug=False, ssl_context=context)
|
|
727
|
-
else:
|
|
728
|
-
app.run(host="0.0.0.0", port=PORT, debug=False)
|