@pheem49/mint 1.4.2 → 1.5.1
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/GUIDE_TH.md +113 -0
- package/README.md +267 -78
- package/assets/CLI_Screen.png +0 -0
- package/main.js +76 -890
- package/mint-cli-logic.js +3 -107
- package/mint-cli.js +594 -29
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
- package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
- package/package.json +37 -4
- package/src/AI_Brain/Gemini_API.js +223 -65
- package/src/AI_Brain/autonomous_brain.js +11 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +354 -10
- package/src/Automation_Layer/file_operations.js +1 -1
- package/src/CLI/chat_router.js +20 -7
- package/src/CLI/chat_ui.js +596 -825
- package/src/CLI/code_agent.js +347 -56
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/image_input.js +90 -0
- package/src/CLI/list_features.js +2 -0
- package/src/CLI/onboarding.js +364 -55
- package/src/CLI/updater.js +210 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +178 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +71 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/granular_automation.js +122 -53
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +153 -0
- package/src/System/safety_manager.js +273 -0
- package/src/System/sandbox_runner.js +182 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/system_automation.js +127 -81
- package/src/System/system_info.js +70 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/tool_registry.js +280 -0
- package/src/System/window_manager.js +212 -0
- package/src/UI/live2d_manager.js +368 -0
- package/src/UI/renderer.js +208 -24
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +466 -32
- package/.codex +0 -0
- package/docs/assets/Agent_Mint.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/assets/Settings.png +0 -0
- package/docs/assets/icon.png +0 -0
- package/docs/index.html +0 -132
- package/docs/style.css +0 -579
- package/index.html +0 -16
- package/src/UI/index.html +0 -126
- package/tech_news.txt +0 -3
- package/test_knowledge.txt +0 -3
- package/tests/agent_orchestrator.test.js +0 -41
- package/tests/chat_router.test.js +0 -42
- package/tests/code_agent.test.js +0 -69
- package/tests/config_manager.test.js +0 -141
- package/tests/docker.test.js +0 -46
- package/tests/file_operations.test.js +0 -57
- package/tests/memory_store.test.js +0 -185
- package/tests/provider_routing.test.js +0 -67
- package/tests/spotify.test.js +0 -201
- package/tests/system_monitor.test.js +0 -37
- package/tests/workspace_manager.test.js +0 -56
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const { app, BrowserWindow, Tray, Menu, nativeImage, screen } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function createWindowManager(projectRoot) {
|
|
5
|
+
let mainWindow = null;
|
|
6
|
+
let settingsWindow = null;
|
|
7
|
+
let spotlightWindow = null;
|
|
8
|
+
let widgetWindow = null;
|
|
9
|
+
let tray = null;
|
|
10
|
+
|
|
11
|
+
function createMainWindow() {
|
|
12
|
+
mainWindow = new BrowserWindow({
|
|
13
|
+
width: 1180,
|
|
14
|
+
height: 860,
|
|
15
|
+
minWidth: 900,
|
|
16
|
+
minHeight: 680,
|
|
17
|
+
icon: path.join(projectRoot, 'assets', 'icon.png'),
|
|
18
|
+
webPreferences: {
|
|
19
|
+
preload: path.join(projectRoot, 'preload.js'),
|
|
20
|
+
nodeIntegration: false,
|
|
21
|
+
contextIsolation: true,
|
|
22
|
+
},
|
|
23
|
+
frame: false,
|
|
24
|
+
transparent: true,
|
|
25
|
+
resizable: true,
|
|
26
|
+
show: false
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
mainWindow.loadFile(path.join(projectRoot, 'src/UI/index.html'));
|
|
30
|
+
mainWindow.on('ready-to-show', () => mainWindow.show());
|
|
31
|
+
mainWindow.on('close', (event) => {
|
|
32
|
+
if (!app.isQuiting) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
mainWindow.hide();
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
});
|
|
38
|
+
mainWindow.on('focus', () => {
|
|
39
|
+
// clearFloatingUnread(); // Disabled
|
|
40
|
+
});
|
|
41
|
+
mainWindow.on('closed', () => {
|
|
42
|
+
mainWindow = null;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return mainWindow;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTray() {
|
|
49
|
+
const iconPath = path.join(projectRoot, 'assets', 'icon.png');
|
|
50
|
+
let icon = nativeImage.createFromPath(iconPath);
|
|
51
|
+
icon = icon.resize({ width: 16, height: 16 });
|
|
52
|
+
|
|
53
|
+
tray = new Tray(icon);
|
|
54
|
+
tray.setToolTip('Mint AI Assistant');
|
|
55
|
+
tray.setContextMenu(Menu.buildFromTemplate([
|
|
56
|
+
{ label: 'Show App', click: () => { if (mainWindow) mainWindow.show(); } },
|
|
57
|
+
{ label: 'Settings', click: () => { createSettingsWindow(); } },
|
|
58
|
+
{ type: 'separator' },
|
|
59
|
+
{
|
|
60
|
+
label: 'Quit',
|
|
61
|
+
click: () => {
|
|
62
|
+
app.isQuiting = true;
|
|
63
|
+
app.quit();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
]));
|
|
67
|
+
|
|
68
|
+
tray.on('click', toggleMainWindow);
|
|
69
|
+
return tray;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createSettingsWindow() {
|
|
73
|
+
if (settingsWindow) {
|
|
74
|
+
settingsWindow.focus();
|
|
75
|
+
return settingsWindow;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
settingsWindow = new BrowserWindow({
|
|
79
|
+
width: 720,
|
|
80
|
+
height: 620,
|
|
81
|
+
minWidth: 640,
|
|
82
|
+
minHeight: 560,
|
|
83
|
+
icon: path.join(projectRoot, 'assets', 'icon.png'),
|
|
84
|
+
webPreferences: {
|
|
85
|
+
preload: path.join(projectRoot, 'preload-settings.js'),
|
|
86
|
+
nodeIntegration: false,
|
|
87
|
+
contextIsolation: true,
|
|
88
|
+
},
|
|
89
|
+
frame: false,
|
|
90
|
+
transparent: true,
|
|
91
|
+
resizable: true,
|
|
92
|
+
parent: mainWindow,
|
|
93
|
+
});
|
|
94
|
+
settingsWindow.loadFile(path.join(projectRoot, 'src/UI/settings.html'));
|
|
95
|
+
settingsWindow.on('closed', () => { settingsWindow = null; });
|
|
96
|
+
return settingsWindow;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createSpotlightWindow() {
|
|
100
|
+
if (spotlightWindow) {
|
|
101
|
+
spotlightWindow.show();
|
|
102
|
+
return spotlightWindow;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
|
106
|
+
const windowWidth = 600;
|
|
107
|
+
const windowHeight = 80;
|
|
108
|
+
|
|
109
|
+
spotlightWindow = new BrowserWindow({
|
|
110
|
+
width: windowWidth,
|
|
111
|
+
height: windowHeight,
|
|
112
|
+
x: Math.floor((screenWidth - windowWidth) / 2),
|
|
113
|
+
y: Math.floor(screenHeight * 0.25),
|
|
114
|
+
frame: false,
|
|
115
|
+
transparent: true,
|
|
116
|
+
alwaysOnTop: true,
|
|
117
|
+
skipTaskbar: true,
|
|
118
|
+
show: false,
|
|
119
|
+
webPreferences: {
|
|
120
|
+
preload: path.join(projectRoot, 'src/UI/preload-spotlight.js'),
|
|
121
|
+
nodeIntegration: false,
|
|
122
|
+
contextIsolation: true,
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
spotlightWindow.loadFile(path.join(projectRoot, 'src/UI/spotlight.html'));
|
|
127
|
+
spotlightWindow.on('blur', () => spotlightWindow.hide());
|
|
128
|
+
spotlightWindow.on('closed', () => { spotlightWindow = null; });
|
|
129
|
+
return spotlightWindow;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createWidgetWindow() {
|
|
133
|
+
if (widgetWindow) return widgetWindow;
|
|
134
|
+
|
|
135
|
+
widgetWindow = new BrowserWindow({
|
|
136
|
+
width: 150,
|
|
137
|
+
height: 150,
|
|
138
|
+
frame: false,
|
|
139
|
+
transparent: true,
|
|
140
|
+
resizable: false,
|
|
141
|
+
alwaysOnTop: true,
|
|
142
|
+
skipTaskbar: true,
|
|
143
|
+
show: true,
|
|
144
|
+
webPreferences: {
|
|
145
|
+
preload: path.join(projectRoot, 'src/UI/preload-widget.js'),
|
|
146
|
+
nodeIntegration: false,
|
|
147
|
+
contextIsolation: true
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
widgetWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
152
|
+
widgetWindow.setAlwaysOnTop(true, 'floating');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
156
|
+
const { width, x, y } = primaryDisplay.workArea;
|
|
157
|
+
widgetWindow.setPosition(x + width - 150 - 40, y + 40);
|
|
158
|
+
} catch (_) {}
|
|
159
|
+
|
|
160
|
+
widgetWindow.loadFile(path.join(projectRoot, 'src/UI/widget.html'));
|
|
161
|
+
widgetWindow.on('closed', () => { widgetWindow = null; });
|
|
162
|
+
return widgetWindow;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toggleMainWindow() {
|
|
166
|
+
if (!mainWindow) return;
|
|
167
|
+
if (mainWindow.isVisible()) {
|
|
168
|
+
mainWindow.hide();
|
|
169
|
+
} else {
|
|
170
|
+
mainWindow.show();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toggleSpotlightWindow() {
|
|
175
|
+
if (spotlightWindow && spotlightWindow.isVisible()) {
|
|
176
|
+
spotlightWindow.hide();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
createSpotlightWindow().show();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function closeWidgetWindow() {
|
|
183
|
+
if (widgetWindow && !widgetWindow.isDestroyed()) {
|
|
184
|
+
widgetWindow.close();
|
|
185
|
+
widgetWindow = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ensureWidgetWindow() {
|
|
190
|
+
if (!widgetWindow || widgetWindow.isDestroyed()) {
|
|
191
|
+
createWidgetWindow();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
createMainWindow,
|
|
197
|
+
createTray,
|
|
198
|
+
createSettingsWindow,
|
|
199
|
+
createSpotlightWindow,
|
|
200
|
+
createWidgetWindow,
|
|
201
|
+
toggleMainWindow,
|
|
202
|
+
toggleSpotlightWindow,
|
|
203
|
+
closeWidgetWindow,
|
|
204
|
+
ensureWidgetWindow,
|
|
205
|
+
getMainWindow: () => mainWindow,
|
|
206
|
+
getSettingsWindow: () => settingsWindow,
|
|
207
|
+
getSpotlightWindow: () => spotlightWindow,
|
|
208
|
+
getWidgetWindow: () => widgetWindow
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { createWindowManager };
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live2DManager - Encapsulates Live2D model loading, fitting, and lip-sync logic.
|
|
3
|
+
*/
|
|
4
|
+
window.Live2DManager = {
|
|
5
|
+
app: null,
|
|
6
|
+
model: null,
|
|
7
|
+
resizeObserver: null,
|
|
8
|
+
lipSyncInterval: null,
|
|
9
|
+
expIndex: 0,
|
|
10
|
+
lastInteractionAt: 0,
|
|
11
|
+
expressionToastTimeout: null,
|
|
12
|
+
expressionParamIds: [
|
|
13
|
+
'Param54',
|
|
14
|
+
'Param55',
|
|
15
|
+
'Param68',
|
|
16
|
+
'Param76',
|
|
17
|
+
'Param91',
|
|
18
|
+
'Param93',
|
|
19
|
+
'Param94',
|
|
20
|
+
'Param96',
|
|
21
|
+
'ParamAngleY',
|
|
22
|
+
'ParamAngleZ',
|
|
23
|
+
'ParamEyeBallX',
|
|
24
|
+
'ParamEyeBallY',
|
|
25
|
+
'ParamMouthForm',
|
|
26
|
+
'ParamMouthOpenY'
|
|
27
|
+
],
|
|
28
|
+
expressionNames: [
|
|
29
|
+
{ id: null, label: 'Normal' },
|
|
30
|
+
{ id: 'Apron', label: 'Apron' },
|
|
31
|
+
{ id: 'Dazed', label: 'Dazed' },
|
|
32
|
+
{ id: 'Photo', label: 'Photo' },
|
|
33
|
+
{ id: 'Glasses', label: 'Glasses' },
|
|
34
|
+
{ id: 'Pen', label: 'Writing' },
|
|
35
|
+
{ id: 'Click', label: 'Blush' },
|
|
36
|
+
{ id: 'CatFilter', label: 'Cat Ears' },
|
|
37
|
+
{ id: 'DazedEyes', label: 'Dazed Eyes' }
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
async loadModel(mountEl, statusEl, shellEl) {
|
|
41
|
+
this.statusEl = statusEl; // Store for later use
|
|
42
|
+
if (!mountEl) return;
|
|
43
|
+
if (statusEl) {
|
|
44
|
+
statusEl.classList.remove('is-error');
|
|
45
|
+
statusEl.innerHTML = `
|
|
46
|
+
<div class="loader-dots">
|
|
47
|
+
<span></span><span></span><span></span>
|
|
48
|
+
</div>
|
|
49
|
+
<div style="font-size: 0.7rem; opacity: 0.8; letter-spacing: 0.05em;">SYNCHRONIZING MINT...</div>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
if (!window.PIXI || !window.PIXI.live2d) {
|
|
53
|
+
const message = 'Live2D runtime is not available.';
|
|
54
|
+
console.error(message);
|
|
55
|
+
if (statusEl) {
|
|
56
|
+
statusEl.classList.add('is-error');
|
|
57
|
+
statusEl.textContent = message;
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
window.PIXI.live2d.Live2DModel.registerTicker(window.PIXI.Ticker);
|
|
64
|
+
|
|
65
|
+
this.app = new window.PIXI.Application({
|
|
66
|
+
autoDensity: true,
|
|
67
|
+
antialias: true,
|
|
68
|
+
backgroundAlpha: 0,
|
|
69
|
+
resizeTo: mountEl,
|
|
70
|
+
resolution: window.devicePixelRatio || 1
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
mountEl.prepend(this.app.view);
|
|
74
|
+
|
|
75
|
+
const modelUrl = new URL('../../models/Shiroko_Model/Shiroko/Shiroko_Core/%E9%9D%A2%E9%A5%BC0.model3.json', window.location.href).href;
|
|
76
|
+
this.model = await window.PIXI.live2d.Live2DModel.from(modelUrl, {
|
|
77
|
+
autoInteract: true
|
|
78
|
+
});
|
|
79
|
+
this.expressionToastEl = document.getElementById('expression-toast');
|
|
80
|
+
|
|
81
|
+
this.model.anchor.set(0.5, 0.5);
|
|
82
|
+
this.app.stage.addChild(this.model);
|
|
83
|
+
|
|
84
|
+
// -- Interaction Setup --
|
|
85
|
+
this.model.interactive = true;
|
|
86
|
+
this.model.buttonMode = true;
|
|
87
|
+
|
|
88
|
+
// Tap Interaction. This model does not define Cubism HitAreas, so use
|
|
89
|
+
// normalized model coordinates to provide stable region reactions.
|
|
90
|
+
this.model.on('pointertap', (e) => this.handleModelTap(e));
|
|
91
|
+
this.model.on('hit', (hitAreaNames) => {
|
|
92
|
+
console.log(`[Live2D] Runtime hit detected: ${hitAreaNames}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const fitModel = () => {
|
|
96
|
+
if (!this.model || !mountEl) return;
|
|
97
|
+
const mountWidth = mountEl.clientWidth || 460;
|
|
98
|
+
const mountHeight = mountEl.clientHeight || 620;
|
|
99
|
+
this.app.renderer.resize(mountWidth, mountHeight);
|
|
100
|
+
|
|
101
|
+
const internal = this.model.internalModel || {};
|
|
102
|
+
const modelWidth = internal.width || internal.originalWidth || this.model.width || 1;
|
|
103
|
+
const modelHeight = internal.height || internal.originalHeight || this.model.height || 1;
|
|
104
|
+
const widthScale = mountWidth / Math.max(modelWidth, 1);
|
|
105
|
+
const heightScale = mountHeight / Math.max(modelHeight, 1);
|
|
106
|
+
|
|
107
|
+
// Reduced zoom to 2.0 as requested
|
|
108
|
+
const scale = Math.min(widthScale, heightScale) * 1.8;
|
|
109
|
+
|
|
110
|
+
this.model.scale.set(scale);
|
|
111
|
+
// Adjusted Y offset to 1.0 as requested
|
|
112
|
+
this.model.position.set(mountWidth / 2, mountHeight / 2 + mountHeight * 0.6);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
requestAnimationFrame(() => {
|
|
116
|
+
fitModel();
|
|
117
|
+
requestAnimationFrame(fitModel);
|
|
118
|
+
});
|
|
119
|
+
this.resizeObserver = new ResizeObserver(fitModel);
|
|
120
|
+
this.resizeObserver.observe(mountEl);
|
|
121
|
+
|
|
122
|
+
shellEl?.classList.add('is-live2d-ready');
|
|
123
|
+
if (statusEl) statusEl.textContent = '';
|
|
124
|
+
this.model.motion('Idle', 0).catch(() => {});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Failed to load Live2D model:', error);
|
|
127
|
+
shellEl?.classList.remove('is-live2d-ready');
|
|
128
|
+
if (statusEl) {
|
|
129
|
+
statusEl.classList.add('is-error');
|
|
130
|
+
statusEl.textContent = `Live2D failed: ${error && error.message ? error.message : String(error)}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
showStatus(text, duration = 2000) {
|
|
136
|
+
if (!this.statusEl) return;
|
|
137
|
+
this.statusEl.textContent = text;
|
|
138
|
+
this.statusEl.style.opacity = '1';
|
|
139
|
+
|
|
140
|
+
if (this.statusTimeout) clearTimeout(this.statusTimeout);
|
|
141
|
+
this.statusTimeout = setTimeout(() => {
|
|
142
|
+
this.statusEl.style.opacity = '0';
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
if (this.statusEl.style.opacity === '0') this.statusEl.textContent = '';
|
|
145
|
+
}, 500);
|
|
146
|
+
}, duration);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
handleModelTap(event) {
|
|
150
|
+
if (!this.model) return;
|
|
151
|
+
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (now - this.lastInteractionAt < 3000) return;
|
|
154
|
+
|
|
155
|
+
const region = this.getInteractionRegion(event);
|
|
156
|
+
if (!region) return;
|
|
157
|
+
this.lastInteractionAt = now;
|
|
158
|
+
const expressionId = region.expression || 'Click';
|
|
159
|
+
|
|
160
|
+
console.log(`[Live2D] Interaction: ${region.id}`, region);
|
|
161
|
+
this.applyExpression(expressionId);
|
|
162
|
+
this.showStatus(region.label, 2500);
|
|
163
|
+
|
|
164
|
+
window.dispatchEvent(new CustomEvent('live2d-model-interaction', {
|
|
165
|
+
detail: {
|
|
166
|
+
region: region.id,
|
|
167
|
+
label: region.label,
|
|
168
|
+
prompt: region.prompt
|
|
169
|
+
}
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
const currentIdx = this.expIndex === 0 ? 0 : this.expIndex;
|
|
174
|
+
const prevExp = this.expressionNames[currentIdx]?.id;
|
|
175
|
+
this.applyExpression(prevExp);
|
|
176
|
+
}, 2000);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
getInteractionRegion(event) {
|
|
180
|
+
try {
|
|
181
|
+
const point = this.getPointerViewportPoint(event);
|
|
182
|
+
if (!point) return null;
|
|
183
|
+
const { x, y } = point;
|
|
184
|
+
|
|
185
|
+
if (this.isPointInZone(x, y, 0.37, 0.395, 0.25, 0.13)) {
|
|
186
|
+
return {
|
|
187
|
+
id: 'face',
|
|
188
|
+
label: 'Cat Ears',
|
|
189
|
+
expression: 'CatFilter',
|
|
190
|
+
prompt: 'The user poked Mint model on the cheek. Reply briefly, shyly or with a light tease. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this.isPointInZone(x, y, 0.35, 0.30, 0.29, 0.09)) {
|
|
195
|
+
return {
|
|
196
|
+
id: 'head',
|
|
197
|
+
label: 'Head Pat',
|
|
198
|
+
expression: 'Dazed',
|
|
199
|
+
prompt: 'The user patted Mint model on the head. Reply briefly in a cute, slightly shy way. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const isLeftHand = this.isPointInZone(x, y, 0.17, 0.65, 0.19, 0.17);
|
|
204
|
+
const isRightHand = this.isPointInZone(x, y, 0.70, 0.67, 0.17, 0.17);
|
|
205
|
+
if (isLeftHand || isRightHand) {
|
|
206
|
+
return {
|
|
207
|
+
id: isLeftHand ? 'left-hand' : 'right-hand',
|
|
208
|
+
label: 'Hand Tap',
|
|
209
|
+
expression: 'Pen',
|
|
210
|
+
prompt: 'The user tapped Mint model’s hand. Reply briefly as if ready to help or take a request. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (this.isPointInZone(x, y, 0.31, 0.76, 0.38, 0.24)) {
|
|
215
|
+
return {
|
|
216
|
+
id: 'lower-body',
|
|
217
|
+
label: 'Careful',
|
|
218
|
+
expression: 'Photo',
|
|
219
|
+
prompt: 'The user touched the lower body area of Mint model. Reply briefly in a shy, playful way, similar to “hehe~ what are you playing at, that makes me blush,” then gently invite the user back to chatting or work. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (this.isPointInZone(x, y, 0.36, 0.55, 0.29, 0.15)) {
|
|
224
|
+
return {
|
|
225
|
+
id: 'body',
|
|
226
|
+
label: 'Shoulder Tap',
|
|
227
|
+
expression: 'Click',
|
|
228
|
+
prompt: 'The user tapped Mint model’s body or shoulder. Reply briefly as if turning toward the user and asking what they need help with. Use the same language as the user’s recent conversation; do not switch to Thai unless the user has been speaking Thai.'
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('[Live2D] Failed to resolve interaction region:', error);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
getPointerViewportPoint(event) {
|
|
240
|
+
const originalEvent = event?.data?.originalEvent;
|
|
241
|
+
const rect = this.app?.view?.getBoundingClientRect?.();
|
|
242
|
+
if (originalEvent && rect) {
|
|
243
|
+
return {
|
|
244
|
+
x: (originalEvent.clientX - rect.left) / Math.max(rect.width, 1),
|
|
245
|
+
y: (originalEvent.clientY - rect.top) / Math.max(rect.height, 1)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const globalPoint = event?.data?.global;
|
|
250
|
+
const screen = this.app?.screen;
|
|
251
|
+
if (!globalPoint || !screen) return null;
|
|
252
|
+
return {
|
|
253
|
+
x: globalPoint.x / Math.max(screen.width, 1),
|
|
254
|
+
y: globalPoint.y / Math.max(screen.height, 1)
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
isPointInZone(x, y, left, top, width, height) {
|
|
259
|
+
return x >= left && x <= left + width && y >= top && y <= top + height;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
cycleExpression() {
|
|
263
|
+
if (!this.model) return;
|
|
264
|
+
this.expIndex = (this.expIndex + 1) % this.expressionNames.length;
|
|
265
|
+
const nextExp = this.expressionNames[this.expIndex];
|
|
266
|
+
|
|
267
|
+
console.log(`[Live2D] Triggering expression: ${nextExp.id} (${nextExp.label})`);
|
|
268
|
+
this.applyExpression(nextExp.id);
|
|
269
|
+
|
|
270
|
+
this.showStatus(nextExp.label);
|
|
271
|
+
this.showExpressionToast(`Expression: ${nextExp.label}`);
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
showExpressionToast(text, duration = 1600) {
|
|
275
|
+
const toast = this.expressionToastEl || document.getElementById('expression-toast');
|
|
276
|
+
if (!toast) return;
|
|
277
|
+
|
|
278
|
+
this.expressionToastEl = toast;
|
|
279
|
+
toast.textContent = text;
|
|
280
|
+
toast.classList.add('is-visible');
|
|
281
|
+
|
|
282
|
+
if (this.expressionToastTimeout) clearTimeout(this.expressionToastTimeout);
|
|
283
|
+
this.expressionToastTimeout = setTimeout(() => {
|
|
284
|
+
toast.classList.remove('is-visible');
|
|
285
|
+
}, duration);
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
applyExpression(expressionId) {
|
|
289
|
+
if (!this.model) return;
|
|
290
|
+
|
|
291
|
+
this.resetExpressionParams();
|
|
292
|
+
if (!expressionId) {
|
|
293
|
+
this.clearExpressionState();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
this.model.expression(expressionId);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error(`[Live2D] Failed to apply expression: ${expressionId}`, error);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
resetExpressionParams() {
|
|
305
|
+
const core = this.model?.internalModel?.coreModel;
|
|
306
|
+
if (!core) return;
|
|
307
|
+
|
|
308
|
+
this.expressionParamIds.forEach(id => {
|
|
309
|
+
try { core.setParameterValueById(id, 0); } catch (_) {}
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
clearExpressionState() {
|
|
314
|
+
const expressionManager =
|
|
315
|
+
this.model?.internalModel?.motionManager?.expressionManager ||
|
|
316
|
+
this.model?.internalModel?.expressionManager;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
if (expressionManager?.defaultExpression) {
|
|
320
|
+
expressionManager.currentExpression = expressionManager.defaultExpression;
|
|
321
|
+
expressionManager.reserveExpressionIndex = -1;
|
|
322
|
+
expressionManager.resetExpression();
|
|
323
|
+
} else {
|
|
324
|
+
this.model.expression(null);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('[Live2D] Failed to clear expression state:', error);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.resetExpressionParams();
|
|
331
|
+
requestAnimationFrame(() => this.resetExpressionParams());
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
startLipSync() {
|
|
335
|
+
if (!this.model || this.lipSyncInterval) return;
|
|
336
|
+
|
|
337
|
+
this.model.motion('Speak', 0).catch(() => {});
|
|
338
|
+
|
|
339
|
+
this.lipSyncInterval = setInterval(() => {
|
|
340
|
+
if (!this.model) return;
|
|
341
|
+
const value = Math.random() * 0.8;
|
|
342
|
+
if (this.model.internalModel && this.model.internalModel.coreModel) {
|
|
343
|
+
const core = this.model.internalModel.coreModel;
|
|
344
|
+
const mouthIds = ['ParamMouthOpenY', 'ParamMouthOpen', 'PARAM_MOUTH_OPEN_Y'];
|
|
345
|
+
mouthIds.forEach(id => {
|
|
346
|
+
try { core.setParameterValueById(id, value); } catch(e) {}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}, 80);
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
stopLipSync() {
|
|
353
|
+
if (this.lipSyncInterval) {
|
|
354
|
+
clearInterval(this.lipSyncInterval);
|
|
355
|
+
this.lipSyncInterval = null;
|
|
356
|
+
}
|
|
357
|
+
if (this.model) {
|
|
358
|
+
if (this.model.internalModel && this.model.internalModel.coreModel) {
|
|
359
|
+
const core = this.model.internalModel.coreModel;
|
|
360
|
+
const mouthIds = ['ParamMouthOpenY', 'ParamMouthOpen', 'PARAM_MOUTH_OPEN_Y'];
|
|
361
|
+
mouthIds.forEach(id => {
|
|
362
|
+
try { core.setParameterValueById(id, 0); } catch(e) {}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
this.model.motion('Idle', 0).catch(() => {});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|