localcoder 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- localcoder-0.3.0/.gitignore +20 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/PKG-INFO +1 -1
- {localcoder-0.2.1 → localcoder-0.3.0}/pyproject.toml +1 -1
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/localcoder_agent.py +205 -117
- localcoder-0.3.0/src/localcoder/templates/framework/_adapters/chat.js +78 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_adapters/image.js +61 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_adapters/text.js +16 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_adapters/voice.js +75 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_server/package.json +6 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_server/server.js +75 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_shell/base.css +357 -0
- localcoder-0.3.0/src/localcoder/templates/framework/_shell/base.js +63 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/chatbot/config.json +20 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/homework-helper/config.json +19 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/image-analyzer/config.json +19 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/ingredients-scanner/config.json +19 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/meeting-notes/config.json +18 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/photo-todo/config.json +19 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/translator/config.json +20 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/voice-analyzer/config.json +18 -0
- localcoder-0.3.0/src/localcoder/templates/framework/apps/voice-memo/config.json +18 -0
- localcoder-0.3.0/src/localcoder/templates/framework/build.py +234 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/tests/test_basic.py +43 -5
- localcoder-0.2.1/.gitignore +0 -7
- localcoder-0.2.1/uv.lock +0 -449
- {localcoder-0.2.1 → localcoder-0.3.0}/LICENSE +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/README.md +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/__init__.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/__main__.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/agent.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/backends.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/bench.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/cli.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/localcoder_display.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/setup.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/.env.local +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/next.config.ts +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/package.json +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/postcss.config.mjs +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/api/ai/route.ts +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/globals.css +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/layout.tsx +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/page.tsx +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/components/Chat.tsx +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/tsconfig.json +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/tui.py +0 -0
- {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/voice.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.eggs/
|
|
7
|
+
*.log
|
|
8
|
+
uv.lock
|
|
9
|
+
.venv/
|
|
10
|
+
.webui_secret_key
|
|
11
|
+
.localcoder-history.json
|
|
12
|
+
.localcoder-input-history
|
|
13
|
+
.localcoder-snapshots/
|
|
14
|
+
node_modules/
|
|
15
|
+
package-lock.json
|
|
16
|
+
dist/
|
|
17
|
+
build/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
__pycache__/
|
|
20
|
+
.pytest_cache/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localcoder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Local AI coding agent — auto-installs, auto-serves, zero config. Works with Gemma 4, Qwen 3.5, and any model via llama.cpp or Ollama.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AnassKartit/localcoder
|
|
6
6
|
Project-URL: Repository, https://github.com/AnassKartit/localcoder
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "localcoder"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Local AI coding agent — auto-installs, auto-serves, zero config. Works with Gemma 4, Qwen 3.5, and any model via llama.cpp or Ollama."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -2399,6 +2399,96 @@ def main(argv=None):
|
|
|
2399
2399
|
f" You CAN browse any website. Never say you cannot access a site.\n"
|
|
2400
2400
|
f"- PDFs: use read_pdf tool\n"
|
|
2401
2401
|
f"- Images: download with bash curl (auto-displays in terminal)\n\n"
|
|
2402
|
+
f"WHEN TO USE WHAT:\n"
|
|
2403
|
+
f"- Static page (fan page, portfolio, gallery, landing): just write ONE index.html file. No server needed. Use web_search to find images, embed them with <img src=url>.\n"
|
|
2404
|
+
f"- AI-powered app (analyzer, chatbot, scanner): use the 3-file pattern below (index.html + server.js + package.json).\n"
|
|
2405
|
+
f"- NEVER build an Express server for a simple static page.\n\n"
|
|
2406
|
+
f"WEB APP ARCHITECTURE (ONLY for AI-powered apps that need a backend):\n\n"
|
|
2407
|
+
f"APP STRUCTURE: Always 3 files in the SAME directory:\n"
|
|
2408
|
+
f" package.json, server.js, index.html (all inline CSS+JS)\n"
|
|
2409
|
+
f" server.js must serve index.html with: app.get('/', (req,res) => res.sendFile(__dirname+'/index.html'));\n\n"
|
|
2410
|
+
f"SERVER.JS TEMPLATE (copy this pattern exactly):\n"
|
|
2411
|
+
f" const express = require('express');\n"
|
|
2412
|
+
f" const app = express();\n"
|
|
2413
|
+
f" app.use(express.json({{limit:'50mb'}}));\n"
|
|
2414
|
+
f" const API_BASE = process.env.LLM_API_BASE || 'http://127.0.0.1:8089/v1';\n"
|
|
2415
|
+
f" const MODEL = process.env.LLM_MODEL || 'local';\n"
|
|
2416
|
+
f" app.get('/', (req,res) => res.sendFile(__dirname+'/index.html'));\n"
|
|
2417
|
+
f" app.post('/api/analyze', async (req,res) => {{\n"
|
|
2418
|
+
f" const {{message, image}} = req.body;\n"
|
|
2419
|
+
f" const userContent = image\n"
|
|
2420
|
+
f" ? [{{type:'text',text:message}}, {{type:'image_url',image_url:{{url:image}}}}]\n"
|
|
2421
|
+
f" : message;\n"
|
|
2422
|
+
f" const r = await fetch(API_BASE+'/chat/completions', {{\n"
|
|
2423
|
+
f" method:'POST', headers:{{'Content-Type':'application/json'}},\n"
|
|
2424
|
+
f" body:JSON.stringify({{model:MODEL, stream:false, max_tokens:2048,\n"
|
|
2425
|
+
f" messages:[{{role:'system',content:SYSTEM_PROMPT}}, {{role:'user',content:userContent}}]}}) }});\n"
|
|
2426
|
+
f" const data = await r.json();\n"
|
|
2427
|
+
f" res.json({{analysis: data.choices[0].message.content}}); }});\n"
|
|
2428
|
+
f" app.listen(3000);\n\n"
|
|
2429
|
+
f"FRONTEND INDEX.HTML PATTERNS:\n"
|
|
2430
|
+
f"- DESIGN: Dark theme bg:#0a0a14. Card: background:rgba(255,255,255,0.04); backdrop-filter:blur(20px); border:1px solid rgba(255,255,255,0.08); border-radius:24px.\n"
|
|
2431
|
+
f" Buttons: border-radius:14px; background:linear-gradient(135deg,#6366f1,#8b5cf6); color:white; font-weight:600; padding:14px 28px.\n"
|
|
2432
|
+
f" Title: font-size:2rem; font-weight:700; background:linear-gradient(to right,#f97316,#22c55e); -webkit-background-clip:text; color:transparent.\n"
|
|
2433
|
+
f" Result area: background:rgba(255,255,255,0.03); border-left:3px solid #22c55e; border-radius:16px; padding:24px; white-space:pre-wrap.\n"
|
|
2434
|
+
f" Use system-ui font. Add transition:all 0.2s on buttons. Loading: spinner animation.\n\n"
|
|
2435
|
+
f"- IMAGE UPLOAD (must use FileReader, never fake it):\n"
|
|
2436
|
+
f" let imageBase64 = null;\n"
|
|
2437
|
+
f" function uploadImage() {{\n"
|
|
2438
|
+
f" const input = document.createElement('input');\n"
|
|
2439
|
+
f" input.type='file'; input.accept='image/*';\n"
|
|
2440
|
+
f" input.onchange = e => {{\n"
|
|
2441
|
+
f" const file = e.target.files[0]; if(!file) return;\n"
|
|
2442
|
+
f" const reader = new FileReader();\n"
|
|
2443
|
+
f" reader.onload = ev => {{ imageBase64 = ev.target.result;\n"
|
|
2444
|
+
f" document.getElementById('preview').src = imageBase64;\n"
|
|
2445
|
+
f" document.getElementById('preview').style.display = 'block'; }};\n"
|
|
2446
|
+
f" reader.readAsDataURL(file); }};\n"
|
|
2447
|
+
f" input.click(); }}\n\n"
|
|
2448
|
+
f"- CAMERA CAPTURE (must use getUserMedia, never fake it):\n"
|
|
2449
|
+
f" async function openCamera() {{\n"
|
|
2450
|
+
f" const stream = await navigator.mediaDevices.getUserMedia({{video:{{facingMode:'environment'}}}});\n"
|
|
2451
|
+
f" const video = document.getElementById('camVideo');\n"
|
|
2452
|
+
f" video.srcObject = stream; video.style.display='block'; video.play();\n"
|
|
2453
|
+
f" document.getElementById('captureBtn').style.display='inline-block'; }}\n"
|
|
2454
|
+
f" function capturePhoto() {{\n"
|
|
2455
|
+
f" const video = document.getElementById('camVideo');\n"
|
|
2456
|
+
f" const canvas = document.createElement('canvas');\n"
|
|
2457
|
+
f" canvas.width=video.videoWidth; canvas.height=video.videoHeight;\n"
|
|
2458
|
+
f" canvas.getContext('2d').drawImage(video,0,0);\n"
|
|
2459
|
+
f" imageBase64 = canvas.toDataURL('image/jpeg',0.8);\n"
|
|
2460
|
+
f" document.getElementById('preview').src = imageBase64;\n"
|
|
2461
|
+
f" document.getElementById('preview').style.display='block';\n"
|
|
2462
|
+
f" video.srcObject.getTracks().forEach(t=>t.stop()); video.style.display='none'; }}\n\n"
|
|
2463
|
+
f"- SEND TO API (always this pattern):\n"
|
|
2464
|
+
f" async function analyze() {{\n"
|
|
2465
|
+
f" const msg = document.getElementById('input').value;\n"
|
|
2466
|
+
f" if(!msg && !imageBase64) return;\n"
|
|
2467
|
+
f" document.getElementById('result').innerHTML = '<div class=\"loading\">Analyzing...</div>';\n"
|
|
2468
|
+
f" const res = await fetch('/api/analyze', {{\n"
|
|
2469
|
+
f" method:'POST', headers:{{'Content-Type':'application/json'}},\n"
|
|
2470
|
+
f" body:JSON.stringify({{message:msg||'Analyze this', image:imageBase64}}) }});\n"
|
|
2471
|
+
f" const data = await res.json();\n"
|
|
2472
|
+
f" document.getElementById('result').innerHTML = formatMarkdown(data.analysis);\n"
|
|
2473
|
+
f" imageBase64 = null; }}\n\n"
|
|
2474
|
+
f"- MARKDOWN RENDERER:\n"
|
|
2475
|
+
f" function formatMarkdown(text) {{\n"
|
|
2476
|
+
f" return text.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>')\n"
|
|
2477
|
+
f" .replace(/^### (.+)$/gm,'<h3>$1</h3>').replace(/^## (.+)$/gm,'<h2>$1</h2>')\n"
|
|
2478
|
+
f" .replace(/^\\* (.+)$/gm,'<li>$1</li>').replace(/\\n/g,'<br>'); }}\n\n"
|
|
2479
|
+
f"- LOADING/SCANNING ANIMATION (always show while waiting for API):\n"
|
|
2480
|
+
f" CSS: @keyframes scan {{ 0%{{transform:translateY(-100%)}} 100%{{transform:translateY(100%)}} }}\n"
|
|
2481
|
+
f" .scanning {{ position:relative; overflow:hidden; }}\n"
|
|
2482
|
+
f" .scanning::after {{ content:''; position:absolute; left:0; right:0; height:2px;\n"
|
|
2483
|
+
f" background:linear-gradient(90deg,transparent,#22c55e,transparent); animation:scan 1.5s infinite; }}\n"
|
|
2484
|
+
f" Also add a pulsing text: <div class='loading'>🔬 Scanning ingredients<span class='dots'></span></div>\n"
|
|
2485
|
+
f" CSS: @keyframes dots {{ 0%{{content:''}} 33%{{content:'.'}} 66%{{content:'..'}} 100%{{content:'...'}} }}\n"
|
|
2486
|
+
f" .dots::after {{ content:''; animation:dots 1.5s infinite steps(4); }}\n"
|
|
2487
|
+
f" Show loading BEFORE fetch, hide AFTER response. Disable button during loading.\n\n"
|
|
2488
|
+
f"- NEVER fake FileReader/camera/API calls. ALWAYS use real implementations above.\n"
|
|
2489
|
+
f"- NEVER use SSE/streaming. Use stream:false and return full JSON.\n"
|
|
2490
|
+
f"- ALWAYS serve index.html from __dirname, NOT from a public/ subdirectory.\n"
|
|
2491
|
+
f"- ALWAYS test after building: npm install, node server.js &, curl POST to verify.\n\n"
|
|
2402
2492
|
f"RULES:\n"
|
|
2403
2493
|
f"- After reading a file ONCE, do NOT re-read it\n"
|
|
2404
2494
|
f"- Write complete code with write_file, not code blocks in chat\n"
|
|
@@ -2683,10 +2773,16 @@ def main(argv=None):
|
|
|
2683
2773
|
if text.startswith("/"):
|
|
2684
2774
|
for cmd, desc in SLASH_COMMANDS.items():
|
|
2685
2775
|
if text.lower() in cmd.lower() or cmd.startswith(text):
|
|
2776
|
+
# Escape XML-invalid chars in description
|
|
2777
|
+
safe_desc = desc.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
2778
|
+
try:
|
|
2779
|
+
display = PT_HTML_CMD(f'<b>{cmd}</b> <style fg="ansigray">{safe_desc}</style>')
|
|
2780
|
+
except Exception:
|
|
2781
|
+
display = f"{cmd} {desc}"
|
|
2686
2782
|
yield Completion(
|
|
2687
2783
|
cmd,
|
|
2688
2784
|
start_position=-len(text),
|
|
2689
|
-
display=
|
|
2785
|
+
display=display,
|
|
2690
2786
|
)
|
|
2691
2787
|
|
|
2692
2788
|
session = PromptSession(
|
|
@@ -3035,29 +3131,54 @@ def main(argv=None):
|
|
|
3035
3131
|
|
|
3036
3132
|
|
|
3037
3133
|
def _handle_deploy(task, messages, perms, system, console):
|
|
3038
|
-
"""
|
|
3039
|
-
import shutil
|
|
3134
|
+
"""Build an AI app from the framework templates."""
|
|
3040
3135
|
from rich.rule import Rule
|
|
3041
3136
|
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
"1": ("chatbot", "AI Chatbot", "You are a helpful AI assistant. Be concise and friendly.", "Ask me anything..."),
|
|
3050
|
-
"2": ("ingredients", "Ingredients Analyzer", "You are a food ingredients expert. When given a list of ingredients or a food product name, analyze each ingredient: explain what it is, whether it's natural or artificial, any health concerns, allergens, and give an overall healthiness rating from 1-10. Be specific and scientific but easy to understand.", "Paste ingredients list or food product name..."),
|
|
3051
|
-
"3": ("writer", "AI Writer", "You are a professional writer. Rewrite, summarize, translate, or improve any text the user provides. Match the requested tone and style. Be creative but accurate.", "Paste text to rewrite, summarize, or translate..."),
|
|
3052
|
-
"4": ("code-review", "Code Reviewer", "You are a senior software engineer. Review the code provided: find bugs, security issues, performance problems, and suggest improvements. Be specific with line references. Rate code quality 1-10.", "Paste code to review..."),
|
|
3053
|
-
"5": ("csv-analyzer","CSV Data Analyzer", "You are a data analyst. When given CSV data, analyze it: identify patterns, outliers, trends, and provide summary statistics. Give actionable insights. Format numbers clearly.", "Paste CSV data or describe your dataset..."),
|
|
3054
|
-
"6": ("custom", "Custom App", None, None),
|
|
3055
|
-
}
|
|
3137
|
+
# Load framework apps
|
|
3138
|
+
framework_dir = os.path.join(os.path.dirname(__file__), "templates", "framework")
|
|
3139
|
+
build_module = os.path.join(framework_dir, "build.py")
|
|
3140
|
+
|
|
3141
|
+
if not os.path.exists(build_module):
|
|
3142
|
+
console.print(f" [red]Framework not found[/]")
|
|
3143
|
+
return
|
|
3056
3144
|
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3145
|
+
# Import builder
|
|
3146
|
+
import importlib.util
|
|
3147
|
+
spec = importlib.util.spec_from_file_location("build", build_module)
|
|
3148
|
+
builder = importlib.util.module_from_spec(spec)
|
|
3149
|
+
spec.loader.exec_module(builder)
|
|
3150
|
+
|
|
3151
|
+
apps = builder.list_apps()
|
|
3152
|
+
if not apps:
|
|
3153
|
+
console.print(f" [red]No app templates found[/]")
|
|
3154
|
+
return
|
|
3155
|
+
|
|
3156
|
+
# Parse: /deploy or /deploy app-id or /deploy "description"
|
|
3157
|
+
parts = task.split(None, 1)
|
|
3158
|
+
arg = parts[1].strip() if len(parts) > 1 else None
|
|
3159
|
+
|
|
3160
|
+
# Direct app ID match
|
|
3161
|
+
if arg and any(a['id'] == arg for a in apps):
|
|
3162
|
+
selected = next(a for a in apps if a['id'] == arg)
|
|
3163
|
+
elif arg:
|
|
3164
|
+
# Fuzzy match or treat as custom description
|
|
3165
|
+
matched = [a for a in apps if arg.lower() in a['id'] or arg.lower() in a.get('title', '').lower()]
|
|
3166
|
+
if matched:
|
|
3167
|
+
selected = matched[0]
|
|
3168
|
+
else:
|
|
3169
|
+
# Custom: use chatbot template with custom prompt
|
|
3170
|
+
selected = next((a for a in apps if a['id'] == 'chatbot'), apps[0]).copy()
|
|
3171
|
+
selected['title'] = arg[:40]
|
|
3172
|
+
selected['subtitle'] = arg
|
|
3173
|
+
selected['system_prompt'] = f"You are an AI expert for: {arg}. Help the user with detailed, accurate responses. Use emoji and structured formatting."
|
|
3174
|
+
else:
|
|
3175
|
+
# Interactive picker
|
|
3176
|
+
console.print(f"\n [bold #34d399]⚡ Deploy — AI App Framework[/]\n")
|
|
3177
|
+
for i, a in enumerate(apps, 1):
|
|
3178
|
+
inputs = ', '.join(a.get('inputs', []))
|
|
3179
|
+
model = a.get('model', 'any')
|
|
3180
|
+
console.print(f" [bold cyan]{i}[/] {a['icon']} {a['title']:<20} [dim]{inputs:<18} {model}[/]")
|
|
3181
|
+
console.print(f" [bold cyan]{len(apps)+1}[/] 🛠️ Custom App")
|
|
3061
3182
|
console.print()
|
|
3062
3183
|
|
|
3063
3184
|
try:
|
|
@@ -3065,53 +3186,44 @@ def _handle_deploy(task, messages, perms, system, console):
|
|
|
3065
3186
|
except (EOFError, KeyboardInterrupt):
|
|
3066
3187
|
return
|
|
3067
3188
|
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
if
|
|
3189
|
+
try:
|
|
3190
|
+
idx = int(choice) - 1
|
|
3191
|
+
if idx == len(apps):
|
|
3192
|
+
# Custom
|
|
3071
3193
|
try:
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
title = description.split(".")[0][:40]
|
|
3194
|
+
desc = input(" Describe your app: ").strip()
|
|
3195
|
+
if not desc:
|
|
3196
|
+
return
|
|
3076
3197
|
except (EOFError, KeyboardInterrupt):
|
|
3077
3198
|
return
|
|
3078
|
-
if
|
|
3079
|
-
|
|
3199
|
+
selected = next((a for a in apps if a['id'] == 'chatbot'), apps[0]).copy()
|
|
3200
|
+
selected['title'] = desc[:40]
|
|
3201
|
+
selected['subtitle'] = desc
|
|
3202
|
+
selected['system_prompt'] = f"You are an AI expert for: {desc}. Help the user. Use emoji and structured markdown."
|
|
3203
|
+
elif 0 <= idx < len(apps):
|
|
3204
|
+
selected = apps[idx]
|
|
3080
3205
|
else:
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
# /deploy "ingredients analyzer"
|
|
3090
|
-
title = description[:40]
|
|
3091
|
-
sys_prompt = f"You are an AI expert for: {description}. Help the user with detailed, accurate responses."
|
|
3092
|
-
placeholder = "Type here..."
|
|
3093
|
-
|
|
3094
|
-
# App name
|
|
3206
|
+
return
|
|
3207
|
+
except ValueError:
|
|
3208
|
+
# Typed an app name
|
|
3209
|
+
matched = [a for a in apps if choice.lower() in a['id'] or choice.lower() in a.get('title', '').lower()]
|
|
3210
|
+
selected = matched[0] if matched else apps[0]
|
|
3211
|
+
|
|
3212
|
+
# App output directory
|
|
3213
|
+
default_name = selected['id']
|
|
3095
3214
|
try:
|
|
3096
|
-
default_name = re.sub(r'[^a-z0-9-]', '-', title.lower().replace(" ", "-"))
|
|
3097
3215
|
app_name = input(f" App name [{default_name}]: ").strip() or default_name
|
|
3098
3216
|
except (EOFError, KeyboardInterrupt):
|
|
3099
3217
|
return
|
|
3100
3218
|
app_name = re.sub(r'[^a-z0-9-]', '-', app_name.lower())
|
|
3101
3219
|
app_dir = os.path.join(CWD, app_name)
|
|
3102
3220
|
|
|
3103
|
-
console.print(f"\n [bold]
|
|
3104
|
-
console.print(f" [
|
|
3105
|
-
console.print(f" [
|
|
3221
|
+
console.print(f"\n {selected['icon']} [bold]{selected['title']}[/]")
|
|
3222
|
+
console.print(f" [dim]{selected.get('subtitle', '')}[/]")
|
|
3223
|
+
console.print(f" [dim]Inputs: {', '.join(selected.get('inputs', []))} Model: {selected.get('model', 'any')}[/]")
|
|
3106
3224
|
console.print(Rule(style="dim"))
|
|
3107
3225
|
|
|
3108
|
-
#
|
|
3109
|
-
template_dir = os.path.join(os.path.dirname(__file__), "templates", "ai-app")
|
|
3110
|
-
if not os.path.isdir(template_dir):
|
|
3111
|
-
console.print(f" [red]Template not found: {template_dir}[/]")
|
|
3112
|
-
return
|
|
3113
|
-
|
|
3114
|
-
console.print(f" [dim]Scaffolding {app_name}...[/]")
|
|
3226
|
+
# Build
|
|
3115
3227
|
if os.path.exists(app_dir):
|
|
3116
3228
|
try:
|
|
3117
3229
|
ans = input(f" {app_name}/ exists. Overwrite? (y/n): ").strip().lower()
|
|
@@ -3119,44 +3231,32 @@ def _handle_deploy(task, messages, perms, system, console):
|
|
|
3119
3231
|
return
|
|
3120
3232
|
if ans not in ("y", "yes"):
|
|
3121
3233
|
return
|
|
3234
|
+
import shutil
|
|
3122
3235
|
shutil.rmtree(app_dir)
|
|
3123
3236
|
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
#
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
"
|
|
3132
|
-
|
|
3133
|
-
"
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
for k, v in replacements.items():
|
|
3143
|
-
content = content.replace(k, v)
|
|
3144
|
-
with open(fpath, "w") as f:
|
|
3145
|
-
f.write(content)
|
|
3146
|
-
except Exception:
|
|
3147
|
-
pass
|
|
3148
|
-
|
|
3149
|
-
# Fix .env.local with actual API base
|
|
3150
|
-
env_path = os.path.join(app_dir, ".env.local")
|
|
3151
|
-
env_content = open(env_path).read()
|
|
3152
|
-
env_content = env_content.replace("http://localhost:8089/v1", f"{api_base}/v1")
|
|
3153
|
-
with open(env_path, "w") as f:
|
|
3154
|
-
f.write(env_content)
|
|
3237
|
+
console.print(f" [dim]Building {app_name}...[/]")
|
|
3238
|
+
|
|
3239
|
+
# Write custom config if modified
|
|
3240
|
+
app_config_dir = os.path.join(framework_dir, "apps", selected['id'])
|
|
3241
|
+
if selected.get('title') != next((a['title'] for a in apps if a['id'] == selected['id']), None):
|
|
3242
|
+
# Custom app — write temp config
|
|
3243
|
+
import json as _json, tempfile
|
|
3244
|
+
tmp_app_dir = os.path.join(framework_dir, "apps", "_custom")
|
|
3245
|
+
os.makedirs(tmp_app_dir, exist_ok=True)
|
|
3246
|
+
with open(os.path.join(tmp_app_dir, "config.json"), "w") as f:
|
|
3247
|
+
_json.dump(selected, f, indent=2)
|
|
3248
|
+
try:
|
|
3249
|
+
builder.build_app("_custom", app_dir)
|
|
3250
|
+
finally:
|
|
3251
|
+
import shutil
|
|
3252
|
+
shutil.rmtree(tmp_app_dir, ignore_errors=True)
|
|
3253
|
+
else:
|
|
3254
|
+
builder.build_app(selected['id'], app_dir)
|
|
3155
3255
|
|
|
3156
3256
|
file_count = sum(len(files) for _, _, files in os.walk(app_dir))
|
|
3157
3257
|
console.print(f" [green]✓[/] Created {file_count} files in {app_name}/")
|
|
3158
3258
|
|
|
3159
|
-
#
|
|
3259
|
+
# npm install
|
|
3160
3260
|
console.print(f" [dim]Installing dependencies...[/]")
|
|
3161
3261
|
try:
|
|
3162
3262
|
r = subprocess.run("npm install", shell=True, cwd=app_dir,
|
|
@@ -3164,34 +3264,24 @@ def _handle_deploy(task, messages, perms, system, console):
|
|
|
3164
3264
|
if r.returncode == 0:
|
|
3165
3265
|
console.print(f" [green]✓[/] Dependencies installed")
|
|
3166
3266
|
else:
|
|
3167
|
-
console.print(f" [yellow]npm install
|
|
3168
|
-
except
|
|
3169
|
-
console.print(f" [yellow]npm install
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
console.print(f"
|
|
3175
|
-
console.print(f" [bold]
|
|
3176
|
-
console.print(f"
|
|
3177
|
-
console.print(f"
|
|
3178
|
-
console.print(f"
|
|
3179
|
-
console.print(f"
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
console.print(f"\n [bold]Deploy anywhere:[/]")
|
|
3183
|
-
console.print(f" [dim]vercel[/] cd {app_name} && vercel")
|
|
3184
|
-
console.print(f" [dim]docker[/] docker build -t {app_name} {app_name}/")
|
|
3185
|
-
console.print(f" [dim]coolify[/] git push → auto-deploy")
|
|
3186
|
-
console.print(f"\n [bold]Switch AI backend:[/] edit {app_name}/.env.local")
|
|
3187
|
-
console.print(f" [dim]Local:[/] LLM_API_BASE=http://localhost:8089/v1")
|
|
3188
|
-
console.print(f" [dim]RunPod:[/] LLM_API_BASE=https://api.runpod.ai/v2/ID/openai/v1")
|
|
3189
|
-
console.print(f" [dim]OpenAI:[/] LLM_API_BASE=https://api.openai.com/v1")
|
|
3190
|
-
|
|
3191
|
-
# ── Start dev server? ──
|
|
3267
|
+
console.print(f" [yellow]npm install warnings (may still work)[/]")
|
|
3268
|
+
except Exception:
|
|
3269
|
+
console.print(f" [yellow]npm install issue — run manually[/]")
|
|
3270
|
+
|
|
3271
|
+
# Summary
|
|
3272
|
+
console.print(f"\n [green bold]✓ {selected['icon']} {selected['title']} is ready![/]\n")
|
|
3273
|
+
console.print(f" [bold]Run:[/] cd {app_name} && npm start")
|
|
3274
|
+
console.print(f" [bold]Open:[/] http://localhost:3000")
|
|
3275
|
+
console.print(f"\n [bold]Switch AI provider:[/]")
|
|
3276
|
+
console.print(f" [dim]Local:[/] LLM_API_BASE=http://localhost:8089/v1 npm start")
|
|
3277
|
+
console.print(f" [dim]OpenAI:[/] LLM_API_BASE=https://api.openai.com/v1 LLM_API_KEY=sk-... npm start")
|
|
3278
|
+
console.print(f" [dim]Gemini:[/] LLM_API_BASE=https://generativelanguage.googleapis.com/v1beta/openai LLM_API_KEY=... npm start")
|
|
3279
|
+
console.print(f" [dim]Groq:[/] LLM_API_BASE=https://api.groq.com/openai/v1 LLM_API_KEY=... npm start")
|
|
3280
|
+
|
|
3281
|
+
# Start?
|
|
3192
3282
|
console.print()
|
|
3193
3283
|
try:
|
|
3194
|
-
ans = input(" Start
|
|
3284
|
+
ans = input(" Start now? (y/n): ").strip().lower()
|
|
3195
3285
|
except (EOFError, KeyboardInterrupt):
|
|
3196
3286
|
ans = "n"
|
|
3197
3287
|
|
|
@@ -3199,11 +3289,9 @@ def _handle_deploy(task, messages, perms, system, console):
|
|
|
3199
3289
|
console.print(f"\n [green]Starting on http://localhost:3000...[/]")
|
|
3200
3290
|
console.print(f" [dim]Ctrl+C to stop[/]\n")
|
|
3201
3291
|
try:
|
|
3202
|
-
subprocess.run("
|
|
3292
|
+
subprocess.run("node server.js", shell=True, cwd=app_dir)
|
|
3203
3293
|
except KeyboardInterrupt:
|
|
3204
|
-
console.print(f"\n [dim]
|
|
3205
|
-
else:
|
|
3206
|
-
console.print(f"\n [yellow]App directory not found. Check output above for errors.[/]")
|
|
3294
|
+
console.print(f"\n [dim]Server stopped[/]")
|
|
3207
3295
|
|
|
3208
3296
|
|
|
3209
3297
|
def _cleanup_on_exit():
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* ── Chat Adapter: Multi-turn conversation with message bubbles ── */
|
|
2
|
+
|
|
3
|
+
let chatHistory = [];
|
|
4
|
+
|
|
5
|
+
function addMessage(role, content) {
|
|
6
|
+
chatHistory.push({ role, content });
|
|
7
|
+
renderMessages();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function renderMessages() {
|
|
11
|
+
const container = document.getElementById('messages');
|
|
12
|
+
container.innerHTML = chatHistory.map((m, i) => {
|
|
13
|
+
const isUser = m.role === 'user';
|
|
14
|
+
const align = isUser ? 'flex-end' : 'flex-start';
|
|
15
|
+
const bubbleClass = isUser ? 'bubble-user' : 'bubble-ai';
|
|
16
|
+
const label = isUser ? 'You' : 'AI';
|
|
17
|
+
const content = m.role === 'assistant' ? md(m.content) : escapeHtml(m.content);
|
|
18
|
+
return `<div class="msg" style="align-self:${align}">
|
|
19
|
+
<div class="msg-label">${label}</div>
|
|
20
|
+
<div class="${bubbleClass}">${content}</div>
|
|
21
|
+
</div>`;
|
|
22
|
+
}).join('');
|
|
23
|
+
container.scrollTop = container.scrollHeight;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function escapeHtml(text) {
|
|
27
|
+
return text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function updateLastAI(content) {
|
|
31
|
+
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === 'assistant') {
|
|
32
|
+
chatHistory[chatHistory.length - 1].content = content;
|
|
33
|
+
renderMessages();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function sendMessage() {
|
|
38
|
+
const input = document.getElementById('chatInput');
|
|
39
|
+
const msg = input.value.trim();
|
|
40
|
+
const img = typeof getImageBase64 === 'function' ? getImageBase64() : null;
|
|
41
|
+
const audio = typeof getAudioBase64 === 'function' ? getAudioBase64() : null;
|
|
42
|
+
if (!msg && !img && !audio) return;
|
|
43
|
+
|
|
44
|
+
// Add user message
|
|
45
|
+
addMessage('user', msg || (img ? '📷 Image' : '🎙️ Audio'));
|
|
46
|
+
input.value = '';
|
|
47
|
+
input.style.height = 'auto';
|
|
48
|
+
|
|
49
|
+
// Add placeholder AI message
|
|
50
|
+
addMessage('assistant', '');
|
|
51
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
52
|
+
sendBtn.disabled = true;
|
|
53
|
+
|
|
54
|
+
// Show typing indicator
|
|
55
|
+
updateLastAI('<div class="typing"><span></span><span></span><span></span></div>');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const body = { message: msg || 'Analyze this', history: chatHistory.slice(0, -1) };
|
|
59
|
+
if (img) body.image = img;
|
|
60
|
+
if (audio) body.audio = audio;
|
|
61
|
+
|
|
62
|
+
const res = await fetch('/api/analyze', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify(body)
|
|
66
|
+
});
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
if (data.error) throw new Error(data.error);
|
|
69
|
+
updateLastAI(data.analysis);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
updateLastAI(`<span style="color:var(--danger)">⚠️ ${err.message}</span>`);
|
|
72
|
+
} finally {
|
|
73
|
+
sendBtn.disabled = false;
|
|
74
|
+
if (typeof clearImage === 'function') clearImage();
|
|
75
|
+
if (typeof clearAudio === 'function') clearAudio();
|
|
76
|
+
input.focus();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* ── Image Adapter: Upload + Camera ── */
|
|
2
|
+
|
|
3
|
+
let imageBase64 = null;
|
|
4
|
+
|
|
5
|
+
function getImageBase64() { return imageBase64; }
|
|
6
|
+
function clearImage() { imageBase64 = null; const p = document.getElementById('preview'); if(p) p.style.display='none'; }
|
|
7
|
+
|
|
8
|
+
function uploadImage() {
|
|
9
|
+
const input = document.createElement('input');
|
|
10
|
+
input.type = 'file';
|
|
11
|
+
input.accept = 'image/*';
|
|
12
|
+
input.onchange = e => {
|
|
13
|
+
const file = e.target.files[0];
|
|
14
|
+
if (!file) return;
|
|
15
|
+
const reader = new FileReader();
|
|
16
|
+
reader.onload = ev => {
|
|
17
|
+
imageBase64 = ev.target.result;
|
|
18
|
+
const preview = document.getElementById('preview');
|
|
19
|
+
if (preview) { preview.src = imageBase64; preview.style.display = 'block'; }
|
|
20
|
+
};
|
|
21
|
+
reader.readAsDataURL(file);
|
|
22
|
+
};
|
|
23
|
+
input.click();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function openCamera() {
|
|
27
|
+
const video = document.getElementById('camVideo');
|
|
28
|
+
const captureBtn = document.getElementById('captureBtn');
|
|
29
|
+
if (!video) return;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
|
|
33
|
+
video.srcObject = stream;
|
|
34
|
+
video.style.display = 'block';
|
|
35
|
+
video.play();
|
|
36
|
+
if (captureBtn) captureBtn.style.display = 'inline-flex';
|
|
37
|
+
} catch (err) {
|
|
38
|
+
alert('Camera not available: ' + err.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function capturePhoto() {
|
|
43
|
+
const video = document.getElementById('camVideo');
|
|
44
|
+
if (!video || !video.srcObject) return;
|
|
45
|
+
|
|
46
|
+
const canvas = document.createElement('canvas');
|
|
47
|
+
canvas.width = video.videoWidth;
|
|
48
|
+
canvas.height = video.videoHeight;
|
|
49
|
+
canvas.getContext('2d').drawImage(video, 0, 0);
|
|
50
|
+
imageBase64 = canvas.toDataURL('image/jpeg', 0.85);
|
|
51
|
+
|
|
52
|
+
// Show preview
|
|
53
|
+
const preview = document.getElementById('preview');
|
|
54
|
+
if (preview) { preview.src = imageBase64; preview.style.display = 'block'; }
|
|
55
|
+
|
|
56
|
+
// Stop camera
|
|
57
|
+
video.srcObject.getTracks().forEach(t => t.stop());
|
|
58
|
+
video.style.display = 'none';
|
|
59
|
+
const captureBtn = document.getElementById('captureBtn');
|
|
60
|
+
if (captureBtn) captureBtn.style.display = 'none';
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* ── Text Adapter: Textarea + Chat Input ── */
|
|
2
|
+
|
|
3
|
+
function getTextInput(id = 'input') {
|
|
4
|
+
const el = document.getElementById(id);
|
|
5
|
+
return el ? el.value.trim() : '';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function clearTextInput(id = 'input') {
|
|
9
|
+
const el = document.getElementById(id);
|
|
10
|
+
if (el) el.value = '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function autoResize(textarea) {
|
|
14
|
+
textarea.style.height = 'auto';
|
|
15
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px';
|
|
16
|
+
}
|