halbot 1995.1.42 → 1995.1.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.
@@ -0,0 +1,28 @@
1
+
2
+ import { dbio, hal, storage as _storage } from './index.mjs';
3
+
4
+ const run = async () => {
5
+ const { config } = await _storage.getConfig();
6
+ await dbio.init(config.storage);
7
+ const token = 'TOKEN|8df8db54-3ef1-4d63-b91d-da05ae6eccff-d46ae000661b3509e743d37d55a7e30be8158c008ad93cad88d7af07503e2061c53bce89f042762495d168af02f';
8
+ const result = await dbio.queryOne(
9
+ `SELECT * FROM ${hal.table} WHERE token = $1`,
10
+ [token]
11
+ );
12
+
13
+ if (result) {
14
+ console.log('Result found.');
15
+ const response = JSON.parse(result.response);
16
+ console.log('Response count:', response.length);
17
+
18
+ response.forEach((msg, i) => {
19
+ console.log(`\nMessage ${i}:`);
20
+ console.log(JSON.stringify(msg, null, 2));
21
+ });
22
+ } else {
23
+ console.log('No result found for token.');
24
+ }
25
+ process.exit(0);
26
+ };
27
+
28
+ run().catch(console.error);
package/lib/hal.mjs CHANGED
@@ -168,6 +168,7 @@ const init = async (options) => {
168
168
  controllerPath: options?.web?.controllerPath || getPath('../web'),
169
169
  cfTunnel: options?.web?.cfTunnel,
170
170
  });
171
+ const url = options?.web?.url || web?.listen;
171
172
  if (callosum.isPrimary) {
172
173
  const { ais } = await alan.initChat({ sessions: null });
173
174
  const cmds = options?.cmds || [];
@@ -178,6 +179,10 @@ const init = async (options) => {
178
179
  return x.model;
179
180
  }).map(x => x.supportedMimeTypes || []).flat().map(x => x.toLowerCase()));
180
181
  const _bot = await bot.init(options);
182
+ callosum.register(
183
+ 'getFileLink', async id => await _bot.telegram.getFileLink(id)
184
+ );
185
+ callosum.register('getUrl', async () => url);
181
186
  const pkg = await utilitas.which();
182
187
  _ = {
183
188
  args: { ...options?.args || {} },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "halbot",
3
3
  "description": "Just another AI powered Telegram bot, which is simple design, easy to use, extendable and fun.",
4
- "version": "1995.1.42",
4
+ "version": "1995.1.44",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/halbot",
7
7
  "type": "module",
@@ -42,13 +42,13 @@
42
42
  "mime": "^4.1.0",
43
43
  "mysql2": "^3.16.3",
44
44
  "office-text-extractor": "^4.0.0",
45
- "openai": "^6.17.0",
45
+ "openai": "^6.18.0",
46
46
  "pg": "^8.18.0",
47
47
  "pgvector": "^0.2.1",
48
48
  "telegraf": "^4.16.3",
49
49
  "tellegram": "^1.1.18",
50
50
  "tesseract.js": "^7.0.0",
51
- "webjam": "^1995.3.5",
51
+ "webjam": "^1995.3.6",
52
52
  "youtube-transcript": "^1.2.1"
53
53
  }
54
54
  }
@@ -105,9 +105,9 @@ const memorize = async (ctx) => {
105
105
  const received_text = ctx._.request || ctx._.text || '';
106
106
  const id = received.update_id;
107
107
  let response = {};
108
- ctx._.done.map(m => m?.text && (response[m.message_id] = m));
108
+ ctx._.done.map(m => response[m.message_id] = m);
109
109
  response = Object.values(response).sort((a, b) => a.message_id - b.message_id);
110
- const response_text = ctx?._.response || response.map(x => x.text).join('\n');
110
+ const response_text = ctx?._?.response || response.map(x => x?.text || '').filter(Boolean).join('\n');
111
111
  const collected = ctx._.collected.filter(x => String.isString(x.content));
112
112
  const distilled = compact(bot.lines([
113
113
  received_text, response_text, ...collected.map(x => x.content)
@@ -1,4 +1,4 @@
1
- import { alan } from '../index.mjs';
1
+ import { callosum, alan } from '../index.mjs';
2
2
  import { token } from 'webjam';
3
3
 
4
4
  const _name = 'Chat';
@@ -92,7 +92,7 @@ const action = async (ctx, next) => {
92
92
  // 11: How are image, audio, and video formats displayed? (File tokens must be protected and require server-side forwarding.)
93
93
  await ctx.edit(lastMsg.message_id,
94
94
  lastMsg.raw
95
- + `\n\n\-\-\-\n\n✨ [View in well-formatted page](https://hal.leaskh.com/turns/${ctx._.token}).`
95
+ + `\n\n\-\-\-\n\n✨ [View web version](${await callosum.call('getUrl')}/turns/${ctx._.token}).`
96
96
  );
97
97
  await next();
98
98
  };
package/web/turn.html CHANGED
@@ -6,6 +6,10 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>HAL9000 Chat</title>
8
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <link rel="stylesheet"
10
+ href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.6.1/github-markdown-light.min.css">
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
9
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
15
  <link
@@ -52,8 +56,7 @@
52
56
  background-color: transparent;
53
57
  color: var(--text-color);
54
58
  font-family: var(--font-body);
55
- height: 100%;
56
- overflow: hidden;
59
+ min-height: 100%;
57
60
  }
58
61
 
59
62
 
@@ -133,23 +136,11 @@
133
136
 
134
137
  /* Main Content */
135
138
  main {
136
- height: 100vh;
137
- /* Fallback */
138
- height: 100dvh;
139
- /* Mobile viewport fix */
140
139
  padding-top: calc(70px + 1rem);
141
- /* Header height + Gap (same as gap between bubbles) */
142
140
  padding-bottom: 80px;
143
- /* Footer height + gap */
144
141
  padding-left: 20px;
145
142
  padding-right: 20px;
146
143
  box-sizing: border-box;
147
- overflow-y: auto;
148
- -webkit-overflow-scrolling: touch;
149
- /* iOS Momentum Scrolling */
150
- scroll-behavior: smooth;
151
- overscroll-behavior-y: contain;
152
- /* Prevent pull-to-refresh on body */
153
144
  max-width: 900px;
154
145
  margin: 0 auto;
155
146
  width: 100%;
@@ -241,39 +232,64 @@
241
232
  }
242
233
 
243
234
  /* Markdown Styles */
244
- .message-bubble h1,
245
- .message-bubble h2,
246
- .message-bubble h3 {
247
- margin-top: 0;
248
- font-family: var(--font-display);
249
- color: #333;
235
+ /* Markdown Overrides */
236
+ .markdown-body {
237
+ background-color: transparent !important;
238
+ font-family: inherit !important;
239
+ font-size: 1em !important;
240
+ padding: 16px !important;
250
241
  }
251
242
 
252
- .message-bubble code {
253
- background: rgba(0, 0, 0, 0.05);
254
- /* Light gray */
255
- padding: 2px 6px;
256
- border-radius: 4px;
257
- font-family: 'Courier New', Courier, monospace;
258
- font-size: 0.9em;
259
- color: #d63384;
260
- /* Pinkish code color */
261
- }
262
-
263
- .message-bubble pre {
264
- background: #f4f4f4;
265
- /* Light background */
266
- padding: 1rem;
267
- border-radius: 4px;
268
- overflow-x: auto;
269
- border: 1px solid #ddd;
270
- color: #333;
243
+ .markdown-body img {
244
+ max-width: 100%;
245
+ border-radius: 8px;
246
+ display: block;
247
+ margin: 10px 0;
271
248
  }
272
249
 
273
- .message-bubble pre code {
274
- background: transparent;
275
- padding: 0;
276
- color: inherit;
250
+ .markdown-body> :first-child {
251
+ margin-top: 0 !important;
252
+ }
253
+
254
+ .markdown-body> :last-child {
255
+ margin-bottom: 0 !important;
256
+ }
257
+
258
+
259
+
260
+ /* Code Block Fixes */
261
+ .markdown-body pre {
262
+ padding: 36px 16px 16px 16px !important;
263
+ background-color: #f6f8fa !important;
264
+ border-radius: 6px !important;
265
+ position: relative !important;
266
+ }
267
+
268
+ .markdown-body pre[data-language]::before {
269
+ content: attr(data-language);
270
+ position: absolute;
271
+ top: 0;
272
+ left: 0;
273
+ right: 0;
274
+ height: 28px;
275
+ background: #e1e4e8;
276
+ color: #586069;
277
+ font-size: 12px;
278
+ font-weight: 600;
279
+ padding: 0 16px;
280
+ display: flex;
281
+ align-items: center;
282
+ border-radius: 6px 6px 0 0;
283
+ text-transform: uppercase;
284
+ }
285
+
286
+ .markdown-body pre:not([data-language]) {
287
+ padding: 16px !important;
288
+ }
289
+
290
+ .markdown-body pre code {
291
+ padding: 0 !important;
292
+ background: transparent !important;
277
293
  }
278
294
 
279
295
  /* Table Styles */
@@ -438,14 +454,16 @@
438
454
  // Determine role class based on specific role string
439
455
  // "HAL9000" -> robot
440
456
  // Anything else -> human
441
- const roleClass = msg.role === 'HAL9000' ? 'robot' : 'human';
457
+ const roleClass = msg.role.includes('HAL9000') ? 'robot' : 'human';
442
458
  row.className = `message-row ${roleClass}`;
443
459
 
444
460
  const group = document.createElement('div');
445
461
  group.className = 'message-group';
446
462
 
447
463
  // Mapping sender name
448
- const senderName = msg.role === 'HAL9000' ? '🤖 HAL9000' : `😺 ${msg.role}`;
464
+ const senderName = /^\p{Extended_Pictographic}/u.test(msg.role)
465
+ ? msg.role
466
+ : (msg.role.includes('HAL9000') ? `🤖 ${msg.role}` : `😺 ${msg.role}`);
449
467
 
450
468
  const bubble = document.createElement('div');
451
469
  bubble.className = 'message-bubble';
@@ -476,8 +494,42 @@
476
494
 
477
495
  // Content
478
496
  const contentDiv = document.createElement('div');
479
- contentDiv.className = 'message-content';
480
- contentDiv.innerHTML = marked.parse(msg.text || ''); // Use msg.text from JSON
497
+ contentDiv.className = 'message-content markdown-body';
498
+ contentDiv.innerHTML = marked.parse(msg.text || '');
499
+
500
+ // Highlight Code
501
+ contentDiv.querySelectorAll('pre code').forEach((block) => {
502
+ // Trim think block content
503
+ if (block.classList.contains('language-think')) {
504
+ block.textContent = block.textContent.trim();
505
+ }
506
+
507
+ hljs.highlightElement(block);
508
+
509
+ // Detect language and set attribute
510
+ let lang = '';
511
+ block.classList.forEach(cls => {
512
+ if (cls.startsWith('language-')) {
513
+ lang = cls.replace('language-', '');
514
+ }
515
+ });
516
+ if (lang) {
517
+ block.parentElement.setAttribute('data-language', lang);
518
+ }
519
+
520
+ if (block.classList.contains('language-think')) {
521
+ const pre = block.parentElement;
522
+ pre.classList.add('think-block');
523
+ pre.style.whiteSpace = 'pre-wrap';
524
+ pre.style.wordBreak = 'break-word';
525
+ block.style.whiteSpace = 'pre-wrap';
526
+ block.style.wordBreak = 'break-word';
527
+ }
528
+ });
529
+
530
+ // Open links in new tab
531
+ contentDiv.querySelectorAll('a').forEach(a => a.target = '_blank');
532
+
481
533
  bubble.appendChild(contentDiv);
482
534
 
483
535
  group.appendChild(bubble);
@@ -495,17 +547,15 @@
495
547
 
496
548
  // Check if it is the last message
497
549
  if (index === chatData.messages.length - 1) {
498
- // For the last message, scroll to its top (start) with a small offset
499
550
  const lastMsgElement = document.getElementById('chatContainer').lastElementChild;
500
551
  if (lastMsgElement) {
501
- main.scrollTo({
502
- top: lastMsgElement.offsetTop - 25,
552
+ window.scrollTo({
553
+ top: lastMsgElement.getBoundingClientRect().top + window.scrollY - 100,
503
554
  behavior: 'smooth'
504
555
  });
505
556
  }
506
557
  } else {
507
- // For other messages, keep scrolling to bottom
508
- main.scrollTop = main.scrollHeight;
558
+ window.scrollTo(0, document.body.scrollHeight);
509
559
  }
510
560
  }, delay);
511
561
  delay += 800; // Keep delay effect
package/web/turn.mjs CHANGED
@@ -1,30 +1,104 @@
1
- import { dbio, hal, utilitas } from '../index.mjs';
1
+ import { callosum, dbio, hal, utilitas } from '../index.mjs';
2
2
  import { readFile } from 'fs/promises';
3
3
 
4
4
  const getPath = (subPath) => utilitas.__(import.meta.url, subPath);
5
5
  const getHtml = async () => await readFile(getPath('turn.html'), 'utf-8');
6
6
  const renderHtml = async (data) => await getHtml().then((html) => html.replace("'{{data}}'", data));
7
7
 
8
+ const file = async (ctx) => {
9
+ try {
10
+ const url = await callosum.call('getFileLink', { args: [ctx.params.id] });
11
+ const resp = await fetch(url);
12
+ if (!resp.ok) { throw new Error(`Fetch failed: ${resp.status}`); }
13
+ ctx.set('Content-Type', resp.headers.get('Content-Type'));
14
+ ctx.body = Buffer.from(await resp.arrayBuffer());
15
+ } catch (err) {
16
+ console.error('Error serving file:', ctx.params.id, err);
17
+ ctx.status = 404;
18
+ ctx.body = 'Not Found';
19
+ }
20
+ };
21
+
8
22
  const process = async (ctx, next) => {
9
23
  const result = await dbio.queryOne(
10
24
  `SELECT * FROM ${hal.table} WHERE token = $1`,
11
25
  [ctx.params.token]
12
26
  );
27
+ if (!result) { return await next(); }
13
28
  const prompt_count = await dbio.countAll(hal.table);
14
29
  result.received = JSON.parse(result.received);
15
30
  result.response = JSON.parse(result.response);
16
- // print(result);
31
+
32
+ const msg = result.received.message;
33
+ let userText = result.received_text || '';
34
+
35
+ const p = msg.photo?.[msg.photo?.length - 1];
36
+ if (p) { userText = `![Image](/file/${p.file_id})\n\n${userText}`; }
37
+
38
+ const a = msg.audio || msg.voice;
39
+ if (a) { userText = `<audio controls src="/file/${a.file_id}" style="width: 100%; display: block; margin-bottom: 8px;"></audio>\n\n${userText}`; }
40
+
41
+ const v = msg.video || msg.video_note;
42
+ if (v) { userText = `<video controls src="/file/${v.file_id}" style="width: 100%; display: block; border-radius: 8px; margin-bottom: 8px;"></video>\n\n${userText}`; }
43
+
17
44
  const messages = [{
18
- role: `${result.received.message.from.username} (${result.received.message.from.first_name} ${result.received.message.from.last_name})`,
19
- text: result.received_text,
20
- time: new Date(result.received.message.date * 1000),
45
+ role: `${msg.from.username} (${msg.from.first_name} ${msg.from.last_name})`,
46
+ text: userText.trim(),
47
+ time: new Date(msg.date * 1000),
21
48
  }];
22
- result.response.forEach(r => {
49
+
50
+ const first = result.response?.[0];
51
+ let modelLine = first?.text?.split('\n')?.[0] || '';
52
+ modelLine = modelLine.includes('/') ? modelLine.replace(/:.*$/g, '') : '';
53
+ let role = 'HAL9000';
54
+ if (/^\p{Extended_Pictographic}/u.test(modelLine)) {
55
+ role = `${modelLine.split(' ')[0]} ${role}`;
56
+ modelLine = modelLine.split(' ').slice(1).join(' ');
57
+ }
58
+ role = `${role} (${modelLine})`;
59
+
60
+ const last = result.response?.[result.response?.length - 1];
61
+ const defaultTime = new Date((last?.edit_date || last?.date || result.received.message.date) * 1000);
62
+
63
+ if (result.response_text) {
23
64
  messages.push({
24
- role: 'HAL9000',
25
- text: r.raw,
26
- time: new Date((r.edit_date || r.date) * 1000),
65
+ role,
66
+ text: result.response_text,
67
+ time: defaultTime,
27
68
  });
69
+ }
70
+
71
+ result.response.map(x => {
72
+ const p = x.photo?.[x.photo?.length - 1];
73
+ if (p) {
74
+ let text = `![Image](/file/${p.file_id})`;
75
+ if (x.caption) { text += `\n\n${x.caption}`; }
76
+ messages.push({
77
+ role,
78
+ text,
79
+ time: new Date((x.edit_date || x.date || result.received.message.date) * 1000),
80
+ });
81
+ }
82
+ const a = x.audio || x.voice;
83
+ if (a) {
84
+ let text = `<audio controls src="/file/${a.file_id}" style="width: 100%; display: block;"></audio>`;
85
+ if (x.caption) { text += `\n\n${x.caption}`; }
86
+ messages.push({
87
+ role,
88
+ text,
89
+ time: new Date((x.edit_date || x.date || result.received.message.date) * 1000),
90
+ });
91
+ }
92
+ const v = x.video || x.video_note;
93
+ if (v) {
94
+ let text = `<video controls src="/file/${v.file_id}" style="width: 100%; display: block; border-radius: 8px;"></video>`;
95
+ if (x.caption) { text += `\n\n${x.caption}`; }
96
+ messages.push({
97
+ role,
98
+ text,
99
+ time: new Date((x.edit_date || x.date || result.received.message.date) * 1000),
100
+ });
101
+ }
28
102
  });
29
103
  ctx.body = await renderHtml(JSON.stringify({
30
104
  bot_id: result.bot_id, chat_id: result.chat_id,
@@ -39,5 +113,10 @@ export const { actions } = {
39
113
  method: 'GET',
40
114
  process,
41
115
  },
116
+ {
117
+ path: 'file/:id',
118
+ method: 'GET',
119
+ process: file,
120
+ },
42
121
  ]
43
122
  };