@pheem49/mint 1.5.0 → 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/README.md +27 -1
- package/main.js +28 -14
- package/mint-cli-logic.js +3 -119
- package/mint-cli.js +497 -23
- 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 +26 -1
- package/src/AI_Brain/Gemini_API.js +147 -46
- package/src/AI_Brain/autonomous_brain.js +2 -1
- package/src/AI_Brain/memory_store.js +299 -3
- package/src/CLI/chat_router.js +18 -6
- package/src/CLI/chat_ui.js +396 -50
- package/src/CLI/code_agent.js +203 -14
- package/src/CLI/image_input.js +90 -0
- package/src/CLI/onboarding.js +72 -15
- package/src/CLI/updater.js +6 -4
- package/src/System/action_executor.js +59 -10
- package/src/System/config_manager.js +31 -1
- package/src/System/granular_automation.js +122 -53
- package/src/System/proactive_loop.js +19 -3
- package/src/System/safety_manager.js +108 -0
- package/src/System/sandbox_runner.js +182 -0
- package/src/System/system_automation.js +127 -81
- package/src/System/system_info.js +70 -0
- package/src/System/tool_registry.js +280 -0
- package/src/System/window_manager.js +4 -2
- package/src/UI/live2d_manager.js +368 -0
- package/src/UI/renderer.js +176 -18
- package/src/UI/styles.css +452 -31
- 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/guide.html +0 -632
- package/docs/index.html +0 -133
- 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/action_executor_safety.test.js +0 -67
- 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/gmail.test.js +0 -135
- package/tests/gmail_auth.test.js +0 -129
- package/tests/google_calendar.test.js +0 -113
- package/tests/google_tts_urls.test.js +0 -24
- package/tests/memory_store.test.js +0 -185
- package/tests/notion.test.js +0 -121
- package/tests/provider_routing.test.js +0 -83
- package/tests/safety_manager.test.js +0 -40
- package/tests/spotify.test.js +0 -201
- package/tests/system_monitor.test.js +0 -37
- package/tests/updater.test.js +0 -32
- package/tests/workspace_manager.test.js +0 -56
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const TOOL_REGISTRY = Object.freeze({
|
|
2
|
+
none: {
|
|
3
|
+
permission: 'safe',
|
|
4
|
+
required: [],
|
|
5
|
+
description: 'No action.'
|
|
6
|
+
},
|
|
7
|
+
web_search: {
|
|
8
|
+
permission: 'safe',
|
|
9
|
+
required: ['query'],
|
|
10
|
+
codeAgentOnly: true,
|
|
11
|
+
description: 'Search the internet when outside knowledge is required.'
|
|
12
|
+
},
|
|
13
|
+
list_files: {
|
|
14
|
+
permission: 'safe',
|
|
15
|
+
required: [],
|
|
16
|
+
codeAgentOnly: true,
|
|
17
|
+
description: 'List files under a workspace-relative path.'
|
|
18
|
+
},
|
|
19
|
+
read_file: {
|
|
20
|
+
permission: 'safe',
|
|
21
|
+
required: ['path'],
|
|
22
|
+
codeAgentOnly: true,
|
|
23
|
+
description: 'Read a workspace file, optionally bounded by startLine/endLine.'
|
|
24
|
+
},
|
|
25
|
+
search_code: {
|
|
26
|
+
permission: 'safe',
|
|
27
|
+
required: ['query'],
|
|
28
|
+
codeAgentOnly: true,
|
|
29
|
+
description: 'Search text in the workspace.'
|
|
30
|
+
},
|
|
31
|
+
find_path: {
|
|
32
|
+
permission: 'safe',
|
|
33
|
+
required: ['query'],
|
|
34
|
+
chatAction: true,
|
|
35
|
+
description: 'Find files or folders by name.'
|
|
36
|
+
},
|
|
37
|
+
run_shell: {
|
|
38
|
+
permission: 'approval',
|
|
39
|
+
required: ['command'],
|
|
40
|
+
codeAgentOnly: true,
|
|
41
|
+
important: true,
|
|
42
|
+
description: 'Run a non-destructive shell command after user approval.'
|
|
43
|
+
},
|
|
44
|
+
apply_patch: {
|
|
45
|
+
permission: 'approval',
|
|
46
|
+
required: ['patch'],
|
|
47
|
+
codeAgentOnly: true,
|
|
48
|
+
important: true,
|
|
49
|
+
description: 'Patch an existing file after user approval.'
|
|
50
|
+
},
|
|
51
|
+
write_file: {
|
|
52
|
+
permission: 'approval',
|
|
53
|
+
required: ['path', 'content'],
|
|
54
|
+
codeAgentOnly: true,
|
|
55
|
+
important: true,
|
|
56
|
+
description: 'Create or replace a file after user approval.'
|
|
57
|
+
},
|
|
58
|
+
ask_user: {
|
|
59
|
+
permission: 'safe',
|
|
60
|
+
required: ['question'],
|
|
61
|
+
codeAgentOnly: true,
|
|
62
|
+
description: 'Ask the user for clarification.'
|
|
63
|
+
},
|
|
64
|
+
open_url: {
|
|
65
|
+
permission: 'safe',
|
|
66
|
+
required: ['target'],
|
|
67
|
+
chatAction: true,
|
|
68
|
+
description: 'Open a URL.'
|
|
69
|
+
},
|
|
70
|
+
search: {
|
|
71
|
+
permission: 'safe',
|
|
72
|
+
required: ['target'],
|
|
73
|
+
chatAction: true,
|
|
74
|
+
description: 'Open a web search.'
|
|
75
|
+
},
|
|
76
|
+
open_app: {
|
|
77
|
+
permission: 'safe',
|
|
78
|
+
required: ['target'],
|
|
79
|
+
chatAction: true,
|
|
80
|
+
description: 'Open a local application.'
|
|
81
|
+
},
|
|
82
|
+
web_automation: {
|
|
83
|
+
permission: 'safe',
|
|
84
|
+
required: ['target'],
|
|
85
|
+
chatAction: true,
|
|
86
|
+
important: true,
|
|
87
|
+
description: 'Perform browser automation.'
|
|
88
|
+
},
|
|
89
|
+
create_folder: {
|
|
90
|
+
permission: 'safe',
|
|
91
|
+
required: ['target'],
|
|
92
|
+
chatAction: true,
|
|
93
|
+
description: 'Create a folder.'
|
|
94
|
+
},
|
|
95
|
+
open_file: {
|
|
96
|
+
permission: 'safe',
|
|
97
|
+
required: ['target'],
|
|
98
|
+
chatAction: true,
|
|
99
|
+
description: 'Open a local file.'
|
|
100
|
+
},
|
|
101
|
+
open_folder: {
|
|
102
|
+
permission: 'safe',
|
|
103
|
+
required: ['target'],
|
|
104
|
+
chatAction: true,
|
|
105
|
+
description: 'Open a local folder.'
|
|
106
|
+
},
|
|
107
|
+
delete_file: {
|
|
108
|
+
permission: 'dangerous',
|
|
109
|
+
required: ['target'],
|
|
110
|
+
chatAction: true,
|
|
111
|
+
important: true,
|
|
112
|
+
description: 'Delete a file only after explicit dangerous-action permission.'
|
|
113
|
+
},
|
|
114
|
+
clipboard_write: {
|
|
115
|
+
permission: 'safe',
|
|
116
|
+
required: ['target'],
|
|
117
|
+
chatAction: true,
|
|
118
|
+
description: 'Write text to clipboard.'
|
|
119
|
+
},
|
|
120
|
+
learn_file: {
|
|
121
|
+
permission: 'safe',
|
|
122
|
+
required: ['target'],
|
|
123
|
+
chatAction: true,
|
|
124
|
+
description: 'Index a file into the knowledge base.'
|
|
125
|
+
},
|
|
126
|
+
learn_folder: {
|
|
127
|
+
permission: 'safe',
|
|
128
|
+
required: ['target'],
|
|
129
|
+
chatAction: true,
|
|
130
|
+
description: 'Index a folder into the knowledge base.'
|
|
131
|
+
},
|
|
132
|
+
system_info: {
|
|
133
|
+
permission: 'safe',
|
|
134
|
+
required: [],
|
|
135
|
+
chatAction: true,
|
|
136
|
+
description: 'Read local system info, or weather when target is a city.'
|
|
137
|
+
},
|
|
138
|
+
plugin: {
|
|
139
|
+
permission: 'safe',
|
|
140
|
+
required: ['pluginName', 'target'],
|
|
141
|
+
chatAction: true,
|
|
142
|
+
description: 'Run a Mint plugin.'
|
|
143
|
+
},
|
|
144
|
+
mcp_tool: {
|
|
145
|
+
permission: 'safe',
|
|
146
|
+
required: ['server', 'target'],
|
|
147
|
+
chatAction: true,
|
|
148
|
+
description: 'Call an MCP tool.'
|
|
149
|
+
},
|
|
150
|
+
mouse_click: {
|
|
151
|
+
permission: 'safe',
|
|
152
|
+
required: ['x', 'y'],
|
|
153
|
+
chatAction: true,
|
|
154
|
+
important: true,
|
|
155
|
+
description: 'Click at screen coordinates.'
|
|
156
|
+
},
|
|
157
|
+
mouse_move: {
|
|
158
|
+
permission: 'safe',
|
|
159
|
+
required: ['x', 'y'],
|
|
160
|
+
chatAction: true,
|
|
161
|
+
description: 'Move the mouse.'
|
|
162
|
+
},
|
|
163
|
+
type_text: {
|
|
164
|
+
permission: 'safe',
|
|
165
|
+
required: ['target'],
|
|
166
|
+
chatAction: true,
|
|
167
|
+
important: true,
|
|
168
|
+
description: 'Type text into the active UI.'
|
|
169
|
+
},
|
|
170
|
+
key_tap: {
|
|
171
|
+
permission: 'safe',
|
|
172
|
+
required: ['target'],
|
|
173
|
+
chatAction: true,
|
|
174
|
+
important: true,
|
|
175
|
+
description: 'Press a key.'
|
|
176
|
+
},
|
|
177
|
+
system_automation: {
|
|
178
|
+
permission: 'approval',
|
|
179
|
+
required: ['target'],
|
|
180
|
+
chatAction: true,
|
|
181
|
+
important: true,
|
|
182
|
+
description: 'Change system settings after approval.'
|
|
183
|
+
},
|
|
184
|
+
finish: {
|
|
185
|
+
permission: 'safe',
|
|
186
|
+
required: ['summary'],
|
|
187
|
+
codeAgentOnly: true,
|
|
188
|
+
description: 'Finish the task and reply.'
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function getTool(name) {
|
|
193
|
+
return TOOL_REGISTRY[name] || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function listToolNames(filter = {}) {
|
|
197
|
+
return Object.entries(TOOL_REGISTRY)
|
|
198
|
+
.filter(([, tool]) => {
|
|
199
|
+
if (filter.chatAction === true && tool.chatAction !== true) return false;
|
|
200
|
+
if (filter.codeAgent === true && tool.chatAction === true && tool.codeAgentOnly !== true) return true;
|
|
201
|
+
return true;
|
|
202
|
+
})
|
|
203
|
+
.map(([name]) => name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function listChatActionNames() {
|
|
207
|
+
return Object.entries(TOOL_REGISTRY)
|
|
208
|
+
.filter(([, tool]) => tool.chatAction === true)
|
|
209
|
+
.map(([name]) => name);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function listCodeAgentActionNames() {
|
|
213
|
+
return Object.entries(TOOL_REGISTRY)
|
|
214
|
+
.filter(([, tool]) => tool.codeAgentOnly === true || tool.chatAction === true || tool.required)
|
|
215
|
+
.map(([name]) => name);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isEmptyToolValue(value) {
|
|
219
|
+
if (value === undefined || value === null) return true;
|
|
220
|
+
if (typeof value === 'string') return value.trim() === '';
|
|
221
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
222
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validateToolInput(action, input = {}) {
|
|
227
|
+
const tool = getTool(action);
|
|
228
|
+
if (!tool) {
|
|
229
|
+
throw new Error(`Unsupported action: ${action}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const missing = (tool.required || []).filter(field => {
|
|
233
|
+
if (!isEmptyToolValue(input[field])) return false;
|
|
234
|
+
if (field === 'target' && (!isEmptyToolValue(input.path) || !isEmptyToolValue(input.query))) return false;
|
|
235
|
+
if (field === 'query' && !isEmptyToolValue(input.target)) return false;
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
if (missing.length > 0) {
|
|
239
|
+
throw new Error(`Action "${action}" is missing required input field(s): ${missing.join(', ')}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (action === 'apply_patch') {
|
|
243
|
+
const patchInput = input.patch || {};
|
|
244
|
+
if (!patchInput.path || !Array.isArray(patchInput.hunks) || patchInput.hunks.length === 0) {
|
|
245
|
+
throw new Error('Action "apply_patch" requires input.patch.path and at least one hunk.');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return tool;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isImportantAction(action) {
|
|
253
|
+
const tool = getTool(action);
|
|
254
|
+
return !!(tool && tool.important);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildChatActionTypeUnion() {
|
|
258
|
+
return ['none', ...listChatActionNames()].filter((name, index, arr) => arr.indexOf(name) === index).map(name => `"${name}"`).join(' | ');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildToolPromptSection() {
|
|
262
|
+
const lines = ['\n\nAVAILABLE BUILT-IN ACTIONS:'];
|
|
263
|
+
for (const name of listChatActionNames()) {
|
|
264
|
+
const tool = getTool(name);
|
|
265
|
+
lines.push(`- ${name}: ${tool.description}`);
|
|
266
|
+
}
|
|
267
|
+
return lines.join('\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
TOOL_REGISTRY,
|
|
272
|
+
getTool,
|
|
273
|
+
listToolNames,
|
|
274
|
+
listChatActionNames,
|
|
275
|
+
listCodeAgentActionNames,
|
|
276
|
+
validateToolInput,
|
|
277
|
+
isImportantAction,
|
|
278
|
+
buildChatActionTypeUnion,
|
|
279
|
+
buildToolPromptSection
|
|
280
|
+
};
|
|
@@ -10,8 +10,10 @@ function createWindowManager(projectRoot) {
|
|
|
10
10
|
|
|
11
11
|
function createMainWindow() {
|
|
12
12
|
mainWindow = new BrowserWindow({
|
|
13
|
-
width:
|
|
14
|
-
height:
|
|
13
|
+
width: 1180,
|
|
14
|
+
height: 860,
|
|
15
|
+
minWidth: 900,
|
|
16
|
+
minHeight: 680,
|
|
15
17
|
icon: path.join(projectRoot, 'assets', 'icon.png'),
|
|
16
18
|
webPreferences: {
|
|
17
19
|
preload: path.join(projectRoot, 'preload.js'),
|
|
@@ -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
|
+
};
|