localcoder 0.3.0__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.
- localcoder-0.4.0/.agents/skills/flashcards/SKILL.md +42 -0
- localcoder-0.4.0/.agents/skills/flashcards/assets/index.html +158 -0
- localcoder-0.4.0/.agents/skills/gallery/assets/index.html +92 -0
- localcoder-0.4.0/.agents/skills/quiz-game/SKILL.md +48 -0
- localcoder-0.4.0/.agents/skills/quiz-game/assets/index.html +213 -0
- localcoder-0.4.0/.agents/skills/web-app-patterns/SKILL.md +107 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/.gitignore +20 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/PKG-INFO +5 -2
- {localcoder-0.3.0 → localcoder-0.4.0}/pyproject.toml +8 -2
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/agent.py +8 -0
- localcoder-0.4.0/src/localcoder/agent_session.py +206 -0
- localcoder-0.4.0/src/localcoder/cli.py +1487 -0
- localcoder-0.4.0/src/localcoder/compaction.py +276 -0
- localcoder-0.4.0/src/localcoder/finetune/__init__.py +1 -0
- localcoder-0.4.0/src/localcoder/finetune/dataset.py +331 -0
- localcoder-0.4.0/src/localcoder/finetune/localcoder_finetune.ipynb +291 -0
- localcoder-0.4.0/src/localcoder/finetune/train.py +244 -0
- localcoder-0.4.0/src/localcoder/localcoder_agent.py +6452 -0
- localcoder-0.4.0/src/localcoder/mcp_client.py +319 -0
- localcoder-0.4.0/src/localcoder/safe_commands.py +244 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_server/server.js +11 -1
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/build.py +26 -7
- localcoder-0.4.0/src/localcoder/templates/static/flashcards/index.html +158 -0
- localcoder-0.4.0/src/localcoder/templates/static/gallery/index.html +92 -0
- localcoder-0.4.0/src/localcoder/templates/static/quiz-game/index.html +213 -0
- localcoder-0.3.0/src/localcoder/cli.py +0 -837
- localcoder-0.3.0/src/localcoder/localcoder_agent.py +0 -3369
- {localcoder-0.3.0 → localcoder-0.4.0}/LICENSE +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/README.md +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/__init__.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/__main__.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/backends.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/bench.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/localcoder_display.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/setup.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/.env.local +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/next.config.ts +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/package.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/postcss.config.mjs +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/api/ai/route.ts +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/globals.css +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/layout.tsx +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/app/page.tsx +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/src/components/Chat.tsx +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/ai-app/tsconfig.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_adapters/chat.js +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_adapters/image.js +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_adapters/text.js +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_adapters/voice.js +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_server/package.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_shell/base.css +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/_shell/base.js +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/chatbot/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/homework-helper/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/image-analyzer/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/ingredients-scanner/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/meeting-notes/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/photo-todo/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/translator/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/voice-analyzer/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/templates/framework/apps/voice-memo/config.json +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/tui.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/src/localcoder/voice.py +0 -0
- {localcoder-0.3.0 → localcoder-0.4.0}/tests/test_basic.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">×</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.
|