anno-extension 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +30 -0
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/dist/browser-fs.d.ts +14 -0
  5. package/dist/browser-fs.d.ts.map +1 -0
  6. package/dist/browser-fs.js +69 -0
  7. package/dist/browser-fs.js.map +1 -0
  8. package/dist/config.d.ts +81 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +82 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/consolidate-v2.d.ts +15 -0
  13. package/dist/consolidate-v2.d.ts.map +1 -0
  14. package/dist/consolidate-v2.js +125 -0
  15. package/dist/consolidate-v2.js.map +1 -0
  16. package/dist/consolidation.d.ts +12 -0
  17. package/dist/consolidation.d.ts.map +1 -0
  18. package/dist/consolidation.js +199 -0
  19. package/dist/consolidation.js.map +1 -0
  20. package/dist/dedup.d.ts +18 -0
  21. package/dist/dedup.d.ts.map +1 -0
  22. package/dist/dedup.js +85 -0
  23. package/dist/dedup.js.map +1 -0
  24. package/dist/diagnostics.d.ts +24 -0
  25. package/dist/diagnostics.d.ts.map +1 -0
  26. package/dist/diagnostics.js +53 -0
  27. package/dist/diagnostics.js.map +1 -0
  28. package/dist/embedding-index.d.ts +27 -0
  29. package/dist/embedding-index.d.ts.map +1 -0
  30. package/dist/embedding-index.js +111 -0
  31. package/dist/embedding-index.js.map +1 -0
  32. package/dist/engine-archive.d.ts +37 -0
  33. package/dist/engine-archive.d.ts.map +1 -0
  34. package/dist/engine-archive.js +155 -0
  35. package/dist/engine-archive.js.map +1 -0
  36. package/dist/engine-episode-sync.d.ts +19 -0
  37. package/dist/engine-episode-sync.d.ts.map +1 -0
  38. package/dist/engine-episode-sync.js +186 -0
  39. package/dist/engine-episode-sync.js.map +1 -0
  40. package/dist/engine-injection-text.d.ts +46 -0
  41. package/dist/engine-injection-text.d.ts.map +1 -0
  42. package/dist/engine-injection-text.js +271 -0
  43. package/dist/engine-injection-text.js.map +1 -0
  44. package/dist/engine-insights.d.ts +22 -0
  45. package/dist/engine-insights.d.ts.map +1 -0
  46. package/dist/engine-insights.js +127 -0
  47. package/dist/engine-insights.js.map +1 -0
  48. package/dist/engine-links.d.ts +43 -0
  49. package/dist/engine-links.d.ts.map +1 -0
  50. package/dist/engine-links.js +223 -0
  51. package/dist/engine-links.js.map +1 -0
  52. package/dist/engine-recall-select.d.ts +84 -0
  53. package/dist/engine-recall-select.d.ts.map +1 -0
  54. package/dist/engine-recall-select.js +127 -0
  55. package/dist/engine-recall-select.js.map +1 -0
  56. package/dist/engine-reinforce.d.ts +18 -0
  57. package/dist/engine-reinforce.d.ts.map +1 -0
  58. package/dist/engine-reinforce.js +116 -0
  59. package/dist/engine-reinforce.js.map +1 -0
  60. package/dist/engine-scoring-v2.d.ts +35 -0
  61. package/dist/engine-scoring-v2.d.ts.map +1 -0
  62. package/dist/engine-scoring-v2.js +178 -0
  63. package/dist/engine-scoring-v2.js.map +1 -0
  64. package/dist/engine-v2-mapping.d.ts +17 -0
  65. package/dist/engine-v2-mapping.d.ts.map +1 -0
  66. package/dist/engine-v2-mapping.js +190 -0
  67. package/dist/engine-v2-mapping.js.map +1 -0
  68. package/dist/engine.d.ts +339 -0
  69. package/dist/engine.d.ts.map +1 -0
  70. package/dist/engine.js +1964 -0
  71. package/dist/engine.js.map +1 -0
  72. package/dist/eviction.d.ts +14 -0
  73. package/dist/eviction.d.ts.map +1 -0
  74. package/dist/eviction.js +26 -0
  75. package/dist/eviction.js.map +1 -0
  76. package/dist/extraction.d.ts +11 -0
  77. package/dist/extraction.d.ts.map +1 -0
  78. package/dist/extraction.js +261 -0
  79. package/dist/extraction.js.map +1 -0
  80. package/dist/full-insights-html.d.ts +9 -0
  81. package/dist/full-insights-html.d.ts.map +1 -0
  82. package/dist/full-insights-html.js +422 -0
  83. package/dist/full-insights-html.js.map +1 -0
  84. package/dist/full-insights-sections.d.ts +48 -0
  85. package/dist/full-insights-sections.d.ts.map +1 -0
  86. package/dist/full-insights-sections.js +231 -0
  87. package/dist/full-insights-sections.js.map +1 -0
  88. package/dist/full-insights.d.ts +22 -0
  89. package/dist/full-insights.d.ts.map +1 -0
  90. package/dist/full-insights.js +370 -0
  91. package/dist/full-insights.js.map +1 -0
  92. package/dist/hash-embedding.d.ts +9 -0
  93. package/dist/hash-embedding.d.ts.map +1 -0
  94. package/dist/hash-embedding.js +44 -0
  95. package/dist/hash-embedding.js.map +1 -0
  96. package/dist/human-insights.d.ts +28 -0
  97. package/dist/human-insights.d.ts.map +1 -0
  98. package/dist/human-insights.js +260 -0
  99. package/dist/human-insights.js.map +1 -0
  100. package/dist/i18n.d.ts +64 -0
  101. package/dist/i18n.d.ts.map +1 -0
  102. package/dist/i18n.js +367 -0
  103. package/dist/i18n.js.map +1 -0
  104. package/dist/index.d.ts +27 -0
  105. package/dist/index.d.ts.map +1 -0
  106. package/dist/index.js +21 -0
  107. package/dist/index.js.map +1 -0
  108. package/dist/insights-html.d.ts +9 -0
  109. package/dist/insights-html.d.ts.map +1 -0
  110. package/dist/insights-html.js +432 -0
  111. package/dist/insights-html.js.map +1 -0
  112. package/dist/linking.d.ts +22 -0
  113. package/dist/linking.d.ts.map +1 -0
  114. package/dist/linking.js +192 -0
  115. package/dist/linking.js.map +1 -0
  116. package/dist/llm-json.d.ts +10 -0
  117. package/dist/llm-json.d.ts.map +1 -0
  118. package/dist/llm-json.js +193 -0
  119. package/dist/llm-json.js.map +1 -0
  120. package/dist/privacy.d.ts +17 -0
  121. package/dist/privacy.d.ts.map +1 -0
  122. package/dist/privacy.js +53 -0
  123. package/dist/privacy.js.map +1 -0
  124. package/dist/procedural-v2.d.ts +9 -0
  125. package/dist/procedural-v2.d.ts.map +1 -0
  126. package/dist/procedural-v2.js +123 -0
  127. package/dist/procedural-v2.js.map +1 -0
  128. package/dist/reconsolidate-v2.d.ts +19 -0
  129. package/dist/reconsolidate-v2.d.ts.map +1 -0
  130. package/dist/reconsolidate-v2.js +115 -0
  131. package/dist/reconsolidate-v2.js.map +1 -0
  132. package/dist/scoring.d.ts +32 -0
  133. package/dist/scoring.d.ts.map +1 -0
  134. package/dist/scoring.js +108 -0
  135. package/dist/scoring.js.map +1 -0
  136. package/dist/store-v2.d.ts +35 -0
  137. package/dist/store-v2.d.ts.map +1 -0
  138. package/dist/store-v2.js +98 -0
  139. package/dist/store-v2.js.map +1 -0
  140. package/dist/store.d.ts +22 -0
  141. package/dist/store.d.ts.map +1 -0
  142. package/dist/store.js +116 -0
  143. package/dist/store.js.map +1 -0
  144. package/dist/turn-context.d.ts +37 -0
  145. package/dist/turn-context.d.ts.map +1 -0
  146. package/dist/turn-context.js +31 -0
  147. package/dist/turn-context.js.map +1 -0
  148. package/dist/types-v2.d.ts +190 -0
  149. package/dist/types-v2.d.ts.map +1 -0
  150. package/dist/types-v2.js +8 -0
  151. package/dist/types-v2.js.map +1 -0
  152. package/dist/types.d.ts +330 -0
  153. package/dist/types.d.ts.map +1 -0
  154. package/dist/types.js +8 -0
  155. package/dist/types.js.map +1 -0
  156. package/dist/update.d.ts +31 -0
  157. package/dist/update.d.ts.map +1 -0
  158. package/dist/update.js +250 -0
  159. package/dist/update.js.map +1 -0
  160. package/package.json +34 -0
  161. package/src/browser-fs.ts +67 -0
  162. package/src/config.ts +138 -0
  163. package/src/consolidate-v2.ts +158 -0
  164. package/src/consolidation.ts +229 -0
  165. package/src/dedup.ts +91 -0
  166. package/src/diagnostics.ts +81 -0
  167. package/src/embedding-index.ts +138 -0
  168. package/src/engine-archive.ts +167 -0
  169. package/src/engine-episode-sync.ts +237 -0
  170. package/src/engine-injection-text.ts +334 -0
  171. package/src/engine-insights.ts +182 -0
  172. package/src/engine-links.ts +311 -0
  173. package/src/engine-recall-select.ts +298 -0
  174. package/src/engine-reinforce.ts +154 -0
  175. package/src/engine-scoring-v2.ts +188 -0
  176. package/src/engine-v2-mapping.ts +204 -0
  177. package/src/engine.ts +2499 -0
  178. package/src/eviction.ts +38 -0
  179. package/src/extraction.ts +300 -0
  180. package/src/full-insights-html.ts +474 -0
  181. package/src/full-insights-sections.ts +305 -0
  182. package/src/full-insights.ts +423 -0
  183. package/src/hash-embedding.ts +48 -0
  184. package/src/human-insights.ts +345 -0
  185. package/src/i18n.ts +455 -0
  186. package/src/index.ts +117 -0
  187. package/src/insights-html.ts +478 -0
  188. package/src/linking.ts +228 -0
  189. package/src/llm-json.ts +194 -0
  190. package/src/privacy.ts +54 -0
  191. package/src/procedural-v2.ts +142 -0
  192. package/src/reconsolidate-v2.ts +145 -0
  193. package/src/scoring.ts +156 -0
  194. package/src/store-v2.ts +138 -0
  195. package/src/store.ts +137 -0
  196. package/src/turn-context.ts +56 -0
  197. package/src/types-v2.ts +244 -0
  198. package/src/types.ts +372 -0
  199. package/src/update.ts +271 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * [WHO]: renderInsightsHtml
3
+ * [FROM]: Depends on ./i18n.js, ./types.js
4
+ * [TO]: Consumed by packages/mem-core/src/index.ts
5
+ * [HERE]: packages/mem-core/src/insights-html.ts - pure HTML renderer for insights report, no side effects
6
+ */
7
+
8
+
9
+ import { PROMPTS } from "./i18n.js";
10
+ import type { InsightsReport, MemoryEntry, PatternInsight, StruggleInsight } from "./types.js";
11
+
12
+ type UiText = {
13
+ subtitle: string;
14
+ atAGlance: string;
15
+ whatsWorking: string;
16
+ whatsHindering: string;
17
+ quickWins: string;
18
+ focusArea: string;
19
+ noneYet: string;
20
+ workOn: string;
21
+ workOnIntro: string;
22
+ sessionWord: string;
23
+ distribution: string;
24
+ topProjects: string;
25
+ frequentTags: string;
26
+ successes: string;
27
+ successesIntro: string;
28
+ frictions: string;
29
+ frictionsIntro: string;
30
+ details: string;
31
+ weight: string;
32
+ access: string;
33
+ importance: string;
34
+ attempts: string;
35
+ solution: string;
36
+ status: string;
37
+ resolved: string;
38
+ unresolved: string;
39
+ knowledgePrefs: string;
40
+ lessons: string;
41
+ knowledge: string;
42
+ preferences: string;
43
+ generatedBy: string;
44
+ };
45
+
46
+ const UI: Record<"en" | "zh", UiText> = {
47
+ en: {
48
+ subtitle: "sessions analyzed",
49
+ atAGlance: "At a Glance",
50
+ whatsWorking: "What's Working",
51
+ whatsHindering: "What's Hindering",
52
+ quickWins: "Quick Wins",
53
+ focusArea: "Focus Area",
54
+ noneYet: "No strong signal yet.",
55
+ workOn: "What You Work On",
56
+ workOnIntro: "Top project areas inferred from your memory graph.",
57
+ sessionWord: "sessions",
58
+ distribution: "Focus Distribution",
59
+ topProjects: "Top Projects",
60
+ frequentTags: "Frequent Tags",
61
+ successes: "Impressive Things You Solved",
62
+ successesIntro: "Resolved struggles and high-value lessons worth repeating.",
63
+ frictions: "Where Things Still Go Wrong",
64
+ frictionsIntro: "Open struggles that are still costing time.",
65
+ details: "Details",
66
+ weight: "Weight",
67
+ access: "Access",
68
+ importance: "Importance",
69
+ attempts: "Attempts",
70
+ solution: "Solution",
71
+ status: "Status",
72
+ resolved: "resolved",
73
+ unresolved: "open",
74
+ knowledgePrefs: "Knowledge and Preferences",
75
+ lessons: "Top Lessons",
76
+ knowledge: "Knowledge Base",
77
+ preferences: "User Preferences",
78
+ generatedBy: "Generated by NanoMem",
79
+ },
80
+ zh: {
81
+ subtitle: "已分析会话",
82
+ atAGlance: "总览",
83
+ whatsWorking: "做得好的地方",
84
+ whatsHindering: "当前阻碍",
85
+ quickWins: "快速改进",
86
+ focusArea: "主要方向",
87
+ noneYet: "暂时没有明显信号。",
88
+ workOn: "你在做什么",
89
+ workOnIntro: "基于记忆图谱推断的项目重点领域。",
90
+ sessionWord: "次会话",
91
+ distribution: "关注分布",
92
+ topProjects: "主要项目",
93
+ frequentTags: "高频标签",
94
+ successes: "你解决得很好的事",
95
+ successesIntro: "已解决问题与高价值经验,建议重复复用。",
96
+ frictions: "仍在反复消耗的点",
97
+ frictionsIntro: "尚未关闭的问题,持续影响效率。",
98
+ details: "详情",
99
+ weight: "权重",
100
+ access: "访问",
101
+ importance: "重要度",
102
+ attempts: "尝试",
103
+ solution: "解法",
104
+ status: "状态",
105
+ resolved: "已解决",
106
+ unresolved: "待处理",
107
+ knowledgePrefs: "知识与偏好",
108
+ lessons: "关键经验",
109
+ knowledge: "知识库",
110
+ preferences: "用户偏好",
111
+ generatedBy: "由 NanoMem 自动生成",
112
+ },
113
+ };
114
+
115
+ function escapeHtml(str: string): string {
116
+ return str
117
+ .replace(/&/g, "&amp;")
118
+ .replace(/</g, "&lt;")
119
+ .replace(/>/g, "&gt;")
120
+ .replace(/"/g, "&quot;")
121
+ .replace(/'/g, "&#039;");
122
+ }
123
+
124
+ function formatDate(iso: string, locale: string): string {
125
+ const d = new Date(iso);
126
+ if (Number.isNaN(d.getTime())) return iso;
127
+ return d.toLocaleString(locale === "zh" ? "zh-CN" : "en-US");
128
+ }
129
+
130
+ function dedupeById(entries: MemoryEntry[]): MemoryEntry[] {
131
+ const map = new Map<string, MemoryEntry>();
132
+ for (const entry of entries) map.set(entry.id, entry);
133
+ return [...map.values()];
134
+ }
135
+
136
+ function topTags(entries: MemoryEntry[], limit = 8): Array<{ label: string; value: number }> {
137
+ const counts = new Map<string, number>();
138
+ for (const entry of entries) {
139
+ for (const tag of entry.tags ?? []) counts.set(tag, (counts.get(tag) ?? 0) + 1);
140
+ }
141
+ return [...counts.entries()]
142
+ .map(([label, value]) => ({ label, value }))
143
+ .sort((a, b) => b.value - a.value)
144
+ .slice(0, limit);
145
+ }
146
+
147
+ function topProjects(entries: MemoryEntry[], limit = 6): Array<{ label: string; value: number }> {
148
+ const counts = new Map<string, number>();
149
+ for (const entry of entries) {
150
+ if (!entry.project) continue;
151
+ counts.set(entry.project, (counts.get(entry.project) ?? 0) + 1);
152
+ }
153
+ return [...counts.entries()]
154
+ .map(([label, value]) => ({ label, value }))
155
+ .sort((a, b) => b.value - a.value)
156
+ .slice(0, limit);
157
+ }
158
+
159
+ function renderBarRows(rows: Array<{ label: string; value: number }>, colorClass: string, empty: string): string {
160
+ if (!rows.length) return `<p class="empty">${escapeHtml(empty)}</p>`;
161
+ const max = Math.max(...rows.map((row) => row.value), 1);
162
+ return rows
163
+ .map((row) => {
164
+ const width = Math.max(8, Math.round((row.value / max) * 100));
165
+ return `<div class="bar-row">
166
+ <div class="bar-label" title="${escapeHtml(row.label)}">${escapeHtml(row.label)}</div>
167
+ <div class="bar-track"><div class="bar-fill ${colorClass}" style="width:${width}%"></div></div>
168
+ <div class="bar-value">${row.value}</div>
169
+ </div>`;
170
+ })
171
+ .join("");
172
+ }
173
+
174
+ function renderProjectAreas(projects: Array<{ label: string; value: number }>, entries: MemoryEntry[], ui: UiText): string {
175
+ if (!projects.length) return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
176
+ return projects
177
+ .map((project) => {
178
+ const scopeEntries = entries.filter((entry) => entry.project === project.label);
179
+ const tags = topTags(scopeEntries, 4).map((item) => item.label);
180
+ const desc =
181
+ tags.length > 0
182
+ ? `Top signals: ${tags.map((tag) => `#${escapeHtml(tag)}`).join(" ")}`
183
+ : "No dominant tag signal yet.";
184
+ return `<article class="project-area">
185
+ <div class="area-header">
186
+ <span class="area-name">${escapeHtml(project.label)}</span>
187
+ <span class="area-count">~${project.value} ${escapeHtml(ui.sessionWord)}</span>
188
+ </div>
189
+ <p class="area-desc">${desc}</p>
190
+ </article>`;
191
+ })
192
+ .join("");
193
+ }
194
+
195
+ function renderPatternRows(patterns: PatternInsight[], ui: UiText): string {
196
+ if (!patterns.length) return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
197
+ const maxWeight = Math.max(...patterns.map((item) => item.weight), 1);
198
+ return patterns
199
+ .slice(0, 12)
200
+ .map((item) => {
201
+ const width = Math.max(8, Math.round((item.weight / maxWeight) * 100));
202
+ return `<article class="item-card">
203
+ <div class="item-title"><strong>${escapeHtml(item.trigger)}</strong> <span class="arrow">-></span> ${escapeHtml(item.behavior)}</div>
204
+ <div class="weight-track"><div class="weight-fill" style="width:${width}%"></div></div>
205
+ <div class="meta-row">
206
+ <span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
207
+ <span>${escapeHtml(ui.access)}: ${item.entry.accessCount}</span>
208
+ <span>${escapeHtml(ui.importance)}: ${item.entry.importance}</span>
209
+ </div>
210
+ </article>`;
211
+ })
212
+ .join("");
213
+ }
214
+
215
+ function renderStruggleRows(struggles: StruggleInsight[], ui: UiText): string {
216
+ if (!struggles.length) return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
217
+ return struggles
218
+ .slice(0, 12)
219
+ .map((item) => {
220
+ const attempts = item.attempts.length
221
+ ? `<div class="sub-line"><strong>${escapeHtml(ui.attempts)}:</strong> ${item.attempts.map((a) => escapeHtml(a)).join(" | ")}</div>`
222
+ : "";
223
+ const solution = item.solution
224
+ ? `<div class="sub-line"><strong>${escapeHtml(ui.solution)}:</strong> ${escapeHtml(item.solution)}</div>`
225
+ : "";
226
+ return `<article class="item-card ${item.resolved ? "ok" : "warn"}">
227
+ <div class="item-title">${escapeHtml(item.problem)}</div>
228
+ ${attempts}
229
+ ${solution}
230
+ <div class="meta-row">
231
+ <span>${escapeHtml(ui.status)}: ${item.resolved ? escapeHtml(ui.resolved) : escapeHtml(ui.unresolved)}</span>
232
+ <span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
233
+ </div>
234
+ </article>`;
235
+ })
236
+ .join("");
237
+ }
238
+
239
+ function renderMemoryList(entries: MemoryEntry[], empty: string): string {
240
+ if (!entries.length) return `<p class="empty">${escapeHtml(empty)}</p>`;
241
+ return `<ul class="list">${entries
242
+ .map((entry) => `<li>${escapeHtml(entry.summary || entry.detail || entry.content || "")}${entry.tags.length ? ` <span class="tags">${entry.tags.slice(0, 4).map((tag) => `#${escapeHtml(tag)}`).join(" ")}</span>` : ""}</li>`)
243
+ .join("")}</ul>`;
244
+ }
245
+
246
+ function renderAtAGlance(report: InsightsReport, projects: Array<{ label: string; value: number }>, ui: UiText): string {
247
+ const unresolved = report.struggles.filter((item) => !item.resolved);
248
+ const resolved = report.struggles.filter((item) => item.resolved);
249
+ const topPattern = report.patterns[0];
250
+ const topRec = report.recommendations[0];
251
+ const topProject = projects[0];
252
+ const working =
253
+ resolved.length > 0
254
+ ? `${resolved.length} struggles are already resolved; keep reusing those fixes.`
255
+ : report.topLessons.length
256
+ ? `${report.topLessons.length} lessons captured; convert the top ones into repeatable checklists.`
257
+ : ui.noneYet;
258
+ const hindering = unresolved.length
259
+ ? `${unresolved.length} open struggles remain. Most frequent: "${unresolved[0]?.problem ?? ""}".`
260
+ : "No unresolved struggles currently visible.";
261
+ const quickWins = topRec ? topRec : ui.noneYet;
262
+ const focus = topProject
263
+ ? `${topProject.label} appears most often (${topProject.value} memories).`
264
+ : topPattern
265
+ ? `Dominant behavior: when "${topPattern.trigger}", you often "${topPattern.behavior}".`
266
+ : ui.noneYet;
267
+ return `<section class="glance">
268
+ <h2>${escapeHtml(ui.atAGlance)}</h2>
269
+ <div class="glance-grid">
270
+ <article class="glance-card">
271
+ <h3>${escapeHtml(ui.whatsWorking)}</h3>
272
+ <p>${escapeHtml(working)}</p>
273
+ </article>
274
+ <article class="glance-card warn">
275
+ <h3>${escapeHtml(ui.whatsHindering)}</h3>
276
+ <p>${escapeHtml(hindering)}</p>
277
+ </article>
278
+ <article class="glance-card">
279
+ <h3>${escapeHtml(ui.quickWins)}</h3>
280
+ <p>${escapeHtml(quickWins)}</p>
281
+ </article>
282
+ <article class="glance-card">
283
+ <h3>${escapeHtml(ui.focusArea)}</h3>
284
+ <p>${escapeHtml(focus)}</p>
285
+ </article>
286
+ </div>
287
+ </section>`;
288
+ }
289
+
290
+ function renderRecommendations(report: InsightsReport, title: string, empty: string): string {
291
+ if (!report.recommendations.length) return `<section class="section"><h2>${escapeHtml(title)}</h2><p class="empty">${escapeHtml(empty)}</p></section>`;
292
+ return `<section id="section-recommendations" class="section">
293
+ <h2>${escapeHtml(title)}</h2>
294
+ <ul class="recommend-list">${report.recommendations.map((rec) => `<li>${escapeHtml(rec)}</li>`).join("")}</ul>
295
+ </section>`;
296
+ }
297
+
298
+ export function renderInsightsHtml(report: InsightsReport, locale: string): string {
299
+ const isZh = locale === "zh";
300
+ const ui = UI[isZh ? "zh" : "en"];
301
+ const prompts = PROMPTS[locale] ?? PROMPTS.en;
302
+
303
+ const allEntries = dedupeById([...report.topKnowledge, ...report.topLessons, ...report.preferences]);
304
+ const projects = topProjects(allEntries);
305
+ const tags = topTags(allEntries);
306
+ const unresolved = report.struggles.filter((item) => !item.resolved);
307
+ const resolved = report.struggles.filter((item) => item.resolved);
308
+
309
+ const statsRow = [
310
+ { label: "Sessions", value: report.stats.totalSessions },
311
+ { label: "Knowledge", value: report.stats.knowledge },
312
+ { label: "Lessons", value: report.stats.lessons },
313
+ { label: "Preferences", value: report.stats.preferences },
314
+ { label: "Struggles", value: report.struggles.length },
315
+ { label: "Patterns", value: report.patterns.length },
316
+ ];
317
+
318
+ const css = `
319
+ *{box-sizing:border-box}
320
+ body{margin:0;padding:48px 24px;font-family:Inter,"Segoe UI",Arial,sans-serif;background:#f8fafc;color:#334155;line-height:1.65}
321
+ .container{max-width:960px;margin:0 auto}
322
+ h1{font-size:34px;font-weight:700;color:#0f172a;margin:0 0 8px}
323
+ h2{font-size:22px;font-weight:700;color:#0f172a;margin:0 0 14px}
324
+ h3{font-size:15px;font-weight:700;color:#1e293b;margin:0 0 8px}
325
+ .subtitle{color:#64748b;font-size:14px;margin:0 0 24px}
326
+ .toc{display:flex;flex-wrap:wrap;gap:8px;padding:14px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;margin:0 0 24px}
327
+ .toc a{font-size:12px;color:#475569;background:#f1f5f9;border-radius:6px;padding:6px 10px;text-decoration:none}
328
+ .toc a:hover{background:#e2e8f0}
329
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;padding:20px 0;border-top:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;margin:0 0 28px}
330
+ .stat{text-align:center}
331
+ .stat-value{font-size:24px;font-weight:700;color:#0f172a}
332
+ .stat-label{font-size:11px;color:#64748b;text-transform:uppercase}
333
+ .glance{background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border:1px solid #f59e0b;border-radius:12px;padding:20px 22px;margin:0 0 28px}
334
+ .glance-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
335
+ .glance-card{background:rgba(255,255,255,.62);border:1px solid rgba(245,158,11,.28);border-radius:8px;padding:12px}
336
+ .glance-card.warn{background:rgba(255,241,242,.82);border-color:#fca5a5}
337
+ .glance-card p{margin:0;font-size:13px;color:#78350f}
338
+ .section{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:18px;margin:0 0 16px}
339
+ .section-intro{font-size:13px;color:#64748b;margin:0 0 12px}
340
+ .project-list{display:flex;flex-direction:column;gap:10px}
341
+ .project-area{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
342
+ .area-header{display:flex;justify-content:space-between;align-items:center;gap:10px}
343
+ .area-name{font-size:15px;font-weight:700;color:#0f172a}
344
+ .area-count{font-size:12px;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:4px}
345
+ .area-desc{margin:8px 0 0;font-size:13px;color:#475569}
346
+ .charts{display:grid;grid-template-columns:1fr 1fr;gap:16px}
347
+ .chart{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
348
+ .chart-title{font-size:12px;color:#64748b;text-transform:uppercase;margin:0 0 10px}
349
+ .bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}
350
+ .bar-label{width:120px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:#475569}
351
+ .bar-track{flex:1;height:7px;background:#f1f5f9;border-radius:99px}
352
+ .bar-fill{height:100%;border-radius:99px}
353
+ .bar-fill.blue{background:#2563eb}
354
+ .bar-fill.cyan{background:#0891b2}
355
+ .bar-value{width:24px;text-align:right;font-size:12px;color:#64748b}
356
+ .recommend-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
357
+ .recommend-list li{background:#f0fdf4;border:1px solid #bbf7d0;border-left:4px solid #16a34a;border-radius:8px;padding:11px 12px;font-size:14px}
358
+ .item-grid{display:flex;flex-direction:column;gap:10px}
359
+ .item-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
360
+ .item-card.ok{background:#f0fdf4;border-color:#86efac}
361
+ .item-card.warn{background:#fef2f2;border-color:#fca5a5}
362
+ .item-title{font-size:14px;color:#0f172a}
363
+ .arrow{color:#94a3b8}
364
+ .weight-track{margin-top:8px;height:6px;background:#e2e8f0;border-radius:99px}
365
+ .weight-fill{height:100%;background:#2563eb;border-radius:99px}
366
+ .sub-line{margin-top:8px;font-size:13px;color:#334155}
367
+ .meta-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:#64748b}
368
+ details{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}
369
+ summary{cursor:pointer;font-size:14px;font-weight:700;color:#334155}
370
+ .list{margin:10px 0 0;padding-left:18px}
371
+ .list li{margin:0 0 8px;font-size:13px;color:#334155}
372
+ .tags{font-size:12px;color:#64748b}
373
+ .empty{font-size:13px;color:#94a3b8}
374
+ footer{margin-top:22px;text-align:center;font-size:12px;color:#94a3b8}
375
+ @media (max-width:760px){body{padding:28px 14px}.charts{grid-template-columns:1fr}.bar-label{width:96px}}
376
+ `;
377
+
378
+ return `<!doctype html>
379
+ <html lang="${isZh ? "zh-CN" : "en"}">
380
+ <head>
381
+ <meta charset="utf-8" />
382
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
383
+ <title>${escapeHtml(prompts.insightsTitle)}</title>
384
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
385
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
386
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
387
+ <style>${css}</style>
388
+ </head>
389
+ <body>
390
+ <main class="container">
391
+ <h1>${escapeHtml(prompts.insightsTitle)}</h1>
392
+ <p class="subtitle">${report.stats.totalSessions} ${escapeHtml(ui.subtitle)} | ${escapeHtml(prompts.insightsGeneratedAt)}: ${escapeHtml(formatDate(report.generatedAt, locale))}</p>
393
+
394
+ <nav class="toc">
395
+ <a href="#section-work">${escapeHtml(ui.workOn)}</a>
396
+ <a href="#section-distribution">${escapeHtml(ui.distribution)}</a>
397
+ <a href="#section-recommendations">${escapeHtml(prompts.insightsSectionRecommendations)}</a>
398
+ <a href="#section-patterns">${escapeHtml(prompts.insightsSectionPatterns)}</a>
399
+ <a href="#section-struggles">${escapeHtml(prompts.insightsSectionStruggles)}</a>
400
+ <a href="#section-memory">${escapeHtml(ui.knowledgePrefs)}</a>
401
+ </nav>
402
+
403
+ <section class="stats">
404
+ ${statsRow
405
+ .map((item) => `<div class="stat"><div class="stat-value">${item.value}</div><div class="stat-label">${escapeHtml(item.label)}</div></div>`)
406
+ .join("")}
407
+ </section>
408
+
409
+ ${renderAtAGlance(report, projects, ui)}
410
+
411
+ <section id="section-work" class="section">
412
+ <h2>${escapeHtml(ui.workOn)}</h2>
413
+ <p class="section-intro">${escapeHtml(ui.workOnIntro)}</p>
414
+ <div class="project-list">${renderProjectAreas(projects, allEntries, ui)}</div>
415
+ </section>
416
+
417
+ <section id="section-distribution" class="section">
418
+ <h2>${escapeHtml(ui.distribution)}</h2>
419
+ <div class="charts">
420
+ <div class="chart">
421
+ <p class="chart-title">${escapeHtml(ui.topProjects)}</p>
422
+ ${renderBarRows(projects, "blue", ui.noneYet)}
423
+ </div>
424
+ <div class="chart">
425
+ <p class="chart-title">${escapeHtml(ui.frequentTags)}</p>
426
+ ${renderBarRows(tags, "cyan", ui.noneYet)}
427
+ </div>
428
+ </div>
429
+ </section>
430
+
431
+ ${renderRecommendations(report, prompts.insightsSectionRecommendations, prompts.insightsNoData)}
432
+
433
+ <section class="section">
434
+ <h2>${escapeHtml(ui.successes)}</h2>
435
+ <p class="section-intro">${escapeHtml(ui.successesIntro)}</p>
436
+ <div class="item-grid">
437
+ ${renderStruggleRows(resolved, ui)}
438
+ ${renderMemoryList(report.topLessons.slice(0, 5), ui.noneYet)}
439
+ </div>
440
+ </section>
441
+
442
+ <section class="section">
443
+ <h2>${escapeHtml(ui.frictions)}</h2>
444
+ <p class="section-intro">${escapeHtml(ui.frictionsIntro)}</p>
445
+ <div class="item-grid">${renderStruggleRows(unresolved, ui)}</div>
446
+ </section>
447
+
448
+ <section id="section-patterns" class="section">
449
+ <h2>${escapeHtml(prompts.insightsSectionPatterns)}</h2>
450
+ <div class="item-grid">${renderPatternRows(report.patterns, ui)}</div>
451
+ </section>
452
+
453
+ <section id="section-struggles" class="section">
454
+ <h2>${escapeHtml(prompts.insightsSectionStruggles)}</h2>
455
+ <div class="item-grid">${renderStruggleRows(report.struggles, ui)}</div>
456
+ </section>
457
+
458
+ <section id="section-memory" class="section">
459
+ <h2>${escapeHtml(ui.knowledgePrefs)}</h2>
460
+ <details open>
461
+ <summary>${escapeHtml(ui.lessons)} (${report.topLessons.length})</summary>
462
+ ${renderMemoryList(report.topLessons.slice(0, 12), ui.noneYet)}
463
+ </details>
464
+ <details>
465
+ <summary>${escapeHtml(ui.knowledge)} (${report.topKnowledge.length})</summary>
466
+ ${renderMemoryList(report.topKnowledge.slice(0, 12), ui.noneYet)}
467
+ </details>
468
+ <details>
469
+ <summary>${escapeHtml(ui.preferences)} (${report.preferences.length})</summary>
470
+ ${renderMemoryList(report.preferences.slice(0, 12), ui.noneYet)}
471
+ </details>
472
+ </section>
473
+
474
+ <footer>${escapeHtml(ui.generatedBy)}</footer>
475
+ </main>
476
+ </body>
477
+ </html>`;
478
+ }