@pencil-agent/nano-pencil 1.9.0 → 1.9.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/dist/core/extensions/runner.d.ts +3 -0
- package/dist/core/extensions/runner.js +16 -8
- package/dist/core/mcp-manager.d.ts +9 -0
- package/dist/core/mcp-manager.js +24 -1
- package/dist/core/sdk.js +10 -0
- package/dist/core/soul-integration.js +24 -8
- package/dist/modes/interactive/interactive-mode.js +29 -1
- package/dist/packages/nanomem/insights-html.d.ts +2 -2
- package/dist/packages/nanomem/insights-html.js +405 -417
- package/package.json +1 -1
|
@@ -82,8 +82,11 @@ export declare class ExtensionRunner {
|
|
|
82
82
|
private shortcutDiagnostics;
|
|
83
83
|
private commandDiagnostics;
|
|
84
84
|
private readonly beforeAgentStartTimeoutMs;
|
|
85
|
+
private readonly beforeAgentStartTimeoutSentinel;
|
|
86
|
+
private beforeAgentStartTimeoutLastReported;
|
|
85
87
|
constructor(extensions: Extension[], runtime: ExtensionRuntime, cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry);
|
|
86
88
|
private withTimeout;
|
|
89
|
+
private reportBeforeAgentStartTimeout;
|
|
87
90
|
bindCore(actions: ExtensionActions, contextActions: ExtensionContextActions): void;
|
|
88
91
|
bindCommandContext(actions?: ExtensionCommandContextActions): void;
|
|
89
92
|
setUIContext(uiContext?: ExtensionUIContext): void;
|
|
@@ -105,6 +105,8 @@ export class ExtensionRunner {
|
|
|
105
105
|
shortcutDiagnostics = [];
|
|
106
106
|
commandDiagnostics = [];
|
|
107
107
|
beforeAgentStartTimeoutMs = 1500;
|
|
108
|
+
beforeAgentStartTimeoutSentinel = Symbol("before_agent_start_timeout");
|
|
109
|
+
beforeAgentStartTimeoutLastReported = new Map();
|
|
108
110
|
constructor(extensions, runtime, cwd, sessionManager, modelRegistry) {
|
|
109
111
|
this.extensions = extensions;
|
|
110
112
|
this.runtime = runtime;
|
|
@@ -115,7 +117,7 @@ export class ExtensionRunner {
|
|
|
115
117
|
}
|
|
116
118
|
async withTimeout(promise, timeoutMs) {
|
|
117
119
|
return new Promise((resolve) => {
|
|
118
|
-
const timer = setTimeout(() => resolve(
|
|
120
|
+
const timer = setTimeout(() => resolve(this.beforeAgentStartTimeoutSentinel), timeoutMs);
|
|
119
121
|
promise
|
|
120
122
|
.then((value) => {
|
|
121
123
|
clearTimeout(timer);
|
|
@@ -123,10 +125,20 @@ export class ExtensionRunner {
|
|
|
123
125
|
})
|
|
124
126
|
.catch(() => {
|
|
125
127
|
clearTimeout(timer);
|
|
126
|
-
resolve(
|
|
128
|
+
resolve(this.beforeAgentStartTimeoutSentinel);
|
|
127
129
|
});
|
|
128
130
|
});
|
|
129
131
|
}
|
|
132
|
+
reportBeforeAgentStartTimeout(extensionPath) {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const last = this.beforeAgentStartTimeoutLastReported.get(extensionPath) ?? 0;
|
|
135
|
+
// Rate-limit to once per minute per extension.
|
|
136
|
+
if (now - last < 60_000)
|
|
137
|
+
return;
|
|
138
|
+
this.beforeAgentStartTimeoutLastReported.set(extensionPath, now);
|
|
139
|
+
// Soft warning only; do not surface as user-facing extension error.
|
|
140
|
+
console.warn(`Extension before_agent_start timed out (${this.beforeAgentStartTimeoutMs}ms): ${extensionPath}`);
|
|
141
|
+
}
|
|
130
142
|
bindCore(actions, contextActions) {
|
|
131
143
|
// Copy actions into the shared runtime (all extension APIs reference this)
|
|
132
144
|
this.runtime.sendMessage = actions.sendMessage;
|
|
@@ -556,12 +568,8 @@ export class ExtensionRunner {
|
|
|
556
568
|
systemPrompt: currentSystemPrompt,
|
|
557
569
|
};
|
|
558
570
|
const handlerResult = await this.withTimeout(handler(event, ctx), this.beforeAgentStartTimeoutMs);
|
|
559
|
-
if (handlerResult ===
|
|
560
|
-
this.
|
|
561
|
-
extensionPath: ext.path,
|
|
562
|
-
event: "before_agent_start",
|
|
563
|
-
error: `handler timed out after ${this.beforeAgentStartTimeoutMs}ms`,
|
|
564
|
-
});
|
|
571
|
+
if (handlerResult === this.beforeAgentStartTimeoutSentinel) {
|
|
572
|
+
this.reportBeforeAgentStartTimeout(ext.path);
|
|
565
573
|
continue;
|
|
566
574
|
}
|
|
567
575
|
if (handlerResult) {
|
|
@@ -8,6 +8,9 @@ import type { ToolDefinition } from "./extensions/index.js";
|
|
|
8
8
|
export declare class MCPManager {
|
|
9
9
|
private client;
|
|
10
10
|
private tools;
|
|
11
|
+
private enabledServerIds;
|
|
12
|
+
private startedServerIds;
|
|
13
|
+
private failedServerIds;
|
|
11
14
|
constructor();
|
|
12
15
|
/**
|
|
13
16
|
* Initialize MCP manager and load tools
|
|
@@ -21,6 +24,12 @@ export declare class MCPManager {
|
|
|
21
24
|
* Get the MCP client instance
|
|
22
25
|
*/
|
|
23
26
|
getClient(): MCPClient;
|
|
27
|
+
getStatus(): {
|
|
28
|
+
enabledServers: string[];
|
|
29
|
+
startedServers: string[];
|
|
30
|
+
failedServers: string[];
|
|
31
|
+
toolCount: number;
|
|
32
|
+
};
|
|
24
33
|
/**
|
|
25
34
|
* Cleanup: stop all servers
|
|
26
35
|
*/
|
package/dist/core/mcp-manager.js
CHANGED
|
@@ -9,6 +9,9 @@ import { listEnabledMCPServers } from "./mcp/mcp-config.js";
|
|
|
9
9
|
export class MCPManager {
|
|
10
10
|
client;
|
|
11
11
|
tools = [];
|
|
12
|
+
enabledServerIds = [];
|
|
13
|
+
startedServerIds = [];
|
|
14
|
+
failedServerIds = [];
|
|
12
15
|
constructor() {
|
|
13
16
|
this.client = new MCPClient();
|
|
14
17
|
}
|
|
@@ -18,11 +21,23 @@ export class MCPManager {
|
|
|
18
21
|
async initialize() {
|
|
19
22
|
// Load enabled servers
|
|
20
23
|
const enabledServers = listEnabledMCPServers();
|
|
24
|
+
this.enabledServerIds = enabledServers.map((s) => s.id);
|
|
25
|
+
this.startedServerIds = [];
|
|
26
|
+
this.failedServerIds = [];
|
|
21
27
|
for (const serverConfig of enabledServers) {
|
|
22
28
|
this.client.addServer(serverConfig);
|
|
23
29
|
// Start stdio-based servers
|
|
24
30
|
if (serverConfig.transport !== "sse") {
|
|
25
|
-
await this.client.startServer(serverConfig.id);
|
|
31
|
+
const ok = await this.client.startServer(serverConfig.id);
|
|
32
|
+
if (ok) {
|
|
33
|
+
this.startedServerIds.push(serverConfig.id);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.failedServerIds.push(serverConfig.id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.startedServerIds.push(serverConfig.id);
|
|
26
41
|
}
|
|
27
42
|
}
|
|
28
43
|
// Load tools from all servers
|
|
@@ -40,6 +55,14 @@ export class MCPManager {
|
|
|
40
55
|
getClient() {
|
|
41
56
|
return this.client;
|
|
42
57
|
}
|
|
58
|
+
getStatus() {
|
|
59
|
+
return {
|
|
60
|
+
enabledServers: [...this.enabledServerIds],
|
|
61
|
+
startedServers: [...this.startedServerIds],
|
|
62
|
+
failedServers: [...this.failedServerIds],
|
|
63
|
+
toolCount: this.tools.length,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
43
66
|
/**
|
|
44
67
|
* Cleanup: stop all servers
|
|
45
68
|
*/
|
package/dist/core/sdk.js
CHANGED
|
@@ -241,6 +241,16 @@ export async function createAgentSession(options = {}) {
|
|
|
241
241
|
await mcpManager.initialize();
|
|
242
242
|
mcpTools.push(...mcpManager.getTools());
|
|
243
243
|
time("mcp.initialize");
|
|
244
|
+
const mcpStatus = mcpManager.getStatus();
|
|
245
|
+
if (mcpStatus.toolCount === 0) {
|
|
246
|
+
const failed = mcpStatus.failedServers.length > 0
|
|
247
|
+
? ` failed=${mcpStatus.failedServers.join(",")}`
|
|
248
|
+
: "";
|
|
249
|
+
console.warn(`MCP enabled but no tools loaded (enabled=${mcpStatus.enabledServers.length}, started=${mcpStatus.startedServers.length}, tools=0).${failed}`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(`MCP tools loaded: ${mcpStatus.toolCount}`);
|
|
253
|
+
}
|
|
244
254
|
process.once("exit", () => mcpManager?.dispose());
|
|
245
255
|
}
|
|
246
256
|
catch (error) {
|
|
@@ -11,8 +11,24 @@ import { dirname } from "node:path";
|
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
function getBundledSoulCandidates() {
|
|
15
|
+
return [
|
|
16
|
+
// Published package runtime: dist/core -> dist/packages/nanosoul
|
|
17
|
+
join(__dirname, "..", "packages", "nanosoul"),
|
|
18
|
+
// Legacy/runtime fallback
|
|
19
|
+
join(__dirname, "packages", "nanosoul"),
|
|
20
|
+
// Dev workspace runtime
|
|
21
|
+
join(process.cwd(), "packages", "nanosoul", "dist"),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
function resolveBundledSoulEntry() {
|
|
25
|
+
for (const dir of getBundledSoulCandidates()) {
|
|
26
|
+
const entry = join(dir, "index.js");
|
|
27
|
+
if (existsSync(entry))
|
|
28
|
+
return entry;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
16
32
|
/**
|
|
17
33
|
* Default Soul configuration for NanoPencil
|
|
18
34
|
*/
|
|
@@ -49,9 +65,10 @@ export function getSoulConfig() {
|
|
|
49
65
|
*/
|
|
50
66
|
export async function createSoulManager() {
|
|
51
67
|
try {
|
|
52
|
-
// Try bundled package first
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
// Try bundled package first
|
|
69
|
+
const bundledEntry = resolveBundledSoulEntry();
|
|
70
|
+
if (bundledEntry) {
|
|
71
|
+
const { SoulManager: SM } = await import(bundledEntry);
|
|
55
72
|
return new SM({
|
|
56
73
|
config: getSoulConfig(),
|
|
57
74
|
});
|
|
@@ -72,9 +89,8 @@ export async function createSoulManager() {
|
|
|
72
89
|
*/
|
|
73
90
|
export function isSoulAvailable() {
|
|
74
91
|
// Check bundled version first
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
77
|
-
}
|
|
92
|
+
if (resolveBundledSoulEntry())
|
|
93
|
+
return true;
|
|
78
94
|
// Fall back to checking node_modules
|
|
79
95
|
try {
|
|
80
96
|
require.resolve("nanosoul");
|
|
@@ -4020,6 +4020,34 @@ export class InteractiveMode {
|
|
|
4020
4020
|
this.ui.requestRender();
|
|
4021
4021
|
return;
|
|
4022
4022
|
}
|
|
4023
|
+
if (action === "status" || action === "tools") {
|
|
4024
|
+
const runtimeTools = this.session
|
|
4025
|
+
.getAllTools()
|
|
4026
|
+
.filter((t) => t.name.startsWith("mcp_"));
|
|
4027
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
4028
|
+
if (runtimeTools.length === 0) {
|
|
4029
|
+
this.chatContainer.addChild(new Text([
|
|
4030
|
+
theme.bold("MCP Runtime Status"),
|
|
4031
|
+
"",
|
|
4032
|
+
"No MCP tools are currently registered in this session.",
|
|
4033
|
+
theme.fg("dim", "Tip: run /reload and check startup logs for MCP errors."),
|
|
4034
|
+
].join("\n"), 1, 0));
|
|
4035
|
+
}
|
|
4036
|
+
else {
|
|
4037
|
+
const lines = [
|
|
4038
|
+
theme.bold("MCP Runtime Status"),
|
|
4039
|
+
"",
|
|
4040
|
+
`Registered MCP tools: ${runtimeTools.length}`,
|
|
4041
|
+
...runtimeTools.slice(0, 30).map((t) => `- ${t.name}`),
|
|
4042
|
+
];
|
|
4043
|
+
if (runtimeTools.length > 30) {
|
|
4044
|
+
lines.push(theme.fg("dim", `...and ${runtimeTools.length - 30} more`));
|
|
4045
|
+
}
|
|
4046
|
+
this.chatContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
4047
|
+
}
|
|
4048
|
+
this.ui.requestRender();
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4023
4051
|
if ((action === "enable" || action === "disable") && target) {
|
|
4024
4052
|
setMCPServerEnabled(target, action === "enable");
|
|
4025
4053
|
this.chatContainer.addChild(new Spacer(1));
|
|
@@ -4028,7 +4056,7 @@ export class InteractiveMode {
|
|
|
4028
4056
|
return;
|
|
4029
4057
|
}
|
|
4030
4058
|
this.chatContainer.addChild(new Spacer(1));
|
|
4031
|
-
this.chatContainer.addChild(new Text("Usage: /mcp [list|enable <id>|disable <id>]", 1, 0));
|
|
4059
|
+
this.chatContainer.addChild(new Text("Usage: /mcp [list|status|tools|enable <id>|disable <id>]", 1, 0));
|
|
4032
4060
|
this.ui.requestRender();
|
|
4033
4061
|
}
|
|
4034
4062
|
async handleUpdateCommand() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [INPUT]: InsightsReport, locale
|
|
3
|
-
* [OUTPUT]:
|
|
4
|
-
* [POS]: Pure
|
|
3
|
+
* [OUTPUT]: Standalone HTML report
|
|
4
|
+
* [POS]: Pure renderer, no side effects
|
|
5
5
|
*/
|
|
6
6
|
import type { InsightsReport } from "./types.js";
|
|
7
7
|
export declare function renderInsightsHtml(report: InsightsReport, locale: string): string;
|
|
@@ -1,9 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [INPUT]: InsightsReport, locale
|
|
3
|
-
* [OUTPUT]:
|
|
4
|
-
* [POS]: Pure
|
|
3
|
+
* [OUTPUT]: Standalone HTML report
|
|
4
|
+
* [POS]: Pure renderer, no side effects
|
|
5
5
|
*/
|
|
6
6
|
import { PROMPTS } from "./i18n.js";
|
|
7
|
+
const UI = {
|
|
8
|
+
en: {
|
|
9
|
+
subtitle: "sessions analyzed",
|
|
10
|
+
atAGlance: "At a Glance",
|
|
11
|
+
whatsWorking: "What's Working",
|
|
12
|
+
whatsHindering: "What's Hindering",
|
|
13
|
+
quickWins: "Quick Wins",
|
|
14
|
+
focusArea: "Focus Area",
|
|
15
|
+
noneYet: "No strong signal yet.",
|
|
16
|
+
workOn: "What You Work On",
|
|
17
|
+
workOnIntro: "Top project areas inferred from your memory graph.",
|
|
18
|
+
sessionWord: "sessions",
|
|
19
|
+
distribution: "Focus Distribution",
|
|
20
|
+
topProjects: "Top Projects",
|
|
21
|
+
frequentTags: "Frequent Tags",
|
|
22
|
+
successes: "Impressive Things You Solved",
|
|
23
|
+
successesIntro: "Resolved struggles and high-value lessons worth repeating.",
|
|
24
|
+
frictions: "Where Things Still Go Wrong",
|
|
25
|
+
frictionsIntro: "Open struggles that are still costing time.",
|
|
26
|
+
details: "Details",
|
|
27
|
+
weight: "Weight",
|
|
28
|
+
access: "Access",
|
|
29
|
+
importance: "Importance",
|
|
30
|
+
attempts: "Attempts",
|
|
31
|
+
solution: "Solution",
|
|
32
|
+
status: "Status",
|
|
33
|
+
resolved: "resolved",
|
|
34
|
+
unresolved: "open",
|
|
35
|
+
knowledgePrefs: "Knowledge and Preferences",
|
|
36
|
+
lessons: "Top Lessons",
|
|
37
|
+
knowledge: "Knowledge Base",
|
|
38
|
+
preferences: "User Preferences",
|
|
39
|
+
generatedBy: "Generated by NanoMem",
|
|
40
|
+
},
|
|
41
|
+
zh: {
|
|
42
|
+
subtitle: "已分析会话",
|
|
43
|
+
atAGlance: "总览",
|
|
44
|
+
whatsWorking: "做得好的地方",
|
|
45
|
+
whatsHindering: "当前阻碍",
|
|
46
|
+
quickWins: "快速改进",
|
|
47
|
+
focusArea: "主要方向",
|
|
48
|
+
noneYet: "暂时没有明显信号。",
|
|
49
|
+
workOn: "你在做什么",
|
|
50
|
+
workOnIntro: "基于记忆图谱推断的项目重点领域。",
|
|
51
|
+
sessionWord: "次会话",
|
|
52
|
+
distribution: "关注分布",
|
|
53
|
+
topProjects: "主要项目",
|
|
54
|
+
frequentTags: "高频标签",
|
|
55
|
+
successes: "你解决得很好的事",
|
|
56
|
+
successesIntro: "已解决问题与高价值经验,建议重复复用。",
|
|
57
|
+
frictions: "仍在反复消耗的点",
|
|
58
|
+
frictionsIntro: "尚未关闭的问题,持续影响效率。",
|
|
59
|
+
details: "详情",
|
|
60
|
+
weight: "权重",
|
|
61
|
+
access: "访问",
|
|
62
|
+
importance: "重要度",
|
|
63
|
+
attempts: "尝试",
|
|
64
|
+
solution: "解法",
|
|
65
|
+
status: "状态",
|
|
66
|
+
resolved: "已解决",
|
|
67
|
+
unresolved: "待处理",
|
|
68
|
+
knowledgePrefs: "知识与偏好",
|
|
69
|
+
lessons: "关键经验",
|
|
70
|
+
knowledge: "知识库",
|
|
71
|
+
preferences: "用户偏好",
|
|
72
|
+
generatedBy: "由 NanoMem 自动生成",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
7
75
|
function escapeHtml(str) {
|
|
8
76
|
return str
|
|
9
77
|
.replace(/&/g, "&")
|
|
@@ -12,432 +80,352 @@ function escapeHtml(str) {
|
|
|
12
80
|
.replace(/"/g, """)
|
|
13
81
|
.replace(/'/g, "'");
|
|
14
82
|
}
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
83
|
+
function formatDate(iso, locale) {
|
|
84
|
+
const d = new Date(iso);
|
|
85
|
+
if (Number.isNaN(d.getTime()))
|
|
86
|
+
return iso;
|
|
87
|
+
return d.toLocaleString(locale === "zh" ? "zh-CN" : "en-US");
|
|
88
|
+
}
|
|
89
|
+
function dedupeById(entries) {
|
|
90
|
+
const map = new Map();
|
|
91
|
+
for (const entry of entries)
|
|
92
|
+
map.set(entry.id, entry);
|
|
93
|
+
return [...map.values()];
|
|
94
|
+
}
|
|
95
|
+
function topTags(entries, limit = 8) {
|
|
96
|
+
const counts = new Map();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
for (const tag of entry.tags ?? [])
|
|
99
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
return [...counts.entries()]
|
|
102
|
+
.map(([label, value]) => ({ label, value }))
|
|
103
|
+
.sort((a, b) => b.value - a.value)
|
|
104
|
+
.slice(0, limit);
|
|
105
|
+
}
|
|
106
|
+
function topProjects(entries, limit = 6) {
|
|
107
|
+
const counts = new Map();
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.project)
|
|
110
|
+
continue;
|
|
111
|
+
counts.set(entry.project, (counts.get(entry.project) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
return [...counts.entries()]
|
|
114
|
+
.map(([label, value]) => ({ label, value }))
|
|
115
|
+
.sort((a, b) => b.value - a.value)
|
|
116
|
+
.slice(0, limit);
|
|
117
|
+
}
|
|
118
|
+
function renderBarRows(rows, colorClass, empty) {
|
|
119
|
+
if (!rows.length)
|
|
120
|
+
return `<p class="empty">${escapeHtml(empty)}</p>`;
|
|
121
|
+
const max = Math.max(...rows.map((row) => row.value), 1);
|
|
122
|
+
return rows
|
|
123
|
+
.map((row) => {
|
|
124
|
+
const width = Math.max(8, Math.round((row.value / max) * 100));
|
|
125
|
+
return `<div class="bar-row">
|
|
126
|
+
<div class="bar-label" title="${escapeHtml(row.label)}">${escapeHtml(row.label)}</div>
|
|
127
|
+
<div class="bar-track"><div class="bar-fill ${colorClass}" style="width:${width}%"></div></div>
|
|
128
|
+
<div class="bar-value">${row.value}</div>
|
|
129
|
+
</div>`;
|
|
130
|
+
})
|
|
33
131
|
.join("");
|
|
34
132
|
}
|
|
35
|
-
function
|
|
36
|
-
if (!
|
|
37
|
-
return ""
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
133
|
+
function renderProjectAreas(projects, entries, ui) {
|
|
134
|
+
if (!projects.length)
|
|
135
|
+
return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
|
|
136
|
+
return projects
|
|
137
|
+
.map((project) => {
|
|
138
|
+
const scopeEntries = entries.filter((entry) => entry.project === project.label);
|
|
139
|
+
const tags = topTags(scopeEntries, 4).map((item) => item.label);
|
|
140
|
+
const desc = tags.length > 0
|
|
141
|
+
? `Top signals: ${tags.map((tag) => `#${escapeHtml(tag)}`).join(" ")}`
|
|
142
|
+
: "No dominant tag signal yet.";
|
|
143
|
+
return `<article class="project-area">
|
|
144
|
+
<div class="area-header">
|
|
145
|
+
<span class="area-name">${escapeHtml(project.label)}</span>
|
|
146
|
+
<span class="area-count">~${project.value} ${escapeHtml(ui.sessionWord)}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<p class="area-desc">${desc}</p>
|
|
149
|
+
</article>`;
|
|
150
|
+
})
|
|
151
|
+
.join("");
|
|
50
152
|
}
|
|
51
|
-
function
|
|
153
|
+
function renderPatternRows(patterns, ui) {
|
|
52
154
|
if (!patterns.length)
|
|
53
|
-
return ""
|
|
54
|
-
const maxWeight = Math.max(...patterns.map((
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div class="weight-bar-container">
|
|
71
|
-
<div class="weight-bar" style="width: ${(pa.weight / maxWeight) * 100}%"></div>
|
|
72
|
-
<span class="weight-value">${pa.weight.toFixed(1)}</span>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="access-badge">👁 ${pa.entry.accessCount}</div>
|
|
75
|
-
</div>`)
|
|
76
|
-
.join("")}
|
|
77
|
-
</div>
|
|
78
|
-
</section>`;
|
|
155
|
+
return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
|
|
156
|
+
const maxWeight = Math.max(...patterns.map((item) => item.weight), 1);
|
|
157
|
+
return patterns
|
|
158
|
+
.slice(0, 12)
|
|
159
|
+
.map((item) => {
|
|
160
|
+
const width = Math.max(8, Math.round((item.weight / maxWeight) * 100));
|
|
161
|
+
return `<article class="item-card">
|
|
162
|
+
<div class="item-title"><strong>${escapeHtml(item.trigger)}</strong> <span class="arrow">-></span> ${escapeHtml(item.behavior)}</div>
|
|
163
|
+
<div class="weight-track"><div class="weight-fill" style="width:${width}%"></div></div>
|
|
164
|
+
<div class="meta-row">
|
|
165
|
+
<span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
|
|
166
|
+
<span>${escapeHtml(ui.access)}: ${item.entry.accessCount}</span>
|
|
167
|
+
<span>${escapeHtml(ui.importance)}: ${item.entry.importance}</span>
|
|
168
|
+
</div>
|
|
169
|
+
</article>`;
|
|
170
|
+
})
|
|
171
|
+
.join("");
|
|
79
172
|
}
|
|
80
|
-
function
|
|
173
|
+
function renderStruggleRows(struggles, ui) {
|
|
81
174
|
if (!struggles.length)
|
|
82
|
-
return ""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
?
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<span class="toggle-icon">▼</span>
|
|
104
|
-
${escapeHtml(p.insightsSectionStruggles)} (${struggles.length})
|
|
105
|
-
</h2>
|
|
106
|
-
<div class="section-content">
|
|
107
|
-
<div class="tab-container">
|
|
108
|
-
<button class="tab-btn active" onclick="switchTab(this, 'unresolved')">${escapeHtml(p.insightsUnresolved)} (${unresolved.length})</button>
|
|
109
|
-
<button class="tab-btn" onclick="switchTab(this, 'resolved')">${escapeHtml(p.insightsResolved)} (${resolved.length})</button>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="tab-content unresolved active">
|
|
112
|
-
${unresolved.length ? unresolved.map(renderStruggleItem).join("") : `<p class="empty-state">No unresolved struggles</p>`}
|
|
113
|
-
</div>
|
|
114
|
-
<div class="tab-content resolved">
|
|
115
|
-
${resolved.length ? resolved.map(renderStruggleItem).join("") : `<p class="empty-state">No resolved struggles</p>`}
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</section>`;
|
|
175
|
+
return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
|
|
176
|
+
return struggles
|
|
177
|
+
.slice(0, 12)
|
|
178
|
+
.map((item) => {
|
|
179
|
+
const attempts = item.attempts.length
|
|
180
|
+
? `<div class="sub-line"><strong>${escapeHtml(ui.attempts)}:</strong> ${item.attempts.map((a) => escapeHtml(a)).join(" | ")}</div>`
|
|
181
|
+
: "";
|
|
182
|
+
const solution = item.solution
|
|
183
|
+
? `<div class="sub-line"><strong>${escapeHtml(ui.solution)}:</strong> ${escapeHtml(item.solution)}</div>`
|
|
184
|
+
: "";
|
|
185
|
+
return `<article class="item-card ${item.resolved ? "ok" : "warn"}">
|
|
186
|
+
<div class="item-title">${escapeHtml(item.problem)}</div>
|
|
187
|
+
${attempts}
|
|
188
|
+
${solution}
|
|
189
|
+
<div class="meta-row">
|
|
190
|
+
<span>${escapeHtml(ui.status)}: ${item.resolved ? escapeHtml(ui.resolved) : escapeHtml(ui.unresolved)}</span>
|
|
191
|
+
<span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
|
|
192
|
+
</div>
|
|
193
|
+
</article>`;
|
|
194
|
+
})
|
|
195
|
+
.join("");
|
|
119
196
|
}
|
|
120
|
-
function
|
|
121
|
-
if (!
|
|
122
|
-
return ""
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<span class="toggle-icon">▼</span>
|
|
127
|
-
${escapeHtml(p.insightsSectionLessons)} (${lessons.length})
|
|
128
|
-
</h2>
|
|
129
|
-
<div class="section-content">
|
|
130
|
-
<ul class="lessons-list">
|
|
131
|
-
${lessons
|
|
132
|
-
.map((l) => `
|
|
133
|
-
<li>
|
|
134
|
-
<span class="lesson-content">${escapeHtml(l.content)}</span>
|
|
135
|
-
<span class="importance-stars">${"★".repeat(Math.min(Math.ceil(l.importance / 2), 5))}</span>
|
|
136
|
-
</li>`)
|
|
137
|
-
.join("")}
|
|
138
|
-
</ul>
|
|
139
|
-
</div>
|
|
140
|
-
</section>`;
|
|
197
|
+
function renderMemoryList(entries, empty) {
|
|
198
|
+
if (!entries.length)
|
|
199
|
+
return `<p class="empty">${escapeHtml(empty)}</p>`;
|
|
200
|
+
return `<ul class="list">${entries
|
|
201
|
+
.map((entry) => `<li>${escapeHtml(entry.content)}${entry.tags.length ? ` <span class="tags">${entry.tags.slice(0, 4).map((tag) => `#${escapeHtml(tag)}`).join(" ")}</span>` : ""}</li>`)
|
|
202
|
+
.join("")}</ul>`;
|
|
141
203
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
function renderAtAGlance(report, projects, ui) {
|
|
205
|
+
const unresolved = report.struggles.filter((item) => !item.resolved);
|
|
206
|
+
const resolved = report.struggles.filter((item) => item.resolved);
|
|
207
|
+
const topPattern = report.patterns[0];
|
|
208
|
+
const topRec = report.recommendations[0];
|
|
209
|
+
const topProject = projects[0];
|
|
210
|
+
const working = resolved.length > 0
|
|
211
|
+
? `${resolved.length} struggles are already resolved; keep reusing those fixes.`
|
|
212
|
+
: report.topLessons.length
|
|
213
|
+
? `${report.topLessons.length} lessons captured; convert the top ones into repeatable checklists.`
|
|
214
|
+
: ui.noneYet;
|
|
215
|
+
const hindering = unresolved.length
|
|
216
|
+
? `${unresolved.length} open struggles remain. Most frequent: "${unresolved[0]?.problem ?? ""}".`
|
|
217
|
+
: "No unresolved struggles currently visible.";
|
|
218
|
+
const quickWins = topRec ? topRec : ui.noneYet;
|
|
219
|
+
const focus = topProject
|
|
220
|
+
? `${topProject.label} appears most often (${topProject.value} memories).`
|
|
221
|
+
: topPattern
|
|
222
|
+
? `Dominant behavior: when "${topPattern.trigger}", you often "${topPattern.behavior}".`
|
|
223
|
+
: ui.noneYet;
|
|
224
|
+
return `<section class="glance">
|
|
225
|
+
<h2>${escapeHtml(ui.atAGlance)}</h2>
|
|
226
|
+
<div class="glance-grid">
|
|
227
|
+
<article class="glance-card">
|
|
228
|
+
<h3>${escapeHtml(ui.whatsWorking)}</h3>
|
|
229
|
+
<p>${escapeHtml(working)}</p>
|
|
230
|
+
</article>
|
|
231
|
+
<article class="glance-card warn">
|
|
232
|
+
<h3>${escapeHtml(ui.whatsHindering)}</h3>
|
|
233
|
+
<p>${escapeHtml(hindering)}</p>
|
|
234
|
+
</article>
|
|
235
|
+
<article class="glance-card">
|
|
236
|
+
<h3>${escapeHtml(ui.quickWins)}</h3>
|
|
237
|
+
<p>${escapeHtml(quickWins)}</p>
|
|
238
|
+
</article>
|
|
239
|
+
<article class="glance-card">
|
|
240
|
+
<h3>${escapeHtml(ui.focusArea)}</h3>
|
|
241
|
+
<p>${escapeHtml(focus)}</p>
|
|
242
|
+
</article>
|
|
243
|
+
</div>
|
|
244
|
+
</section>`;
|
|
167
245
|
}
|
|
168
|
-
function
|
|
169
|
-
if (!
|
|
170
|
-
return ""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<span class="toggle-icon">▼</span>
|
|
176
|
-
${title} (${preferences.length})
|
|
177
|
-
</h2>
|
|
178
|
-
<div class="section-content">
|
|
179
|
-
<ul class="preferences-list">
|
|
180
|
-
${preferences.map((pr) => `<li>${escapeHtml(pr.content)}</li>`).join("")}
|
|
181
|
-
</ul>
|
|
182
|
-
</div>
|
|
183
|
-
</section>`;
|
|
246
|
+
function renderRecommendations(report, title, empty) {
|
|
247
|
+
if (!report.recommendations.length)
|
|
248
|
+
return `<section class="section"><h2>${escapeHtml(title)}</h2><p class="empty">${escapeHtml(empty)}</p></section>`;
|
|
249
|
+
return `<section id="section-recommendations" class="section">
|
|
250
|
+
<h2>${escapeHtml(title)}</h2>
|
|
251
|
+
<ul class="recommend-list">${report.recommendations.map((rec) => `<li>${escapeHtml(rec)}</li>`).join("")}</ul>
|
|
252
|
+
</section>`;
|
|
184
253
|
}
|
|
185
254
|
export function renderInsightsHtml(report, locale) {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
background: var(--bg);
|
|
361
|
-
border-radius: 0.5rem;
|
|
362
|
-
display: flex;
|
|
363
|
-
justify-content: space-between;
|
|
364
|
-
align-items: center;
|
|
365
|
-
}
|
|
366
|
-
.importance-stars { color: var(--accent-lesson); }
|
|
367
|
-
.knowledge-list, .preferences-list { list-style: none; }
|
|
368
|
-
.knowledge-list li, .preferences-list li {
|
|
369
|
-
padding: 0.75rem;
|
|
370
|
-
margin-bottom: 0.5rem;
|
|
371
|
-
background: var(--bg);
|
|
372
|
-
border-radius: 0.5rem;
|
|
373
|
-
}
|
|
374
|
-
.tags { display: flex; gap: 0.25rem; margin-top: 0.5rem; flex-wrap: wrap; }
|
|
375
|
-
.tag {
|
|
376
|
-
font-size: 0.7rem;
|
|
377
|
-
padding: 0.125rem 0.375rem;
|
|
378
|
-
background: var(--border);
|
|
379
|
-
border-radius: 0.25rem;
|
|
380
|
-
color: var(--text-secondary);
|
|
381
|
-
}
|
|
382
|
-
.empty-state {
|
|
383
|
-
text-align: center;
|
|
384
|
-
padding: 2rem;
|
|
385
|
-
color: var(--text-secondary);
|
|
386
|
-
}
|
|
387
|
-
`;
|
|
388
|
-
const js = `
|
|
389
|
-
function toggleSection(el) {
|
|
390
|
-
el.parentElement.classList.toggle('collapsed');
|
|
391
|
-
}
|
|
392
|
-
function switchTab(btn, tabName) {
|
|
393
|
-
const container = btn.closest('.section-content');
|
|
394
|
-
container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
395
|
-
container.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
396
|
-
btn.classList.add('active');
|
|
397
|
-
container.querySelector('.tab-content.' + tabName).classList.add('active');
|
|
398
|
-
}
|
|
399
|
-
function toggleTheme() {
|
|
400
|
-
const current = document.documentElement.getAttribute('data-theme');
|
|
401
|
-
const next = current === 'dark' ? 'light' : 'dark';
|
|
402
|
-
document.documentElement.setAttribute('data-theme', next);
|
|
403
|
-
localStorage.setItem('nanomem-theme', next);
|
|
404
|
-
document.querySelector('.theme-toggle').textContent = next === 'dark' ? '☀️' : '🌙';
|
|
405
|
-
}
|
|
406
|
-
(function() {
|
|
407
|
-
const saved = localStorage.getItem('nanomem-theme') || 'light';
|
|
408
|
-
document.documentElement.setAttribute('data-theme', saved);
|
|
409
|
-
document.querySelector('.theme-toggle').textContent = saved === 'dark' ? '☀️' : '🌙';
|
|
410
|
-
})();
|
|
411
|
-
`;
|
|
412
|
-
const body = hasData
|
|
413
|
-
? `
|
|
414
|
-
<div class="stats-row">${renderStatsCards(report, locale)}</div>
|
|
415
|
-
${renderRecommendations(report.recommendations, p)}
|
|
416
|
-
${renderPatterns(report.patterns, p)}
|
|
417
|
-
${renderStruggles(report.struggles, p)}
|
|
418
|
-
${renderLessons(report.topLessons, p)}
|
|
419
|
-
${renderKnowledge(report.topKnowledge, locale)}
|
|
420
|
-
${renderPreferences(report.preferences, locale)}`
|
|
421
|
-
: `<div class="empty-state">${escapeHtml(p.insightsNoData)}</div>`;
|
|
422
|
-
return `<!DOCTYPE html>
|
|
423
|
-
<html lang="${locale}">
|
|
424
|
-
<head>
|
|
425
|
-
<meta charset="UTF-8">
|
|
426
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
427
|
-
<title>${escapeHtml(p.insightsTitle)}</title>
|
|
428
|
-
<style>${css}</style>
|
|
429
|
-
</head>
|
|
430
|
-
<body>
|
|
431
|
-
<header>
|
|
432
|
-
<div>
|
|
433
|
-
<h1>${escapeHtml(p.insightsTitle)}</h1>
|
|
434
|
-
<div class="meta">${escapeHtml(p.insightsGeneratedAt)}: ${report.generatedAt}</div>
|
|
435
|
-
</div>
|
|
436
|
-
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
|
|
437
|
-
</header>
|
|
438
|
-
${body}
|
|
439
|
-
<script>${js}</script>
|
|
440
|
-
</body>
|
|
255
|
+
const isZh = locale === "zh";
|
|
256
|
+
const ui = UI[isZh ? "zh" : "en"];
|
|
257
|
+
const prompts = PROMPTS[locale] ?? PROMPTS.en;
|
|
258
|
+
const allEntries = dedupeById([...report.topKnowledge, ...report.topLessons, ...report.preferences]);
|
|
259
|
+
const projects = topProjects(allEntries);
|
|
260
|
+
const tags = topTags(allEntries);
|
|
261
|
+
const unresolved = report.struggles.filter((item) => !item.resolved);
|
|
262
|
+
const resolved = report.struggles.filter((item) => item.resolved);
|
|
263
|
+
const statsRow = [
|
|
264
|
+
{ label: "Sessions", value: report.stats.totalSessions },
|
|
265
|
+
{ label: "Knowledge", value: report.stats.knowledge },
|
|
266
|
+
{ label: "Lessons", value: report.stats.lessons },
|
|
267
|
+
{ label: "Preferences", value: report.stats.preferences },
|
|
268
|
+
{ label: "Struggles", value: report.struggles.length },
|
|
269
|
+
{ label: "Patterns", value: report.patterns.length },
|
|
270
|
+
];
|
|
271
|
+
const css = `
|
|
272
|
+
*{box-sizing:border-box}
|
|
273
|
+
body{margin:0;padding:48px 24px;font-family:Inter,"Segoe UI",Arial,sans-serif;background:#f8fafc;color:#334155;line-height:1.65}
|
|
274
|
+
.container{max-width:960px;margin:0 auto}
|
|
275
|
+
h1{font-size:34px;font-weight:700;color:#0f172a;margin:0 0 8px}
|
|
276
|
+
h2{font-size:22px;font-weight:700;color:#0f172a;margin:0 0 14px}
|
|
277
|
+
h3{font-size:15px;font-weight:700;color:#1e293b;margin:0 0 8px}
|
|
278
|
+
.subtitle{color:#64748b;font-size:14px;margin:0 0 24px}
|
|
279
|
+
.toc{display:flex;flex-wrap:wrap;gap:8px;padding:14px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;margin:0 0 24px}
|
|
280
|
+
.toc a{font-size:12px;color:#475569;background:#f1f5f9;border-radius:6px;padding:6px 10px;text-decoration:none}
|
|
281
|
+
.toc a:hover{background:#e2e8f0}
|
|
282
|
+
.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}
|
|
283
|
+
.stat{text-align:center}
|
|
284
|
+
.stat-value{font-size:24px;font-weight:700;color:#0f172a}
|
|
285
|
+
.stat-label{font-size:11px;color:#64748b;text-transform:uppercase}
|
|
286
|
+
.glance{background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border:1px solid #f59e0b;border-radius:12px;padding:20px 22px;margin:0 0 28px}
|
|
287
|
+
.glance-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
|
|
288
|
+
.glance-card{background:rgba(255,255,255,.62);border:1px solid rgba(245,158,11,.28);border-radius:8px;padding:12px}
|
|
289
|
+
.glance-card.warn{background:rgba(255,241,242,.82);border-color:#fca5a5}
|
|
290
|
+
.glance-card p{margin:0;font-size:13px;color:#78350f}
|
|
291
|
+
.section{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:18px;margin:0 0 16px}
|
|
292
|
+
.section-intro{font-size:13px;color:#64748b;margin:0 0 12px}
|
|
293
|
+
.project-list{display:flex;flex-direction:column;gap:10px}
|
|
294
|
+
.project-area{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
|
|
295
|
+
.area-header{display:flex;justify-content:space-between;align-items:center;gap:10px}
|
|
296
|
+
.area-name{font-size:15px;font-weight:700;color:#0f172a}
|
|
297
|
+
.area-count{font-size:12px;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:4px}
|
|
298
|
+
.area-desc{margin:8px 0 0;font-size:13px;color:#475569}
|
|
299
|
+
.charts{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
300
|
+
.chart{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
|
|
301
|
+
.chart-title{font-size:12px;color:#64748b;text-transform:uppercase;margin:0 0 10px}
|
|
302
|
+
.bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}
|
|
303
|
+
.bar-label{width:120px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:#475569}
|
|
304
|
+
.bar-track{flex:1;height:7px;background:#f1f5f9;border-radius:99px}
|
|
305
|
+
.bar-fill{height:100%;border-radius:99px}
|
|
306
|
+
.bar-fill.blue{background:#2563eb}
|
|
307
|
+
.bar-fill.cyan{background:#0891b2}
|
|
308
|
+
.bar-value{width:24px;text-align:right;font-size:12px;color:#64748b}
|
|
309
|
+
.recommend-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
|
|
310
|
+
.recommend-list li{background:#f0fdf4;border:1px solid #bbf7d0;border-left:4px solid #16a34a;border-radius:8px;padding:11px 12px;font-size:14px}
|
|
311
|
+
.item-grid{display:flex;flex-direction:column;gap:10px}
|
|
312
|
+
.item-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
|
|
313
|
+
.item-card.ok{background:#f0fdf4;border-color:#86efac}
|
|
314
|
+
.item-card.warn{background:#fef2f2;border-color:#fca5a5}
|
|
315
|
+
.item-title{font-size:14px;color:#0f172a}
|
|
316
|
+
.arrow{color:#94a3b8}
|
|
317
|
+
.weight-track{margin-top:8px;height:6px;background:#e2e8f0;border-radius:99px}
|
|
318
|
+
.weight-fill{height:100%;background:#2563eb;border-radius:99px}
|
|
319
|
+
.sub-line{margin-top:8px;font-size:13px;color:#334155}
|
|
320
|
+
.meta-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:#64748b}
|
|
321
|
+
details{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}
|
|
322
|
+
summary{cursor:pointer;font-size:14px;font-weight:700;color:#334155}
|
|
323
|
+
.list{margin:10px 0 0;padding-left:18px}
|
|
324
|
+
.list li{margin:0 0 8px;font-size:13px;color:#334155}
|
|
325
|
+
.tags{font-size:12px;color:#64748b}
|
|
326
|
+
.empty{font-size:13px;color:#94a3b8}
|
|
327
|
+
footer{margin-top:22px;text-align:center;font-size:12px;color:#94a3b8}
|
|
328
|
+
@media (max-width:760px){body{padding:28px 14px}.charts{grid-template-columns:1fr}.bar-label{width:96px}}
|
|
329
|
+
`;
|
|
330
|
+
return `<!doctype html>
|
|
331
|
+
<html lang="${isZh ? "zh-CN" : "en"}">
|
|
332
|
+
<head>
|
|
333
|
+
<meta charset="utf-8" />
|
|
334
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
335
|
+
<title>${escapeHtml(prompts.insightsTitle)}</title>
|
|
336
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
337
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
338
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
339
|
+
<style>${css}</style>
|
|
340
|
+
</head>
|
|
341
|
+
<body>
|
|
342
|
+
<main class="container">
|
|
343
|
+
<h1>${escapeHtml(prompts.insightsTitle)}</h1>
|
|
344
|
+
<p class="subtitle">${report.stats.totalSessions} ${escapeHtml(ui.subtitle)} | ${escapeHtml(prompts.insightsGeneratedAt)}: ${escapeHtml(formatDate(report.generatedAt, locale))}</p>
|
|
345
|
+
|
|
346
|
+
<nav class="toc">
|
|
347
|
+
<a href="#section-work">${escapeHtml(ui.workOn)}</a>
|
|
348
|
+
<a href="#section-distribution">${escapeHtml(ui.distribution)}</a>
|
|
349
|
+
<a href="#section-recommendations">${escapeHtml(prompts.insightsSectionRecommendations)}</a>
|
|
350
|
+
<a href="#section-patterns">${escapeHtml(prompts.insightsSectionPatterns)}</a>
|
|
351
|
+
<a href="#section-struggles">${escapeHtml(prompts.insightsSectionStruggles)}</a>
|
|
352
|
+
<a href="#section-memory">${escapeHtml(ui.knowledgePrefs)}</a>
|
|
353
|
+
</nav>
|
|
354
|
+
|
|
355
|
+
<section class="stats">
|
|
356
|
+
${statsRow
|
|
357
|
+
.map((item) => `<div class="stat"><div class="stat-value">${item.value}</div><div class="stat-label">${escapeHtml(item.label)}</div></div>`)
|
|
358
|
+
.join("")}
|
|
359
|
+
</section>
|
|
360
|
+
|
|
361
|
+
${renderAtAGlance(report, projects, ui)}
|
|
362
|
+
|
|
363
|
+
<section id="section-work" class="section">
|
|
364
|
+
<h2>${escapeHtml(ui.workOn)}</h2>
|
|
365
|
+
<p class="section-intro">${escapeHtml(ui.workOnIntro)}</p>
|
|
366
|
+
<div class="project-list">${renderProjectAreas(projects, allEntries, ui)}</div>
|
|
367
|
+
</section>
|
|
368
|
+
|
|
369
|
+
<section id="section-distribution" class="section">
|
|
370
|
+
<h2>${escapeHtml(ui.distribution)}</h2>
|
|
371
|
+
<div class="charts">
|
|
372
|
+
<div class="chart">
|
|
373
|
+
<p class="chart-title">${escapeHtml(ui.topProjects)}</p>
|
|
374
|
+
${renderBarRows(projects, "blue", ui.noneYet)}
|
|
375
|
+
</div>
|
|
376
|
+
<div class="chart">
|
|
377
|
+
<p class="chart-title">${escapeHtml(ui.frequentTags)}</p>
|
|
378
|
+
${renderBarRows(tags, "cyan", ui.noneYet)}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</section>
|
|
382
|
+
|
|
383
|
+
${renderRecommendations(report, prompts.insightsSectionRecommendations, prompts.insightsNoData)}
|
|
384
|
+
|
|
385
|
+
<section class="section">
|
|
386
|
+
<h2>${escapeHtml(ui.successes)}</h2>
|
|
387
|
+
<p class="section-intro">${escapeHtml(ui.successesIntro)}</p>
|
|
388
|
+
<div class="item-grid">
|
|
389
|
+
${renderStruggleRows(resolved, ui)}
|
|
390
|
+
${renderMemoryList(report.topLessons.slice(0, 5), ui.noneYet)}
|
|
391
|
+
</div>
|
|
392
|
+
</section>
|
|
393
|
+
|
|
394
|
+
<section class="section">
|
|
395
|
+
<h2>${escapeHtml(ui.frictions)}</h2>
|
|
396
|
+
<p class="section-intro">${escapeHtml(ui.frictionsIntro)}</p>
|
|
397
|
+
<div class="item-grid">${renderStruggleRows(unresolved, ui)}</div>
|
|
398
|
+
</section>
|
|
399
|
+
|
|
400
|
+
<section id="section-patterns" class="section">
|
|
401
|
+
<h2>${escapeHtml(prompts.insightsSectionPatterns)}</h2>
|
|
402
|
+
<div class="item-grid">${renderPatternRows(report.patterns, ui)}</div>
|
|
403
|
+
</section>
|
|
404
|
+
|
|
405
|
+
<section id="section-struggles" class="section">
|
|
406
|
+
<h2>${escapeHtml(prompts.insightsSectionStruggles)}</h2>
|
|
407
|
+
<div class="item-grid">${renderStruggleRows(report.struggles, ui)}</div>
|
|
408
|
+
</section>
|
|
409
|
+
|
|
410
|
+
<section id="section-memory" class="section">
|
|
411
|
+
<h2>${escapeHtml(ui.knowledgePrefs)}</h2>
|
|
412
|
+
<details open>
|
|
413
|
+
<summary>${escapeHtml(ui.lessons)} (${report.topLessons.length})</summary>
|
|
414
|
+
${renderMemoryList(report.topLessons.slice(0, 12), ui.noneYet)}
|
|
415
|
+
</details>
|
|
416
|
+
<details>
|
|
417
|
+
<summary>${escapeHtml(ui.knowledge)} (${report.topKnowledge.length})</summary>
|
|
418
|
+
${renderMemoryList(report.topKnowledge.slice(0, 12), ui.noneYet)}
|
|
419
|
+
</details>
|
|
420
|
+
<details>
|
|
421
|
+
<summary>${escapeHtml(ui.preferences)} (${report.preferences.length})</summary>
|
|
422
|
+
${renderMemoryList(report.preferences.slice(0, 12), ui.noneYet)}
|
|
423
|
+
</details>
|
|
424
|
+
</section>
|
|
425
|
+
|
|
426
|
+
<footer>${escapeHtml(ui.generatedBy)}</footer>
|
|
427
|
+
</main>
|
|
428
|
+
</body>
|
|
441
429
|
</html>`;
|
|
442
430
|
}
|
|
443
431
|
//# sourceMappingURL=insights-html.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|