devcoach 0.7.4 → 0.8.1
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.
- package/README.md +15 -8
- package/assets/SKILL.md +124 -598
- package/assets/references/calibration.md +51 -0
- package/assets/references/onboarding.md +132 -0
- package/dist/app-IXWAZDVH.js +535 -0
- package/dist/bin.js +1 -625
- package/dist/chunk-227BHE2B.js +69 -0
- package/dist/chunk-2OWIYKQC.js +16 -0
- package/dist/chunk-CBUZNGSX.js +2 -0
- package/dist/commands-NL54QLEU.js +27 -0
- package/dist/hooks-7QPFYLPI.js +2 -0
- package/dist/server-BHRDYYAU.js +16 -0
- package/package.json +2 -1
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r,U,w,P,Q,j,G}from'./chunk-227BHE2B.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function C(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function S(t){let e=(r,a,s)=>html`<a href="${r}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${s?"text-gray-900 dark:text-white font-semibold":""}">${a}</a>`;return html`<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>${t.title}</title>
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
|
9
|
+
<meta name="devcoach-theme" content="${t.uiTheme}" />
|
|
10
|
+
<script>
|
|
11
|
+
(function () {
|
|
12
|
+
var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
|
|
13
|
+
var session = localStorage.getItem('theme-override');
|
|
14
|
+
var active = session || serverTheme;
|
|
15
|
+
var dark = active === 'dark' || (active === 'system' && globalThis.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
16
|
+
if (dark) document.documentElement.classList.add('dark');
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
19
|
+
<script src="/static/vendor/tailwind.js"></script>
|
|
20
|
+
<script>tailwind.config = { darkMode: 'class' }</script>
|
|
21
|
+
<script src="/static/vendor/htmx.min.js"></script>
|
|
22
|
+
<script src="/static/vendor/alpinejs.min.js" defer></script>
|
|
23
|
+
<link rel="stylesheet" href="/static/style.css" />
|
|
24
|
+
${t.head??""}
|
|
25
|
+
</head>
|
|
26
|
+
<body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen font-mono transition-colors duration-200">
|
|
27
|
+
<nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-3 flex items-center gap-6">
|
|
28
|
+
<!-- Brand wordmark, mirrors the docs site (website/src/css/custom.css): "dev" in the
|
|
29
|
+
theme foreground, "coach" in the teal accent (#0d9488 / #5eead4 = teal-600/300). -->
|
|
30
|
+
<span class="font-extrabold text-lg tracking-tight text-gray-900 dark:text-gray-100"
|
|
31
|
+
>🎓 dev<span class="text-teal-600 dark:text-teal-300">coach</span></span
|
|
32
|
+
>
|
|
33
|
+
${e("/","Profile",t.currentPath==="/")}
|
|
34
|
+
${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
|
|
35
|
+
${e("/settings","Settings",t.currentPath==="/settings")}
|
|
36
|
+
<div class="ml-auto">
|
|
37
|
+
<button id="theme-toggle" onclick="toggleTheme()"
|
|
38
|
+
class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition text-lg leading-none px-1"
|
|
39
|
+
title="Toggle theme"></button>
|
|
40
|
+
</div>
|
|
41
|
+
</nav>
|
|
42
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 py-8">${t.body}</main>
|
|
43
|
+
<script>
|
|
44
|
+
function isDark() { return document.documentElement.classList.contains('dark'); }
|
|
45
|
+
function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
|
|
46
|
+
function updateHljsTheme() {
|
|
47
|
+
var link = document.getElementById('hljs-theme'); if (!link) return;
|
|
48
|
+
link.href = isDark() ? '/static/vendor/hljs-dark.min.css' : '/static/vendor/hljs-light.min.css';
|
|
49
|
+
}
|
|
50
|
+
function toggleTheme() {
|
|
51
|
+
document.documentElement.classList.toggle('dark');
|
|
52
|
+
localStorage.setItem('theme-override', isDark() ? 'dark' : 'light');
|
|
53
|
+
updateThemeIcon(); updateHljsTheme();
|
|
54
|
+
}
|
|
55
|
+
(function () {
|
|
56
|
+
var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
|
|
57
|
+
if (serverTheme !== 'system') localStorage.removeItem('theme-override');
|
|
58
|
+
})();
|
|
59
|
+
updateThemeIcon(); updateHljsTheme();
|
|
60
|
+
</script>
|
|
61
|
+
${t.scripts??""}
|
|
62
|
+
</body>
|
|
63
|
+
</html>`}function se(t){let e=a=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===a?"selected":""}>${s}</option>`),r=html`
|
|
64
|
+
<div x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
|
|
65
|
+
|
|
66
|
+
<div class="flex items-center justify-between mb-6">
|
|
67
|
+
<h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Knowledge Map</h1>
|
|
68
|
+
<div class="flex items-center gap-2">
|
|
69
|
+
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
|
|
70
|
+
<button type="button" @click="open = !open"
|
|
71
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add group</button>
|
|
72
|
+
<div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-3 w-56" style="display:none">
|
|
73
|
+
<form method="post" action="/groups" class="flex gap-2">
|
|
74
|
+
<input type="text" name="group_name" placeholder="Group name…" required class="flex-1 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
75
|
+
<button type="submit" class="px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
|
|
76
|
+
</form>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
|
|
80
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add topic</button>
|
|
81
|
+
<div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-4 w-72" style="display:none">
|
|
82
|
+
<form method="post" action="/knowledge" class="space-y-3">
|
|
83
|
+
<div>
|
|
84
|
+
<label for="add-topic-id" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Topic ID</label>
|
|
85
|
+
<input id="add-topic-id" type="text" name="topic" placeholder="e.g. rust_lifetimes" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex gap-2">
|
|
88
|
+
<div class="w-24">
|
|
89
|
+
<label for="add-topic-confidence" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Confidence</label>
|
|
90
|
+
<input id="add-topic-confidence" type="number" name="confidence" value="5" min="0" max="10" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
91
|
+
</div>
|
|
92
|
+
<div class="flex-1">
|
|
93
|
+
<label for="add-topic-group" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Group</label>
|
|
94
|
+
<select id="add-topic-group" name="group" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
|
95
|
+
<option value="">Other (ungrouped)</option>
|
|
96
|
+
${e("")}
|
|
97
|
+
</select>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<button type="submit" class="w-full px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
|
|
101
|
+
</form>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<button type="button" @click="toggle()"
|
|
105
|
+
:class="editMode ? 'bg-indigo-600 text-white border-indigo-600 hover:bg-indigo-500' : 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400'"
|
|
106
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition">
|
|
107
|
+
<span x-text="editMode ? '✓ Done' : '✎ Edit'">✎ Edit</span>
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="mb-6 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
|
|
113
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.total_lessons??0)}</span> lessons total</span>
|
|
114
|
+
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
115
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_today??0)}</span> / ${t.maxPerDay} today</span>
|
|
116
|
+
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
117
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_this_week??0)}</span> this week</span>
|
|
118
|
+
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
119
|
+
${t.rateLimit.allowed?html`<span class="text-green-600 dark:text-green-400 font-medium">Available now</span>`:html`<span class="text-yellow-600 dark:text-yellow-400">${t.rateLimit.reason}</span>`}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
${Object.entries(t.categorised).map(([a,s])=>html`
|
|
123
|
+
<section class="mb-6 group/section">
|
|
124
|
+
<div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
|
|
125
|
+
<div class="flex items-center gap-1 flex-1 min-w-0">
|
|
126
|
+
<h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${a}</h2>
|
|
127
|
+
${a!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(a)}/delete" onsubmit="return confirm('Remove group "${a}"? Topics will move to Other.')">
|
|
128
|
+
<button type="submit" class="text-xs text-gray-300 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/section:opacity-100" title="Delete group">×</button>
|
|
129
|
+
</form>`:""}
|
|
130
|
+
</div>
|
|
131
|
+
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
|
|
132
|
+
<button type="button" @click="open = !open" class="text-xs text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition px-1">+ topic</button>
|
|
133
|
+
<div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-3 w-64" style="display:none">
|
|
134
|
+
<form method="post" action="/knowledge" class="space-y-2">
|
|
135
|
+
<input type="hidden" name="group" value="${a!=="Other"?a:""}" />
|
|
136
|
+
<input type="text" name="topic" placeholder="topic_id (e.g. rust_lifetimes)" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
137
|
+
<div class="flex items-center gap-2">
|
|
138
|
+
<label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
|
|
139
|
+
<input type="number" name="confidence" value="5" min="0" max="10" class="w-16 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
140
|
+
<button type="submit" class="flex-1 px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
|
|
141
|
+
</div>
|
|
142
|
+
</form>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
|
|
147
|
+
${s.map(i=>{let g=i.confidence,l=Math.trunc(g/10*100),d=g>=7?"bg-green-500":g>=4?"bg-yellow-500":"bg-red-500",y=g>=7?"text-green-600 dark:text-green-400":g>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
|
|
148
|
+
<div class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
|
|
149
|
+
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
|
|
150
|
+
<button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-indigo-500 dark:hover:text-indigo-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
|
|
151
|
+
<div x-show="open" @click.outside="open = false" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-1 w-40" style="display:none">
|
|
152
|
+
<form method="post" action="/knowledge/${encodeURIComponent(i.topic)}/group">
|
|
153
|
+
<select name="group" onchange="this.form.submit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
|
|
154
|
+
<option value="Other" ${a==="Other"?"selected":""}>Other (ungrouped)</option>
|
|
155
|
+
${e(a)}
|
|
156
|
+
</select>
|
|
157
|
+
</form>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="flex items-center gap-1 flex-1 min-w-0">
|
|
161
|
+
<a x-show="!editMode" href="/lessons?search=${encodeURIComponent(i.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${i.topic}">${i.topic}</a>
|
|
162
|
+
<span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${i.topic}">${i.topic}</span>
|
|
163
|
+
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}/delete" onsubmit="return confirm('Remove ${i.topic} from your knowledge map?')" class="shrink-0 flex items-center">
|
|
164
|
+
<button type="submit" class="text-xs text-gray-200 dark:text-gray-700 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Remove topic">×</button>
|
|
165
|
+
</form>
|
|
166
|
+
</div>
|
|
167
|
+
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" class="shrink-0 flex items-center">
|
|
168
|
+
<input type="hidden" name="delta" value="-1" />
|
|
169
|
+
<button class="text-xs text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 transition w-5 text-center leading-none">−</button>
|
|
170
|
+
</form>
|
|
171
|
+
<div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
|
172
|
+
<div class="${d} h-full rounded-full transition-all" style="width: ${l}%"></div>
|
|
173
|
+
</div>
|
|
174
|
+
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" class="shrink-0 flex items-center">
|
|
175
|
+
<input type="hidden" name="delta" value="1" />
|
|
176
|
+
<button class="text-xs text-gray-300 dark:text-gray-600 hover:text-green-600 dark:hover:text-green-400 transition w-5 text-center leading-none">+</button>
|
|
177
|
+
</form>
|
|
178
|
+
<span class="text-xs font-bold ${y} w-5 text-right shrink-0">${g}</span>
|
|
179
|
+
</div>`})}
|
|
180
|
+
</div>
|
|
181
|
+
</section>`)}
|
|
182
|
+
</div>`;return S({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:r})}var re={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},ae={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},oe={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function m(t,e={}){let r={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(r.starred="1"),Object.assign(r,e);let a=new URLSearchParams;for(let[s,i]of Object.entries(r))i&&a.set(s,i);return `?${a.toString()}`}function ne(t){let e=t.s,r=!!(e.date_from||e.date_to),a=e.period!=="all"&&!r||r||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),s=r?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:re[e.period]??"All time",i=e.feedback?ae[e.feedback]??"Feedback":"Feedback",g={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},l=(n,c)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${n}<a href="${c}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,d=(n,c,h="")=>{let f=e.sort===c,ce=f&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${h}">
|
|
183
|
+
<a href="${m(e,{sort:c,order:ce})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${n}
|
|
184
|
+
${f?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
|
|
185
|
+
</a></th>`},y=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,u=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
|
|
186
|
+
<style>
|
|
187
|
+
.dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
|
|
188
|
+
.dark .flatpickr-day { color:#e5e7eb; }
|
|
189
|
+
.dark .flatpickr-day:hover { background:#374151; }
|
|
190
|
+
.dark .flatpickr-day.selected, .dark .flatpickr-day.startRange, .dark .flatpickr-day.endRange, .dark .flatpickr-day.inRange { background:#4f46e5; border-color:#4f46e5; color:#fff; }
|
|
191
|
+
.dark .flatpickr-day.today { border-color:#6366f1; }
|
|
192
|
+
.dark .flatpickr-months, .dark .flatpickr-month { background:#1f2937; color:#e5e7eb; fill:#e5e7eb; }
|
|
193
|
+
.dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
|
|
194
|
+
.dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
|
|
195
|
+
.dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
|
|
196
|
+
</style>`,w=t.total===0?"No lessons":t.totalPages===1?`${t.total} lesson${t.total!==1?"s":""}`:`${(t.page-1)*t.perPage+1}\u2013${Math.min(t.page*t.perPage,t.total)} of ${t.total}`,$=html`
|
|
197
|
+
<form id="filter-form" method="get" action="/lessons">
|
|
198
|
+
<input type="hidden" name="period" id="h-period" value="${e.period}">
|
|
199
|
+
<input type="hidden" name="date_from" id="h-date-from" value="${e.date_from}">
|
|
200
|
+
<input type="hidden" name="date_to" id="h-date-to" value="${e.date_to}">
|
|
201
|
+
<input type="hidden" name="feedback" id="h-feedback" value="${e.feedback}">
|
|
202
|
+
<input type="hidden" name="starred" id="h-starred" value="${e.starred?"1":""}">
|
|
203
|
+
<input type="hidden" name="category" value="${e.category}">
|
|
204
|
+
<input type="hidden" name="level" id="h-level" value="${e.level}">
|
|
205
|
+
<input type="hidden" name="project" value="${e.project}">
|
|
206
|
+
<input type="hidden" name="repository" value="${e.repository}">
|
|
207
|
+
<input type="hidden" name="branch" value="${e.branch}">
|
|
208
|
+
<input type="hidden" name="commit" value="${e.commit}">
|
|
209
|
+
|
|
210
|
+
<div class="flex items-center gap-3 mb-4">
|
|
211
|
+
<div class="relative flex-1">
|
|
212
|
+
<span class="absolute inset-y-0 left-3.5 flex items-center text-gray-400 pointer-events-none text-sm">🔍</span>
|
|
213
|
+
<input type="text" name="search" value="${e.search}" placeholder="Search lessons…" autocomplete="off" class="w-full pl-9 pr-10 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />
|
|
214
|
+
${e.search?html`<button type="submit" name="search" value="" class="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-xl leading-none">×</button>`:""}
|
|
215
|
+
</div>
|
|
216
|
+
<p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${w}</p>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div class="flex flex-wrap items-center gap-2 mb-3">
|
|
220
|
+
<button type="button" onclick="var h=document.getElementById('h-starred'); h.value=h.value?'':'1'; document.getElementById('filter-form').submit()"
|
|
221
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.starred?"bg-yellow-400 text-yellow-900 border-yellow-400":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-yellow-400 hover:text-yellow-500"}">★ Starred</button>
|
|
222
|
+
|
|
223
|
+
<div class="relative" x-data="periodPicker()" @keydown.escape="close()">
|
|
224
|
+
<button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${r||e.period!=="all"?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">
|
|
225
|
+
<span>📅</span><span x-text="label">${s}</span>${y}
|
|
226
|
+
</button>
|
|
227
|
+
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
|
|
228
|
+
<div class="p-1">
|
|
229
|
+
${Object.entries(re).map(([n,c])=>{let h=e.period===n&&!r;return html`<button type="button" @click="selectPreset('${n}', '${c}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${h?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${c}${h?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="border-t border-gray-100 dark:border-gray-800 p-1">
|
|
232
|
+
<button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${r?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
|
|
233
|
+
<span>Custom range</span>
|
|
234
|
+
<svg class="w-3 h-3 opacity-60 transition-transform" :class="showCustom ? 'rotate-180' : ''" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>
|
|
235
|
+
</button>
|
|
236
|
+
<div x-show="showCustom" class="px-2 pb-2 pt-1">
|
|
237
|
+
<input type="text" x-ref="fp" placeholder="Select date range…" class="w-full text-xs bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1.5 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer" />
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
244
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${i}${y}</button>
|
|
245
|
+
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-44 p-1 overflow-hidden" style="display:none">
|
|
246
|
+
${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([n,c])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${n}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.feedback===n?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${c}${e.feedback===n?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
251
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${oe[e.level]??""} ${e.level}`:"Level"}${y}</button>
|
|
252
|
+
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
|
|
253
|
+
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([n,c])=>html`<button type="button" onclick="document.getElementById('h-level').value='${n}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===n?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${c}${e.level===n?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
${t.allCategories.length||t.allProjects.length||t.allRepositories.length||t.allBranches.length||t.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
258
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${y}</button>
|
|
259
|
+
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 p-3 space-y-2.5" style="display:none">
|
|
260
|
+
${t.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${t.allCategories.map(n=>html`<option value="${n}" ${e.category===n?"selected":""}>${n}</option>`)}</select></div>`:""}
|
|
261
|
+
${t.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${t.allProjects.map(n=>html`<option value="${n}" ${e.project===n?"selected":""}>${n}</option>`)}</select></div>`:""}
|
|
262
|
+
${t.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All repositories</option>${t.allRepositories.map(n=>html`<option value="${n}" ${e.repository===n?"selected":""}>${n}</option>`)}</select></div>`:""}
|
|
263
|
+
${t.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="branch-list">${t.allBranches.map(n=>html`<option value="${n}">`)}</datalist></div>`:""}
|
|
264
|
+
${t.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="commit-list">${t.allCommits.map(n=>html`<option value="${n.slice(0,7)}">`)}</datalist></div>`:""}
|
|
265
|
+
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Apply</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>`:""}
|
|
268
|
+
|
|
269
|
+
${a?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
${a?html`<div class="flex flex-wrap gap-1.5 mb-4">
|
|
273
|
+
${r?l(`\u{1F4C5} ${s}`,m(e,{date_from:"",date_to:""})):""}
|
|
274
|
+
${e.period!=="all"&&!r?l(`\u{1F550} ${s}`,m(e,{period:"all"})):""}
|
|
275
|
+
${e.feedback?l(ae[e.feedback]??e.feedback,m(e,{feedback:""})):""}
|
|
276
|
+
${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${m({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
|
|
277
|
+
${e.search?l(`\u{1F50D} "${e.search}"`,m(e,{search:""})):""}
|
|
278
|
+
${e.category?l(e.category,m(e,{category:""})):""}
|
|
279
|
+
${e.level?l(`${oe[e.level]??""} ${e.level}`,m(e,{level:""})):""}
|
|
280
|
+
${e.project?l(`\u{1F4C1} ${e.project}`,m(e,{project:""})):""}
|
|
281
|
+
${e.repository?l(`\u2387 ${e.repository}`,m(e,{repository:""})):""}
|
|
282
|
+
${e.branch?l(`\u2387 ${e.branch}`,m(e,{branch:""})):""}
|
|
283
|
+
${e.commit?l(e.commit.slice(0,7),m(e,{commit:""})):""}
|
|
284
|
+
</div>`:""}
|
|
285
|
+
</form>
|
|
286
|
+
|
|
287
|
+
${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
|
|
288
|
+
<table class="w-full text-sm">
|
|
289
|
+
<thead>
|
|
290
|
+
<tr class="bg-gray-50 dark:bg-gray-800/60 border-b border-gray-200 dark:border-gray-800 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
|
291
|
+
<th class="w-8 px-3 py-3"></th>
|
|
292
|
+
${d("Date","timestamp")}
|
|
293
|
+
${d("Topic","topic_id","hidden sm:table-cell")}
|
|
294
|
+
${d("Title","title")}
|
|
295
|
+
${d("Level","level")}
|
|
296
|
+
<th class="px-3 py-3 hidden lg:table-cell">Categories</th>
|
|
297
|
+
${d("Feedback","feedback","hidden xl:table-cell")}
|
|
298
|
+
</tr>
|
|
299
|
+
</thead>
|
|
300
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
301
|
+
${t.lessons.map(n=>{let c=n.timestamp.slice(0,10),h=n.timestamp.slice(0,16).replace("T"," ");return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(n.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(n.id)}'" role="link">
|
|
302
|
+
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
|
|
303
|
+
<form method="post" action="/lessons/${encodeURIComponent(n.id)}/star">
|
|
304
|
+
<input type="hidden" name="starred" value="${n.starred?"0":"1"}" />
|
|
305
|
+
<input type="hidden" name="next" value="/lessons${m(e)}" />
|
|
306
|
+
<button type="submit" title="${n.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${n.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${n.starred?"\u2605":"\u2606"}</button>
|
|
307
|
+
</form>
|
|
308
|
+
</td>
|
|
309
|
+
<td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
|
|
310
|
+
<span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${n.timestamp}">${c}</span>
|
|
311
|
+
<div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${h}</div>
|
|
312
|
+
</td>
|
|
313
|
+
<td class="px-3 py-3 hidden sm:table-cell"><span class="text-xs font-mono text-cyan-600 dark:text-cyan-400">${n.topic_id}</span></td>
|
|
314
|
+
<td class="px-3 py-3 max-w-xs"><a href="/lessons/${encodeURIComponent(n.id)}" class="font-medium text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${n.title}</a></td>
|
|
315
|
+
<td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${m(e,{level:n.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${g[n.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${n.level}</a></td>
|
|
316
|
+
<td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${n.categories.map(f=>html`<a href="${m(e,{category:f})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${f}</a>`)}</div></td>
|
|
317
|
+
<td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${n.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:n.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
|
|
318
|
+
</tr>`})}
|
|
319
|
+
</tbody>
|
|
320
|
+
</table>
|
|
321
|
+
</div>
|
|
322
|
+
${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
323
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
|
|
324
|
+
<div class="flex items-center gap-1">
|
|
325
|
+
${t.page>1?html`<a href="${m(e,{page:String(t.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
|
|
326
|
+
${pe(t.page,t.totalPages).map(n=>n===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:n===t.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${n}</span>`:html`<a href="${m(e,{page:String(n)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${n}</a>`)}
|
|
327
|
+
${t.page<t.totalPages?html`<a href="${m(e,{page:String(t.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
|
|
328
|
+
</div>
|
|
329
|
+
</div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
330
|
+
<p class="text-3xl mb-3">📭</p>
|
|
331
|
+
<p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
|
|
332
|
+
${a?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
|
|
333
|
+
</div>`}`,x=html`<script src="/static/vendor/flatpickr.min.js"></script>
|
|
334
|
+
<script src="/static/relative-time.js"></script>
|
|
335
|
+
<script>
|
|
336
|
+
function periodPicker() {
|
|
337
|
+
return {
|
|
338
|
+
open: false,
|
|
339
|
+
showCustom: ${raw(r?"true":"false")},
|
|
340
|
+
label: ${raw(C(s))},
|
|
341
|
+
fp: null,
|
|
342
|
+
init() {
|
|
343
|
+
const self = this;
|
|
344
|
+
this.$watch('showCustom', val => {
|
|
345
|
+
if (val && !this.fp) {
|
|
346
|
+
this.fp = flatpickr(this.$refs.fp, {
|
|
347
|
+
mode: 'range', dateFormat: 'Y-m-d', inline: false,
|
|
348
|
+
${raw(e.date_from?`defaultDate: [${C(e.date_from)}${e.date_to?`, ${C(e.date_to)}`:""}],`:"")}
|
|
349
|
+
onChange(dates) {
|
|
350
|
+
if (dates.length === 2) {
|
|
351
|
+
document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
|
|
352
|
+
document.getElementById('h-date-to').value = self.fp.formatDate(dates[1], 'Y-m-d');
|
|
353
|
+
document.getElementById('h-period').value = '';
|
|
354
|
+
document.getElementById('filter-form').submit();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
toggle() { this.open = !this.open; },
|
|
362
|
+
close() { this.open = false; },
|
|
363
|
+
selectPreset(val) {
|
|
364
|
+
document.getElementById('h-period').value = val;
|
|
365
|
+
document.getElementById('h-date-from').value = '';
|
|
366
|
+
document.getElementById('h-date-to').value = '';
|
|
367
|
+
document.getElementById('filter-form').submit();
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
</script>`;return S({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:u,body:$,scripts:x})}function pe(t,e){let r=[],a=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(a&&s-a>1&&r.push(0),r.push(s),a=s);return r}var R={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function ie(t){let e=t.lesson,r=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",a=e.repository_platform==="local",s=null;a&&e.repository?s=`vscode://file/${e.repository}`:e.repository_platform&&R[e.repository_platform]&&e.repository&&(s=`https://${R[e.repository_platform]}/${e.repository}`);let i=null;s&&e.commit_hash&&!a&&(i=e.repository_platform==="gitlab"?`${s}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${s}/commits/${e.commit_hash}`:`${s}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),l=e.timestamp.slice(0,10),d=e.timestamp.slice(0,16).replace("T"," "),y=html`
|
|
372
|
+
<div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
|
|
373
|
+
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
374
|
+
<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
375
|
+
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/star">
|
|
376
|
+
<input type="hidden" name="starred" value="${e.starred?"0":"1"}" />
|
|
377
|
+
<input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" />
|
|
378
|
+
<button type="submit" title="${e.starred?"Unstar":"Star"}" class="w-6 text-center text-xl leading-none transition ${e.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${e.starred?"\u2605":"\u2606"}</button>
|
|
379
|
+
</form>
|
|
380
|
+
<h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
|
|
381
|
+
<a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${r} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
|
|
384
|
+
<span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${l}</span>
|
|
385
|
+
<span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${d}</span>
|
|
386
|
+
</span>
|
|
387
|
+
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
|
|
388
|
+
${e.categories.map($=>html`<a href="/lessons?category=${encodeURIComponent($)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${$}</a>`)}
|
|
389
|
+
${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I know this</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ I don't know this</span>`}
|
|
390
|
+
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><input type="hidden" name="feedback" value="clear" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button></form>`:""}
|
|
391
|
+
</div>
|
|
392
|
+
<div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
|
|
393
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
|
|
394
|
+
<div id="summary-content" class="markdown-body text-sm text-indigo-900 dark:text-indigo-100"></div>
|
|
395
|
+
</div>
|
|
396
|
+
<div id="body-content" class="markdown-body"></div>
|
|
397
|
+
${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
|
|
398
|
+
${g?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
399
|
+
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${s?html`<a href="${s}" ${a?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
|
|
400
|
+
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&R[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${s?html`<a href="${s}" ${a?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
|
|
401
|
+
${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
|
|
402
|
+
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${i?html`<a href="${i}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
|
|
403
|
+
${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
|
|
404
|
+
</div></div>`:""}
|
|
405
|
+
${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
|
|
406
|
+
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><input type="hidden" name="feedback" value="know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I know this</button></form>
|
|
407
|
+
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ I don't know this</button></form>
|
|
408
|
+
</div>`}
|
|
409
|
+
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">ID: ${e.id}</div>
|
|
410
|
+
</div>`,u=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,w=html`<script src="/static/vendor/highlight.min.js"></script>
|
|
411
|
+
<script src="/static/vendor/marked.min.js"></script>
|
|
412
|
+
<script src="/static/relative-time.js"></script>
|
|
413
|
+
<script>
|
|
414
|
+
updateHljsTheme();
|
|
415
|
+
marked.setOptions({
|
|
416
|
+
highlight: function(code, lang) {
|
|
417
|
+
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
418
|
+
return hljs.highlight(code, { language }).value;
|
|
419
|
+
},
|
|
420
|
+
breaks: true, gfm: true,
|
|
421
|
+
});
|
|
422
|
+
document.getElementById('summary-content').innerHTML = marked.parse(${raw(C(e.summary))});
|
|
423
|
+
document.getElementById('body-content').innerHTML = marked.parse(${raw(C(e.body))});
|
|
424
|
+
</script>`;return S({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:u,body:y,scripts:w})}var me=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]];function de(t){let e=t.settings.min_gap_minutes,r=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],a=t.flash,s=html`
|
|
425
|
+
<h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
|
|
426
|
+
${a?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${a.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
|
|
427
|
+
${a.imported} lesson${a.imported!==1?"s":""} imported.${a.skipped?` ${a.skipped} skipped (already in DB).`:""}${a.invalid?` ${a.invalid} rejected (failed validation).`:""}${a.groups?` ${a.groups} group${a.groups!==1?"s":""} added.`:""}${a.notebook?" Notebook restored.":""}
|
|
428
|
+
</div>`:""}
|
|
429
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
430
|
+
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
431
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-5">Coaching</p>
|
|
432
|
+
<form method="post" action="/settings" class="space-y-4">
|
|
433
|
+
<div class="grid grid-cols-2 gap-4">
|
|
434
|
+
<div>
|
|
435
|
+
<label for="max-per-day" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Max lessons per day</label>
|
|
436
|
+
<input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${t.settings.max_per_day}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
|
|
437
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Lessons in a 24h window.</p>
|
|
438
|
+
</div>
|
|
439
|
+
<div>
|
|
440
|
+
<label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
|
|
441
|
+
<select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
442
|
+
${me.map(([g,l])=>html`<option value="${g}" ${e===g?"selected":""}>${l}</option>`)}
|
|
443
|
+
</select>
|
|
444
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
<div class="grid grid-cols-2 gap-4">
|
|
448
|
+
<div>
|
|
449
|
+
<label for="nudge-every" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Interactions between lessons</label>
|
|
450
|
+
<input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${t.settings.nudge_every}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
|
|
451
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cue at most once every N (0 = every turn).</p>
|
|
452
|
+
</div>
|
|
453
|
+
<div>
|
|
454
|
+
<label for="nudge-scope" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count interactions</label>
|
|
455
|
+
<select id="nudge-scope" name="nudge_scope" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
456
|
+
<option value="session" ${t.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
|
|
457
|
+
<option value="global" ${t.settings.nudge_scope==="global"?"selected":""}>Globally</option>
|
|
458
|
+
</select>
|
|
459
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Per-chat or across all sessions.</p>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
<fieldset class="border-0 p-0 m-0">
|
|
463
|
+
<legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
|
|
464
|
+
<div class="flex gap-2">
|
|
465
|
+
${r.map(([g,l,d])=>html`<label class="flex-1 cursor-pointer">
|
|
466
|
+
<input type="radio" name="ui_theme" value="${g}" ${t.settings.ui_theme===g?"checked":""} class="sr-only peer" />
|
|
467
|
+
<span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${d}</span>${l}</span>
|
|
468
|
+
</label>`)}
|
|
469
|
+
</div>
|
|
470
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
|
|
471
|
+
</fieldset>
|
|
472
|
+
<button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded px-4 py-2 text-sm transition">Save settings</button>
|
|
473
|
+
</form>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
477
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-5">Backup & Restore</p>
|
|
478
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
479
|
+
<div class="flex flex-col gap-1">
|
|
480
|
+
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">Full backup</p>
|
|
481
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Settings + knowledge map + lessons + coaching notebook as a zip file.</p>
|
|
482
|
+
<a href="/settings/export" class="mt-auto inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition">↓ Download backup</a>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="flex flex-col gap-1">
|
|
485
|
+
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">Restore backup</p>
|
|
486
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Overwrites settings, knowledge map & coaching notebook; duplicate lessons are skipped.</p>
|
|
487
|
+
<form method="post" action="/settings/import" enctype="multipart/form-data" class="mt-auto flex flex-col gap-2">
|
|
488
|
+
<input id="file-restore" type="file" name="file" accept=".zip,.json" class="hidden" onchange="updateLabel('file-restore','label-restore','restore-submit')" />
|
|
489
|
+
<label id="label-restore" for="file-restore" class="cursor-pointer inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition truncate">Choose file…</label>
|
|
490
|
+
<button id="restore-submit" type="submit" disabled class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Restore</button>
|
|
491
|
+
</form>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="flex flex-col gap-1">
|
|
494
|
+
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">Lessons only</p>
|
|
495
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Export just the lesson log as JSON (no settings).</p>
|
|
496
|
+
<a href="/lessons/export" class="mt-auto inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition">↓ Download lessons</a>
|
|
497
|
+
</div>
|
|
498
|
+
<div class="flex flex-col gap-1">
|
|
499
|
+
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">Import lessons</p>
|
|
500
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Merge a lessons JSON file; duplicates are skipped.</p>
|
|
501
|
+
<form method="post" action="/lessons/import" enctype="multipart/form-data" class="mt-auto flex flex-col gap-2">
|
|
502
|
+
<input id="file-import" type="file" name="file" accept=".json" class="hidden" onchange="updateLabel('file-import','label-import','import-submit')" />
|
|
503
|
+
<label id="label-import" for="file-import" class="cursor-pointer inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition truncate">Choose file…</label>
|
|
504
|
+
<button id="import-submit" type="submit" disabled class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Import</button>
|
|
505
|
+
</form>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6 lg:col-span-2" x-data="{ nbMode: 'preview' }">
|
|
511
|
+
<div class="flex flex-wrap items-center justify-between gap-2 mb-4">
|
|
512
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Coaching Notebook</p>
|
|
513
|
+
<div class="flex items-center gap-2">
|
|
514
|
+
<div class="flex rounded border border-gray-200 dark:border-gray-700 overflow-hidden text-xs font-medium">
|
|
515
|
+
<button type="button" @click="nbMode = 'preview'" :class="nbMode === 'preview' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition">Preview</button>
|
|
516
|
+
<button type="button" @click="nbMode = 'source'" :class="nbMode === 'source' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition border-l border-gray-200 dark:border-gray-700">Source</button>
|
|
517
|
+
</div>
|
|
518
|
+
<a href="/settings/notebook/download" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition">↓ Download</a>
|
|
519
|
+
<a href="vscode://file/${t.notebookPath}" title="Open in VS Code" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition"><img src="/static/vscode.svg" class="w-4 h-4" alt="VS Code" />Open in VS Code</a>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
<div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
|
|
523
|
+
<pre x-show="nbMode === 'source'" style="display:none" class="text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto whitespace-pre-wrap text-gray-700 dark:text-gray-200 min-h-24">${t.notebookContent}</pre>
|
|
524
|
+
</div>
|
|
525
|
+
</div>`,i=html`<script src="/static/vendor/marked.min.js"></script>
|
|
526
|
+
<script>
|
|
527
|
+
function updateLabel(inputId, labelId, submitId) {
|
|
528
|
+
var input = document.getElementById(inputId);
|
|
529
|
+
document.getElementById(labelId).textContent = input.files.length ? input.files[0].name : 'Choose file…';
|
|
530
|
+
document.getElementById(submitId).disabled = !input.files.length;
|
|
531
|
+
}
|
|
532
|
+
marked.setOptions({ breaks: true, gfm: true });
|
|
533
|
+
document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(C(t.notebookContent))});
|
|
534
|
+
</script>`;return S({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:s,scripts:i})}var L=25;function B(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function b(t,e,r=""){let a=t[e];return typeof a=="string"?a:r}var N=dirname(fileURLToPath(import.meta.url)),le=[join(N,"../assets/static"),join(N,"../../assets/static")].find(t=>existsSync(t))??join(N,"../assets/static"),ke={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"};function ve(){try{return m$1(t=>F(t).ui_theme)}catch{return "system"}}function we(){let t$1=new Hono;return t$1.get("/static/*",e=>{let r=decodeURIComponent(e.req.path.slice(8)),a=join(le,r);if(!a.startsWith(le))return e.notFound();try{let s=readFileSync(a);return new Response(new Uint8Array(s),{headers:{"content-type":ke[extname(a)]??"application/octet-stream"}})}catch{return e.notFound()}}),t$1.get("/",e=>{let{profile:r,stats:a,rateLimit:s,settings:i}=m$1(d=>({profile:S$1(d),stats:V(d),rateLimit:R$1(d),settings:F(d)})),g=new Map;for(let d of r.groups)for(let y of d.topics)g.set(y,d.name);let l={};for(let d of r.groups)l[d.name]=[];for(let d of r.knowledge){let y=g.get(d.topic)??"Other",u=l[y]??[];u.push(d),l[y]=u;}return e.html(se({categorised:l,allGroups:r.groups.map(d=>d.name),stats:a,rateLimit:s,maxPerDay:i.max_per_day,uiTheme:i.ui_theme}))}),t$1.post("/knowledge",async e=>{let r=await e.req.parseBody(),a=b(r,"topic").trim();if(a){let s=Number.parseInt(b(r,"confidence","5"),10)||5,i=b(r,"group").trim();m$1(g=>{z(g,a,s),i&&i!=="Other"&&D(g,a,i);});}return e.redirect("/",303)}),t$1.post("/knowledge/:topic/delete",e=>(m$1(r=>A(r,e.req.param("topic"))),e.redirect("/",303))),t$1.post("/knowledge/:topic/group",async e=>{let r=b(await e.req.parseBody(),"group").trim(),a=e.req.param("topic");return m$1(s=>{r&&r!=="Other"?D(s,a,r):E(s,a);}),e.redirect("/",303)}),t$1.post("/knowledge/:topic",async e=>{let r=Number.parseInt(b(await e.req.parseBody(),"delta","0"),10)||0;return m$1(a=>T(a,e.req.param("topic"),r)),e.redirect("/",303)}),t$1.post("/groups",async e=>{let r=b(await e.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&m$1(a=>B$1(a,r)),e.redirect("/",303)}),t$1.post("/groups/:group_name/delete",e=>(m$1(r=>C$1(r,e.req.param("group_name"))),e.redirect("/",303))),t$1.get("/lessons/export",()=>{let e=m$1(r=>t(r));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t$1.post("/lessons/import",async e=>{let r=(await e.req.parseBody()).file,a;try{a=JSON.parse(r instanceof File?await r.text():String(r));}catch{return e.redirect("/settings?imported=0&skipped=0&invalid=1",303)}if(!Array.isArray(a))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=m$1(i=>u(i,a));return e.redirect(`/settings?imported=${s.inserted}&skipped=${s.duplicated}&invalid=${s.invalid}`,303)}),t$1.get("/lessons",e=>{let r=e.req.query(),a=new Set(["junior","mid","senior"]),s=r.level&&a.has(r.level)?r.level:"",i=r.date_from||"",g=r.date_to||"",l=r.starred==="1",d=r.sort||"timestamp",y=r.order==="asc"?"asc":"desc",u=Math.max(1,Number.parseInt(r.page??"1",10)||1),w=r.period||"all",$={period:i||g?null:w!=="all"?w:null,category:r.category||null,level:s||null,project:r.project||null,repository:r.repository||null,branch:r.branch||null,commit:r.commit||null,starred:l?true:null,search:r.search||null,feedback:r.feedback||null,date_from:i||null,date_to:g||null},x$1=m$1(c=>{let h=o(c,$),f=Math.max(1,Math.ceil(h/L));return u=Math.min(u,f),{total:h,totalPages:f,lessons:p(c,{...$,sort:d,order:y,page:u,per_page:L}),allCategories:x(c),allProjects:v(c,"project"),allRepositories:v(c,"repository"),allBranches:v(c,"branch"),allCommits:v(c,"commit_hash"),theme:F(c).ui_theme}}),n={period:w,category:r.category||"",level:s,project:r.project||"",repository:r.repository||"",branch:r.branch||"",commit:r.commit||"",starred:l,search:r.search||"",feedback:r.feedback||"",date_from:i,date_to:g,sort:d,order:y};return e.html(ne({lessons:x$1.lessons,allCategories:x$1.allCategories,allProjects:x$1.allProjects,allRepositories:x$1.allRepositories,allBranches:x$1.allBranches,allCommits:x$1.allCommits,s:n,page:u,perPage:L,total:x$1.total,totalPages:x$1.totalPages,uiTheme:x$1.theme}))}),t$1.post("/lessons/:lesson_id/star",async e=>{let r$1=await e.req.parseBody();return m$1(a=>r(a,e.req.param("lesson_id"),r$1.starred==="1")),e.redirect(B(b(r$1,"next")||void 0),303)}),t$1.post("/lessons/:lesson_id/feedback",async e=>{let r=await e.req.parseBody(),a=b(r,"feedback"),s=a===""||a==="clear"?null:a;return m$1(i=>U(i,e.req.param("lesson_id"),s)),e.redirect(B(b(r,"next")||void 0),303)}),t$1.get("/lessons/:lesson_id",e=>{let r=m$1(a=>w(a,e.req.param("lesson_id")));return r?e.html(ie({lesson:r,uiTheme:ve()})):e.html("<h1>Lesson not found</h1>",404)}),t$1.get("/settings/export",()=>{let e=m$1(r=>P(r));return new Response(new Uint8Array(e),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),t$1.post("/settings/import",async e=>{let r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let a=new Uint8Array(await r.arrayBuffer()),s=m$1(i=>Q(i,a));return e.redirect(`/settings?imported=${s.lessons}&skipped=${s.skipped}&invalid=${s.invalid}&groups=${s.groups}¬ebook=${s.learning_state}`,303)}),t$1.get("/settings/notebook/download",()=>{let e=existsSync(j)?readFileSync(j,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t$1.post("/settings/notebook",async e=>{let r=await e.req.parseBody();return mkdirSync(dirname(j),{recursive:true}),writeFileSync(j,b(r,"content"),"utf8"),e.redirect(B(b(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),t$1.post("/settings",async e=>{let r=await e.req.parseBody(),a=Number.parseInt(b(r,"max_per_day","2"),10)||2,s=Number.parseInt(b(r,"min_gap_minutes","240"),10),i=b(r,"ui_theme","system");["system","dark","light"].includes(i)||(i="system");let g=Math.max(0,Number.parseInt(b(r,"nudge_every","10"),10)||0),l=b(r,"nudge_scope","session");return l!=="session"&&l!=="global"&&(l="session"),m$1(d=>{G(d,"max_per_day",String(a)),G(d,"min_gap_minutes",String(Number.isNaN(s)?240:s)),G(d,"ui_theme",i),G(d,"nudge_every",String(g)),G(d,"nudge_scope",l);}),e.redirect("/settings",303)}),t$1.get("/settings",e=>{let r=m$1(g=>F(g)),a=existsSync(j)?readFileSync(j,"utf8"):"",s=e.req.query(),i=s.imported!==void 0?{imported:Number(s.imported??0),skipped:Number(s.skipped??0),invalid:Number(s.invalid??0),groups:Number(s.groups??0),notebook:Number(s.notebook??0)}:null;return e.html(de({settings:r,notebookContent:a,notebookPath:j,uiTheme:r.ui_theme,flash:i}))}),t$1}function Le(t){let e=we();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},r=>{console.log(`devcoach UI running at http://localhost:${r.port}`);});}
|
|
535
|
+
export{we as createApp,Le as startUi};
|