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.
Files changed (47) hide show
  1. localcoder-0.3.0/.gitignore +20 -0
  2. {localcoder-0.2.1 → localcoder-0.3.0}/PKG-INFO +1 -1
  3. {localcoder-0.2.1 → localcoder-0.3.0}/pyproject.toml +1 -1
  4. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/localcoder_agent.py +205 -117
  5. localcoder-0.3.0/src/localcoder/templates/framework/_adapters/chat.js +78 -0
  6. localcoder-0.3.0/src/localcoder/templates/framework/_adapters/image.js +61 -0
  7. localcoder-0.3.0/src/localcoder/templates/framework/_adapters/text.js +16 -0
  8. localcoder-0.3.0/src/localcoder/templates/framework/_adapters/voice.js +75 -0
  9. localcoder-0.3.0/src/localcoder/templates/framework/_server/package.json +6 -0
  10. localcoder-0.3.0/src/localcoder/templates/framework/_server/server.js +75 -0
  11. localcoder-0.3.0/src/localcoder/templates/framework/_shell/base.css +357 -0
  12. localcoder-0.3.0/src/localcoder/templates/framework/_shell/base.js +63 -0
  13. localcoder-0.3.0/src/localcoder/templates/framework/apps/chatbot/config.json +20 -0
  14. localcoder-0.3.0/src/localcoder/templates/framework/apps/homework-helper/config.json +19 -0
  15. localcoder-0.3.0/src/localcoder/templates/framework/apps/image-analyzer/config.json +19 -0
  16. localcoder-0.3.0/src/localcoder/templates/framework/apps/ingredients-scanner/config.json +19 -0
  17. localcoder-0.3.0/src/localcoder/templates/framework/apps/meeting-notes/config.json +18 -0
  18. localcoder-0.3.0/src/localcoder/templates/framework/apps/photo-todo/config.json +19 -0
  19. localcoder-0.3.0/src/localcoder/templates/framework/apps/translator/config.json +20 -0
  20. localcoder-0.3.0/src/localcoder/templates/framework/apps/voice-analyzer/config.json +18 -0
  21. localcoder-0.3.0/src/localcoder/templates/framework/apps/voice-memo/config.json +18 -0
  22. localcoder-0.3.0/src/localcoder/templates/framework/build.py +234 -0
  23. {localcoder-0.2.1 → localcoder-0.3.0}/tests/test_basic.py +43 -5
  24. localcoder-0.2.1/.gitignore +0 -7
  25. localcoder-0.2.1/uv.lock +0 -449
  26. {localcoder-0.2.1 → localcoder-0.3.0}/LICENSE +0 -0
  27. {localcoder-0.2.1 → localcoder-0.3.0}/README.md +0 -0
  28. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/__init__.py +0 -0
  29. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/__main__.py +0 -0
  30. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/agent.py +0 -0
  31. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/backends.py +0 -0
  32. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/bench.py +0 -0
  33. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/cli.py +0 -0
  34. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/localcoder_display.py +0 -0
  35. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/setup.py +0 -0
  36. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/.env.local +0 -0
  37. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/next.config.ts +0 -0
  38. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/package.json +0 -0
  39. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/postcss.config.mjs +0 -0
  40. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/api/ai/route.ts +0 -0
  41. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/globals.css +0 -0
  42. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/layout.tsx +0 -0
  43. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/app/page.tsx +0 -0
  44. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/src/components/Chat.tsx +0 -0
  45. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/templates/ai-app/tsconfig.json +0 -0
  46. {localcoder-0.2.1 → localcoder-0.3.0}/src/localcoder/tui.py +0 -0
  47. {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.2.1
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.2.1"
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
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=PT_HTML_CMD(f'<b>{cmd}</b> <style fg="ansigray">{desc}</style>'),
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
- """Generate and deploy an AI-powered React app from template."""
3039
- import shutil
3134
+ """Build an AI app from the framework templates."""
3040
3135
  from rich.rule import Rule
3041
3136
 
3042
- parts = task.split(None, 1)
3043
- description = parts[1] if len(parts) > 1 else None
3044
-
3045
- console.print(f"\n [bold #e07a5f]🚀 Deploy — AI App Generator[/]\n")
3046
-
3047
- # ── Templates: LLM only writes system prompt + page description ──
3048
- TEMPLATES = {
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
- if not description:
3058
- console.print(f" [bold]Pick a template:[/]\n")
3059
- for k, (tid, title, _, _) in TEMPLATES.items():
3060
- console.print(f" [bold cyan]{k}[/] {title}")
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
- if choice in TEMPLATES:
3069
- tid, title, sys_prompt, placeholder = TEMPLATES[choice]
3070
- if tid == "custom":
3189
+ try:
3190
+ idx = int(choice) - 1
3191
+ if idx == len(apps):
3192
+ # Custom
3071
3193
  try:
3072
- description = input(" App description: ").strip()
3073
- sys_prompt = input(" System prompt for AI: ").strip()
3074
- placeholder = input(" Input placeholder: ").strip() or "Type here..."
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 not description or not sys_prompt:
3079
- return
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
- description = title
3082
- else:
3083
- description = choice
3084
- sys_prompt = f"You are an AI assistant for: {choice}. Help the user with their request."
3085
- placeholder = "Type here..."
3086
- title = choice[:40]
3087
-
3088
- else:
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]App:[/] {app_name}")
3104
- console.print(f" [bold]Title:[/] {title}")
3105
- console.print(f" [bold]API:[/] {API_BASE}")
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
- # ── Copy template ──
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
- shutil.copytree(template_dir, app_dir)
3125
-
3126
- # ── Fill in placeholders ──
3127
- api_base = API_BASE.replace("/v1", "")
3128
- replacements = {
3129
- "{{APP_NAME}}": app_name,
3130
- "{{APP_TITLE}}": title,
3131
- "{{APP_DESCRIPTION}}": description,
3132
- "{{SYSTEM_PROMPT}}": sys_prompt.replace("`", "\\`").replace("$", "\\$"),
3133
- "{{PLACEHOLDER}}": placeholder,
3134
- }
3135
-
3136
- for root, dirs, files in os.walk(app_dir):
3137
- for fname in files:
3138
- fpath = os.path.join(root, fname)
3139
- if fname.endswith((".ts", ".tsx", ".json", ".css", ".mjs", ".local")):
3140
- try:
3141
- content = open(fpath).read()
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
- # ── npm install ──
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 had warnings (may still work)[/]")
3168
- except subprocess.TimeoutExpired:
3169
- console.print(f" [yellow]npm install timed out — run manually[/]")
3170
- except Exception as e:
3171
- console.print(f" [red]npm install failed: {e}[/]")
3172
-
3173
- # ── Summary ──
3174
- console.print(f"\n [green bold]✓ {title} is ready![/]\n")
3175
- console.print(f" [bold]Files:[/]")
3176
- console.print(f" {app_name}/src/app/page.tsx [dim]← UI[/]")
3177
- console.print(f" {app_name}/src/app/api/ai/route.ts [dim]← AI backend[/]")
3178
- console.print(f" {app_name}/src/components/Chat.tsx [dim]← chat component[/]")
3179
- console.print(f" {app_name}/.env.local [dim] swap AI provider[/]")
3180
- console.print(f"\n [bold]Run:[/] cd {app_name} && npm run dev")
3181
- console.print(f" [bold]Open:[/] http://localhost:3000")
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 dev server? (y/n): ").strip().lower()
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("npm run dev", shell=True, cwd=app_dir)
3292
+ subprocess.run("node server.js", shell=True, cwd=app_dir)
3203
3293
  except KeyboardInterrupt:
3204
- console.print(f"\n [dim]Dev server stopped[/]")
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').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
+ }