vibe-monitor 1.0.1 → 1.0.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 +18 -0
- package/index.html +2 -1
- package/main.js +12 -0
- package/package.json +4 -3
- package/preload.js +2 -1
- package/renderer.js +12 -5
- package/shared/animation.js +29 -0
- package/shared/character.js +79 -0
- package/shared/config.js +146 -0
- package/shared/effects.js +240 -0
- package/shared/icons.js +93 -0
- package/shared/styles.css +129 -0
- package/shared/utils.js +77 -0
- package/styles.css +11 -0
package/README.md
CHANGED
|
@@ -18,6 +18,16 @@ AI coding assistant status monitor with pixel art character.
|
|
|
18
18
|
npx vibe-monitor
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
### Stop
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
curl -X POST http://127.0.0.1:19280/quit
|
|
25
|
+
|
|
26
|
+
# or
|
|
27
|
+
pkill -f vibe-monitor
|
|
28
|
+
killall Electron
|
|
29
|
+
```
|
|
30
|
+
|
|
21
31
|
## Installation
|
|
22
32
|
|
|
23
33
|
### From npm
|
|
@@ -135,6 +145,14 @@ Show window:
|
|
|
135
145
|
curl -X POST http://127.0.0.1:19280/show
|
|
136
146
|
```
|
|
137
147
|
|
|
148
|
+
### POST /quit
|
|
149
|
+
|
|
150
|
+
Quit application:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
curl -X POST http://127.0.0.1:19280/quit
|
|
154
|
+
```
|
|
155
|
+
|
|
138
156
|
## Tray Menu
|
|
139
157
|
|
|
140
158
|
Click the system tray icon to:
|
package/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Vibe Monitor</title>
|
|
7
|
-
<link rel="stylesheet" href="
|
|
7
|
+
<link rel="stylesheet" href="./shared/styles.css">
|
|
8
8
|
<link rel="stylesheet" href="styles.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
<div class="memory-bar-container" id="memory-bar-container">
|
|
49
49
|
<div class="memory-bar" id="memory-bar"></div>
|
|
50
50
|
</div>
|
|
51
|
+
<div class="version-text" id="version-text"></div>
|
|
51
52
|
</div>
|
|
52
53
|
</div>
|
|
53
54
|
|
package/main.js
CHANGED
|
@@ -256,6 +256,10 @@ function updateTrayMenu() {
|
|
|
256
256
|
label: `HTTP Server: localhost:${HTTP_PORT}`,
|
|
257
257
|
enabled: false
|
|
258
258
|
},
|
|
259
|
+
{
|
|
260
|
+
label: `Version: ${app.getVersion()}`,
|
|
261
|
+
enabled: false
|
|
262
|
+
},
|
|
259
263
|
{ type: 'separator' },
|
|
260
264
|
{
|
|
261
265
|
label: 'Quit',
|
|
@@ -445,6 +449,10 @@ function startHttpServer() {
|
|
|
445
449
|
window: windowBounds,
|
|
446
450
|
platform: process.platform
|
|
447
451
|
}, null, 2));
|
|
452
|
+
} else if (req.method === 'POST' && req.url === '/quit') {
|
|
453
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
|
+
res.end(JSON.stringify({ success: true }));
|
|
455
|
+
setTimeout(() => app.quit(), 100);
|
|
448
456
|
} else {
|
|
449
457
|
res.writeHead(404);
|
|
450
458
|
res.end('Not Found');
|
|
@@ -464,6 +472,10 @@ function startHttpServer() {
|
|
|
464
472
|
}
|
|
465
473
|
|
|
466
474
|
// IPC handlers
|
|
475
|
+
ipcMain.handle('get-version', () => {
|
|
476
|
+
return app.getVersion();
|
|
477
|
+
});
|
|
478
|
+
|
|
467
479
|
ipcMain.on('close-window', () => {
|
|
468
480
|
if (mainWindow) {
|
|
469
481
|
mainWindow.hide();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-monitor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AI coding assistant status monitor",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"author": "nalbam",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"canvas": "^3.2.1",
|
|
21
22
|
"electron": "^33.0.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"canvas": "^3.2.1",
|
|
25
25
|
"electron-builder": "^25.0.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"index.html",
|
|
32
32
|
"renderer.js",
|
|
33
33
|
"styles.css",
|
|
34
|
-
"assets/"
|
|
34
|
+
"assets/",
|
|
35
|
+
"shared/"
|
|
35
36
|
],
|
|
36
37
|
"build": {
|
|
37
38
|
"appId": "com.nalbam.vibe-monitor",
|
package/preload.js
CHANGED
|
@@ -5,5 +5,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
5
5
|
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
|
6
6
|
onStateUpdate: (callback) => {
|
|
7
7
|
ipcRenderer.on('state-update', (event, data) => callback(data));
|
|
8
|
-
}
|
|
8
|
+
},
|
|
9
|
+
getVersion: () => ipcRenderer.invoke('get-version')
|
|
9
10
|
});
|
package/renderer.js
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
BLINK_START_FRAME, BLINK_END_FRAME,
|
|
6
6
|
PROJECT_NAME_MAX_LENGTH, PROJECT_NAME_TRUNCATE_AT,
|
|
7
7
|
MODEL_NAME_MAX_LENGTH, MODEL_NAME_TRUNCATE_AT
|
|
8
|
-
} from '
|
|
9
|
-
import { getThinkingText, getWorkingText, updateMemoryBar } from '
|
|
10
|
-
import { initRenderer, drawCharacter } from '
|
|
11
|
-
import { drawInfoIcons } from '
|
|
12
|
-
import { getFloatOffsetX, getFloatOffsetY, needsAnimationRedraw } from '
|
|
8
|
+
} from './shared/config.js';
|
|
9
|
+
import { getThinkingText, getWorkingText, updateMemoryBar } from './shared/utils.js';
|
|
10
|
+
import { initRenderer, drawCharacter } from './shared/character.js';
|
|
11
|
+
import { drawInfoIcons } from './shared/icons.js';
|
|
12
|
+
import { getFloatOffsetX, getFloatOffsetY, needsAnimationRedraw } from './shared/animation.js';
|
|
13
13
|
|
|
14
14
|
// Platform detection: macOS uses emoji, Windows/Linux uses pixel art
|
|
15
15
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
@@ -70,6 +70,13 @@ function init() {
|
|
|
70
70
|
initRenderer(ctx);
|
|
71
71
|
initDomCache();
|
|
72
72
|
|
|
73
|
+
// Display version (set in HTML, updated dynamically if needed)
|
|
74
|
+
if (window.electronAPI?.getVersion) {
|
|
75
|
+
window.electronAPI.getVersion().then(version => {
|
|
76
|
+
document.getElementById('version-text').textContent = `v${version}`;
|
|
77
|
+
}).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
updateDisplay();
|
|
74
81
|
startAnimation();
|
|
75
82
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Shared animation utilities
|
|
2
|
+
import { FLOAT_AMPLITUDE_X, FLOAT_AMPLITUDE_Y } from './config.js';
|
|
3
|
+
|
|
4
|
+
// Calculate floating X offset using cosine wave (~3.2 second cycle at 100ms interval)
|
|
5
|
+
export function getFloatOffsetX(animFrame) {
|
|
6
|
+
const angle = (animFrame % 32) * (2.0 * Math.PI / 32.0);
|
|
7
|
+
return Math.cos(angle) * FLOAT_AMPLITUDE_X;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Calculate floating Y offset using sine wave
|
|
11
|
+
export function getFloatOffsetY(animFrame) {
|
|
12
|
+
const angle = (animFrame % 32) * (2.0 * Math.PI / 32.0);
|
|
13
|
+
return Math.sin(angle) * FLOAT_AMPLITUDE_Y;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if animation frame requires redraw for given state
|
|
17
|
+
export function needsAnimationRedraw(state, animFrame, blinkFrame) {
|
|
18
|
+
switch (state) {
|
|
19
|
+
case 'start':
|
|
20
|
+
case 'thinking':
|
|
21
|
+
case 'working':
|
|
22
|
+
case 'sleep':
|
|
23
|
+
return true; // Always animate these states
|
|
24
|
+
case 'idle':
|
|
25
|
+
return blinkFrame === 30 || blinkFrame === 31; // Only during blink
|
|
26
|
+
default:
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CHAR_SIZE, SCALE, CHARACTER_CONFIG, DEFAULT_CHARACTER, states } from './config.js';
|
|
2
|
+
import { drawEyes, drawMatrixBackground } from './effects.js';
|
|
3
|
+
|
|
4
|
+
let ctx = null;
|
|
5
|
+
|
|
6
|
+
// Character images cache
|
|
7
|
+
const characterImages = {};
|
|
8
|
+
let imagesLoaded = false;
|
|
9
|
+
|
|
10
|
+
// Image paths for each character
|
|
11
|
+
const CHARACTER_IMAGES = {
|
|
12
|
+
clawd: '../images/clawd-128.png',
|
|
13
|
+
kiro: '../images/kiro-128.png'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Preload character images
|
|
17
|
+
function preloadImages() {
|
|
18
|
+
if (imagesLoaded) return Promise.resolve();
|
|
19
|
+
|
|
20
|
+
const promises = Object.entries(CHARACTER_IMAGES).map(([name, path]) => {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const img = new Image();
|
|
23
|
+
img.onload = () => {
|
|
24
|
+
characterImages[name] = img;
|
|
25
|
+
resolve();
|
|
26
|
+
};
|
|
27
|
+
img.onerror = () => {
|
|
28
|
+
console.warn(`Failed to load image for ${name}: ${path}`);
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
img.src = path;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return Promise.all(promises).then(() => {
|
|
36
|
+
imagesLoaded = true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if character has image
|
|
41
|
+
function hasImage(characterName) {
|
|
42
|
+
return characterImages[characterName] !== undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Initialize renderer with canvas context
|
|
46
|
+
export function initRenderer(canvasCtx) {
|
|
47
|
+
ctx = canvasCtx;
|
|
48
|
+
preloadImages();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper: draw scaled rect
|
|
52
|
+
export function drawRect(x, y, w, h, color) {
|
|
53
|
+
ctx.fillStyle = color;
|
|
54
|
+
ctx.fillRect(x * SCALE, y * SCALE, w * SCALE, h * SCALE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Draw character (128x128, scaled 2x from 64x64)
|
|
58
|
+
export function drawCharacter(eyeType, currentState, currentCharacter, animFrame) {
|
|
59
|
+
const state = states[currentState] || states.idle;
|
|
60
|
+
const char = CHARACTER_CONFIG[currentCharacter] || CHARACTER_CONFIG[DEFAULT_CHARACTER];
|
|
61
|
+
|
|
62
|
+
// Clear with background color
|
|
63
|
+
ctx.fillStyle = state.bgColor;
|
|
64
|
+
ctx.fillRect(0, 0, CHAR_SIZE, CHAR_SIZE);
|
|
65
|
+
|
|
66
|
+
// Draw matrix background for working state (behind character)
|
|
67
|
+
if (currentState === 'working') {
|
|
68
|
+
drawMatrixBackground(animFrame, drawRect, CHAR_SIZE / SCALE, char.body);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Draw character image
|
|
72
|
+
if (hasImage(currentCharacter)) {
|
|
73
|
+
const img = characterImages[currentCharacter];
|
|
74
|
+
ctx.drawImage(img, 0, 0, CHAR_SIZE, CHAR_SIZE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Draw eyes (for all characters)
|
|
78
|
+
drawEyes(eyeType, char, animFrame, drawRect);
|
|
79
|
+
}
|
package/shared/config.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Character size (128x128, doubled from 64)
|
|
2
|
+
export const CHAR_SIZE = 128;
|
|
3
|
+
export const SCALE = 2;
|
|
4
|
+
|
|
5
|
+
// Colors
|
|
6
|
+
export const COLOR_EYE = '#000000';
|
|
7
|
+
export const COLOR_WHITE = '#FFFFFF';
|
|
8
|
+
|
|
9
|
+
// State configuration
|
|
10
|
+
export const states = {
|
|
11
|
+
start: {
|
|
12
|
+
bgColor: '#00CCCC',
|
|
13
|
+
text: 'Hello!',
|
|
14
|
+
eyeType: 'sparkle',
|
|
15
|
+
showLoading: false,
|
|
16
|
+
textColor: '#000000'
|
|
17
|
+
},
|
|
18
|
+
idle: {
|
|
19
|
+
bgColor: '#00AA00',
|
|
20
|
+
text: 'Ready',
|
|
21
|
+
eyeType: 'normal',
|
|
22
|
+
showLoading: false,
|
|
23
|
+
textColor: '#FFFFFF'
|
|
24
|
+
},
|
|
25
|
+
thinking: {
|
|
26
|
+
bgColor: '#6633CC',
|
|
27
|
+
text: 'Thinking',
|
|
28
|
+
eyeType: 'thinking',
|
|
29
|
+
showLoading: true,
|
|
30
|
+
textColor: '#FFFFFF'
|
|
31
|
+
},
|
|
32
|
+
working: {
|
|
33
|
+
bgColor: '#0066CC',
|
|
34
|
+
text: 'Working',
|
|
35
|
+
eyeType: 'focused',
|
|
36
|
+
showLoading: true,
|
|
37
|
+
textColor: '#FFFFFF'
|
|
38
|
+
},
|
|
39
|
+
notification: {
|
|
40
|
+
bgColor: '#FFCC00',
|
|
41
|
+
text: 'Input?',
|
|
42
|
+
eyeType: 'alert',
|
|
43
|
+
showLoading: false,
|
|
44
|
+
textColor: '#000000'
|
|
45
|
+
},
|
|
46
|
+
done: {
|
|
47
|
+
bgColor: '#00AA00',
|
|
48
|
+
text: 'Done!',
|
|
49
|
+
eyeType: 'happy',
|
|
50
|
+
showLoading: false,
|
|
51
|
+
textColor: '#FFFFFF'
|
|
52
|
+
},
|
|
53
|
+
sleep: {
|
|
54
|
+
bgColor: '#1a1a4e',
|
|
55
|
+
text: 'Zzz...',
|
|
56
|
+
eyeType: 'sleep',
|
|
57
|
+
showLoading: false,
|
|
58
|
+
textColor: '#FFFFFF'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Character configurations - Single source of truth
|
|
63
|
+
export const CHARACTER_CONFIG = {
|
|
64
|
+
clawd: {
|
|
65
|
+
name: 'clawd',
|
|
66
|
+
displayName: 'Clawd',
|
|
67
|
+
color: '#D97757',
|
|
68
|
+
body: { x: 6, y: 8, w: 52, h: 36 },
|
|
69
|
+
arms: { left: { x: 0, y: 22, w: 6, h: 10 }, right: { x: 58, y: 22, w: 6, h: 10 } },
|
|
70
|
+
legs: [
|
|
71
|
+
{ x: 10, y: 44, w: 6, h: 12 },
|
|
72
|
+
{ x: 18, y: 44, w: 6, h: 12 },
|
|
73
|
+
{ x: 40, y: 44, w: 6, h: 12 },
|
|
74
|
+
{ x: 48, y: 44, w: 6, h: 12 }
|
|
75
|
+
],
|
|
76
|
+
tail: null,
|
|
77
|
+
eyes: { left: { x: 14, y: 22 }, right: { x: 44, y: 22 }, size: 6 },
|
|
78
|
+
isGhost: false
|
|
79
|
+
},
|
|
80
|
+
kiro: {
|
|
81
|
+
name: 'kiro',
|
|
82
|
+
displayName: 'Kiro',
|
|
83
|
+
color: '#FFFFFF',
|
|
84
|
+
// Sprite-based rendering (see sprites.js) - 64x64 sprite
|
|
85
|
+
body: { x: 10, y: 3, w: 44, h: 30 },
|
|
86
|
+
arms: null,
|
|
87
|
+
legs: [],
|
|
88
|
+
tail: [],
|
|
89
|
+
// Eyes positioned in sprite (drawn by drawEyes, not in sprite)
|
|
90
|
+
eyes: { left: { x: 29, y: 21 }, right: { x: 39, y: 21 }, w: 5, h: 8 },
|
|
91
|
+
isGhost: true
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const CHARACTER_NAMES = Object.keys(CHARACTER_CONFIG);
|
|
96
|
+
export const DEFAULT_CHARACTER = 'clawd';
|
|
97
|
+
|
|
98
|
+
// Thinking state texts (random selection)
|
|
99
|
+
export const THINKING_TEXTS = ['Thinking', 'Hmm', 'Let me see'];
|
|
100
|
+
|
|
101
|
+
// Tool-based status texts for working state (lowercase keys)
|
|
102
|
+
export const TOOL_TEXTS = {
|
|
103
|
+
'bash': ['Running', 'Executing', 'Processing'],
|
|
104
|
+
'read': ['Reading', 'Scanning', 'Checking'],
|
|
105
|
+
'edit': ['Editing', 'Modifying', 'Fixing'],
|
|
106
|
+
'write': ['Writing', 'Creating', 'Saving'],
|
|
107
|
+
'grep': ['Searching', 'Finding', 'Looking'],
|
|
108
|
+
'glob': ['Scanning', 'Browsing', 'Finding'],
|
|
109
|
+
'task': ['Thinking', 'Working', 'Planning'],
|
|
110
|
+
'webfetch': ['Fetching', 'Loading', 'Getting'],
|
|
111
|
+
'websearch': ['Searching', 'Googling', 'Looking'],
|
|
112
|
+
'default': ['Working', 'Busy', 'Coding']
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Floating animation constants
|
|
116
|
+
export const FLOAT_AMPLITUDE_X = 3;
|
|
117
|
+
export const FLOAT_AMPLITUDE_Y = 5;
|
|
118
|
+
export const CHAR_X_BASE = 22;
|
|
119
|
+
export const CHAR_Y_BASE = 20;
|
|
120
|
+
|
|
121
|
+
// State timeouts
|
|
122
|
+
export const DONE_TO_IDLE_TIMEOUT = 60 * 1000; // 1 minute
|
|
123
|
+
export const SLEEP_TIMEOUT = 600 * 1000; // 10 minutes
|
|
124
|
+
|
|
125
|
+
// Animation constants
|
|
126
|
+
export const FRAME_INTERVAL = 100; // 100ms per frame
|
|
127
|
+
export const FLOAT_CYCLE_FRAMES = 32; // ~3.2 seconds at 100ms tick
|
|
128
|
+
export const LOADING_DOT_COUNT = 4;
|
|
129
|
+
export const THINKING_ANIMATION_SLOWDOWN = 3; // 3x slower for thinking state
|
|
130
|
+
export const BLINK_START_FRAME = 30;
|
|
131
|
+
export const BLINK_END_FRAME = 31;
|
|
132
|
+
|
|
133
|
+
// Text truncation limits
|
|
134
|
+
export const PROJECT_NAME_MAX_LENGTH = 16;
|
|
135
|
+
export const PROJECT_NAME_TRUNCATE_AT = 13;
|
|
136
|
+
export const MODEL_NAME_MAX_LENGTH = 14;
|
|
137
|
+
export const MODEL_NAME_TRUNCATE_AT = 11;
|
|
138
|
+
|
|
139
|
+
// Matrix effect constants
|
|
140
|
+
export const MATRIX_STREAM_DENSITY = 0.7; // 70% of streams visible
|
|
141
|
+
export const MATRIX_SPEED_MIN = 1;
|
|
142
|
+
export const MATRIX_SPEED_MAX = 6;
|
|
143
|
+
export const MATRIX_COLUMN_WIDTH = 4;
|
|
144
|
+
export const MATRIX_FLICKER_PERIOD = 3;
|
|
145
|
+
export const MATRIX_TAIL_LENGTH_FAST = 8; // speed > 3
|
|
146
|
+
export const MATRIX_TAIL_LENGTH_SLOW = 6;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COLOR_EYE, COLOR_WHITE, CHARACTER_CONFIG, DEFAULT_CHARACTER,
|
|
3
|
+
MATRIX_STREAM_DENSITY, MATRIX_SPEED_MIN, MATRIX_SPEED_MAX,
|
|
4
|
+
MATRIX_COLUMN_WIDTH, MATRIX_FLICKER_PERIOD,
|
|
5
|
+
MATRIX_TAIL_LENGTH_FAST, MATRIX_TAIL_LENGTH_SLOW
|
|
6
|
+
} from './config.js';
|
|
7
|
+
|
|
8
|
+
// Effect color for white characters (orange)
|
|
9
|
+
const COLOR_EFFECT_ALT = '#FFA500';
|
|
10
|
+
|
|
11
|
+
// Sunglasses colors
|
|
12
|
+
const COLOR_SUNGLASSES_FRAME = '#111111';
|
|
13
|
+
const COLOR_SUNGLASSES_LENS = '#001100';
|
|
14
|
+
const COLOR_SUNGLASSES_SHINE = '#003300';
|
|
15
|
+
|
|
16
|
+
// Get eye cover position (used by sunglasses and sleep eyes)
|
|
17
|
+
function getEyeCoverPosition(leftX, rightX, eyeY, eyeW, eyeH, isKiro = false) {
|
|
18
|
+
const lensW = eyeW + 4;
|
|
19
|
+
const lensH = eyeH + 2;
|
|
20
|
+
// Kiro: shift up 2px
|
|
21
|
+
const lensY = eyeY - 1 - (isKiro ? 2 : 0);
|
|
22
|
+
// Kiro: left lens 2px right, right lens 5px right
|
|
23
|
+
const leftLensX = leftX - 2 + (isKiro ? 2 : 0);
|
|
24
|
+
const rightLensX = rightX - 2 + (isKiro ? 5 : 0);
|
|
25
|
+
return { lensW, lensH, lensY, leftLensX, rightLensX };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Draw sunglasses (Matrix style)
|
|
29
|
+
function drawSunglasses(leftX, rightX, eyeY, eyeW, eyeH, drawRect, isKiro = false) {
|
|
30
|
+
const { lensW, lensH, lensY, leftLensX, rightLensX } = getEyeCoverPosition(leftX, rightX, eyeY, eyeW, eyeH, isKiro);
|
|
31
|
+
|
|
32
|
+
// Sunglasses colors (same for all characters)
|
|
33
|
+
const frameColor = COLOR_SUNGLASSES_FRAME;
|
|
34
|
+
const lensColor = COLOR_SUNGLASSES_LENS;
|
|
35
|
+
const shineColor = COLOR_SUNGLASSES_SHINE;
|
|
36
|
+
|
|
37
|
+
// Left lens (dark green tint)
|
|
38
|
+
drawRect(leftLensX, lensY, lensW, lensH, lensColor);
|
|
39
|
+
// Left lens shine
|
|
40
|
+
drawRect(leftLensX + 1, lensY + 1, 2, 1, shineColor);
|
|
41
|
+
|
|
42
|
+
// Right lens (dark green tint)
|
|
43
|
+
drawRect(rightLensX, lensY, lensW, lensH, lensColor);
|
|
44
|
+
// Right lens shine
|
|
45
|
+
drawRect(rightLensX + 1, lensY + 1, 2, 1, shineColor);
|
|
46
|
+
|
|
47
|
+
// Frame - top
|
|
48
|
+
drawRect(leftLensX - 1, lensY - 1, lensW + 2, 1, frameColor);
|
|
49
|
+
drawRect(rightLensX - 1, lensY - 1, lensW + 2, 1, frameColor);
|
|
50
|
+
|
|
51
|
+
// Frame - bottom
|
|
52
|
+
drawRect(leftLensX - 1, lensY + lensH, lensW + 2, 1, frameColor);
|
|
53
|
+
drawRect(rightLensX - 1, lensY + lensH, lensW + 2, 1, frameColor);
|
|
54
|
+
|
|
55
|
+
// Frame - sides
|
|
56
|
+
drawRect(leftLensX - 1, lensY, 1, lensH, frameColor);
|
|
57
|
+
drawRect(leftLensX + lensW, lensY, 1, lensH, frameColor);
|
|
58
|
+
drawRect(rightLensX - 1, lensY, 1, lensH, frameColor);
|
|
59
|
+
drawRect(rightLensX + lensW, lensY, 1, lensH, frameColor);
|
|
60
|
+
|
|
61
|
+
// Bridge (connects two lenses)
|
|
62
|
+
const bridgeY = lensY + Math.floor(lensH / 2);
|
|
63
|
+
drawRect(leftLensX + lensW, bridgeY, rightLensX - leftLensX - lensW, 1, frameColor);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Draw sleep eyes (closed eyes with body color background)
|
|
67
|
+
function drawSleepEyes(leftX, rightX, eyeY, eyeW, eyeH, drawRect, bodyColor, isKiro = false) {
|
|
68
|
+
const { lensW, lensH, lensY, leftLensX, rightLensX } = getEyeCoverPosition(leftX, rightX, eyeY, eyeW, eyeH, isKiro);
|
|
69
|
+
|
|
70
|
+
// Cover original eyes with body color (same area as sunglasses)
|
|
71
|
+
drawRect(leftLensX, lensY, lensW, lensH, bodyColor);
|
|
72
|
+
drawRect(rightLensX, lensY, lensW, lensH, bodyColor);
|
|
73
|
+
|
|
74
|
+
// Draw closed eyes (horizontal lines in the middle)
|
|
75
|
+
const closedEyeY = lensY + Math.floor(lensH / 2);
|
|
76
|
+
const closedEyeH = 2; // 2px thick line
|
|
77
|
+
drawRect(leftLensX + 1, closedEyeY, lensW - 2, closedEyeH, COLOR_EYE);
|
|
78
|
+
drawRect(rightLensX + 1, closedEyeY, lensW - 2, closedEyeH, COLOR_EYE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get effect color based on character color
|
|
82
|
+
function getEffectColor(char) {
|
|
83
|
+
return char.color === '#FFFFFF' ? COLOR_EFFECT_ALT : COLOR_WHITE;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Draw eyes (scaled 2x)
|
|
87
|
+
// Note: Eyes are now part of character images, only draw effects and sunglasses
|
|
88
|
+
export function drawEyes(eyeType, char, animFrame, drawRect) {
|
|
89
|
+
char = char || CHARACTER_CONFIG[DEFAULT_CHARACTER];
|
|
90
|
+
const leftX = char.eyes.left.x;
|
|
91
|
+
const rightX = char.eyes.right.x;
|
|
92
|
+
const eyeY = char.eyes.left.y;
|
|
93
|
+
// Support separate width/height or fallback to size
|
|
94
|
+
const eyeW = char.eyes.w || char.eyes.size || 6;
|
|
95
|
+
const eyeH = char.eyes.h || char.eyes.size || 6;
|
|
96
|
+
const isKiro = char.name === 'kiro';
|
|
97
|
+
const effectColor = getEffectColor(char);
|
|
98
|
+
|
|
99
|
+
// Effect position (relative to character, above eyes)
|
|
100
|
+
const effectX = rightX + eyeW + 2;
|
|
101
|
+
const effectY = eyeY - 18;
|
|
102
|
+
|
|
103
|
+
switch (eyeType) {
|
|
104
|
+
case 'focused':
|
|
105
|
+
// Sunglasses for Matrix style (working state)
|
|
106
|
+
drawSunglasses(leftX, rightX, eyeY, eyeW, eyeH, drawRect, isKiro);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'alert':
|
|
110
|
+
// Question mark effect (notification state)
|
|
111
|
+
drawQuestionMark(effectX, effectY, drawRect);
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case 'sparkle':
|
|
115
|
+
// Sparkle effect (start state)
|
|
116
|
+
drawSparkle(effectX, effectY + 2, animFrame, drawRect, effectColor);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case 'thinking':
|
|
120
|
+
// Thought bubble effect (thinking state)
|
|
121
|
+
drawThoughtBubble(effectX, effectY, animFrame, drawRect, effectColor);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'sleep':
|
|
125
|
+
// Sleep eyes (closed eyes) and Zzz effect
|
|
126
|
+
drawSleepEyes(leftX, rightX, eyeY, eyeW, eyeH, drawRect, char.color, isKiro);
|
|
127
|
+
drawZzz(effectX, effectY, animFrame, drawRect, effectColor);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Draw sparkle (scaled 2x)
|
|
133
|
+
export function drawSparkle(x, y, animFrame, drawRect, color = COLOR_WHITE) {
|
|
134
|
+
const frame = animFrame % 4;
|
|
135
|
+
drawRect(x + 2, y + 2, 2, 2, color);
|
|
136
|
+
|
|
137
|
+
if (frame === 0 || frame === 2) {
|
|
138
|
+
drawRect(x + 2, y, 2, 2, color);
|
|
139
|
+
drawRect(x + 2, y + 4, 2, 2, color);
|
|
140
|
+
drawRect(x, y + 2, 2, 2, color);
|
|
141
|
+
drawRect(x + 4, y + 2, 2, 2, color);
|
|
142
|
+
} else {
|
|
143
|
+
drawRect(x, y, 2, 2, color);
|
|
144
|
+
drawRect(x + 4, y, 2, 2, color);
|
|
145
|
+
drawRect(x, y + 4, 2, 2, color);
|
|
146
|
+
drawRect(x + 4, y + 4, 2, 2, color);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Draw question mark
|
|
151
|
+
export function drawQuestionMark(x, y, drawRect) {
|
|
152
|
+
const color = '#000000';
|
|
153
|
+
drawRect(x + 1, y, 4, 2, color);
|
|
154
|
+
drawRect(x + 4, y + 2, 2, 2, color);
|
|
155
|
+
drawRect(x + 2, y + 4, 2, 2, color);
|
|
156
|
+
drawRect(x + 2, y + 6, 2, 2, color);
|
|
157
|
+
drawRect(x + 2, y + 10, 2, 2, color);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Draw Zzz animation for sleep state
|
|
161
|
+
export function drawZzz(x, y, animFrame, drawRect, color = COLOR_WHITE) {
|
|
162
|
+
const frame = animFrame % 20;
|
|
163
|
+
if (frame < 10) {
|
|
164
|
+
drawRect(x, y, 6, 1, color);
|
|
165
|
+
drawRect(x + 4, y + 1, 2, 1, color);
|
|
166
|
+
drawRect(x + 3, y + 2, 2, 1, color);
|
|
167
|
+
drawRect(x + 2, y + 3, 2, 1, color);
|
|
168
|
+
drawRect(x + 1, y + 4, 2, 1, color);
|
|
169
|
+
drawRect(x, y + 5, 6, 1, color);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Draw thought bubble animation for thinking state
|
|
174
|
+
export function drawThoughtBubble(x, y, animFrame, drawRect, color = COLOR_WHITE) {
|
|
175
|
+
const frame = animFrame % 12;
|
|
176
|
+
// Small dots leading to bubble (always visible)
|
|
177
|
+
drawRect(x, y + 6, 2, 2, color);
|
|
178
|
+
drawRect(x + 2, y + 3, 2, 2, color);
|
|
179
|
+
// Main bubble (animated size)
|
|
180
|
+
if (frame < 6) {
|
|
181
|
+
// Larger bubble
|
|
182
|
+
drawRect(x + 3, y - 2, 6, 2, color);
|
|
183
|
+
drawRect(x + 2, y, 8, 3, color);
|
|
184
|
+
drawRect(x + 3, y + 3, 6, 1, color);
|
|
185
|
+
} else {
|
|
186
|
+
// Smaller bubble
|
|
187
|
+
drawRect(x + 4, y - 1, 4, 2, color);
|
|
188
|
+
drawRect(x + 3, y + 1, 6, 2, color);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Matrix rain colors (green shades - movie style)
|
|
193
|
+
const COLOR_MATRIX_WHITE = '#CCFFCC';
|
|
194
|
+
const COLOR_MATRIX_BRIGHT = '#00FF00';
|
|
195
|
+
const COLOR_MATRIX_MID = '#00BB00';
|
|
196
|
+
const COLOR_MATRIX_DIM = '#008800';
|
|
197
|
+
const COLOR_MATRIX_DARK = '#004400';
|
|
198
|
+
|
|
199
|
+
// Pseudo-random number generator for consistent randomness
|
|
200
|
+
function pseudoRandom(seed) {
|
|
201
|
+
const x = Math.sin(seed * 9999) * 10000;
|
|
202
|
+
return x - Math.floor(x);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Draw matrix background effect (full area, movie style)
|
|
206
|
+
export function drawMatrixBackground(animFrame, drawRect, size = 64, body = null) {
|
|
207
|
+
// Draw streams across entire area (character will be drawn on top)
|
|
208
|
+
const streamCount = Math.floor(size / MATRIX_COLUMN_WIDTH);
|
|
209
|
+
for (let i = 0; i < streamCount; i++) {
|
|
210
|
+
const seed = i * 23 + 7;
|
|
211
|
+
// Show streams based on density setting
|
|
212
|
+
if (pseudoRandom(seed + 100) > MATRIX_STREAM_DENSITY) continue;
|
|
213
|
+
const x = i * MATRIX_COLUMN_WIDTH;
|
|
214
|
+
const offset = Math.floor(pseudoRandom(seed) * size);
|
|
215
|
+
// Variable speed
|
|
216
|
+
const speedRange = MATRIX_SPEED_MAX - MATRIX_SPEED_MIN + 1;
|
|
217
|
+
const speed = MATRIX_SPEED_MIN + Math.floor(pseudoRandom(seed + 1) * speedRange);
|
|
218
|
+
// Variable tail length based on speed
|
|
219
|
+
const tailLen = speed > 3 ? MATRIX_TAIL_LENGTH_FAST : MATRIX_TAIL_LENGTH_SLOW;
|
|
220
|
+
drawMatrixStreamMovie(x, 0, animFrame, drawRect, offset, size, speed, tailLen, seed);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Draw matrix stream with movie-style effect
|
|
225
|
+
function drawMatrixStreamMovie(x, y, animFrame, drawRect, offset, height, speed, tailLen, seed) {
|
|
226
|
+
if (height < MATRIX_COLUMN_WIDTH) return;
|
|
227
|
+
const pos = (animFrame * speed + offset) % height;
|
|
228
|
+
|
|
229
|
+
// Head: bright white/green (flicker effect)
|
|
230
|
+
const flicker = (animFrame + seed) % MATRIX_FLICKER_PERIOD === 0;
|
|
231
|
+
const headColor = flicker ? COLOR_MATRIX_WHITE : COLOR_MATRIX_BRIGHT;
|
|
232
|
+
drawRect(x, y + pos, 2, 2, headColor);
|
|
233
|
+
|
|
234
|
+
// Tail with gradient
|
|
235
|
+
if (pos >= 2) drawRect(x, y + pos - 2, 2, 2, COLOR_MATRIX_BRIGHT);
|
|
236
|
+
if (pos >= 4) drawRect(x, y + pos - 4, 2, 2, COLOR_MATRIX_MID);
|
|
237
|
+
if (pos >= 6) drawRect(x, y + pos - 6, 2, 2, COLOR_MATRIX_MID);
|
|
238
|
+
if (tailLen >= 8 && pos >= 8) drawRect(x, y + pos - 8, 2, 2, COLOR_MATRIX_DIM);
|
|
239
|
+
if (tailLen >= 8 && pos >= 10) drawRect(x, y + pos - 10, 2, 2, COLOR_MATRIX_DARK);
|
|
240
|
+
}
|
package/shared/icons.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Cached DOM elements and canvas contexts
|
|
2
|
+
let iconCache = null;
|
|
3
|
+
|
|
4
|
+
// Initialize icon cache (called once on first drawInfoIcons call)
|
|
5
|
+
function initIconCache() {
|
|
6
|
+
const iconProject = document.getElementById('icon-project');
|
|
7
|
+
const iconTool = document.getElementById('icon-tool');
|
|
8
|
+
const iconModel = document.getElementById('icon-model');
|
|
9
|
+
const iconMemory = document.getElementById('icon-memory');
|
|
10
|
+
|
|
11
|
+
iconCache = {
|
|
12
|
+
emojiIcons: document.querySelectorAll('.emoji-icon'),
|
|
13
|
+
pixelIcons: document.querySelectorAll('.pixel-icon'),
|
|
14
|
+
// Cache both canvas elements and their contexts
|
|
15
|
+
canvases: [
|
|
16
|
+
{ canvas: iconProject, ctx: iconProject?.getContext('2d') },
|
|
17
|
+
{ canvas: iconTool, ctx: iconTool?.getContext('2d') },
|
|
18
|
+
{ canvas: iconModel, ctx: iconModel?.getContext('2d') },
|
|
19
|
+
{ canvas: iconMemory, ctx: iconMemory?.getContext('2d') }
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Draw folder icon - 8x7 pixels
|
|
25
|
+
export function drawFolderIcon(iconCtx, color) {
|
|
26
|
+
iconCtx.fillStyle = color;
|
|
27
|
+
iconCtx.fillRect(0, 0, 3, 1);
|
|
28
|
+
iconCtx.fillRect(0, 1, 8, 6);
|
|
29
|
+
iconCtx.fillStyle = '#000000';
|
|
30
|
+
iconCtx.fillRect(1, 2, 6, 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Draw tool/wrench icon - 8x8 pixels
|
|
34
|
+
export function drawToolIcon(iconCtx, color) {
|
|
35
|
+
iconCtx.fillStyle = color;
|
|
36
|
+
iconCtx.fillRect(1, 0, 6, 3);
|
|
37
|
+
iconCtx.fillStyle = '#000000';
|
|
38
|
+
iconCtx.fillRect(3, 0, 2, 1);
|
|
39
|
+
iconCtx.fillStyle = color;
|
|
40
|
+
iconCtx.fillRect(3, 3, 2, 5);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Draw robot icon - 8x8 pixels
|
|
44
|
+
export function drawRobotIcon(iconCtx, color) {
|
|
45
|
+
iconCtx.fillStyle = color;
|
|
46
|
+
iconCtx.fillRect(3, 0, 2, 1);
|
|
47
|
+
iconCtx.fillRect(1, 1, 6, 5);
|
|
48
|
+
iconCtx.fillStyle = '#000000';
|
|
49
|
+
iconCtx.fillRect(2, 2, 1, 2);
|
|
50
|
+
iconCtx.fillRect(5, 2, 1, 2);
|
|
51
|
+
iconCtx.fillRect(2, 5, 4, 1);
|
|
52
|
+
iconCtx.fillStyle = color;
|
|
53
|
+
iconCtx.fillRect(0, 2, 1, 2);
|
|
54
|
+
iconCtx.fillRect(7, 2, 1, 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Draw brain icon - 8x7 pixels
|
|
58
|
+
export function drawBrainIcon(iconCtx, color) {
|
|
59
|
+
iconCtx.fillStyle = color;
|
|
60
|
+
iconCtx.fillRect(1, 0, 6, 7);
|
|
61
|
+
iconCtx.fillRect(0, 1, 8, 5);
|
|
62
|
+
iconCtx.fillStyle = '#000000';
|
|
63
|
+
iconCtx.fillRect(4, 1, 1, 5);
|
|
64
|
+
iconCtx.fillRect(2, 0, 1, 1);
|
|
65
|
+
iconCtx.fillRect(5, 0, 1, 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Draw all info icons
|
|
69
|
+
export function drawInfoIcons(color, bgColor, useEmoji) {
|
|
70
|
+
// Initialize cache on first call
|
|
71
|
+
if (!iconCache) {
|
|
72
|
+
initIconCache();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const c = iconCache;
|
|
76
|
+
|
|
77
|
+
// Toggle emoji/pixel icon visibility (using cached elements)
|
|
78
|
+
c.emojiIcons.forEach(el => el.style.display = useEmoji ? 'inline' : 'none');
|
|
79
|
+
c.pixelIcons.forEach(el => el.style.display = useEmoji ? 'none' : 'inline-block');
|
|
80
|
+
|
|
81
|
+
if (!useEmoji) {
|
|
82
|
+
const drawFuncs = [drawFolderIcon, drawToolIcon, drawRobotIcon, drawBrainIcon];
|
|
83
|
+
|
|
84
|
+
// Use cached contexts instead of calling getContext('2d') each time
|
|
85
|
+
c.canvases.forEach((item, index) => {
|
|
86
|
+
if (item.ctx) {
|
|
87
|
+
item.ctx.fillStyle = bgColor;
|
|
88
|
+
item.ctx.fillRect(0, 0, 8, 8);
|
|
89
|
+
drawFuncs[index](item.ctx, color);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Display Screen */
|
|
2
|
+
.display {
|
|
3
|
+
width: 172px;
|
|
4
|
+
height: 320px;
|
|
5
|
+
background: #000;
|
|
6
|
+
border-radius: 4px;
|
|
7
|
+
position: relative;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
image-rendering: pixelated;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Character Canvas - 128x128, centered */
|
|
13
|
+
#character-canvas {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 20px;
|
|
16
|
+
left: 22px;
|
|
17
|
+
width: 128px;
|
|
18
|
+
height: 128px;
|
|
19
|
+
image-rendering: pixelated;
|
|
20
|
+
transition: top 0.1s ease-out, left 0.1s ease-out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Status Text */
|
|
24
|
+
.status-text {
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: 160px;
|
|
27
|
+
width: 100%;
|
|
28
|
+
text-align: center;
|
|
29
|
+
font-family: 'Courier New', monospace;
|
|
30
|
+
font-size: 24px;
|
|
31
|
+
font-weight: bold;
|
|
32
|
+
color: white;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Loading Dots */
|
|
36
|
+
.loading-dots {
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: 190px;
|
|
39
|
+
width: 100%;
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dot {
|
|
46
|
+
width: 8px;
|
|
47
|
+
height: 8px;
|
|
48
|
+
border-radius: 50%;
|
|
49
|
+
background: #7BEF7B;
|
|
50
|
+
transition: background 0.1s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dot.dim {
|
|
54
|
+
background: #3a3a3a;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Info Text */
|
|
58
|
+
.info-text {
|
|
59
|
+
position: absolute;
|
|
60
|
+
font-family: 'Courier New', monospace;
|
|
61
|
+
font-size: 10px;
|
|
62
|
+
color: white;
|
|
63
|
+
left: 10px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.project-text {
|
|
67
|
+
top: 220px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.tool-text {
|
|
71
|
+
top: 235px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.model-text {
|
|
75
|
+
top: 250px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.memory-text {
|
|
79
|
+
top: 265px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.memory-bar-container {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 285px;
|
|
85
|
+
left: 10px;
|
|
86
|
+
width: 152px;
|
|
87
|
+
height: 8px;
|
|
88
|
+
background: rgba(0, 0, 0, 0.4);
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
border: 1px solid rgba(0, 0, 0, 0.6);
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.memory-bar {
|
|
95
|
+
height: 100%;
|
|
96
|
+
border-radius: 0px;
|
|
97
|
+
transition: width 0.3s, background 0.3s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.info-label {
|
|
101
|
+
color: white;
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.info-value {
|
|
107
|
+
color: #aaa;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.pixel-icon {
|
|
111
|
+
width: 8px;
|
|
112
|
+
height: 8px;
|
|
113
|
+
margin-right: 2px;
|
|
114
|
+
image-rendering: pixelated;
|
|
115
|
+
vertical-align: middle;
|
|
116
|
+
display: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.emoji-icon {
|
|
120
|
+
display: inline;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.icon-canvas {
|
|
124
|
+
width: 8px;
|
|
125
|
+
height: 8px;
|
|
126
|
+
image-rendering: pixelated;
|
|
127
|
+
vertical-align: middle;
|
|
128
|
+
margin-right: 2px;
|
|
129
|
+
}
|
package/shared/utils.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { TOOL_TEXTS, THINKING_TEXTS } from './config.js';
|
|
2
|
+
|
|
3
|
+
// Get thinking text (random selection)
|
|
4
|
+
export function getThinkingText() {
|
|
5
|
+
return THINKING_TEXTS[Math.floor(Math.random() * THINKING_TEXTS.length)];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Get working text based on tool (random selection, case-insensitive)
|
|
9
|
+
export function getWorkingText(tool) {
|
|
10
|
+
const key = (tool || '').toLowerCase();
|
|
11
|
+
const texts = TOOL_TEXTS[key] || TOOL_TEXTS['default'];
|
|
12
|
+
return texts[Math.floor(Math.random() * texts.length)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Interpolate between two colors based on ratio (0-1)
|
|
16
|
+
export function lerpColor(color1, color2, ratio) {
|
|
17
|
+
const r1 = parseInt(color1.slice(1, 3), 16);
|
|
18
|
+
const g1 = parseInt(color1.slice(3, 5), 16);
|
|
19
|
+
const b1 = parseInt(color1.slice(5, 7), 16);
|
|
20
|
+
const r2 = parseInt(color2.slice(1, 3), 16);
|
|
21
|
+
const g2 = parseInt(color2.slice(3, 5), 16);
|
|
22
|
+
const b2 = parseInt(color2.slice(5, 7), 16);
|
|
23
|
+
|
|
24
|
+
const r = Math.round(r1 + (r2 - r1) * ratio);
|
|
25
|
+
const g = Math.round(g1 + (g2 - g1) * ratio);
|
|
26
|
+
const b = Math.round(b1 + (b2 - b1) * ratio);
|
|
27
|
+
|
|
28
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get gradient colors based on percentage (smooth transition)
|
|
32
|
+
export function getMemoryGradient(percent) {
|
|
33
|
+
const green = '#00AA00';
|
|
34
|
+
const yellow = '#FFCC00';
|
|
35
|
+
const red = '#FF4444';
|
|
36
|
+
|
|
37
|
+
let startColor, endColor;
|
|
38
|
+
if (percent < 50) {
|
|
39
|
+
const ratio = percent / 50;
|
|
40
|
+
startColor = lerpColor(green, yellow, ratio * 0.5);
|
|
41
|
+
endColor = lerpColor(green, yellow, Math.min(1, ratio * 0.5 + 0.3));
|
|
42
|
+
} else {
|
|
43
|
+
const ratio = (percent - 50) / 50;
|
|
44
|
+
startColor = lerpColor(yellow, red, ratio * 0.7);
|
|
45
|
+
endColor = lerpColor(yellow, red, Math.min(1, ratio * 0.7 + 0.3));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `linear-gradient(to right, ${startColor}, ${endColor})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Update memory bar display
|
|
52
|
+
export function updateMemoryBar(memoryUsage, bgColor) {
|
|
53
|
+
const memoryBar = document.getElementById('memory-bar');
|
|
54
|
+
const memoryBarContainer = document.getElementById('memory-bar-container');
|
|
55
|
+
|
|
56
|
+
if (!memoryUsage || memoryUsage === '-') {
|
|
57
|
+
memoryBarContainer.style.display = 'none';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
memoryBarContainer.style.display = 'block';
|
|
62
|
+
|
|
63
|
+
const isDarkBg = (bgColor === '#0066CC' || bgColor === '#1a1a4e');
|
|
64
|
+
if (isDarkBg) {
|
|
65
|
+
memoryBarContainer.style.borderColor = 'rgba(255, 255, 255, 0.6)';
|
|
66
|
+
memoryBarContainer.style.background = 'rgba(255, 255, 255, 0.2)';
|
|
67
|
+
} else {
|
|
68
|
+
memoryBarContainer.style.borderColor = 'rgba(0, 0, 0, 0.6)';
|
|
69
|
+
memoryBarContainer.style.background = 'rgba(0, 0, 0, 0.3)';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const percent = parseInt(memoryUsage.replace('%', '')) || 0;
|
|
73
|
+
const clampedPercent = Math.min(100, Math.max(0, percent));
|
|
74
|
+
|
|
75
|
+
memoryBar.style.width = clampedPercent + '%';
|
|
76
|
+
memoryBar.style.background = getMemoryGradient(clampedPercent);
|
|
77
|
+
}
|
package/styles.css
CHANGED
|
@@ -64,3 +64,14 @@ html, body {
|
|
|
64
64
|
border-radius: 0 0 12px 12px;
|
|
65
65
|
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/* Version text */
|
|
69
|
+
.version-text {
|
|
70
|
+
position: absolute;
|
|
71
|
+
bottom: 4px;
|
|
72
|
+
width: 100%;
|
|
73
|
+
text-align: center;
|
|
74
|
+
font-size: 9px;
|
|
75
|
+
color: #666;
|
|
76
|
+
font-family: monospace;
|
|
77
|
+
}
|