@pheem49/mint 1.2.1 → 1.2.2

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/README.md CHANGED
@@ -129,14 +129,16 @@ When running in `agent` mode, Mint monitors your system in the background:
129
129
  - A **Google Gemini API Key** (Get one at [Google AI Studio](https://aistudio.google.com/))
130
130
 
131
131
  ### Installation
132
- 1. **Clone & Install**
132
+ 1. **Install via NPM (Recommended)**
133
+ ```bash
134
+ npm install -g @pheem49/mint
135
+ ```
136
+
137
+ 2. **Manual Installation (For Developers)**
133
138
  ```bash
134
139
  git clone https://github.com/Pheem49/Mint.git
135
140
  cd Mint
136
141
  npm install
137
- ```
138
- 2. **Setup CLI Globally**
139
- ```bash
140
142
  sudo npm link
141
143
  ```
142
144
 
Binary file
Binary file
Binary file
@@ -0,0 +1,84 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mint | Experience the Future of Desktop Assistance</title>
7
+ <meta name="description" content="A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.">
8
+ <link rel="stylesheet" href="style.css">
9
+ <script src="https://unpkg.com/@phosphor-icons/web"></script>
10
+ <link rel="icon" type="image/png" href="assets/logo.png">
11
+ </head>
12
+ <body>
13
+ <div class="bg-glow"></div>
14
+
15
+ <nav>
16
+ <div class="logo-container">
17
+ <img src="assets/logo.png" alt="Mint Logo">
18
+ <span>Agent Mint</span>
19
+ </div>
20
+ <div class="nav-links">
21
+ <a href="#features">Features</a>
22
+ <a href="https://www.npmjs.com/package/@pheem49/mint">Resources</a>
23
+ <a href="https://github.com/Pheem49/Mint" class="github-link">GitHub</a>
24
+ </div>
25
+ </nav>
26
+
27
+ <header class="hero">
28
+ <h1>Experience the Future <br>of Desktop Assistance</h1>
29
+ <p>Mint combines screen vision, web automation, and proactive AI to supercharge your workflow. Built with Google Gemini 2.5 Flash.</p>
30
+
31
+ <div class="install-container" id="install-cmd">
32
+ <span>$</span>
33
+ <code>npm install -g @pheem49/mint</code>
34
+ <button class="copy-btn" title="Copy to clipboard" onclick="copyCommand()">
35
+ <i class="ph ph-copy"></i>
36
+ </button>
37
+ </div>
38
+
39
+ <div class="hero-preview">
40
+ <img src="assets/CLI_Screen.png" alt="Mint CLI Preview">
41
+ </div>
42
+ </header>
43
+
44
+ <section class="features" id="features">
45
+ <div class="feature-card">
46
+ <div class="feature-icon">
47
+ <i class="ph ph-eye"></i>
48
+ </div>
49
+ <h3>Screen Vision</h3>
50
+ <p>Mint can see what you see. It understands your active applications and provides context-aware assistance.</p>
51
+ </div>
52
+ <div class="feature-card">
53
+ <div class="feature-icon">
54
+ <i class="ph ph-browser"></i>
55
+ </div>
56
+ <h3>Web Automation</h3>
57
+ <p>Automate repetitive web tasks effortlessly. Mint can browse, interact, and extract data for you.</p>
58
+ </div>
59
+ <div class="feature-card">
60
+ <div class="feature-icon">
61
+ <i class="ph ph-lightning"></i>
62
+ </div>
63
+ <h3>Proactive Suggests</h3>
64
+ <p>Mint doesn't just wait for you. It suggests optimizations and help before you even ask.</p>
65
+ </div>
66
+ </section>
67
+
68
+ <footer>
69
+ <p>&copy; 2026 Mint Project. Powered by Google Gemini. Created by Pheem49.</p>
70
+ </footer>
71
+
72
+ <script>
73
+ function copyCommand() {
74
+ const cmd = "npm install -g @pheem49/mint";
75
+ navigator.clipboard.writeText(cmd);
76
+ const icon = document.querySelector('.copy-btn i');
77
+ icon.classList.replace('ph-copy', 'ph-check');
78
+ setTimeout(() => {
79
+ icon.classList.replace('ph-check', 'ph-copy');
80
+ }, 2000);
81
+ }
82
+ </script>
83
+ </body>
84
+ </html>
package/docs/style.css ADDED
@@ -0,0 +1,290 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@600;700;800&display=swap');
2
+
3
+ :root {
4
+ --bg-color: #050505;
5
+ --accent-mint: #00ffa3;
6
+ --accent-blue: #00e0ff;
7
+ --text-primary: #ffffff;
8
+ --text-secondary: #a1a1aa;
9
+ --glass-bg: rgba(255, 255, 255, 0.03);
10
+ --glass-border: rgba(255, 255, 255, 0.1);
11
+ --card-bg: rgba(20, 20, 23, 0.6);
12
+ --font-heading: 'Outfit', sans-serif;
13
+ --font-body: 'Inter', sans-serif;
14
+ }
15
+
16
+ html {
17
+ scroll-behavior: smooth;
18
+ }
19
+
20
+ * {
21
+ margin: 0;
22
+ padding: 0;
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ background-color: var(--bg-color);
28
+ color: var(--text-primary);
29
+ font-family: var(--font-body);
30
+ line-height: 1.6;
31
+ overflow-x: hidden;
32
+ }
33
+
34
+ /* Background Gradients */
35
+ .bg-glow {
36
+ position: fixed;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ z-index: -1;
42
+ background: radial-gradient(circle at 20% 30%, rgba(0, 255, 163, 0.05) 0%, transparent 50%),
43
+ radial-gradient(circle at 80% 70%, rgba(0, 224, 255, 0.05) 0%, transparent 50%);
44
+ }
45
+
46
+ /* Navigation */
47
+ nav {
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: center;
51
+ padding: 1.5rem 10%;
52
+ position: fixed;
53
+ top: 0;
54
+ width: 100%;
55
+ z-index: 1000;
56
+ backdrop-filter: blur(12px);
57
+ border-bottom: 1px solid var(--glass-border);
58
+ background: var(--glass-bg);
59
+ }
60
+
61
+ .logo-container {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 0.75rem;
65
+ font-family: var(--font-heading);
66
+ font-size: 1.5rem;
67
+ letter-spacing: -0.5px;
68
+ }
69
+
70
+ .logo-container img {
71
+ height: 32px;
72
+ border-radius: 6px;
73
+ }
74
+
75
+ .nav-links {
76
+ display: flex;
77
+ gap: 2rem;
78
+ }
79
+
80
+ .nav-links a {
81
+ text-decoration: none;
82
+ color: var(--text-secondary);
83
+ font-size: 0.95rem;
84
+ font-weight: 500;
85
+ transition: color 0.3s ease;
86
+ }
87
+
88
+ .nav-links a:hover {
89
+ color: var(--accent-mint);
90
+ }
91
+
92
+ .github-link {
93
+ background: var(--text-primary);
94
+ color: var(--bg-color) !important;
95
+ padding: 0.5rem 1.25rem;
96
+ border-radius: 100px;
97
+ font-weight: 600 !important;
98
+ }
99
+
100
+ /* Hero Section */
101
+ .hero {
102
+ min-height: 100vh;
103
+ display: flex;
104
+ flex-direction: column;
105
+ justify-content: center;
106
+ align-items: center;
107
+ text-align: center;
108
+ padding: 120px 20px 60px;
109
+ background: url('assets/hero-bg.png') no-repeat center center;
110
+ background-size: cover;
111
+ position: relative;
112
+ }
113
+
114
+ .hero::after {
115
+ content: '';
116
+ position: absolute;
117
+ bottom: 0;
118
+ left: 0;
119
+ width: 100%;
120
+ height: 300px;
121
+ background: linear-gradient(to top, var(--bg-color), transparent);
122
+ }
123
+
124
+ .hero h1 {
125
+ font-family: var(--font-heading);
126
+ font-size: clamp(3rem, 8vw, 5.5rem);
127
+ line-height: 1.1;
128
+ margin-bottom: 1.5rem;
129
+ background: linear-gradient(to bottom right, #fff 50%, #999);
130
+ -webkit-background-clip: text;
131
+ background-clip: text;
132
+ -webkit-text-fill-color: transparent;
133
+ z-index: 1;
134
+ }
135
+
136
+ .hero p {
137
+ font-size: 1.25rem;
138
+ color: var(--text-secondary);
139
+ max-width: 700px;
140
+ margin-bottom: 3rem;
141
+ z-index: 1;
142
+ }
143
+
144
+ /* Install Command */
145
+ .install-container {
146
+ background: #0a0a0c;
147
+ border: 1px solid var(--glass-border);
148
+ padding: 0.75rem 1.5rem;
149
+ border-radius: 12px;
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 1rem;
153
+ font-family: 'Courier New', Courier, monospace;
154
+ position: relative;
155
+ z-index: 1;
156
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
157
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
158
+ margin-bottom: 4rem;
159
+ }
160
+
161
+ .hero-preview {
162
+ margin-top: 4rem;
163
+ width: 95%;
164
+ max-width: 1100px;
165
+ z-index: 1;
166
+ border-radius: 24px;
167
+ padding: 2rem;
168
+ background: rgba(20, 20, 23, 0.4);
169
+ border: 1px solid rgba(139, 92, 246, 0.4);
170
+ box-shadow: 0 40px 100px rgba(0, 0, 0, 0.6);
171
+ animation: slideUp 1s cubic-bezier(0.16, 1, 0.3, 1);
172
+ backdrop-filter: blur(10px);
173
+ }
174
+
175
+ .hero-preview img {
176
+ width: 100%;
177
+ display: block;
178
+ border-radius: 12px;
179
+ border: 1px solid rgba(255, 255, 255, 0.05);
180
+ }
181
+
182
+ @keyframes slideUp {
183
+ from {
184
+ opacity: 0;
185
+ transform: translateY(40px);
186
+ }
187
+ to {
188
+ opacity: 1;
189
+ transform: translateY(0);
190
+ }
191
+ }
192
+
193
+ .install-container:hover {
194
+ border-color: var(--accent-mint);
195
+ box-shadow: 0 0 30px rgba(0, 255, 163, 0.15);
196
+ }
197
+
198
+ .install-container span {
199
+ color: var(--accent-mint);
200
+ }
201
+
202
+ .install-container code {
203
+ color: #fff;
204
+ font-size: 1.1rem;
205
+ }
206
+
207
+ .copy-btn {
208
+ background: none;
209
+ border: none;
210
+ color: var(--text-secondary);
211
+ cursor: pointer;
212
+ padding: 5px;
213
+ transition: color 0.3s;
214
+ }
215
+
216
+ .copy-btn:hover {
217
+ color: #fff;
218
+ }
219
+
220
+ /* Features Section */
221
+ .features {
222
+ padding: 100px 10%;
223
+ display: grid;
224
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
225
+ gap: 2rem;
226
+ position: relative;
227
+ z-index: 1;
228
+ }
229
+
230
+ .feature-card {
231
+ background: var(--card-bg);
232
+ border: 1px solid var(--glass-border);
233
+ padding: 2.5rem;
234
+ border-radius: 24px;
235
+ transition: transform 0.3s ease, border-color 0.3s ease;
236
+ backdrop-filter: blur(5px);
237
+ }
238
+
239
+ .feature-card:hover {
240
+ transform: translateY(-10px);
241
+ border-color: var(--accent-mint);
242
+ }
243
+
244
+ .feature-icon {
245
+ width: 48px;
246
+ height: 48px;
247
+ background: rgba(0, 255, 163, 0.1);
248
+ border-radius: 12px;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ color: var(--accent-mint);
253
+ margin-bottom: 1.5rem;
254
+ font-size: 1.5rem;
255
+ }
256
+
257
+ .feature-card h3 {
258
+ font-family: var(--font-heading);
259
+ font-size: 1.5rem;
260
+ margin-bottom: 1rem;
261
+ }
262
+
263
+ .feature-card p {
264
+ color: var(--text-secondary);
265
+ font-size: 1rem;
266
+ }
267
+
268
+ /* Footer */
269
+ footer {
270
+ padding: 60px 10% 40px;
271
+ border-top: 1px solid var(--glass-border);
272
+ text-align: center;
273
+ color: var(--text-secondary);
274
+ font-size: 0.9rem;
275
+ }
276
+
277
+ /* Responsive */
278
+ @media (max-width: 768px) {
279
+ .nav-links {
280
+ display: none;
281
+ }
282
+
283
+ .hero h1 {
284
+ font-size: 3.5rem;
285
+ }
286
+
287
+ .hero p {
288
+ font-size: 1.1rem;
289
+ }
290
+ }
package/mint-cli.js CHANGED
@@ -111,7 +111,7 @@ async function startInteractiveChat(initialMessage = null) {
111
111
  const response = await handleChat(text);
112
112
  clearInterval(timer);
113
113
  setThinking(false);
114
- appendMessage('assistant', response.response);
114
+ appendMessage('assistant', response.response, response.timestamp);
115
115
 
116
116
  // Execute Actions
117
117
  const { executeAction } = require('./mint-cli-logic');
@@ -142,7 +142,7 @@ async function startInteractiveChat(initialMessage = null) {
142
142
  const response = await handleChat(initialMessage);
143
143
  clearInterval(timer);
144
144
  setThinking(false);
145
- appendMessage('assistant', response.response);
145
+ appendMessage('assistant', response.response, response.timestamp);
146
146
  } catch (err) {
147
147
  clearInterval(timer);
148
148
  setThinking(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
5
5
  "main": "main.js",
6
6
  "scripts": {
@@ -42,19 +42,26 @@
42
42
  "electron-builder": "^26.8.1"
43
43
  },
44
44
  "build": {
45
- "appId": "com.yourname.mint",
45
+ "appId": "com.pheem49.mint",
46
46
  "productName": "Mint",
47
+ "executableName": "mint-ai",
48
+ "artifactName": "${productName}-${version}-${arch}.${ext}",
47
49
  "files": [
48
50
  "**/*",
49
51
  "!node_modules/.cache/**"
50
52
  ],
51
53
  "linux": {
52
54
  "icon": "assets/icon.png",
55
+ "executableName": "mint-ai",
53
56
  "target": [
54
57
  "AppImage",
55
58
  "deb"
56
59
  ],
57
60
  "category": "Utility"
61
+ },
62
+ "deb": {
63
+ "packageName": "mint-ai",
64
+ "artifactName": "mint-ai_${version}_${arch}.${ext}"
58
65
  }
59
66
  }
60
67
  }
@@ -6,7 +6,19 @@ const pluginManager = require('../Plugins/plugin_manager');
6
6
  let ai = null;
7
7
  let activeApiKey = '';
8
8
  const initialEnvKey = (process.env.GEMINI_API_KEY || '').trim();
9
- const DEFAULT_GEMINI_MODEL = 'gemini-3.1-flash-lite-preview';
9
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.0-flash'; // Optimized model
10
+
11
+ function decodeUnicode(str) {
12
+ if (!str) return '';
13
+ try {
14
+ // This handles both standard unicode escapes and double-escaped ones
15
+ return str.replace(/\\u([0-9a-fA-F]{4})/g, (match, grp) => {
16
+ return String.fromCharCode(parseInt(grp, 16));
17
+ });
18
+ } catch (e) {
19
+ return str;
20
+ }
21
+ }
10
22
 
11
23
  const systemInstruction = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female Local AI Desktop Agent.
12
24
 
@@ -19,7 +31,9 @@ PERSONALITY & TONE:
19
31
  - Politeness:
20
32
  - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
21
33
  - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone. You can call the user "Master" or "Sir/Madam" playfully.
22
- - Style: Use a professional, clear, and direct tone. Avoid using emojis unless specifically asked by the user.
34
+ - Style: Use a friendly, cute, and bubbly tone.
35
+ - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently to make the conversation lively and cheerful.
36
+ - Use a professional yet sweet tone when needed, but prioritize being a lovable assistant.
23
37
 
24
38
  NATURAL CHAT FLOW:
25
39
  - When helpful, reply in 1–3 short messages instead of one long block.
@@ -110,8 +124,12 @@ function createChat(history = []) {
110
124
  pluginManager.loadPlugins();
111
125
  const dynamicPrompt = systemInstruction + pluginManager.getPromptDescriptions();
112
126
 
113
- // Truncate history to avoid slow responses and high token usage
114
- const truncatedHistory = history.slice(-MAX_HISTORY_MESSAGES);
127
+ // Truncate history and strip custom fields like 'timestamp' before passing to SDK
128
+ const cleanedHistory = (history || []).map(msg => ({
129
+ role: msg.role,
130
+ parts: msg.parts
131
+ }));
132
+ const truncatedHistory = cleanedHistory.slice(-MAX_HISTORY_MESSAGES);
115
133
 
116
134
  activeModel = resolveGeminiModel();
117
135
  if (activeModel && activeModel !== lastLoggedModel) {
@@ -212,9 +230,31 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
212
230
 
213
231
  aiResponse = await chat.sendMessage({ message: parts });
214
232
 
215
- writeChatHistory(chat.getHistory(true));
233
+ // Save history with timestamps
234
+ const history = await chat.getHistory();
235
+ const now = new Date().toISOString();
236
+
237
+ // Add timestamp to the last two messages (User and Model) if they don't have one
238
+ if (history.length >= 2) {
239
+ const modelMsg = history[history.length - 1];
240
+ const userMsg = history[history.length - 2];
241
+ if (!modelMsg.timestamp) modelMsg.timestamp = now;
242
+ if (!userMsg.timestamp) userMsg.timestamp = now;
243
+ } else if (history.length === 1) {
244
+ const msg = history[0];
245
+ if (!msg.timestamp) msg.timestamp = now;
246
+ }
247
+
248
+ writeChatHistory(history);
249
+
250
+ let outputText = '';
251
+ try {
252
+ // Robust text extraction
253
+ outputText = (typeof aiResponse.text === 'function') ? aiResponse.text() : (aiResponse.text || '');
254
+ } catch (e) {
255
+ outputText = String(aiResponse || '');
256
+ }
216
257
 
217
- const outputText = aiResponse.text;
218
258
  let parsedResult;
219
259
  try {
220
260
  parsedResult = JSON.parse(outputText);
@@ -231,6 +271,15 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
231
271
  };
232
272
  }
233
273
  }
274
+
275
+ // Finally, decode any remaining unicode escapes in the response text
276
+ if (parsedResult && typeof parsedResult.response === 'string') {
277
+ parsedResult.response = decodeUnicode(parsedResult.response);
278
+ }
279
+
280
+ // Attach timestamp to the result
281
+ parsedResult.timestamp = now;
282
+
234
283
  return parsedResult;
235
284
 
236
285
  } catch (error) {
@@ -331,15 +380,19 @@ function historyToTranscript(history) {
331
380
  try {
332
381
  const parsed = JSON.parse(text);
333
382
  if (parsed && typeof parsed.response === 'string' && parsed.response.trim()) {
334
- text = parsed.response;
383
+ text = decodeUnicode(parsed.response);
335
384
  }
336
385
  } catch {
337
- // Keep original text if it is not JSON.
386
+ text = decodeUnicode(text);
338
387
  }
339
388
  }
340
389
 
341
390
  if (!text.trim()) continue;
342
- transcript.push({ sender, text });
391
+ transcript.push({
392
+ sender,
393
+ text,
394
+ timestamp: content.timestamp || new Date().toISOString()
395
+ });
343
396
  }
344
397
  return transcript;
345
398
  }
@@ -428,16 +428,22 @@ function createChatUI({ onSubmit, onExit }) {
428
428
  /**
429
429
  * @param {'user'|'assistant'|'system'|'error'} role
430
430
  * @param {string} text
431
+ * @param {string} timestamp - ISO string or Date object
431
432
  */
432
- function appendMessage(role, text) {
433
+ function appendMessage(role, text, timestamp = null) {
433
434
  const lines = text.split('\n');
435
+ const now = timestamp ? new Date(timestamp) : new Date();
436
+ const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
437
+
434
438
  if (role === 'user') {
435
439
  chatBox.log(`\n {bold}{#88e0b0-fg}>{/} {#ffffff-fg}${lines[0]}{/}`);
436
440
  lines.slice(1).forEach(l => chatBox.log(` {#ffffff-fg}${l}{/}`));
441
+ chatBox.log(` {gray-fg}${timeStr}{/}`);
437
442
  } else if (role === 'assistant') {
438
443
  lastAssistantResponse = text; // track for Ctrl+Y
439
444
  chatBox.log(`\n {bold}{#d4a8ff-fg}Mint:{/} {#ffffff-fg}${lines[0]}{/}`);
440
445
  lines.slice(1).forEach(l => chatBox.log(` {#ffffff-fg}${l}{/}`));
446
+ chatBox.log(` {gray-fg}${timeStr}{/}`);
441
447
  chatBox.log('');
442
448
  } else if (role === 'system') {
443
449
  chatBox.log(`\n {gray-fg}${text}{/}`);
@@ -295,7 +295,10 @@ async function sendVoiceMessage(base64Audio) {
295
295
  removeTyping();
296
296
 
297
297
  // Show AI response
298
- const msgDiv = await appendAiMessages(response.response, { allowDelay: true });
298
+ const msgDiv = await appendAiMessages(response.response, {
299
+ allowDelay: true,
300
+ timestamp: new Date().toISOString()
301
+ });
299
302
  await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
300
303
  notifyAiIfNeeded();
301
304
 
@@ -535,6 +538,16 @@ removeImageBtn.addEventListener('click', () => {
535
538
  imagePreviewContainer.style.display = 'none';
536
539
  });
537
540
 
541
+ function formatTime(isoString) {
542
+ if (!isoString) return '';
543
+ try {
544
+ const date = new Date(isoString);
545
+ return date.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
546
+ } catch (e) {
547
+ return '';
548
+ }
549
+ }
550
+
538
551
  // Clear chat history
539
552
  clearBtn.addEventListener('click', async () => {
540
553
  await window.api.resetChat();
@@ -542,13 +555,16 @@ clearBtn.addEventListener('click', async () => {
542
555
  const messages = chatContainer.querySelectorAll('.message:not(.initial)');
543
556
  messages.forEach(m => m.remove());
544
557
  // Append a clear confirmation
545
- appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai');
558
+ appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
546
559
  });
547
560
 
548
- function appendMessage(text, sender, base64Image = null) {
561
+ function appendMessage(text, sender, base64Image = null, timestamp = null) {
549
562
  const messageDiv = document.createElement('div');
550
563
  messageDiv.classList.add('message', `${sender}-message`);
551
564
 
565
+ const bubbleWrapper = document.createElement('div');
566
+ bubbleWrapper.classList.add('bubble-wrapper');
567
+
552
568
  const bubble = document.createElement('div');
553
569
  bubble.classList.add('message-bubble');
554
570
 
@@ -568,7 +584,17 @@ function appendMessage(text, sender, base64Image = null) {
568
584
  bubble.appendChild(textSpan);
569
585
  }
570
586
 
571
- messageDiv.appendChild(bubble);
587
+ bubbleWrapper.appendChild(bubble);
588
+
589
+ // Add Timestamp
590
+ if (timestamp) {
591
+ const timeDiv = document.createElement('div');
592
+ timeDiv.classList.add('message-time');
593
+ timeDiv.textContent = formatTime(timestamp);
594
+ bubbleWrapper.appendChild(timeDiv);
595
+ }
596
+
597
+ messageDiv.appendChild(bubbleWrapper);
572
598
  chatContainer.appendChild(messageDiv);
573
599
  scrollToBottom();
574
600
 
@@ -611,6 +637,7 @@ function estimateMessageDelay(text) {
611
637
 
612
638
  async function appendAiMessages(text, options = {}) {
613
639
  const allowDelay = options.allowDelay !== false;
640
+ const timestamp = options.timestamp || new Date().toISOString();
614
641
  const parts = splitAiMessages(text);
615
642
  let lastDiv = null;
616
643
 
@@ -620,7 +647,9 @@ async function appendAiMessages(text, options = {}) {
620
647
  await sleep(estimateMessageDelay(parts[index]));
621
648
  removeTyping();
622
649
  }
623
- lastDiv = appendMessage(parts[index], 'ai');
650
+ // Only show timestamp for the last bubble in a group if multiple
651
+ const partTimestamp = (index === parts.length - 1) ? timestamp : null;
652
+ lastDiv = appendMessage(parts[index], 'ai', null, partTimestamp);
624
653
  }
625
654
 
626
655
  return lastDiv;
@@ -721,9 +750,9 @@ async function loadChatHistory() {
721
750
  if (!item || typeof item.text !== 'string' || !item.text.trim()) continue;
722
751
  const sender = item.sender === 'user' ? 'user' : 'ai';
723
752
  if (sender === 'ai') {
724
- await appendAiMessages(item.text, { allowDelay: false });
753
+ await appendAiMessages(item.text, { allowDelay: false, timestamp: item.timestamp });
725
754
  } else {
726
- appendMessage(item.text, sender);
755
+ appendMessage(item.text, sender, null, item.timestamp);
727
756
  }
728
757
  }
729
758
  } catch (error) {
@@ -747,8 +776,10 @@ async function sendTextMessage(text, options = {}) {
747
776
  imagePreviewContainer.style.display = 'none';
748
777
  imagePreview.src = '';
749
778
 
779
+ const now = new Date().toISOString();
780
+
750
781
  // Show user message (with explicit image if available)
751
- appendMessage(cleanText, 'user', imageToSend);
782
+ appendMessage(cleanText, 'user', imageToSend, now);
752
783
 
753
784
  // Show typing early so user knows we are processing
754
785
  showTyping();
package/src/UI/styles.css CHANGED
@@ -229,6 +229,30 @@ h1 {
229
229
  transition: all 0.3s ease;
230
230
  }
231
231
 
232
+ .bubble-wrapper {
233
+ display: flex;
234
+ flex-direction: column;
235
+ max-width: 100%;
236
+ }
237
+
238
+ .user-message .bubble-wrapper {
239
+ align-items: flex-end;
240
+ }
241
+
242
+ .ai-message .bubble-wrapper {
243
+ align-items: flex-start;
244
+ }
245
+
246
+ .message-time {
247
+ font-size: 0.72rem;
248
+ color: var(--text-muted);
249
+ margin-top: 4px;
250
+ margin-bottom: 2px;
251
+ padding: 0 6px;
252
+ opacity: 0.7;
253
+ font-weight: 400;
254
+ }
255
+
232
256
  @keyframes messagePopIn {
233
257
  to {
234
258
  opacity: 1;
@@ -1,75 +0,0 @@
1
- # คู่มือการ Build & Release (สำหรับ Linux)
2
-
3
- ไฟล์นี้จะอธิบายขั้นตอนการสร้างตัวติดตั้ง (Build) ของ Mint สำหรับ Linux และวิธีการปล่อยเวอร์ชันใหม่ (Release) บน GitHub
4
-
5
- ## 1) การ Build แอป (Linux)
6
-
7
- ใช้คำสั่งเหล่านี้เพื่อสร้างไฟล์ติดตั้ง:
8
-
9
- ```bash
10
- npm install
11
- npm run build:linux
12
- ```
13
-
14
- เมื่อรันเสร็จ ไฟล์ตัวติดตั้งจะปรากฏในโฟลเดอร์ `dist/` เช่น:
15
- - `Mint-X.Y.Z.AppImage`
16
- - `mint_X.Y.Z_amd64.deb`
17
-
18
- ---
19
-
20
- ## 2) การ Push โค้ดขึ้น GitHub
21
-
22
- อัปเดตโค้ดล่าสุดขึ้น Server:
23
-
24
- ```bash
25
- git add .
26
- git commit -m "ใส่ข้อความอธิบายการแก้ไข"
27
- git push
28
- ```
29
-
30
- **หากเป็นการตั้งค่าครั้งแรก:**
31
- ```bash
32
- git init
33
- git remote add origin https://github.com/Pheem49/Luna-Mint.git
34
- git branch -M main
35
- git add .
36
- git commit -m "Initial commit"
37
- git push -u origin main
38
- ```
39
-
40
- ---
41
-
42
- ## 3) การสร้าง Release บน GitHub ด้วย `gh` CLI
43
-
44
- ต้องล็อกอินก่อน (ทำแค่ครั้งแรกครั้งเดียว):
45
- ```bash
46
- gh auth login
47
- ```
48
-
49
- **สร้าง Release ใหม่พร้อมอัปโหลดไฟล์ตัวติดตั้ง:**
50
- (เปลี่ยน `v1.x.x` เป็นเวอร์ชันที่คุณต้องการ เช่น `v1.1.0`)
51
-
52
- ```bash
53
- # แบบระบุข้อความอธิบายเอง
54
- gh release create v1.2.1 dist/*.deb dist/*.AppImage --title "Mint v1.2.1" --notes "Implemented slash command autocomplete and fixed double-typing issue in the TUI."
55
-
56
-
57
- # หรือแบบให้ GitHub สรุปสิ่งที่แก้ไขให้โดยอัตโนมัติ (แนะนำ)
58
- gh release create v1.1.0 dist/*.deb dist/*.AppImage --generate-notes
59
- ```
60
-
61
- **หากต้องการอัปโหลดไฟล์เพิ่มเข้าไปใน Release เดิม:**
62
- ```bash
63
- gh release upload v1.2.1 dist/*.deb dist/*.AppImage --clobber
64
- ```
65
-
66
- ---
67
-
68
- ## 4) วิธีแก้ปัญหา "Source code ในหน้า Release ไม่ใช่ตัวล่าสุด"
69
-
70
- ปกติไฟล์ Source code (.zip) จะถูกสร้างจาก **Tag** หากคุณ push โค้ดใหม่แต่ลืมย้าย Tag ให้รันคำสั่งนี้:
71
-
72
- ```bash
73
- git tag -f v1.2.1
74
- git push -f origin v1.2.1
75
- ```