@cccarv82/freya 1.0.43 → 1.0.44
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/cli/web-ui.js +99 -2
- package/cli/web.js +96 -0
- package/package.json +1 -1
package/cli/web-ui.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
lastApplied: null,
|
|
14
14
|
autoApply: true,
|
|
15
15
|
autoRunReports: false,
|
|
16
|
-
prettyPublish: true
|
|
16
|
+
prettyPublish: true,
|
|
17
|
+
chatSessionId: null,
|
|
18
|
+
chatLoaded: false
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
function applyTheme(theme) {
|
|
@@ -114,6 +116,41 @@
|
|
|
114
116
|
return html;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function ensureChatSession() {
|
|
120
|
+
if (state.chatSessionId) return state.chatSessionId;
|
|
121
|
+
try {
|
|
122
|
+
const fromLocal = localStorage.getItem('freya.chatSessionId');
|
|
123
|
+
if (fromLocal) {
|
|
124
|
+
state.chatSessionId = fromLocal;
|
|
125
|
+
return state.chatSessionId;
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
const id = (typeof crypto !== 'undefined' && crypto.randomUUID)
|
|
130
|
+
? crypto.randomUUID()
|
|
131
|
+
: ('sess-' + Date.now() + '-' + Math.random().toString(16).slice(2, 8));
|
|
132
|
+
|
|
133
|
+
state.chatSessionId = id;
|
|
134
|
+
try { localStorage.setItem('freya.chatSessionId', id); } catch {}
|
|
135
|
+
return id;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function persistChatItem(item) {
|
|
139
|
+
try {
|
|
140
|
+
const sessionId = ensureChatSession();
|
|
141
|
+
await api('/api/chat/append', {
|
|
142
|
+
dir: dirOrDefault(),
|
|
143
|
+
sessionId,
|
|
144
|
+
role: item.role,
|
|
145
|
+
text: item.text,
|
|
146
|
+
markdown: !!item.markdown,
|
|
147
|
+
ts: item.ts
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
// best-effort (chat still works)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
117
154
|
function chatAppend(role, text, opts = {}) {
|
|
118
155
|
const thread = $('chatThread');
|
|
119
156
|
if (!thread) return;
|
|
@@ -123,7 +160,7 @@
|
|
|
123
160
|
|
|
124
161
|
const meta = document.createElement('div');
|
|
125
162
|
meta.className = 'bubbleMeta';
|
|
126
|
-
meta.textContent = role === 'user' ? '
|
|
163
|
+
meta.textContent = role === 'user' ? 'Você' : 'FREYA';
|
|
127
164
|
|
|
128
165
|
const body = document.createElement('div');
|
|
129
166
|
body.className = 'bubbleBody';
|
|
@@ -139,12 +176,70 @@
|
|
|
139
176
|
bubble.appendChild(body);
|
|
140
177
|
thread.appendChild(bubble);
|
|
141
178
|
|
|
179
|
+
// persist
|
|
180
|
+
persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
|
|
181
|
+
|
|
142
182
|
// keep newest in view
|
|
143
183
|
try {
|
|
144
184
|
thread.scrollTop = thread.scrollHeight;
|
|
145
185
|
} catch {}
|
|
146
186
|
}
|
|
147
187
|
|
|
188
|
+
async function loadChatHistory() {
|
|
189
|
+
if (state.chatLoaded) return;
|
|
190
|
+
state.chatLoaded = true;
|
|
191
|
+
const thread = $('chatThread');
|
|
192
|
+
if (!thread) return;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const sessionId = ensureChatSession();
|
|
196
|
+
const r = await api('/api/chat/load', { dir: dirOrDefault(), sessionId });
|
|
197
|
+
const items = (r && Array.isArray(r.items)) ? r.items : [];
|
|
198
|
+
if (items.length) {
|
|
199
|
+
thread.innerHTML = '';
|
|
200
|
+
for (const it of items) {
|
|
201
|
+
const role = it.role === 'user' ? 'user' : 'assistant';
|
|
202
|
+
const text = String(it.text || '');
|
|
203
|
+
const markdown = !!it.markdown;
|
|
204
|
+
// render without re-persisting
|
|
205
|
+
const bubble = document.createElement('div');
|
|
206
|
+
bubble.className = 'bubble ' + (role === 'user' ? 'user' : 'assistant');
|
|
207
|
+
const meta = document.createElement('div');
|
|
208
|
+
meta.className = 'bubbleMeta';
|
|
209
|
+
meta.textContent = role === 'user' ? 'Você' : 'FREYA';
|
|
210
|
+
const body = document.createElement('div');
|
|
211
|
+
body.className = 'bubbleBody';
|
|
212
|
+
if (markdown) body.innerHTML = renderMarkdown(text);
|
|
213
|
+
else body.innerHTML = escapeHtml(text).replace(/\n/g, '<br>');
|
|
214
|
+
bubble.appendChild(meta);
|
|
215
|
+
bubble.appendChild(body);
|
|
216
|
+
thread.appendChild(bubble);
|
|
217
|
+
}
|
|
218
|
+
try { thread.scrollTop = thread.scrollHeight; } catch {}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// ignore
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function exportChatObsidian() {
|
|
226
|
+
try {
|
|
227
|
+
const sessionId = ensureChatSession();
|
|
228
|
+
setPill('run', 'exportando…');
|
|
229
|
+
const r = await api('/api/chat/export-obsidian', { dir: dirOrDefault(), sessionId });
|
|
230
|
+
const rel = r && r.relPath ? r.relPath : '';
|
|
231
|
+
if (rel) {
|
|
232
|
+
chatAppend('assistant', `Conversa exportada para: **${rel}**`, { markdown: true });
|
|
233
|
+
setPill('ok', 'exportado');
|
|
234
|
+
} else {
|
|
235
|
+
setPill('ok', 'exportado');
|
|
236
|
+
}
|
|
237
|
+
setTimeout(() => setPill('ok', 'pronto'), 800);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
setPill('err', 'export falhou');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
148
243
|
function setOut(text) {
|
|
149
244
|
state.lastText = text || '';
|
|
150
245
|
const el = $('reportPreview');
|
|
@@ -906,6 +1001,7 @@
|
|
|
906
1001
|
refreshReports();
|
|
907
1002
|
refreshToday();
|
|
908
1003
|
reloadSlugRules();
|
|
1004
|
+
loadChatHistory();
|
|
909
1005
|
})();
|
|
910
1006
|
|
|
911
1007
|
setPill('ok', 'pronto');
|
|
@@ -938,4 +1034,5 @@
|
|
|
938
1034
|
window.togglePrettyPublish = togglePrettyPublish;
|
|
939
1035
|
window.applyPlan = applyPlan;
|
|
940
1036
|
window.runSuggestedReports = runSuggestedReports;
|
|
1037
|
+
window.exportChatObsidian = exportChatObsidian;
|
|
941
1038
|
})();
|
package/cli/web.js
CHANGED
|
@@ -870,6 +870,7 @@ function buildHtml(safeDefault) {
|
|
|
870
870
|
<div class="composerActions">
|
|
871
871
|
<button class="btn primary" type="button" onclick="saveAndPlan()">Salvar + Processar (Agents)</button>
|
|
872
872
|
<button class="btn" type="button" onclick="runSuggestedReports()">Rodar relatórios sugeridos</button>
|
|
873
|
+
<button class="btn" type="button" onclick="exportChatObsidian()">Exportar conversa (Obsidian)</button>
|
|
873
874
|
</div>
|
|
874
875
|
|
|
875
876
|
<div class="composerToggles">
|
|
@@ -1605,6 +1606,101 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1605
1606
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'export failed', output: out });
|
|
1606
1607
|
}
|
|
1607
1608
|
|
|
1609
|
+
// Chat persistence (per session)
|
|
1610
|
+
if (req.url === '/api/chat/append') {
|
|
1611
|
+
const sessionId = String(payload.sessionId || '').trim();
|
|
1612
|
+
const role = String(payload.role || '').trim();
|
|
1613
|
+
const text = String(payload.text || '').trimEnd();
|
|
1614
|
+
const markdown = !!payload.markdown;
|
|
1615
|
+
const ts = typeof payload.ts === 'number' ? payload.ts : Date.now();
|
|
1616
|
+
if (!sessionId) return safeJson(res, 400, { error: 'Missing sessionId' });
|
|
1617
|
+
if (!role) return safeJson(res, 400, { error: 'Missing role' });
|
|
1618
|
+
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
1619
|
+
|
|
1620
|
+
const d = isoDate();
|
|
1621
|
+
const base = path.join(workspaceDir, 'data', 'chat', d);
|
|
1622
|
+
ensureDir(base);
|
|
1623
|
+
const file = path.join(base, `${sessionId}.jsonl`);
|
|
1624
|
+
const item = { ts, role, markdown, text };
|
|
1625
|
+
fs.appendFileSync(file, JSON.stringify(item) + '\n', 'utf8');
|
|
1626
|
+
return safeJson(res, 200, { ok: true });
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (req.url === '/api/chat/load') {
|
|
1630
|
+
const sessionId = String(payload.sessionId || '').trim();
|
|
1631
|
+
const d = String(payload.date || '').trim() || isoDate();
|
|
1632
|
+
if (!sessionId) return safeJson(res, 400, { error: 'Missing sessionId' });
|
|
1633
|
+
const file = path.join(workspaceDir, 'data', 'chat', d, `${sessionId}.jsonl`);
|
|
1634
|
+
if (!exists(file)) return safeJson(res, 200, { ok: true, items: [] });
|
|
1635
|
+
|
|
1636
|
+
const rawText = fs.readFileSync(file, 'utf8');
|
|
1637
|
+
const lines = rawText.split(/\r?\n/).filter(Boolean);
|
|
1638
|
+
const items = [];
|
|
1639
|
+
for (const line of lines) {
|
|
1640
|
+
try {
|
|
1641
|
+
const obj = JSON.parse(line);
|
|
1642
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
1643
|
+
items.push(obj);
|
|
1644
|
+
} catch {
|
|
1645
|
+
// ignore corrupt line
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return safeJson(res, 200, { ok: true, items });
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (req.url === '/api/chat/export-obsidian') {
|
|
1652
|
+
const sessionId = String(payload.sessionId || '').trim();
|
|
1653
|
+
const d = String(payload.date || '').trim() || isoDate();
|
|
1654
|
+
if (!sessionId) return safeJson(res, 400, { error: 'Missing sessionId' });
|
|
1655
|
+
const file = path.join(workspaceDir, 'data', 'chat', d, `${sessionId}.jsonl`);
|
|
1656
|
+
if (!exists(file)) return safeJson(res, 404, { error: 'Chat not found' });
|
|
1657
|
+
|
|
1658
|
+
const rawText = fs.readFileSync(file, 'utf8');
|
|
1659
|
+
const lines = rawText.split(/\r?\n/).filter(Boolean);
|
|
1660
|
+
const items = [];
|
|
1661
|
+
for (const line of lines) {
|
|
1662
|
+
try {
|
|
1663
|
+
const obj = JSON.parse(line);
|
|
1664
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
1665
|
+
items.push(obj);
|
|
1666
|
+
} catch {}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const outDir = path.join(workspaceDir, 'docs', 'chat');
|
|
1670
|
+
ensureDir(outDir);
|
|
1671
|
+
const outName = `conversa-${d}-${sessionId}.md`;
|
|
1672
|
+
const outPath = path.join(outDir, outName);
|
|
1673
|
+
|
|
1674
|
+
const md = [];
|
|
1675
|
+
md.push('---');
|
|
1676
|
+
md.push(`type: chat`);
|
|
1677
|
+
md.push(`date: ${d}`);
|
|
1678
|
+
md.push(`session: ${sessionId}`);
|
|
1679
|
+
md.push('---');
|
|
1680
|
+
md.push('');
|
|
1681
|
+
md.push(`# Conversa - ${d}`);
|
|
1682
|
+
md.push('');
|
|
1683
|
+
|
|
1684
|
+
for (const it of items) {
|
|
1685
|
+
const when = (() => {
|
|
1686
|
+
try {
|
|
1687
|
+
const dt = new Date(Number(it.ts || 0));
|
|
1688
|
+
const hh = String(dt.getHours()).padStart(2, '0');
|
|
1689
|
+
const mm = String(dt.getMinutes()).padStart(2, '0');
|
|
1690
|
+
return `${hh}:${mm}`;
|
|
1691
|
+
} catch { return ''; }
|
|
1692
|
+
})();
|
|
1693
|
+
const who = it.role === 'user' ? 'Você' : 'FREYA';
|
|
1694
|
+
md.push(`## [${when}] ${who}`);
|
|
1695
|
+
md.push('');
|
|
1696
|
+
md.push(String(it.text || '').trimEnd());
|
|
1697
|
+
md.push('');
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
fs.writeFileSync(outPath, md.join('\n') + '\n', 'utf8');
|
|
1701
|
+
return safeJson(res, 200, { ok: true, relPath: path.relative(workspaceDir, outPath).replace(/\\/g, '/') });
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1608
1704
|
if (req.url === '/api/tasks/list') {
|
|
1609
1705
|
const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
|
|
1610
1706
|
const cat = payload.category ? String(payload.category).trim() : null;
|