localcoder 0.1.1__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.1.1 → localcoder-0.2.1}/PKG-INFO +1 -1
  2. {localcoder-0.1.1 → localcoder-0.2.1}/pyproject.toml +1 -1
  3. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/localcoder_agent.py +176 -0
  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.1.1 → localcoder-0.2.1}/tests/test_basic.py +44 -0
  15. {localcoder-0.1.1 → localcoder-0.2.1}/uv.lock +1 -1
  16. {localcoder-0.1.1 → localcoder-0.2.1}/.gitignore +0 -0
  17. {localcoder-0.1.1 → localcoder-0.2.1}/LICENSE +0 -0
  18. {localcoder-0.1.1 → localcoder-0.2.1}/README.md +0 -0
  19. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/__init__.py +0 -0
  20. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/__main__.py +0 -0
  21. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/agent.py +0 -0
  22. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/backends.py +0 -0
  23. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/bench.py +0 -0
  24. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/cli.py +0 -0
  25. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/localcoder_display.py +0 -0
  26. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/setup.py +0 -0
  27. {localcoder-0.1.1 → localcoder-0.2.1}/src/localcoder/tui.py +0 -0
  28. {localcoder-0.1.1 → 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.1.1
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.1.1"
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"
@@ -2673,6 +2673,7 @@ def main(argv=None):
2673
2673
  "/yolo": "Same as /bypass",
2674
2674
  "/log": "View debug log",
2675
2675
  "/think": "Toggle reasoning: none → low → medium → high",
2676
+ "/deploy": "Generate & deploy an AI-powered React app",
2676
2677
  "/exit": "Exit",
2677
2678
  }
2678
2679
 
@@ -2987,6 +2988,9 @@ def main(argv=None):
2987
2988
  continue
2988
2989
  if task in ("/exit", "/quit"):
2989
2990
  break
2991
+ if task == "/deploy" or task.startswith("/deploy "):
2992
+ _handle_deploy(task, messages, perms, system, console)
2993
+ continue
2990
2994
 
2991
2995
  console.print(Rule(style="dim"))
2992
2996
 
@@ -3030,6 +3034,178 @@ def main(argv=None):
3030
3034
  _cleanup_on_exit()
3031
3035
 
3032
3036
 
3037
+ def _handle_deploy(task, messages, perms, system, console):
3038
+ """Generate and deploy an AI-powered React app from template."""
3039
+ import shutil
3040
+ from rich.rule import Rule
3041
+
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
+ }
3056
+
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}")
3061
+ console.print()
3062
+
3063
+ try:
3064
+ choice = input(" > ").strip()
3065
+ except (EOFError, KeyboardInterrupt):
3066
+ return
3067
+
3068
+ if choice in TEMPLATES:
3069
+ tid, title, sys_prompt, placeholder = TEMPLATES[choice]
3070
+ if tid == "custom":
3071
+ 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]
3076
+ except (EOFError, KeyboardInterrupt):
3077
+ return
3078
+ if not description or not sys_prompt:
3079
+ return
3080
+ 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
3095
+ try:
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
3098
+ except (EOFError, KeyboardInterrupt):
3099
+ return
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"))
3107
+
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 ──
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)
3155
+
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}/")
3158
+
3159
+ # ── npm install ──
3160
+ console.print(f" [dim]Installing dependencies...[/]")
3161
+ try:
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"
3197
+
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")
3201
+ try:
3202
+ subprocess.run("npm run dev", shell=True, cwd=app_dir)
3203
+ 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.[/]")
3207
+
3208
+
3033
3209
  def _cleanup_on_exit():
3034
3210
  """Ask user if they want to free GPU memory on exit."""
3035
3211
  console.print()
@@ -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
+ }
@@ -206,5 +206,49 @@ class TestSandbox(unittest.TestCase):
206
206
  self.assertFalse(self._is_path_blocked("/tmp/test.py"))
207
207
 
208
208
 
209
+ class TestDeploy(unittest.TestCase):
210
+ """Deploy command tests."""
211
+
212
+ def test_deploy_function_exists(self):
213
+ from localcoder.localcoder_agent import _handle_deploy
214
+ self.assertTrue(callable(_handle_deploy))
215
+
216
+ def test_deploy_in_slash_commands(self):
217
+ """Deploy should be registered as a slash command."""
218
+ import inspect
219
+ from localcoder.localcoder_agent import main
220
+ source = inspect.getsource(main)
221
+ self.assertIn("/deploy", source)
222
+
223
+ def test_deploy_templates(self):
224
+ """Deploy should support template types."""
225
+ import inspect
226
+ from localcoder.localcoder_agent import _handle_deploy
227
+ source = inspect.getsource(_handle_deploy)
228
+ self.assertIn("chatbot", source)
229
+ self.assertIn("ingredients", source)
230
+ self.assertIn("code-review", source)
231
+ self.assertIn("custom", source)
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
+
252
+
209
253
  if __name__ == "__main__":
210
254
  unittest.main()
@@ -197,7 +197,7 @@ wheels = [
197
197
 
198
198
  [[package]]
199
199
  name = "localcoder"
200
- version = "0.1.1"
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