localcoder 0.2.1__tar.gz → 0.4.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 (66) hide show
  1. localcoder-0.4.0/.agents/skills/flashcards/SKILL.md +42 -0
  2. localcoder-0.4.0/.agents/skills/flashcards/assets/index.html +158 -0
  3. localcoder-0.4.0/.agents/skills/gallery/assets/index.html +92 -0
  4. localcoder-0.4.0/.agents/skills/quiz-game/SKILL.md +48 -0
  5. localcoder-0.4.0/.agents/skills/quiz-game/assets/index.html +213 -0
  6. localcoder-0.4.0/.agents/skills/web-app-patterns/SKILL.md +107 -0
  7. localcoder-0.4.0/.gitignore +40 -0
  8. {localcoder-0.2.1 → localcoder-0.4.0}/PKG-INFO +5 -2
  9. {localcoder-0.2.1 → localcoder-0.4.0}/pyproject.toml +8 -2
  10. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/agent.py +8 -0
  11. localcoder-0.4.0/src/localcoder/agent_session.py +206 -0
  12. localcoder-0.4.0/src/localcoder/cli.py +1487 -0
  13. localcoder-0.4.0/src/localcoder/compaction.py +276 -0
  14. localcoder-0.4.0/src/localcoder/finetune/__init__.py +1 -0
  15. localcoder-0.4.0/src/localcoder/finetune/dataset.py +331 -0
  16. localcoder-0.4.0/src/localcoder/finetune/localcoder_finetune.ipynb +291 -0
  17. localcoder-0.4.0/src/localcoder/finetune/train.py +244 -0
  18. localcoder-0.4.0/src/localcoder/localcoder_agent.py +6452 -0
  19. localcoder-0.4.0/src/localcoder/mcp_client.py +319 -0
  20. localcoder-0.4.0/src/localcoder/safe_commands.py +244 -0
  21. localcoder-0.4.0/src/localcoder/templates/framework/_adapters/chat.js +78 -0
  22. localcoder-0.4.0/src/localcoder/templates/framework/_adapters/image.js +61 -0
  23. localcoder-0.4.0/src/localcoder/templates/framework/_adapters/text.js +16 -0
  24. localcoder-0.4.0/src/localcoder/templates/framework/_adapters/voice.js +75 -0
  25. localcoder-0.4.0/src/localcoder/templates/framework/_server/package.json +6 -0
  26. localcoder-0.4.0/src/localcoder/templates/framework/_server/server.js +85 -0
  27. localcoder-0.4.0/src/localcoder/templates/framework/_shell/base.css +357 -0
  28. localcoder-0.4.0/src/localcoder/templates/framework/_shell/base.js +63 -0
  29. localcoder-0.4.0/src/localcoder/templates/framework/apps/chatbot/config.json +20 -0
  30. localcoder-0.4.0/src/localcoder/templates/framework/apps/homework-helper/config.json +19 -0
  31. localcoder-0.4.0/src/localcoder/templates/framework/apps/image-analyzer/config.json +19 -0
  32. localcoder-0.4.0/src/localcoder/templates/framework/apps/ingredients-scanner/config.json +19 -0
  33. localcoder-0.4.0/src/localcoder/templates/framework/apps/meeting-notes/config.json +18 -0
  34. localcoder-0.4.0/src/localcoder/templates/framework/apps/photo-todo/config.json +19 -0
  35. localcoder-0.4.0/src/localcoder/templates/framework/apps/translator/config.json +20 -0
  36. localcoder-0.4.0/src/localcoder/templates/framework/apps/voice-analyzer/config.json +18 -0
  37. localcoder-0.4.0/src/localcoder/templates/framework/apps/voice-memo/config.json +18 -0
  38. localcoder-0.4.0/src/localcoder/templates/framework/build.py +253 -0
  39. localcoder-0.4.0/src/localcoder/templates/static/flashcards/index.html +158 -0
  40. localcoder-0.4.0/src/localcoder/templates/static/gallery/index.html +92 -0
  41. localcoder-0.4.0/src/localcoder/templates/static/quiz-game/index.html +213 -0
  42. {localcoder-0.2.1 → localcoder-0.4.0}/tests/test_basic.py +43 -5
  43. localcoder-0.2.1/.gitignore +0 -7
  44. localcoder-0.2.1/src/localcoder/cli.py +0 -837
  45. localcoder-0.2.1/src/localcoder/localcoder_agent.py +0 -3281
  46. localcoder-0.2.1/uv.lock +0 -449
  47. {localcoder-0.2.1 → localcoder-0.4.0}/LICENSE +0 -0
  48. {localcoder-0.2.1 → localcoder-0.4.0}/README.md +0 -0
  49. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/__init__.py +0 -0
  50. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/__main__.py +0 -0
  51. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/backends.py +0 -0
  52. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/bench.py +0 -0
  53. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/localcoder_display.py +0 -0
  54. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/setup.py +0 -0
  55. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/.env.local +0 -0
  56. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/next.config.ts +0 -0
  57. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/package.json +0 -0
  58. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/postcss.config.mjs +0 -0
  59. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/api/ai/route.ts +0 -0
  60. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/globals.css +0 -0
  61. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/layout.tsx +0 -0
  62. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/page.tsx +0 -0
  63. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/components/Chat.tsx +0 -0
  64. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/templates/ai-app/tsconfig.json +0 -0
  65. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/tui.py +0 -0
  66. {localcoder-0.2.1 → localcoder-0.4.0}/src/localcoder/voice.py +0 -0
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: flashcards
3
+ description: Build a flashcard study app. Use when asked to create flashcards, vocabulary cards, study app, memorization tool, language learning cards, or any flip-card learning experience.
4
+ ---
5
+
6
+ # Flashcards Template
7
+
8
+ Build flip-card study apps from this template. Works for vocabulary, language learning, history facts, science terms — any topic with front/back pairs.
9
+
10
+ ## How to use
11
+
12
+ 1. Read the template: `assets/index.html`
13
+ 2. Copy it to the user's project directory with `write_file`
14
+ 3. Customize these parts:
15
+ - Replace `{{TITLE}}` with the app name (e.g. "French Vocabulary")
16
+ - Replace `{{ICON}}` with an emoji (e.g. 🇫🇷, 🧠, 📚)
17
+ - Replace `{{SUBTITLE}}` with a tagline
18
+ - Replace the `CARDS` array with your content:
19
+ ```js
20
+ const CARDS = [
21
+ { front: "Question", back: "Answer", emoji: "📝", detail: "Extra info" },
22
+ ];
23
+ ```
24
+
25
+ ## Built-in features (don't rebuild these)
26
+
27
+ - 3D flip card animation (CSS transform)
28
+ - Know it / Again buttons (spaced repetition)
29
+ - Progress bar and score tracking
30
+ - Cards shuffle on restart
31
+ - "Again" cards go back in the deck
32
+ - End screen with final score
33
+ - Mobile responsive
34
+
35
+ ## Testing
36
+
37
+ After creating the app, verify:
38
+ 1. Card displays front text
39
+ 2. Tapping flips to show back
40
+ 3. Know/Again buttons appear after flip
41
+ 4. Progress bar advances
42
+ 5. End screen shows when deck is empty
@@ -0,0 +1,158 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{TITLE}}</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;900&display=swap');
9
+ *{margin:0;padding:0;box-sizing:border-box}
10
+ :root{--bg:#fefce8;--card:#fff;--primary:#8b5cf6;--accent:#ec4899;--correct:#10b981;--text:#1e293b;--dim:#94a3b8;--radius:20px}
11
+ body{font-family:'Nunito',sans-serif;background:var(--bg);color:var(--text);min-height:100dvh;display:flex;align-items:center;justify-content:center}
12
+ .app{max-width:440px;width:100%;padding:24px;display:flex;flex-direction:column;align-items:center;gap:20px}
13
+ .header{text-align:center}
14
+ h1{font-size:2rem;font-weight:900;color:var(--primary)}
15
+ .subtitle{color:var(--dim);font-size:0.9rem;margin-top:4px}
16
+ .score-row{display:flex;gap:16px;font-size:0.95rem;font-weight:700;color:var(--dim)}
17
+ .score-row span{color:var(--text)}
18
+ /* Flip card */
19
+ .card-container{width:100%;height:280px;perspective:1000px;cursor:pointer}
20
+ .card-inner{position:relative;width:100%;height:100%;transition:transform 0.6s cubic-bezier(.4,0,.2,1);transform-style:preserve-3d}
21
+ .card-container.flipped .card-inner{transform:rotateY(180deg)}
22
+ .card-front,.card-back{position:absolute;inset:0;backface-visibility:hidden;border-radius:var(--radius);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px;box-shadow:0 4px 24px rgba(0,0,0,0.06)}
23
+ .card-front{background:linear-gradient(135deg,#8b5cf6,#6366f1);color:#fff}
24
+ .card-back{background:var(--card);transform:rotateY(180deg);border:2px solid #e2e8f0}
25
+ .card-emoji{font-size:3rem;margin-bottom:12px}
26
+ .card-text{font-size:1.6rem;font-weight:900;text-align:center}
27
+ .card-hint{font-size:0.85rem;margin-top:8px;opacity:0.7}
28
+ .card-answer{font-size:1.4rem;font-weight:700;color:var(--primary);text-align:center}
29
+ .card-detail{font-size:0.9rem;color:var(--dim);margin-top:8px;text-align:center}
30
+ /* Buttons */
31
+ .btn-row{display:flex;gap:10px;width:100%}
32
+ .btn{flex:1;border:none;border-radius:14px;padding:14px;font-size:1rem;font-weight:700;font-family:inherit;cursor:pointer;transition:all .15s}
33
+ .btn-know{background:var(--correct);color:#fff}
34
+ .btn-know:hover{transform:translateY(-2px)}
35
+ .btn-again{background:#fee2e2;color:#ef4444}
36
+ .btn-again:hover{transform:translateY(-2px)}
37
+ .btn-primary{background:var(--accent);color:#fff;width:100%;padding:16px;font-size:1.1rem}
38
+ .progress{width:100%;height:6px;background:#e2e8f0;border-radius:99px;overflow:hidden}
39
+ .progress-fill{height:100%;background:var(--primary);border-radius:99px;transition:width .3s}
40
+ .tap-hint{color:var(--dim);font-size:0.8rem;animation:pulse 2s infinite}
41
+ @keyframes pulse{0%,100%{opacity:0.5}50%{opacity:1}}
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <div class="app">
46
+ <div class="header">
47
+ <h1>{{ICON}} {{TITLE}}</h1>
48
+ <p class="subtitle">{{SUBTITLE}}</p>
49
+ </div>
50
+
51
+ <div class="score-row">
52
+ <div>✅ Know: <span id="known">0</span></div>
53
+ <div>🔄 Again: <span id="again">0</span></div>
54
+ <div>📚 Left: <span id="left">0</span></div>
55
+ </div>
56
+
57
+ <div class="progress"><div class="progress-fill" id="progressBar" style="width:0%"></div></div>
58
+
59
+ <div class="card-container" id="flashcard" onclick="flipCard()">
60
+ <div class="card-inner">
61
+ <div class="card-front">
62
+ <div class="card-emoji" id="cardEmoji">📝</div>
63
+ <div class="card-text" id="cardFront">Loading...</div>
64
+ <div class="card-hint">Tap to flip</div>
65
+ </div>
66
+ <div class="card-back">
67
+ <div class="card-answer" id="cardBack">Answer</div>
68
+ <div class="card-detail" id="cardDetail"></div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <p class="tap-hint" id="tapHint">👆 Tap the card to reveal</p>
74
+
75
+ <div class="btn-row" id="actions" style="display:none">
76
+ <button class="btn btn-again" onclick="markAgain()">🔄 Again</button>
77
+ <button class="btn btn-know" onclick="markKnown()">✅ Know It!</button>
78
+ </div>
79
+
80
+ <!-- End screen -->
81
+ <div id="endScreen" style="display:none;text-align:center">
82
+ <h1>🎉 All Done!</h1>
83
+ <p style="font-size:1.2rem;margin:12px 0">You knew <strong id="finalKnown">0</strong> out of <strong id="finalTotal">0</strong></p>
84
+ <button class="btn btn-primary" onclick="restart()">Study Again 🔄</button>
85
+ </div>
86
+ </div>
87
+
88
+ <script>
89
+ // ── CARD DATA — Agent replaces this array ──
90
+ const CARDS = [
91
+ { front: "Hello", back: "Bonjour", emoji: "👋", detail: "French greeting" },
92
+ { front: "Thank you", back: "Merci", emoji: "🙏", detail: "Express gratitude" },
93
+ { front: "Goodbye", back: "Au revoir", emoji: "👋", detail: "Farewell" },
94
+ { front: "Please", back: "S'il vous plaît", emoji: "🤲", detail: "Polite request" },
95
+ { front: "Yes", back: "Oui", emoji: "✅", detail: "Affirmative" },
96
+ { front: "No", back: "Non", emoji: "❌", detail: "Negative" },
97
+ { front: "Water", back: "Eau", emoji: "💧", detail: "Essential for life" },
98
+ { front: "Food", back: "Nourriture", emoji: "🍽️", detail: "Sustenance" },
99
+ ];
100
+
101
+ let deck = [], known = 0, again = 0, current = null, isFlipped = false;
102
+
103
+ function shuffle(arr) { return [...arr].sort(() => Math.random() - 0.5); }
104
+
105
+ function restart() {
106
+ deck = shuffle(CARDS); known = 0; again = 0; isFlipped = false;
107
+ document.getElementById('endScreen').style.display = 'none';
108
+ document.getElementById('flashcard').style.display = '';
109
+ document.getElementById('tapHint').style.display = '';
110
+ updateUI(); showNext();
111
+ }
112
+
113
+ function updateUI() {
114
+ document.getElementById('known').textContent = known;
115
+ document.getElementById('again').textContent = again;
116
+ document.getElementById('left').textContent = deck.length;
117
+ const total = CARDS.length;
118
+ document.getElementById('progressBar').style.width = `${((total - deck.length)/total)*100}%`;
119
+ }
120
+
121
+ function showNext() {
122
+ if (deck.length === 0) { endGame(); return; }
123
+ current = deck.shift();
124
+ isFlipped = false;
125
+ document.getElementById('flashcard').classList.remove('flipped');
126
+ document.getElementById('cardEmoji').textContent = current.emoji;
127
+ document.getElementById('cardFront').textContent = current.front;
128
+ document.getElementById('cardBack').textContent = current.back;
129
+ document.getElementById('cardDetail').textContent = current.detail || '';
130
+ document.getElementById('actions').style.display = 'none';
131
+ document.getElementById('tapHint').style.display = '';
132
+ updateUI();
133
+ }
134
+
135
+ function flipCard() {
136
+ if (isFlipped) return;
137
+ isFlipped = true;
138
+ document.getElementById('flashcard').classList.add('flipped');
139
+ document.getElementById('actions').style.display = 'flex';
140
+ document.getElementById('tapHint').style.display = 'none';
141
+ }
142
+
143
+ function markKnown() { known++; updateUI(); showNext(); }
144
+ function markAgain() { again++; deck.push(current); updateUI(); showNext(); }
145
+
146
+ function endGame() {
147
+ document.getElementById('flashcard').style.display = 'none';
148
+ document.getElementById('tapHint').style.display = 'none';
149
+ document.getElementById('actions').style.display = 'none';
150
+ document.getElementById('finalKnown').textContent = known;
151
+ document.getElementById('finalTotal').textContent = CARDS.length;
152
+ document.getElementById('endScreen').style.display = '';
153
+ }
154
+
155
+ restart();
156
+ </script>
157
+ </body>
158
+ </html>
@@ -0,0 +1,92 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{TITLE}}</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap');
9
+ *{margin:0;padding:0;box-sizing:border-box}
10
+ :root{--bg:#0a0a0f;--card:rgba(255,255,255,0.04);--border:rgba(255,255,255,0.08);--text:#e4e4e7;--dim:#71717a;--accent:#f97316;--radius:16px}
11
+ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);min-height:100dvh}
12
+ .container{max-width:1200px;margin:0 auto;padding:40px 20px}
13
+ .hero{text-align:center;padding:60px 0 40px}
14
+ h1{font-size:3rem;font-weight:700;background:linear-gradient(135deg,var(--accent),#ec4899);-webkit-background-clip:text;color:transparent}
15
+ .hero-sub{color:var(--dim);font-size:1.1rem;margin-top:8px;max-width:500px;margin-left:auto;margin-right:auto}
16
+ /* Masonry grid */
17
+ .gallery{columns:3;column-gap:16px;padding:20px 0}
18
+ @media(max-width:768px){.gallery{columns:2}}
19
+ @media(max-width:480px){.gallery{columns:1}}
20
+ .gallery-item{break-inside:avoid;margin-bottom:16px;border-radius:var(--radius);overflow:hidden;background:var(--card);border:1px solid var(--border);transition:transform .2s,box-shadow .2s;cursor:pointer}
21
+ .gallery-item:hover{transform:translateY(-4px);box-shadow:0 8px 32px rgba(0,0,0,0.3)}
22
+ .gallery-item img{width:100%;display:block}
23
+ .gallery-item .caption{padding:12px 16px;font-size:0.85rem;color:var(--dim)}
24
+ .gallery-item .caption strong{color:var(--text);display:block;margin-bottom:4px}
25
+ /* Lightbox */
26
+ .lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.9);z-index:100;align-items:center;justify-content:center;cursor:pointer}
27
+ .lightbox.active{display:flex}
28
+ .lightbox img{max-width:90vw;max-height:90vh;border-radius:12px;object-fit:contain}
29
+ .lightbox-close{position:fixed;top:20px;right:20px;color:#fff;font-size:2rem;cursor:pointer;z-index:101}
30
+ /* Footer */
31
+ .footer{text-align:center;padding:40px 0;color:var(--dim);font-size:0.8rem;letter-spacing:0.05em;text-transform:uppercase}
32
+ /* Fade in animation */
33
+ .gallery-item{opacity:0;animation:fadeUp .5s ease-out forwards}
34
+ @keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="container">
39
+ <div class="hero">
40
+ <h1>{{TITLE}}</h1>
41
+ <p class="hero-sub">{{SUBTITLE}}</p>
42
+ </div>
43
+
44
+ <div class="gallery" id="gallery">
45
+ <!-- Agent populates these items with searched images -->
46
+ <div class="gallery-item" style="animation-delay:0.0s" onclick="openLightbox(this)">
47
+ <img src="https://source.unsplash.com/random/600x400/?{{KEYWORD}}&sig=1" alt="" loading="lazy">
48
+ <div class="caption"><strong>{{CAPTION_1}}</strong>{{DETAIL_1}}</div>
49
+ </div>
50
+ <div class="gallery-item" style="animation-delay:0.1s" onclick="openLightbox(this)">
51
+ <img src="https://source.unsplash.com/random/600x800/?{{KEYWORD}}&sig=2" alt="" loading="lazy">
52
+ <div class="caption"><strong>{{CAPTION_2}}</strong>{{DETAIL_2}}</div>
53
+ </div>
54
+ <div class="gallery-item" style="animation-delay:0.2s" onclick="openLightbox(this)">
55
+ <img src="https://source.unsplash.com/random/600x500/?{{KEYWORD}}&sig=3" alt="" loading="lazy">
56
+ <div class="caption"><strong>{{CAPTION_3}}</strong>{{DETAIL_3}}</div>
57
+ </div>
58
+ <div class="gallery-item" style="animation-delay:0.3s" onclick="openLightbox(this)">
59
+ <img src="https://source.unsplash.com/random/600x700/?{{KEYWORD}}&sig=4" alt="" loading="lazy">
60
+ <div class="caption"><strong>{{CAPTION_4}}</strong>{{DETAIL_4}}</div>
61
+ </div>
62
+ <div class="gallery-item" style="animation-delay:0.4s" onclick="openLightbox(this)">
63
+ <img src="https://source.unsplash.com/random/600x600/?{{KEYWORD}}&sig=5" alt="" loading="lazy">
64
+ <div class="caption"><strong>{{CAPTION_5}}</strong>{{DETAIL_5}}</div>
65
+ </div>
66
+ <div class="gallery-item" style="animation-delay:0.5s" onclick="openLightbox(this)">
67
+ <img src="https://source.unsplash.com/random/600x450/?{{KEYWORD}}&sig=6" alt="" loading="lazy">
68
+ <div class="caption"><strong>{{CAPTION_6}}</strong>{{DETAIL_6}}</div>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="footer">{{FOOTER}}</div>
73
+ </div>
74
+
75
+ <div class="lightbox" id="lightbox" onclick="closeLightbox()">
76
+ <span class="lightbox-close">&times;</span>
77
+ <img id="lightbox-img" src="" alt="">
78
+ </div>
79
+
80
+ <script>
81
+ function openLightbox(item) {
82
+ const img = item.querySelector('img');
83
+ document.getElementById('lightbox-img').src = img.src.replace(/\/\d+x\d+\//, '/1200x900/');
84
+ document.getElementById('lightbox').classList.add('active');
85
+ }
86
+ function closeLightbox() {
87
+ document.getElementById('lightbox').classList.remove('active');
88
+ }
89
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeLightbox(); });
90
+ </script>
91
+ </body>
92
+ </html>
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: quiz-game
3
+ description: Build an interactive quiz game app. Use when asked to create a quiz, trivia, math game, learning game, educational game, or any multiple-choice question app. Supports SVG visuals, score tracking, confetti, and sound effects.
4
+ ---
5
+
6
+ # Quiz Game Template
7
+
8
+ Build interactive quiz apps from this template. Works for math, trivia, vocabulary, science — any topic with questions and answers.
9
+
10
+ ## How to use
11
+
12
+ 1. Read the template: `assets/index.html`
13
+ 2. Copy it to the user's project directory with `write_file`
14
+ 3. Customize these parts:
15
+ - Replace `{{TITLE}}` with the app name (e.g. "Math Fractions Quiz")
16
+ - Replace `{{ICON}}` with an emoji (e.g. 🍕, 🧮, 🌍)
17
+ - Replace `{{SUBTITLE}}` with a tagline
18
+ - Replace `{{QUESTION_TEXT}}` with the question prompt
19
+ - Modify the `generateQuestion()` function for your topic
20
+ - Modify the `drawPie()` function or replace with different SVG visuals
21
+ - Update CSS colors in `:root` to match the theme
22
+
23
+ ## Built-in features (don't rebuild these)
24
+
25
+ - Start screen → Game screen → End screen transitions
26
+ - SVG pie chart with real Math.cos/Math.sin arc paths
27
+ - 4 answer buttons with correct/wrong animations
28
+ - Score counter with ⭐ stars
29
+ - Progress bar
30
+ - Confetti celebration on correct answer
31
+ - Web Audio API sound effects
32
+ - Mobile responsive
33
+
34
+ ## Example customizations
35
+
36
+ **Math fractions:** Keep the SVG pie, change questions to fractions
37
+ **World capitals:** Remove SVG, show country flag emoji, change to geography questions
38
+ **Vocabulary:** Remove SVG, show word in large text, answers are definitions
39
+ **Times tables:** Remove SVG, show "7 × 8 = ?", answers are numbers
40
+
41
+ ## Testing
42
+
43
+ After creating the app, verify:
44
+ 1. PLAY button hides start screen and shows game
45
+ 2. SVG visual renders correctly (if using pie chart)
46
+ 3. Clicking an answer shows correct/wrong feedback
47
+ 4. Score increments on correct answers
48
+ 5. End screen shows after all questions
@@ -0,0 +1,213 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{TITLE}}</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;900&display=swap');
9
+ *{margin:0;padding:0;box-sizing:border-box}
10
+ :root{--bg:#f0fdf4;--card:#fff;--primary:#10b981;--accent:#f97316;--wrong:#ef4444;--text:#1e293b;--dim:#94a3b8;--radius:20px}
11
+ body{font-family:'Nunito',sans-serif;background:var(--bg);color:var(--text);min-height:100dvh;display:flex;align-items:center;justify-content:center}
12
+ .screen{display:none;flex-direction:column;align-items:center;gap:24px;padding:32px;max-width:480px;width:100%}
13
+ .screen.active{display:flex}
14
+ .card{background:var(--card);border-radius:var(--radius);padding:32px;width:100%;box-shadow:0 4px 24px rgba(0,0,0,0.06);text-align:center}
15
+ h1{font-size:2.2rem;font-weight:900;color:var(--primary)}
16
+ .subtitle{color:var(--dim);font-size:1rem;margin-top:4px}
17
+ .btn{border:none;border-radius:14px;padding:16px 40px;font-size:1.1rem;font-weight:700;font-family:inherit;cursor:pointer;transition:all .15s}
18
+ .btn-primary{background:var(--accent);color:#fff;box-shadow:0 4px 16px rgba(249,115,22,0.3)}
19
+ .btn-primary:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(249,115,22,0.4)}
20
+ .btn-option{background:var(--card);border:2px solid #e2e8f0;width:100%;padding:14px;font-size:1.05rem;border-radius:14px}
21
+ .btn-option:hover{border-color:var(--primary);background:#f0fdf4}
22
+ .btn-option.correct{background:#d1fae5;border-color:var(--primary);animation:pop .3s}
23
+ .btn-option.wrong{background:#fee2e2;border-color:var(--wrong);animation:shake .3s}
24
+ @keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.05)}100%{transform:scale(1)}}
25
+ @keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
26
+ .options{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%}
27
+ .visual{width:200px;height:200px;margin:0 auto}
28
+ .score-bar{display:flex;align-items:center;gap:8px;font-size:1.1rem;font-weight:700}
29
+ .stars{letter-spacing:4px;font-size:1.3rem}
30
+ .progress{width:100%;height:6px;background:#e2e8f0;border-radius:99px;overflow:hidden}
31
+ .progress-fill{height:100%;background:var(--primary);border-radius:99px;transition:width .3s}
32
+ /* Confetti */
33
+ .confetti-piece{position:fixed;width:10px;height:10px;border-radius:2px;animation:confetti-fall 1.5s ease-out forwards;pointer-events:none;z-index:999}
34
+ @keyframes confetti-fall{0%{transform:translateY(-20px) rotate(0deg);opacity:1}100%{transform:translateY(100vh) rotate(720deg);opacity:0}}
35
+ </style>
36
+ </head>
37
+ <body>
38
+
39
+ <!-- START SCREEN -->
40
+ <div id="start" class="screen active">
41
+ <div class="card">
42
+ <h1>{{ICON}} {{TITLE}}</h1>
43
+ <p class="subtitle">{{SUBTITLE}}</p>
44
+ </div>
45
+ <button class="btn btn-primary" onclick="startGame()">PLAY! ▶️</button>
46
+ </div>
47
+
48
+ <!-- GAME SCREEN -->
49
+ <div id="game" class="screen">
50
+ <div class="score-bar">
51
+ <span id="scoreNum">0</span> pts
52
+ <span class="stars" id="stars"></span>
53
+ </div>
54
+ <div class="progress"><div class="progress-fill" id="progressBar" style="width:0%"></div></div>
55
+ <div class="card">
56
+ <div class="visual" id="visual">
57
+ <!-- SVG or visual goes here — agent customizes this -->
58
+ <svg viewBox="0 0 200 200" id="pieSvg"></svg>
59
+ </div>
60
+ <p id="question" style="margin-top:16px;font-size:1.15rem;font-weight:700">{{QUESTION_TEXT}}</p>
61
+ </div>
62
+ <div class="options" id="options">
63
+ <!-- Agent generates answer buttons dynamically -->
64
+ </div>
65
+ </div>
66
+
67
+ <!-- END SCREEN -->
68
+ <div id="end" class="screen">
69
+ <div class="card">
70
+ <h1>🎉 Great Job!</h1>
71
+ <p style="font-size:1.3rem;margin:12px 0">Score: <strong id="finalScore">0</strong></p>
72
+ <p class="stars" id="finalStars"></p>
73
+ </div>
74
+ <button class="btn btn-primary" onclick="startGame()">Play Again 🔄</button>
75
+ </div>
76
+
77
+ <script>
78
+ // ── CONFIG — Agent customizes these ──
79
+ const TOTAL_QUESTIONS = 10;
80
+ const QUESTION_TEXT = "{{QUESTION_TEXT}}";
81
+
82
+ // ── STATE ──
83
+ let score = 0, question = 0, correctAnswer = null;
84
+
85
+ function showScreen(id) {
86
+ document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
87
+ document.getElementById(id).classList.add('active');
88
+ }
89
+
90
+ function startGame() {
91
+ score = 0; question = 0;
92
+ document.getElementById('scoreNum').textContent = '0';
93
+ document.getElementById('stars').textContent = '';
94
+ document.getElementById('progressBar').style.width = '0%';
95
+ showScreen('game');
96
+ nextQuestion();
97
+ }
98
+
99
+ // ── VISUAL: SVG Pie Chart — draws fraction as colored slices ──
100
+ function drawPie(numerator, denominator) {
101
+ const svg = document.getElementById('pieSvg');
102
+ svg.innerHTML = '';
103
+ const cx=100, cy=100, r=85;
104
+ // Background circle
105
+ const bg = document.createElementNS('http://www.w3.org/2000/svg','circle');
106
+ bg.setAttribute('cx',cx); bg.setAttribute('cy',cy); bg.setAttribute('r',r);
107
+ bg.setAttribute('fill','#f1f5f9'); bg.setAttribute('stroke','#cbd5e1'); bg.setAttribute('stroke-width','2');
108
+ svg.appendChild(bg);
109
+ // Draw slices
110
+ for (let i = 0; i < denominator; i++) {
111
+ const startAngle = (i * 360 / denominator - 90) * Math.PI / 180;
112
+ const endAngle = ((i+1) * 360 / denominator - 90) * Math.PI / 180;
113
+ const x1 = cx + r * Math.cos(startAngle);
114
+ const y1 = cy + r * Math.sin(startAngle);
115
+ const x2 = cx + r * Math.cos(endAngle);
116
+ const y2 = cy + r * Math.sin(endAngle);
117
+ const large = (360/denominator > 180) ? 1 : 0;
118
+ const path = document.createElementNS('http://www.w3.org/2000/svg','path');
119
+ path.setAttribute('d', `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${large},1 ${x2},${y2} Z`);
120
+ path.setAttribute('fill', i < numerator ? 'var(--primary)' : '#e2e8f0');
121
+ path.setAttribute('stroke', '#fff'); path.setAttribute('stroke-width', '2');
122
+ svg.appendChild(path);
123
+ }
124
+ }
125
+
126
+ // ── QUESTION GENERATOR — Agent customizes this function ──
127
+ function generateQuestion() {
128
+ const denominators = [2, 3, 4, 5, 6, 8];
129
+ const d = denominators[Math.floor(Math.random() * denominators.length)];
130
+ const n = Math.floor(Math.random() * (d - 1)) + 1;
131
+ correctAnswer = `${n}/${d}`;
132
+ drawPie(n, d);
133
+ document.getElementById('question').textContent = QUESTION_TEXT;
134
+ // Generate 4 options (1 correct + 3 wrong)
135
+ const opts = new Set([correctAnswer]);
136
+ while (opts.size < 4) {
137
+ const wd = denominators[Math.floor(Math.random() * denominators.length)];
138
+ const wn = Math.floor(Math.random() * (wd - 1)) + 1;
139
+ opts.add(`${wn}/${wd}`);
140
+ }
141
+ const shuffled = [...opts].sort(() => Math.random() - 0.5);
142
+ const container = document.getElementById('options');
143
+ container.innerHTML = '';
144
+ shuffled.forEach(opt => {
145
+ const btn = document.createElement('button');
146
+ btn.className = 'btn btn-option';
147
+ btn.textContent = opt;
148
+ btn.onclick = () => checkAnswer(btn, opt);
149
+ container.appendChild(btn);
150
+ });
151
+ }
152
+
153
+ function checkAnswer(btn, selected) {
154
+ document.querySelectorAll('.btn-option').forEach(b => b.disabled = true);
155
+ if (selected === correctAnswer) {
156
+ btn.classList.add('correct');
157
+ score++;
158
+ document.getElementById('scoreNum').textContent = score;
159
+ document.getElementById('stars').textContent = '⭐'.repeat(Math.floor(score / 2));
160
+ spawnConfetti();
161
+ playSound(800, 0.15);
162
+ } else {
163
+ btn.classList.add('wrong');
164
+ document.querySelectorAll('.btn-option').forEach(b => {
165
+ if (b.textContent === correctAnswer) b.classList.add('correct');
166
+ });
167
+ playSound(200, 0.2);
168
+ }
169
+ question++;
170
+ document.getElementById('progressBar').style.width = `${(question/TOTAL_QUESTIONS)*100}%`;
171
+ setTimeout(() => {
172
+ if (question >= TOTAL_QUESTIONS) endGame();
173
+ else nextQuestion();
174
+ }, 1200);
175
+ }
176
+
177
+ function nextQuestion() { generateQuestion(); }
178
+
179
+ function endGame() {
180
+ document.getElementById('finalScore').textContent = `${score}/${TOTAL_QUESTIONS}`;
181
+ document.getElementById('finalStars').textContent = '⭐'.repeat(Math.floor(score / 2));
182
+ showScreen('end');
183
+ }
184
+
185
+ // ── CONFETTI ──
186
+ function spawnConfetti() {
187
+ const colors = ['#10b981','#f97316','#3b82f6','#ec4899','#eab308'];
188
+ for (let i = 0; i < 30; i++) {
189
+ const el = document.createElement('div');
190
+ el.className = 'confetti-piece';
191
+ el.style.left = Math.random() * 100 + 'vw';
192
+ el.style.background = colors[Math.floor(Math.random()*colors.length)];
193
+ el.style.animationDelay = Math.random() * 0.5 + 's';
194
+ el.style.width = (Math.random()*8+6)+'px';
195
+ el.style.height = (Math.random()*8+6)+'px';
196
+ document.body.appendChild(el);
197
+ setTimeout(() => el.remove(), 2000);
198
+ }
199
+ }
200
+
201
+ // ── SOUND ──
202
+ function playSound(freq, dur) {
203
+ try {
204
+ const ctx = new (window.AudioContext||window.webkitAudioContext)();
205
+ const osc = ctx.createOscillator(); const gain = ctx.createGain();
206
+ osc.connect(gain); gain.connect(ctx.destination);
207
+ osc.frequency.value = freq; gain.gain.value = 0.1;
208
+ osc.start(); osc.stop(ctx.currentTime + dur);
209
+ } catch(e) {}
210
+ }
211
+ </script>
212
+ </body>
213
+ </html>
@@ -0,0 +1,107 @@
1
+ # Web App Patterns
2
+
3
+ Reference patterns for building web applications. Load this skill when the user asks to build a web app, Express server, or any project that needs a backend.
4
+
5
+ ## When to Use
6
+ - User asks to build an AI-powered app (analyzer, chatbot, scanner)
7
+ - User asks for a server/backend
8
+ - NOT for simple static pages (those are just index.html)
9
+
10
+ ## 3-File Pattern (Express + LLM)
11
+
12
+ Always create 3 files in the SAME directory:
13
+
14
+ ### package.json
15
+ ```json
16
+ {"name":"app","scripts":{"start":"node server.js"},"dependencies":{"express":"^4"}}
17
+ ```
18
+
19
+ ### server.js
20
+ ```javascript
21
+ const express = require('express');
22
+ const app = express();
23
+ app.use(express.json({limit:'50mb'}));
24
+ const API_BASE = process.env.LLM_API_BASE || 'http://127.0.0.1:8089/v1';
25
+ const MODEL = process.env.LLM_MODEL || 'local';
26
+ app.get('/', (req,res) => res.sendFile(__dirname+'/index.html'));
27
+ app.post('/api/analyze', async (req,res) => {
28
+ const {message, image} = req.body;
29
+ const userContent = image
30
+ ? [{type:'text',text:message}, {type:'image_url',image_url:{url:image}}]
31
+ : message;
32
+ const r = await fetch(API_BASE+'/chat/completions', {
33
+ method:'POST', headers:{'Content-Type':'application/json'},
34
+ body:JSON.stringify({model:MODEL, stream:false, max_tokens:2048,
35
+ messages:[{role:'system',content:SYSTEM_PROMPT}, {role:'user',content:userContent}]})});
36
+ const data = await r.json();
37
+ res.json({analysis: data.choices[0].message.content});
38
+ });
39
+ app.listen(3000);
40
+ ```
41
+
42
+ ### index.html
43
+ Single file with inline CSS+JS. Dark theme.
44
+
45
+ ## Frontend Design System
46
+ - Background: `#0a0a14`
47
+ - Card: `background:rgba(255,255,255,0.04); backdrop-filter:blur(20px); border:1px solid rgba(255,255,255,0.08); border-radius:24px`
48
+ - Buttons: `border-radius:14px; background:linear-gradient(135deg,#6366f1,#8b5cf6); color:white; font-weight:600; padding:14px 28px`
49
+ - Title gradient: `background:linear-gradient(to right,#f97316,#22c55e); -webkit-background-clip:text; color:transparent`
50
+ - Result area: `background:rgba(255,255,255,0.03); border-left:3px solid #22c55e; border-radius:16px; padding:24px`
51
+ - Font: `system-ui`. Transitions on all interactive elements.
52
+
53
+ ## Image Upload Pattern
54
+ ```javascript
55
+ let imageBase64 = null;
56
+ function uploadImage() {
57
+ const input = document.createElement('input');
58
+ input.type='file'; input.accept='image/*';
59
+ input.onchange = e => {
60
+ const file = e.target.files[0]; if(!file) return;
61
+ const reader = new FileReader();
62
+ reader.onload = ev => { imageBase64 = ev.target.result;
63
+ document.getElementById('preview').src = imageBase64;
64
+ document.getElementById('preview').style.display = 'block'; };
65
+ reader.readAsDataURL(file); };
66
+ input.click();
67
+ }
68
+ ```
69
+
70
+ ## Camera Capture Pattern
71
+ ```javascript
72
+ async function openCamera() {
73
+ const stream = await navigator.mediaDevices.getUserMedia({video:{facingMode:'environment'}});
74
+ const video = document.getElementById('camVideo');
75
+ video.srcObject = stream; video.style.display='block'; video.play();
76
+ }
77
+ function capturePhoto() {
78
+ const video = document.getElementById('camVideo');
79
+ const canvas = document.createElement('canvas');
80
+ canvas.width=video.videoWidth; canvas.height=video.videoHeight;
81
+ canvas.getContext('2d').drawImage(video,0,0);
82
+ imageBase64 = canvas.toDataURL('image/jpeg',0.8);
83
+ video.srcObject.getTracks().forEach(t=>t.stop()); video.style.display='none';
84
+ }
85
+ ```
86
+
87
+ ## Loading Animation
88
+ ```css
89
+ @keyframes scan { 0%{transform:translateY(-100%)} 100%{transform:translateY(100%)} }
90
+ .scanning { position:relative; overflow:hidden; }
91
+ .scanning::after { content:''; position:absolute; left:0; right:0; height:2px;
92
+ background:linear-gradient(90deg,transparent,#22c55e,transparent); animation:scan 1.5s infinite; }
93
+ @keyframes dots { 0%{content:''} 33%{content:'.'} 66%{content:'..'} 100%{content:'...'} }
94
+ .dots::after { content:''; animation:dots 1.5s infinite steps(4); }
95
+ ```
96
+
97
+ ## Testing Checklist
98
+ 1. Static HTML: `python3 -m http.server 8888 &` then `curl -s http://localhost:8888/`
99
+ 2. Express: `npm install && node server.js &` then `curl -s http://localhost:3000/`
100
+ 3. Check JS: `node -e "...parse script tags..."`
101
+ 4. Fix any errors, re-test until passing.
102
+
103
+ ## Common Bugs
104
+ - Start screen not hiding: use `onclick='getElementById("s1").style.display="none"'`
105
+ - SVG arcs: use `Math.cos(angle*Math.PI/180)*radius` — never approximate
106
+ - Fetch to /api without server: static HTML can't call /api. Use local JS logic.
107
+ - Always add `transition` on hover/active states.