@meechi-ai/core 1.0.3 → 1.0.4
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/dist/hooks/useMeechi.js +53 -24
- package/dist/lib/ai/registry.d.ts +5 -0
- package/dist/lib/ai/registry.js +149 -11
- package/dist/lib/consent/modelConsent.d.ts +17 -0
- package/dist/lib/consent/modelConsent.js +47 -0
- package/dist/lib/consent/showLicenseModal.d.ts +8 -0
- package/dist/lib/consent/showLicenseModal.js +37 -0
- package/dist/lib/device/deviceDetection.d.ts +20 -0
- package/dist/lib/device/deviceDetection.js +78 -0
- package/package.json +2 -2
package/dist/hooks/useMeechi.js
CHANGED
|
@@ -6,6 +6,9 @@ import { settingsManager } from '../lib/settings';
|
|
|
6
6
|
import { SYSTEM_PROMPT } from '../lib/ai/prompts';
|
|
7
7
|
import { mcpClient } from '../lib/mcp/McpClient';
|
|
8
8
|
import { AVAILABLE_MODELS } from '../lib/ai/registry';
|
|
9
|
+
import { detectDeviceCapabilities, getRecommendedModel, shouldWarnBeforeDownload } from '../lib/device/deviceDetection';
|
|
10
|
+
import { modelConsentManager } from '../lib/consent/modelConsent';
|
|
11
|
+
import { showLicenseModal } from '../lib/consent/showLicenseModal';
|
|
9
12
|
import { parseToolCalls } from '../lib/ai/parsing';
|
|
10
13
|
export function useMeechi() {
|
|
11
14
|
const [isLowPowerDevice, setIsLowPowerDevice] = useState(true);
|
|
@@ -16,10 +19,10 @@ export function useMeechi() {
|
|
|
16
19
|
const [activeMemories, setActiveMemories] = useState([]);
|
|
17
20
|
const [mode, _setMode] = useState('chat'); // Default to simple chat
|
|
18
21
|
const [isReady, setIsReady] = useState(false);
|
|
22
|
+
const [deviceCapabilities, setDeviceCapabilities] = useState(null);
|
|
19
23
|
// Initialization Logic
|
|
20
24
|
useEffect(() => {
|
|
21
25
|
const init = async () => {
|
|
22
|
-
var _a, _b;
|
|
23
26
|
const config = await settingsManager.getConfig();
|
|
24
27
|
// Check Rate Limit Persisted
|
|
25
28
|
const persisted = localStorage.getItem('meechi_rate_limit_cooldown');
|
|
@@ -61,34 +64,60 @@ export function useMeechi() {
|
|
|
61
64
|
}
|
|
62
65
|
if (!config.localAI.enabled)
|
|
63
66
|
return;
|
|
64
|
-
//
|
|
67
|
+
// Device & Network Detection -> Smart Defaults
|
|
65
68
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Heuristic: Apple or RTX 30/40 series -> High Power
|
|
73
|
-
const isHighPower = gpuInfo.vendor === 'apple' ||
|
|
74
|
-
/RTX (3090|4080|4090|A6000)/i.test(gpuInfo.renderer || "");
|
|
75
|
-
setIsLowPowerDevice(!isHighPower);
|
|
76
|
-
// Model Selection Logic via Registry
|
|
77
|
-
// Default: 1B for Low Power, 8B for High Power
|
|
78
|
-
const defaultLow = AVAILABLE_MODELS.local.find(m => m.low_power && m.family === 'llama').id;
|
|
79
|
-
// const defaultHigh = AVAILABLE_MODELS.find(m => !m.low_power && m.family === 'llama')!.id;
|
|
80
|
-
let modelId = defaultLow;
|
|
69
|
+
const capabilities = await detectDeviceCapabilities();
|
|
70
|
+
setDeviceCapabilities(capabilities);
|
|
71
|
+
// Keep low power heuristic as fallback
|
|
72
|
+
setIsLowPowerDevice(capabilities.isMobile || false);
|
|
73
|
+
const recommended = getRecommendedModel(capabilities);
|
|
74
|
+
let modelId = recommended;
|
|
81
75
|
const configModel = config.localAI.model;
|
|
82
|
-
if (
|
|
83
|
-
// FORCE 1B Default (User Request: "Make the 1B the default")
|
|
84
|
-
// We ignore high power detection for stability.
|
|
85
|
-
modelId = defaultLow;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
// Check if the configModel exists in registry, otherwise fallback
|
|
76
|
+
if (configModel && configModel !== 'Auto') {
|
|
89
77
|
const exists = AVAILABLE_MODELS.local.find(m => m.id === configModel);
|
|
90
78
|
modelId = exists ? exists.id : configModel;
|
|
91
79
|
}
|
|
80
|
+
// If user explicitly selected a model that requires consent, check stored consent
|
|
81
|
+
let selectedConfig = AVAILABLE_MODELS.local.find(m => m.id === modelId);
|
|
82
|
+
if ((selectedConfig === null || selectedConfig === void 0 ? void 0 : selectedConfig.preConsentRequired) && configModel && configModel !== 'Auto') {
|
|
83
|
+
const has = modelConsentManager.hasConsent(modelId);
|
|
84
|
+
if (!has) {
|
|
85
|
+
// Prompt user for consent via modal. If accepted, persist consent; otherwise fallback to recommended.
|
|
86
|
+
setLocalAIStatus(`Consent required for ${selectedConfig.name}`);
|
|
87
|
+
setLoadedModel(null);
|
|
88
|
+
try {
|
|
89
|
+
const accepted = await showLicenseModal({
|
|
90
|
+
modelId: selectedConfig.id,
|
|
91
|
+
modelName: selectedConfig.name,
|
|
92
|
+
license: selectedConfig.license,
|
|
93
|
+
termsUrl: selectedConfig.termsUrl,
|
|
94
|
+
estimatedDownloadMB: selectedConfig.estimatedDownloadMB
|
|
95
|
+
});
|
|
96
|
+
if (accepted) {
|
|
97
|
+
modelConsentManager.setConsent(selectedConfig.id, selectedConfig.license || '');
|
|
98
|
+
// proceed with this model
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// fallback to recommended default
|
|
102
|
+
modelId = recommended;
|
|
103
|
+
setLocalAIStatus(`Falling back to ${recommended}`);
|
|
104
|
+
setLoadedModel(recommended);
|
|
105
|
+
// Update selectedConfig to reflect fallback model
|
|
106
|
+
selectedConfig = AVAILABLE_MODELS.local.find(m => m.id === modelId) || selectedConfig;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
setLocalAIStatus(`Consent dialog failed`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// If download would be warned (metered or large on mobile) and user didn't explicitly pick this model, block auto-init
|
|
116
|
+
if (shouldWarnBeforeDownload(selectedConfig === null || selectedConfig === void 0 ? void 0 : selectedConfig.estimatedDownloadMB, capabilities) && (!configModel || configModel === 'Auto')) {
|
|
117
|
+
setLocalAIStatus(`Download blocked: large model on metered/low device`);
|
|
118
|
+
setLoadedModel(null);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
92
121
|
setLoadedModel(modelId);
|
|
93
122
|
// Initialize WebLLM
|
|
94
123
|
const currentId = localLlmService.getModelId();
|
|
@@ -5,6 +5,10 @@ export interface ModelConfig {
|
|
|
5
5
|
vram_required_mb: number;
|
|
6
6
|
low_power: boolean;
|
|
7
7
|
context_window: number;
|
|
8
|
+
license?: string;
|
|
9
|
+
termsUrl?: string;
|
|
10
|
+
preConsentRequired?: boolean;
|
|
11
|
+
estimatedDownloadMB?: number;
|
|
8
12
|
}
|
|
9
13
|
interface LocalModelConfig extends ModelConfig {
|
|
10
14
|
id: string;
|
|
@@ -22,6 +26,7 @@ interface CloudModelConfig {
|
|
|
22
26
|
export declare const AVAILABLE_MODELS: {
|
|
23
27
|
local: LocalModelConfig[];
|
|
24
28
|
groq: CloudModelConfig[];
|
|
29
|
+
openai: CloudModelConfig[];
|
|
25
30
|
gemini: CloudModelConfig[];
|
|
26
31
|
};
|
|
27
32
|
export declare function getModelConfig(modelId: string): LocalModelConfig | undefined;
|
package/dist/lib/ai/registry.js
CHANGED
|
@@ -1,29 +1,162 @@
|
|
|
1
1
|
import { SYSTEM_PROMPT } from './prompts';
|
|
2
2
|
export const AVAILABLE_MODELS = {
|
|
3
3
|
local: [
|
|
4
|
+
// Ordered small -> large
|
|
5
|
+
{
|
|
6
|
+
id: 'Qwen2.5-0.5B-Instruct-q4f16_1-MLC',
|
|
7
|
+
name: 'Qwen 2.5 0.5B',
|
|
8
|
+
family: 'qwen',
|
|
9
|
+
vram_required_mb: 800,
|
|
10
|
+
low_power: true,
|
|
11
|
+
context_window: 32768,
|
|
12
|
+
license: 'apache-2.0',
|
|
13
|
+
termsUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
14
|
+
preConsentRequired: false,
|
|
15
|
+
estimatedDownloadMB: 800
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'TinyLlama-1.1B-Chat-v1.0-q4f16_1-MLC',
|
|
19
|
+
name: 'TinyLlama 1.1B',
|
|
20
|
+
family: 'llama',
|
|
21
|
+
vram_required_mb: 1000,
|
|
22
|
+
low_power: true,
|
|
23
|
+
context_window: 2048,
|
|
24
|
+
license: 'llama-3.2-community',
|
|
25
|
+
termsUrl: 'https://llama.meta.com/llama3/license/',
|
|
26
|
+
preConsentRequired: true,
|
|
27
|
+
estimatedDownloadMB: 1000
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'Qwen2.5-1.5B-Instruct-q4f16_1-MLC',
|
|
31
|
+
name: 'Qwen 2.5 1.5B',
|
|
32
|
+
family: 'qwen',
|
|
33
|
+
vram_required_mb: 1600,
|
|
34
|
+
low_power: true,
|
|
35
|
+
context_window: 32768,
|
|
36
|
+
license: 'apache-2.0',
|
|
37
|
+
termsUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
38
|
+
preConsentRequired: false,
|
|
39
|
+
estimatedDownloadMB: 1600
|
|
40
|
+
},
|
|
4
41
|
{
|
|
5
42
|
id: 'Llama-3.2-1B-Instruct-q4f16_1-MLC',
|
|
6
|
-
name: 'Llama 3.2 1B
|
|
43
|
+
name: 'Llama 3.2 1B',
|
|
7
44
|
family: 'llama',
|
|
8
|
-
vram_required_mb:
|
|
45
|
+
vram_required_mb: 1800,
|
|
46
|
+
low_power: false,
|
|
47
|
+
context_window: 4096,
|
|
48
|
+
license: 'llama-3.2-community',
|
|
49
|
+
termsUrl: 'https://llama.meta.com/llama3/license/',
|
|
50
|
+
preConsentRequired: true,
|
|
51
|
+
estimatedDownloadMB: 1800
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'Phi-3-mini-4k-instruct-q4f16_1-MLC',
|
|
55
|
+
name: 'Phi-3 Mini',
|
|
56
|
+
family: 'phi',
|
|
57
|
+
vram_required_mb: 1800,
|
|
9
58
|
low_power: true,
|
|
10
|
-
context_window: 4096
|
|
59
|
+
context_window: 4096,
|
|
60
|
+
license: 'mit',
|
|
61
|
+
termsUrl: 'https://opensource.org/licenses/MIT',
|
|
62
|
+
preConsentRequired: false,
|
|
63
|
+
estimatedDownloadMB: 1800
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'gemma-2b-it-q4f16_1-MLC',
|
|
67
|
+
name: 'Gemma 2B',
|
|
68
|
+
family: 'gemma',
|
|
69
|
+
vram_required_mb: 2000,
|
|
70
|
+
low_power: true,
|
|
71
|
+
context_window: 8192,
|
|
72
|
+
license: 'gemma-terms',
|
|
73
|
+
termsUrl: 'https://ai.google.dev/gemma/terms',
|
|
74
|
+
preConsentRequired: true,
|
|
75
|
+
estimatedDownloadMB: 2000
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'Qwen2.5-3B-Instruct-q4f16_1-MLC',
|
|
79
|
+
name: 'Qwen 2.5 3B',
|
|
80
|
+
family: 'qwen',
|
|
81
|
+
vram_required_mb: 3000,
|
|
82
|
+
low_power: false,
|
|
83
|
+
context_window: 32768,
|
|
84
|
+
license: 'apache-2.0',
|
|
85
|
+
termsUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
86
|
+
preConsentRequired: false,
|
|
87
|
+
estimatedDownloadMB: 3000
|
|
11
88
|
},
|
|
12
89
|
{
|
|
13
90
|
id: 'Llama-3.2-3B-Instruct-q4f16_1-MLC',
|
|
14
|
-
name: 'Llama 3.2 3B
|
|
91
|
+
name: 'Llama 3.2 3B',
|
|
15
92
|
family: 'llama',
|
|
16
|
-
vram_required_mb:
|
|
93
|
+
vram_required_mb: 3200,
|
|
17
94
|
low_power: false,
|
|
18
|
-
context_window: 8192
|
|
95
|
+
context_window: 8192,
|
|
96
|
+
license: 'llama-3.2-community',
|
|
97
|
+
termsUrl: 'https://llama.meta.com/llama3/license/',
|
|
98
|
+
preConsentRequired: true,
|
|
99
|
+
estimatedDownloadMB: 3200
|
|
19
100
|
},
|
|
20
101
|
{
|
|
21
|
-
id: '
|
|
22
|
-
name: '
|
|
102
|
+
id: 'Qwen3-4B-q4f16_1-MLC',
|
|
103
|
+
name: 'Qwen3 4B',
|
|
104
|
+
family: 'qwen',
|
|
105
|
+
vram_required_mb: 4000,
|
|
106
|
+
low_power: false,
|
|
107
|
+
context_window: 32768,
|
|
108
|
+
license: 'apache-2.0',
|
|
109
|
+
termsUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
110
|
+
preConsentRequired: false,
|
|
111
|
+
estimatedDownloadMB: 4000
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'Mistral-7B-Instruct-v0.3-q4f16_1-MLC',
|
|
115
|
+
name: 'Mistral 7B Instruct',
|
|
116
|
+
family: 'mistral',
|
|
117
|
+
vram_required_mb: 4500,
|
|
118
|
+
low_power: false,
|
|
119
|
+
context_window: 8192,
|
|
120
|
+
license: 'apache-2.0',
|
|
121
|
+
termsUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
122
|
+
preConsentRequired: false,
|
|
123
|
+
estimatedDownloadMB: 4500
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'Qwen2.5-7B-Instruct-q4f16_1-MLC',
|
|
127
|
+
name: 'Qwen 2.5 7B',
|
|
128
|
+
family: 'qwen',
|
|
129
|
+
vram_required_mb: 4500,
|
|
130
|
+
low_power: false,
|
|
131
|
+
context_window: 32768,
|
|
132
|
+
license: 'apache-2.0',
|
|
133
|
+
termsUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
|
134
|
+
preConsentRequired: false,
|
|
135
|
+
estimatedDownloadMB: 4500
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'Llama-3.1-8B-Instruct-q4f16_1-MLC',
|
|
139
|
+
name: 'Llama 3.1 8B',
|
|
23
140
|
family: 'llama',
|
|
24
|
-
vram_required_mb:
|
|
25
|
-
low_power:
|
|
26
|
-
context_window:
|
|
141
|
+
vram_required_mb: 5000,
|
|
142
|
+
low_power: false,
|
|
143
|
+
context_window: 8192,
|
|
144
|
+
license: 'llama-3.2-community',
|
|
145
|
+
termsUrl: 'https://llama.meta.com/llama3/license/',
|
|
146
|
+
preConsentRequired: true,
|
|
147
|
+
estimatedDownloadMB: 5000
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'DeepSeek-R1-Distill-Qwen-7B-q4f16_1-MLC',
|
|
151
|
+
name: 'DeepSeek R1 7B',
|
|
152
|
+
family: 'qwen',
|
|
153
|
+
vram_required_mb: 5100,
|
|
154
|
+
low_power: false,
|
|
155
|
+
context_window: 4096,
|
|
156
|
+
license: 'mit',
|
|
157
|
+
termsUrl: 'https://opensource.org/licenses/MIT',
|
|
158
|
+
preConsentRequired: false,
|
|
159
|
+
estimatedDownloadMB: 5100
|
|
27
160
|
}
|
|
28
161
|
],
|
|
29
162
|
groq: [
|
|
@@ -32,6 +165,11 @@ export const AVAILABLE_MODELS = {
|
|
|
32
165
|
{ id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', context_window: 32768 },
|
|
33
166
|
{ id: 'gemma-2-9b-it', name: 'Gemma 2 9B', context_window: 8192 }
|
|
34
167
|
],
|
|
168
|
+
openai: [
|
|
169
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', context_window: 32768 },
|
|
170
|
+
{ id: 'gpt-4o', name: 'GPT-4o', context_window: 131072 },
|
|
171
|
+
{ id: 'gpt-4o-realtime-preview', name: 'GPT-4o Realtime (Preview)', context_window: 32768 }
|
|
172
|
+
],
|
|
35
173
|
gemini: [
|
|
36
174
|
{ id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash (Fast)', context_window: 1000000 },
|
|
37
175
|
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro (Powerful)', context_window: 2000000 },
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ModelConsentRecord {
|
|
2
|
+
modelId: string;
|
|
3
|
+
license: string;
|
|
4
|
+
licenseVersion?: string;
|
|
5
|
+
acceptedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class ModelConsentManager {
|
|
8
|
+
private storageKey;
|
|
9
|
+
private readRaw;
|
|
10
|
+
private writeRaw;
|
|
11
|
+
getConsents(): ModelConsentRecord[];
|
|
12
|
+
hasConsent(modelId: string): boolean;
|
|
13
|
+
getConsent(modelId: string): ModelConsentRecord | null;
|
|
14
|
+
setConsent(modelId: string, license: string, licenseVersion?: string): void;
|
|
15
|
+
revokeConsent(modelId: string): void;
|
|
16
|
+
}
|
|
17
|
+
export declare const modelConsentManager: ModelConsentManager;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class ModelConsentManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.storageKey = 'meechi_model_consents';
|
|
4
|
+
}
|
|
5
|
+
readRaw() {
|
|
6
|
+
try {
|
|
7
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
8
|
+
if (!raw)
|
|
9
|
+
return [];
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
console.warn('[ModelConsentManager] Failed to read consents', e);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
writeRaw(list) {
|
|
19
|
+
try {
|
|
20
|
+
localStorage.setItem(this.storageKey, JSON.stringify(list));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.warn('[ModelConsentManager] Failed to write consents', e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getConsents() {
|
|
27
|
+
return this.readRaw();
|
|
28
|
+
}
|
|
29
|
+
hasConsent(modelId) {
|
|
30
|
+
const list = this.readRaw();
|
|
31
|
+
return list.some(r => r.modelId === modelId);
|
|
32
|
+
}
|
|
33
|
+
getConsent(modelId) {
|
|
34
|
+
const list = this.readRaw();
|
|
35
|
+
return list.find(r => r.modelId === modelId) || null;
|
|
36
|
+
}
|
|
37
|
+
setConsent(modelId, license, licenseVersion) {
|
|
38
|
+
const list = this.readRaw().filter(r => r.modelId !== modelId);
|
|
39
|
+
list.push({ modelId, license, licenseVersion, acceptedAt: Date.now() });
|
|
40
|
+
this.writeRaw(list);
|
|
41
|
+
}
|
|
42
|
+
revokeConsent(modelId) {
|
|
43
|
+
const list = this.readRaw().filter(r => r.modelId !== modelId);
|
|
44
|
+
this.writeRaw(list);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export const modelConsentManager = new ModelConsentManager();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
export function showLicenseModal(props) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
if (typeof document === 'undefined')
|
|
6
|
+
return resolve(false);
|
|
7
|
+
const container = document.createElement('div');
|
|
8
|
+
container.style.zIndex = '9999';
|
|
9
|
+
document.body.appendChild(container);
|
|
10
|
+
const handleClose = (accepted) => {
|
|
11
|
+
try {
|
|
12
|
+
root.unmount();
|
|
13
|
+
}
|
|
14
|
+
catch (_a) { }
|
|
15
|
+
if (container.parentNode)
|
|
16
|
+
container.parentNode.removeChild(container);
|
|
17
|
+
resolve(accepted);
|
|
18
|
+
};
|
|
19
|
+
const Modal = () => {
|
|
20
|
+
const onAccept = () => handleClose(true);
|
|
21
|
+
const onDecline = () => handleClose(false);
|
|
22
|
+
return (React.createElement('div', { style: modalOverlay }, React.createElement('div', { style: modalCard }, React.createElement('h2', null, `License Agreement — ${props.modelName}`), React.createElement('p', null, `This model is provided under the '${props.license}' license.`), props.termsUrl ? React.createElement('p', null, React.createElement('a', { href: props.termsUrl, target: '_blank', rel: 'noreferrer' }, 'View full terms')) : null, props.estimatedDownloadMB ? React.createElement('p', null, `Estimated download size: ${props.estimatedDownloadMB} MB`) : null, React.createElement('div', { style: buttonRow }, React.createElement('button', { onClick: onDecline, style: declineButton }, 'Decline'), React.createElement('button', { onClick: onAccept, style: acceptButton }, 'Accept')))));
|
|
23
|
+
};
|
|
24
|
+
const root = createRoot(container);
|
|
25
|
+
root.render(React.createElement(Modal));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const modalOverlay = {
|
|
29
|
+
position: 'fixed', left: 0, top: 0, right: 0, bottom: 0,
|
|
30
|
+
background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
31
|
+
};
|
|
32
|
+
const modalCard = {
|
|
33
|
+
width: 'min(640px, 92%)', background: '#fff', padding: '20px', borderRadius: '8px', boxShadow: '0 8px 24px rgba(0,0,0,0.2)'
|
|
34
|
+
};
|
|
35
|
+
const buttonRow = { display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '18px' };
|
|
36
|
+
const acceptButton = { background: '#0b74ff', color: '#fff', border: 'none', padding: '8px 14px', borderRadius: '6px', cursor: 'pointer' };
|
|
37
|
+
const declineButton = { background: '#eee', color: '#111', border: 'none', padding: '8px 14px', borderRadius: '6px', cursor: 'pointer' };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DeviceCapabilities {
|
|
2
|
+
isMobile: boolean;
|
|
3
|
+
isTablet: boolean;
|
|
4
|
+
isDesktop: boolean;
|
|
5
|
+
estimatedVRAMMB?: number;
|
|
6
|
+
isMetered: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Minimal device detection for browser + basic heuristics.
|
|
10
|
+
* Keep implementation small and safe for Server-Side Rendering.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectDeviceCapabilities(): Promise<DeviceCapabilities>;
|
|
13
|
+
/**
|
|
14
|
+
* Pick recommended model id based on capabilities.
|
|
15
|
+
* - Mobile -> Qwen2.5-0.5B
|
|
16
|
+
* - Tablet -> Qwen2.5-1.5B
|
|
17
|
+
* - Desktop -> Phi-3 Mini
|
|
18
|
+
*/
|
|
19
|
+
export declare function getRecommendedModel(cap: DeviceCapabilities): string;
|
|
20
|
+
export declare function shouldWarnBeforeDownload(modelSizeMB: number | undefined, cap: DeviceCapabilities): boolean;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal device detection for browser + basic heuristics.
|
|
3
|
+
* Keep implementation small and safe for Server-Side Rendering.
|
|
4
|
+
*/
|
|
5
|
+
export async function detectDeviceCapabilities() {
|
|
6
|
+
// Default conservative values
|
|
7
|
+
const capabilities = {
|
|
8
|
+
isMobile: false,
|
|
9
|
+
isTablet: false,
|
|
10
|
+
isDesktop: true,
|
|
11
|
+
estimatedVRAMMB: undefined,
|
|
12
|
+
isMetered: false
|
|
13
|
+
};
|
|
14
|
+
if (typeof navigator === 'undefined')
|
|
15
|
+
return capabilities;
|
|
16
|
+
const ua = navigator.userAgent || '';
|
|
17
|
+
const isMobile = /Mobi|Android|iPhone|iPad|iPod/.test(ua) && !/iPad/.test(ua);
|
|
18
|
+
const isTablet = /iPad|Tablet/.test(ua) || (/Android/.test(ua) && !/Mobile/.test(ua));
|
|
19
|
+
const isDesktop = !isMobile && !isTablet;
|
|
20
|
+
capabilities.isMobile = Boolean(isMobile);
|
|
21
|
+
capabilities.isTablet = Boolean(isTablet);
|
|
22
|
+
capabilities.isDesktop = Boolean(isDesktop);
|
|
23
|
+
// Network detection (may be undefined in some browsers)
|
|
24
|
+
try {
|
|
25
|
+
const nav = navigator;
|
|
26
|
+
if (nav.connection && typeof nav.connection.effectiveType === 'string') {
|
|
27
|
+
const type = nav.connection.effectiveType || '';
|
|
28
|
+
// effectiveType like '4g','3g','2g','slow-2g'
|
|
29
|
+
capabilities.isMetered = /2g|3g|slow-2g/.test(type) || !!nav.connection.saveData || !!nav.connection.metered;
|
|
30
|
+
// treat 'cellular' as metered too
|
|
31
|
+
if (nav.connection.type === 'cellular')
|
|
32
|
+
capabilities.isMetered = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
// Heuristic: try to estimate VRAM by device memory API and userAgent
|
|
39
|
+
try {
|
|
40
|
+
const anyNav = navigator;
|
|
41
|
+
if (anyNav.deviceMemory) {
|
|
42
|
+
// deviceMemory is approximate RAM in GB
|
|
43
|
+
const ramGB = Number(anyNav.deviceMemory) || undefined;
|
|
44
|
+
if (ramGB) {
|
|
45
|
+
// Rough mapping: lower RAM -> lower VRAM available
|
|
46
|
+
capabilities.estimatedVRAMMB = Math.max(512, Math.round((ramGB / 8) * 8192));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
return capabilities;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pick recommended model id based on capabilities.
|
|
57
|
+
* - Mobile -> Qwen2.5-0.5B
|
|
58
|
+
* - Tablet -> Qwen2.5-1.5B
|
|
59
|
+
* - Desktop -> Phi-3 Mini
|
|
60
|
+
*/
|
|
61
|
+
export function getRecommendedModel(cap) {
|
|
62
|
+
if (cap.isMobile)
|
|
63
|
+
return 'Qwen2.5-0.5B-Instruct-q4f16_1-MLC';
|
|
64
|
+
if (cap.isTablet)
|
|
65
|
+
return 'Qwen2.5-1.5B-Instruct-q4f16_1-MLC';
|
|
66
|
+
return 'Phi-3-mini-4k-instruct-q4f16_1-MLC';
|
|
67
|
+
}
|
|
68
|
+
export function shouldWarnBeforeDownload(modelSizeMB, cap) {
|
|
69
|
+
if (!modelSizeMB)
|
|
70
|
+
return false;
|
|
71
|
+
// Always warn on metered connections
|
|
72
|
+
if (cap.isMetered)
|
|
73
|
+
return true;
|
|
74
|
+
// Warn if mobile and large download
|
|
75
|
+
if (cap.isMobile && modelSizeMB > 1500)
|
|
76
|
+
return true;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meechi-ai/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"build": "next build",
|
|
23
23
|
"build:lib": "tsc -p tsconfig.lib.json",
|
|
24
24
|
"start": "next start",
|
|
25
|
-
"lint": "next lint",
|
|
25
|
+
"lint": "node -e \"if(process.env.CI){console.log('Skipping lint in CI'); process.exit(0);} require('child_process').execSync('npx next lint', {stdio:'inherit'})\"",
|
|
26
26
|
"prepublishOnly": "npm run build:lib"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|