localcoder 0.2.0__tar.gz → 0.2.1__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 (28) hide show
  1. {localcoder-0.2.0 → localcoder-0.2.1}/PKG-INFO +1 -1
  2. {localcoder-0.2.0 → localcoder-0.2.1}/pyproject.toml +1 -1
  3. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/localcoder_agent.py +126 -127
  4. localcoder-0.2.1/src/localcoder/templates/ai-app/.env.local +21 -0
  5. localcoder-0.2.1/src/localcoder/templates/ai-app/next.config.ts +3 -0
  6. localcoder-0.2.1/src/localcoder/templates/ai-app/package.json +23 -0
  7. localcoder-0.2.1/src/localcoder/templates/ai-app/postcss.config.mjs +6 -0
  8. localcoder-0.2.1/src/localcoder/templates/ai-app/src/app/api/ai/route.ts +88 -0
  9. localcoder-0.2.1/src/localcoder/templates/ai-app/src/app/globals.css +36 -0
  10. localcoder-0.2.1/src/localcoder/templates/ai-app/src/app/layout.tsx +19 -0
  11. localcoder-0.2.1/src/localcoder/templates/ai-app/src/app/page.tsx +22 -0
  12. localcoder-0.2.1/src/localcoder/templates/ai-app/src/components/Chat.tsx +124 -0
  13. localcoder-0.2.1/src/localcoder/templates/ai-app/tsconfig.json +21 -0
  14. {localcoder-0.2.0 → localcoder-0.2.1}/tests/test_basic.py +21 -2
  15. {localcoder-0.2.0 → localcoder-0.2.1}/uv.lock +1 -1
  16. {localcoder-0.2.0 → localcoder-0.2.1}/.gitignore +0 -0
  17. {localcoder-0.2.0 → localcoder-0.2.1}/LICENSE +0 -0
  18. {localcoder-0.2.0 → localcoder-0.2.1}/README.md +0 -0
  19. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/__init__.py +0 -0
  20. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/__main__.py +0 -0
  21. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/agent.py +0 -0
  22. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/backends.py +0 -0
  23. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/bench.py +0 -0
  24. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/cli.py +0 -0
  25. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/localcoder_display.py +0 -0
  26. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/setup.py +0 -0
  27. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/tui.py +0 -0
  28. {localcoder-0.2.0 → localcoder-0.2.1}/src/localcoder/voice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localcoder
3
- Version: 0.2.0
3
+ Version: 0.2.1
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.0"
7
+ version = "0.2.1"
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"
@@ -3035,30 +3035,29 @@ def main(argv=None):
3035
3035
 
3036
3036
 
3037
3037
  def _handle_deploy(task, messages, perms, system, console):
3038
- """Generate and deploy an AI-powered React app."""
3039
- from rich.panel import Panel
3038
+ """Generate and deploy an AI-powered React app from template."""
3039
+ import shutil
3040
3040
  from rich.rule import Rule
3041
3041
 
3042
- # Parse: /deploy or /deploy "description"
3043
3042
  parts = task.split(None, 1)
3044
3043
  description = parts[1] if len(parts) > 1 else None
3045
3044
 
3046
3045
  console.print(f"\n [bold #e07a5f]🚀 Deploy — AI App Generator[/]\n")
3047
3046
 
3048
- # App templates
3047
+ # ── Templates: LLM only writes system prompt + page description ──
3049
3048
  TEMPLATES = {
3050
- "1": ("chatbot", "AI Chatbot conversational UI with streaming"),
3051
- "2": ("vision", "Vision App image upload + AI analysis"),
3052
- "3": ("writer", "AI Writer text rewriting, summarization, translation"),
3053
- "4": ("csv", "Data Analyzer upload CSV, get AI insights"),
3054
- "5": ("pdf", "PDF Extractor upload PDF, extract & summarize"),
3055
- "6": ("custom", "Custom describe your app"),
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),
3056
3055
  }
3057
3056
 
3058
3057
  if not description:
3059
3058
  console.print(f" [bold]Pick a template:[/]\n")
3060
- for k, (tid, desc) in TEMPLATES.items():
3061
- console.print(f" [bold cyan]{k}[/] {desc}")
3059
+ for k, (tid, title, _, _) in TEMPLATES.items():
3060
+ console.print(f" [bold cyan]{k}[/] {title}")
3062
3061
  console.print()
3063
3062
 
3064
3063
  try:
@@ -3067,142 +3066,142 @@ def _handle_deploy(task, messages, perms, system, console):
3067
3066
  return
3068
3067
 
3069
3068
  if choice in TEMPLATES:
3070
- tid, desc = TEMPLATES[choice]
3069
+ tid, title, sys_prompt, placeholder = TEMPLATES[choice]
3071
3070
  if tid == "custom":
3072
3071
  try:
3073
- description = input(" Describe your app: ").strip()
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]
3074
3076
  except (EOFError, KeyboardInterrupt):
3075
3077
  return
3076
- if not description:
3078
+ if not description or not sys_prompt:
3077
3079
  return
3078
3080
  else:
3079
- description = desc
3081
+ description = title
3080
3082
  else:
3081
- description = choice # treat as freeform description
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]
3082
3087
 
3083
- # Get app name
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
3084
3095
  try:
3085
- app_name = input(f" App name [{description.split()[0].lower()}-app]: ").strip()
3096
+ default_name = re.sub(r'[^a-z0-9-]', '-', title.lower().replace(" ", "-"))
3097
+ app_name = input(f" App name [{default_name}]: ").strip() or default_name
3086
3098
  except (EOFError, KeyboardInterrupt):
3087
3099
  return
3088
- if not app_name:
3089
- app_name = description.split()[0].lower().replace(" ", "-") + "-app"
3090
3100
  app_name = re.sub(r'[^a-z0-9-]', '-', app_name.lower())
3101
+ app_dir = os.path.join(CWD, app_name)
3102
+
3103
+ console.print(f"\n [bold]App:[/] {app_name}")
3104
+ console.print(f" [bold]Title:[/] {title}")
3105
+ console.print(f" [bold]API:[/] {API_BASE}")
3106
+ console.print(Rule(style="dim"))
3091
3107
 
3092
- # Detect API endpoint
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}...[/]")
3115
+ if os.path.exists(app_dir):
3116
+ try:
3117
+ ans = input(f" {app_name}/ exists. Overwrite? (y/n): ").strip().lower()
3118
+ except (EOFError, KeyboardInterrupt):
3119
+ return
3120
+ if ans not in ("y", "yes"):
3121
+ return
3122
+ shutil.rmtree(app_dir)
3123
+
3124
+ shutil.copytree(template_dir, app_dir)
3125
+
3126
+ # ── Fill in placeholders ──
3093
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
+ }
3094
3135
 
3095
- console.print(f"\n [bold]App:[/] {app_name}")
3096
- console.print(f" [bold]Desc:[/] {description}")
3097
- console.print(f" [bold]Model API:[/] {api_base}/v1")
3098
- console.print(Rule(style="dim"))
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
3099
3148
 
3100
- # Build the generation prompt
3101
- deploy_system = f"""You are an expert full-stack React developer. Generate a COMPLETE, production-ready AI-powered web app.
3102
-
3103
- REQUIREMENTS:
3104
- - Next.js 15 + TypeScript + Tailwind CSS
3105
- - Beautiful, modern UI (dark theme, glassmorphism, smooth animations)
3106
- - AI-powered backend using OpenAI-compatible API
3107
- - All code must be COMPLETE — no placeholders, no TODOs, no "add your code here"
3108
- - App must work immediately after `npm install && npm run dev`
3109
-
3110
- AI BACKEND PATTERN — use this exact pattern for all AI calls:
3111
- ```typescript
3112
- // src/app/api/chat/route.ts (or similar)
3113
- const response = await fetch(process.env.LLM_API_BASE + '/chat/completions', {{
3114
- method: 'POST',
3115
- headers: {{ 'Content-Type': 'application/json' }},
3116
- body: JSON.stringify({{
3117
- model: process.env.LLM_MODEL || 'local',
3118
- messages: [...],
3119
- stream: true,
3120
- }}),
3121
- }});
3122
- ```
3123
-
3124
- ENV VARS (create .env.local):
3125
- ```
3126
- LLM_API_BASE={api_base}/v1
3127
- LLM_MODEL={MODEL}
3128
- ```
3129
-
3130
- This makes the app work with:
3131
- - Local: llama-server on localhost:8089 (default)
3132
- - Cloud: RunPod Serverless, OpenAI, Groq — just change LLM_API_BASE
3133
-
3134
- DIRECTORY STRUCTURE — create ALL files:
3135
- {app_name}/
3136
- package.json
3137
- tsconfig.json
3138
- next.config.ts
3139
- tailwind.config.ts
3140
- postcss.config.mjs
3141
- .env.local
3142
- .env.example
3143
- src/app/layout.tsx
3144
- src/app/page.tsx
3145
- src/app/globals.css
3146
- src/app/api/chat/route.ts (or appropriate API route)
3147
- src/components/ (React components)
3148
- public/ (static assets if needed)
3149
-
3150
- IMPORTANT:
3151
- 1. Use write_file for EVERY file. Create the full directory.
3152
- 2. After writing all files, run: cd {app_name} && npm install
3153
- 3. Then show the user how to start: npm run dev
3154
- 4. Make the UI genuinely beautiful — not generic. Use gradients, shadows, animations.
3155
- 5. The app should feel like a polished product, not a tutorial demo."""
3156
-
3157
- deploy_prompt = f"""Create a complete AI-powered React app:
3158
-
3159
- **App name:** {app_name}
3160
- **Description:** {description}
3161
-
3162
- Generate ALL files now. Make it production-quality with a beautiful UI.
3163
- After creating all files, run npm install to verify it works."""
3164
-
3165
- # Fresh conversation for deploy
3166
- deploy_messages = [
3167
- {"role": "system", "content": deploy_system},
3168
- {"role": "user", "content": deploy_prompt},
3169
- ]
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)
3170
3155
 
3171
- console.print(f"\n [magenta]⚡[/] [bold]Generating {app_name}...[/]\n")
3156
+ file_count = sum(len(files) for _, _, files in os.walk(app_dir))
3157
+ console.print(f" [green]✓[/] Created {file_count} files in {app_name}/")
3172
3158
 
3159
+ # ── npm install ──
3160
+ console.print(f" [dim]Installing dependencies...[/]")
3173
3161
  try:
3174
- tokens = agent_loop(deploy_messages, perms)
3175
- except KeyboardInterrupt:
3176
- console.print(f"\n [yellow]Generation interrupted[/]")
3177
- return
3162
+ r = subprocess.run("npm install", shell=True, cwd=app_dir,
3163
+ capture_output=True, text=True, timeout=120)
3164
+ if r.returncode == 0:
3165
+ console.print(f" [green]✓[/] Dependencies installed")
3166
+ 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? ──
3192
+ console.print()
3193
+ try:
3194
+ ans = input(" Start dev server? (y/n): ").strip().lower()
3195
+ except (EOFError, KeyboardInterrupt):
3196
+ ans = "n"
3178
3197
 
3179
- # Check if app was created
3180
- app_dir = os.path.join(CWD, app_name)
3181
- if os.path.isdir(app_dir):
3182
- console.print(f"\n [green]✓[/] App created: [bold]{app_dir}[/]")
3183
- console.print(f"\n [bold]Run locally:[/]")
3184
- console.print(f" [cyan]cd {app_name} && npm run dev[/]")
3185
- console.print(f"\n [bold]Deploy options:[/]")
3186
- console.print(f" [dim]Vercel:[/] cd {app_name} && vercel")
3187
- console.print(f" [dim]Coolify:[/] git push (auto-deploy)")
3188
- console.print(f" [dim]Docker:[/] docker compose up")
3189
-
3190
- # Offer to start dev server
3191
- console.print()
3198
+ if ans in ("y", "yes"):
3199
+ console.print(f"\n [green]Starting on http://localhost:3000...[/]")
3200
+ console.print(f" [dim]Ctrl+C to stop[/]\n")
3192
3201
  try:
3193
- ans = input(" Start dev server now? (y/n): ").strip().lower()
3194
- except (EOFError, KeyboardInterrupt):
3195
- ans = "n"
3196
-
3197
- if ans in ("y", "yes"):
3198
- console.print(f"\n [green]Starting {app_name} on http://localhost:3000...[/]\n")
3199
- try:
3200
- subprocess.run(
3201
- "npm run dev",
3202
- shell=True, cwd=app_dir,
3203
- )
3204
- except KeyboardInterrupt:
3205
- console.print(f"\n [dim]Dev server stopped[/]")
3202
+ subprocess.run("npm run dev", shell=True, cwd=app_dir)
3203
+ except KeyboardInterrupt:
3204
+ console.print(f"\n [dim]Dev server stopped[/]")
3206
3205
  else:
3207
3206
  console.print(f"\n [yellow]App directory not found. Check output above for errors.[/]")
3208
3207
 
@@ -0,0 +1,21 @@
1
+ # ── AI Backend — change ONE line to switch providers ──
2
+
3
+ # Local (default — llama-server or Ollama)
4
+ LLM_API_BASE=http://localhost:8089/v1
5
+ LLM_API_KEY=no-key-required
6
+ LLM_MODEL=local
7
+
8
+ # RunPod Serverless:
9
+ # LLM_API_BASE=https://api.runpod.ai/v2/YOUR_ENDPOINT_ID/openai/v1
10
+ # LLM_API_KEY=your-runpod-key
11
+ # LLM_MODEL=local
12
+
13
+ # OpenAI:
14
+ # LLM_API_BASE=https://api.openai.com/v1
15
+ # LLM_API_KEY=sk-...
16
+ # LLM_MODEL=gpt-4o
17
+
18
+ # Google Gemini:
19
+ # LLM_API_BASE=https://generativelanguage.googleapis.com/v1beta/openai
20
+ # LLM_API_KEY=your-google-key
21
+ # LLM_MODEL=gemini-2.5-flash
@@ -0,0 +1,3 @@
1
+ import type { NextConfig } from "next";
2
+ const nextConfig: NextConfig = {};
3
+ export default nextConfig;
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "next": "^15.3",
12
+ "react": "^19",
13
+ "react-dom": "^19"
14
+ },
15
+ "devDependencies": {
16
+ "@tailwindcss/postcss": "^4",
17
+ "tailwindcss": "^4",
18
+ "typescript": "^5",
19
+ "@types/react": "^19",
20
+ "@types/react-dom": "^19",
21
+ "@types/node": "^22"
22
+ }
23
+ }
@@ -0,0 +1,6 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+ export default config;
@@ -0,0 +1,88 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ // ── Swappable AI Backend ──
4
+ // Change ONE env var to switch between:
5
+ // Local: LLM_API_BASE=http://localhost:8089/v1 (llama-server / Ollama)
6
+ // RunPod: LLM_API_BASE=https://api.runpod.ai/v2/YOUR_ID/openai/v1
7
+ // OpenAI: LLM_API_BASE=https://api.openai.com/v1
8
+ // Gemini: LLM_API_BASE=https://generativelanguage.googleapis.com/v1beta/openai
9
+ // Groq: LLM_API_BASE=https://api.groq.com/openai/v1
10
+
11
+ const API_BASE = process.env.LLM_API_BASE || "http://localhost:8089/v1";
12
+ const API_KEY = process.env.LLM_API_KEY || "no-key-required";
13
+ const MODEL = process.env.LLM_MODEL || "local";
14
+
15
+ // ── System prompt — THIS IS WHAT THE LLM CUSTOMIZES ──
16
+ const SYSTEM_PROMPT = `{{SYSTEM_PROMPT}}`;
17
+
18
+ export async function POST(req: NextRequest) {
19
+ const { message, history = [] } = await req.json();
20
+
21
+ const messages = [
22
+ { role: "system", content: SYSTEM_PROMPT },
23
+ ...history.slice(-10),
24
+ { role: "user", content: message },
25
+ ];
26
+
27
+ try {
28
+ const response = await fetch(`${API_BASE}/chat/completions`, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Authorization: `Bearer ${API_KEY}`,
33
+ },
34
+ body: JSON.stringify({
35
+ model: MODEL,
36
+ messages,
37
+ max_tokens: 2048,
38
+ temperature: 0.7,
39
+ stream: true,
40
+ }),
41
+ });
42
+
43
+ if (!response.ok) {
44
+ const err = await response.text();
45
+ return NextResponse.json({ error: err }, { status: response.status });
46
+ }
47
+
48
+ // Stream the response
49
+ const encoder = new TextEncoder();
50
+ const stream = new ReadableStream({
51
+ async start(controller) {
52
+ const reader = response.body!.getReader();
53
+ const decoder = new TextDecoder();
54
+ let buffer = "";
55
+
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+
60
+ buffer += decoder.decode(value, { stream: true });
61
+ const lines = buffer.split("\n");
62
+ buffer = lines.pop() || "";
63
+
64
+ for (const line of lines) {
65
+ if (!line.startsWith("data: ")) continue;
66
+ const data = line.slice(6).trim();
67
+ if (data === "[DONE]") continue;
68
+ try {
69
+ const json = JSON.parse(data);
70
+ const content = json.choices?.[0]?.delta?.content;
71
+ if (content) {
72
+ controller.enqueue(encoder.encode(content));
73
+ }
74
+ } catch {}
75
+ }
76
+ }
77
+ controller.close();
78
+ },
79
+ });
80
+
81
+ return new Response(stream, {
82
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
83
+ });
84
+ } catch (error: unknown) {
85
+ const msg = error instanceof Error ? error.message : "AI request failed";
86
+ return NextResponse.json({ error: msg }, { status: 500 });
87
+ }
88
+ }
@@ -0,0 +1,36 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --bg: #0a0a0f;
5
+ --surface: #12121a;
6
+ --border: #1e1e2e;
7
+ --text: #e4e4e7;
8
+ --muted: #71717a;
9
+ --accent: #e07a5f;
10
+ --accent2: #81b29a;
11
+ }
12
+
13
+ body {
14
+ background: var(--bg);
15
+ color: var(--text);
16
+ font-family: system-ui, -apple-system, sans-serif;
17
+ }
18
+
19
+ .glass {
20
+ background: rgba(18, 18, 26, 0.8);
21
+ backdrop-filter: blur(12px);
22
+ border: 1px solid var(--border);
23
+ }
24
+
25
+ .glow {
26
+ box-shadow: 0 0 20px rgba(224, 122, 95, 0.1);
27
+ }
28
+
29
+ @keyframes fadeIn {
30
+ from { opacity: 0; transform: translateY(8px); }
31
+ to { opacity: 1; transform: translateY(0); }
32
+ }
33
+
34
+ .animate-in {
35
+ animation: fadeIn 0.3s ease-out;
36
+ }
@@ -0,0 +1,19 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "{{APP_TITLE}}",
6
+ description: "{{APP_DESCRIPTION}}",
7
+ };
8
+
9
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <html lang="en">
12
+ <body className="min-h-screen antialiased">
13
+ <div className="mx-auto max-w-3xl px-4 py-8">
14
+ {children}
15
+ </div>
16
+ </body>
17
+ </html>
18
+ );
19
+ }
@@ -0,0 +1,22 @@
1
+ import Chat from "@/components/Chat";
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main>
6
+ <div className="mb-8">
7
+ <h1 className="text-3xl font-bold bg-gradient-to-r from-[var(--accent)] to-[var(--accent2)] bg-clip-text text-transparent">
8
+ {{APP_TITLE}}
9
+ </h1>
10
+ <p className="text-[var(--muted)] mt-2">{{APP_DESCRIPTION}}</p>
11
+ </div>
12
+
13
+ <div className="glass rounded-2xl p-6 glow">
14
+ <Chat placeholder="{{PLACEHOLDER}}" />
15
+ </div>
16
+
17
+ <p className="text-center text-xs text-[var(--muted)] mt-6">
18
+ Powered by local AI
19
+ </p>
20
+ </main>
21
+ );
22
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+ import { useState, useRef, useEffect, FormEvent } from "react";
3
+
4
+ type Message = { role: "user" | "assistant"; content: string };
5
+
6
+ export default function Chat({
7
+ placeholder = "Type a message...",
8
+ initialMessage,
9
+ }: {
10
+ placeholder?: string;
11
+ initialMessage?: string;
12
+ }) {
13
+ const [messages, setMessages] = useState<Message[]>([]);
14
+ const [input, setInput] = useState(initialMessage || "");
15
+ const [loading, setLoading] = useState(false);
16
+ const endRef = useRef<HTMLDivElement>(null);
17
+
18
+ useEffect(() => {
19
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
20
+ }, [messages]);
21
+
22
+ async function send(e: FormEvent) {
23
+ e.preventDefault();
24
+ if (!input.trim() || loading) return;
25
+
26
+ const userMsg: Message = { role: "user", content: input.trim() };
27
+ setMessages((prev) => [...prev, userMsg]);
28
+ setInput("");
29
+ setLoading(true);
30
+
31
+ const assistantMsg: Message = { role: "assistant", content: "" };
32
+ setMessages((prev) => [...prev, assistantMsg]);
33
+
34
+ try {
35
+ const res = await fetch("/api/ai", {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({
39
+ message: userMsg.content,
40
+ history: messages,
41
+ }),
42
+ });
43
+
44
+ if (!res.ok) {
45
+ const err = await res.json();
46
+ throw new Error(err.error || "Request failed");
47
+ }
48
+
49
+ const reader = res.body!.getReader();
50
+ const decoder = new TextDecoder();
51
+ let full = "";
52
+
53
+ while (true) {
54
+ const { done, value } = await reader.read();
55
+ if (done) break;
56
+ full += decoder.decode(value, { stream: true });
57
+ setMessages((prev) => {
58
+ const updated = [...prev];
59
+ updated[updated.length - 1] = { role: "assistant", content: full };
60
+ return updated;
61
+ });
62
+ }
63
+ } catch (err: unknown) {
64
+ const msg = err instanceof Error ? err.message : "Something went wrong";
65
+ setMessages((prev) => {
66
+ const updated = [...prev];
67
+ updated[updated.length - 1] = {
68
+ role: "assistant",
69
+ content: `Error: ${msg}`,
70
+ };
71
+ return updated;
72
+ });
73
+ } finally {
74
+ setLoading(false);
75
+ }
76
+ }
77
+
78
+ return (
79
+ <div className="flex flex-col gap-4">
80
+ {/* Messages */}
81
+ <div className="space-y-3 min-h-[200px] max-h-[60vh] overflow-y-auto">
82
+ {messages.map((m, i) => (
83
+ <div
84
+ key={i}
85
+ className={`animate-in rounded-xl px-4 py-3 ${
86
+ m.role === "user"
87
+ ? "glass ml-8"
88
+ : "bg-[var(--surface)] mr-8 border border-[var(--border)]"
89
+ }`}
90
+ >
91
+ <p className="text-xs font-medium mb-1 text-[var(--muted)]">
92
+ {m.role === "user" ? "You" : "AI"}
93
+ </p>
94
+ <p className="whitespace-pre-wrap text-sm leading-relaxed">
95
+ {m.content}
96
+ {loading && i === messages.length - 1 && m.role === "assistant" && (
97
+ <span className="inline-block w-2 h-4 bg-[var(--accent)] ml-1 animate-pulse" />
98
+ )}
99
+ </p>
100
+ </div>
101
+ ))}
102
+ <div ref={endRef} />
103
+ </div>
104
+
105
+ {/* Input */}
106
+ <form onSubmit={send} className="flex gap-2">
107
+ <input
108
+ value={input}
109
+ onChange={(e) => setInput(e.target.value)}
110
+ placeholder={placeholder}
111
+ disabled={loading}
112
+ className="flex-1 glass rounded-xl px-4 py-3 text-sm outline-none focus:ring-1 focus:ring-[var(--accent)] placeholder:text-[var(--muted)] disabled:opacity-50"
113
+ />
114
+ <button
115
+ type="submit"
116
+ disabled={loading || !input.trim()}
117
+ className="px-5 py-3 rounded-xl bg-[var(--accent)] text-white font-medium text-sm hover:brightness-110 transition disabled:opacity-40"
118
+ >
119
+ {loading ? "..." : "Send"}
120
+ </button>
121
+ </form>
122
+ </div>
123
+ );
124
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "paths": { "@/*": ["./src/*"] },
17
+ "plugins": [{ "name": "next" }]
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -226,10 +226,29 @@ class TestDeploy(unittest.TestCase):
226
226
  from localcoder.localcoder_agent import _handle_deploy
227
227
  source = inspect.getsource(_handle_deploy)
228
228
  self.assertIn("chatbot", source)
229
- self.assertIn("vision", source)
230
- self.assertIn("csv", source)
229
+ self.assertIn("ingredients", source)
230
+ self.assertIn("code-review", source)
231
231
  self.assertIn("custom", source)
232
232
 
233
+ def test_template_files_exist(self):
234
+ """Template directory should contain all required files."""
235
+ template_dir = os.path.join(os.path.dirname(__file__), "..", "src", "localcoder", "templates", "ai-app")
236
+ self.assertTrue(os.path.isdir(template_dir), f"Template dir missing: {template_dir}")
237
+ required = ["package.json", "tsconfig.json", "next.config.ts", "postcss.config.mjs",
238
+ ".env.local", "src/app/layout.tsx", "src/app/page.tsx",
239
+ "src/app/globals.css", "src/app/api/ai/route.ts", "src/components/Chat.tsx"]
240
+ for f in required:
241
+ self.assertTrue(os.path.exists(os.path.join(template_dir, f)), f"Missing: {f}")
242
+
243
+ def test_template_has_placeholders(self):
244
+ """Template files should contain replaceable placeholders."""
245
+ template_dir = os.path.join(os.path.dirname(__file__), "..", "src", "localcoder", "templates", "ai-app")
246
+ page = open(os.path.join(template_dir, "src/app/page.tsx")).read()
247
+ self.assertIn("{{APP_TITLE}}", page)
248
+ route = open(os.path.join(template_dir, "src/app/api/ai/route.ts")).read()
249
+ self.assertIn("{{SYSTEM_PROMPT}}", route)
250
+ self.assertIn("LLM_API_BASE", route)
251
+
233
252
 
234
253
  if __name__ == "__main__":
235
254
  unittest.main()
@@ -197,7 +197,7 @@ wheels = [
197
197
 
198
198
  [[package]]
199
199
  name = "localcoder"
200
- version = "0.2.0"
200
+ version = "0.2.1"
201
201
  source = { editable = "." }
202
202
  dependencies = [
203
203
  { name = "huggingface-hub" },
File without changes
File without changes
File without changes