@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 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' ? 'You' : 'FREYA';
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",