File size: 20,638 Bytes
9944be9 2c974cb 9944be9 482f023 77be0f2 482f023 77be0f2 482f023 77be0f2 482f023 9944be9 482f023 9944be9 482f023 9944be9 482f023 9944be9 c23678a 0679615 c23678a 9944be9 77be0f2 c23678a 9944be9 c23678a 9944be9 c23678a 08828d7 9944be9 c23678a 9944be9 0f9c150 6d9826c 9944be9 f14ea78 42e9f39 a4676be 74685fd f14ea78 c23678a 9944be9 8c07664 9944be9 77be0f2 9944be9 8c07664 77be0f2 e2d901d 9944be9 77be0f2 9944be9 8c07664 77be0f2 8c07664 77be0f2 c23678a 8c07664 9944be9 8c07664 c23678a 8c07664 9944be9 e2d901d c23678a 08828d7 8c07664 9944be9 77be0f2 9944be9 77be0f2 e2d901d 77be0f2 e2d901d 77be0f2 9944be9 2c974cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 | <!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
<meta name="description" content="llamadart chat: CDN-first web bridge loading for on-device LLM chat with model management and tool calling.">
<meta name="theme-color" content="#0b1220">
<meta name="application-name" content="llamadart chat">
<meta property="og:type" content="website">
<meta property="og:title" content="llamadart chat">
<meta property="og:description" content="CDN-first WebGPU bridge loading for on-device LLM chat powered by llamadart.">
<meta property="og:image" content="icons/logo.svg">
<meta property="og:site_name" content="llamadart chat">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="llamadart chat">
<meta name="twitter:description" content="CDN-first WebGPU bridge loading for on-device LLM chat powered by llamadart.">
<meta name="twitter:image" content="icons/logo.svg">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="llamadart chat">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="icons/logo.svg"/>
<link rel="alternate icon" type="image/png" href="favicon.png"/>
<title>llamadart chat</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script type="module">
async function clearFlutterServiceWorkerCaches() {
if (typeof window === 'undefined') {
return;
}
if ('serviceWorker' in navigator) {
try {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.allSettled(
registrations.map((registration) => registration.unregister()),
);
} catch (_) {
// best-effort cleanup only
}
}
if ('caches' in window) {
try {
const keys = await caches.keys();
const flutterLikeKeys = keys.filter((key) =>
key.startsWith('flutter-') ||
key.startsWith('workbox-') ||
key.includes('flutter-app-cache')
);
await Promise.allSettled(
flutterLikeKeys.map((key) => caches.delete(key)),
);
} catch (_) {
// best-effort cleanup only
}
}
}
await clearFlutterServiceWorkerCaches();
const configuredRepo = window.__llamadartBridgeAssetsRepo;
const configuredTag = window.__llamadartBridgeAssetsTag;
const bootstrapSearchParams = new URLSearchParams(window.location.search);
const configuredMockBridgeScenario =
typeof window.__llamadartMockBridgeScenario === 'string' &&
window.__llamadartMockBridgeScenario.length > 0
? window.__llamadartMockBridgeScenario
: bootstrapSearchParams.get('llamadart_mock_bridge');
const configuredMockBridgeResponse =
typeof window.__llamadartMockBridgeResponse === 'string' &&
window.__llamadartMockBridgeResponse.length > 0
? window.__llamadartMockBridgeResponse
: bootstrapSearchParams.get('llamadart_mock_response');
const configuredMockBridgeDelayMs = Number(
window.__llamadartMockBridgeDelayMs ??
bootstrapSearchParams.get('llamadart_mock_delay_ms'),
);
const bridgeBootstrapVerbose =
window.__llamadartBridgeBootstrapVerbose === true;
const configuredThreadPoolSize = Number(window.__llamadartBridgeThreadPoolSize);
const hardwareConcurrency = Number(window.navigator?.hardwareConcurrency);
const inferredThreadPoolSize =
window.crossOriginIsolated === true
? Number.isFinite(hardwareConcurrency) && hardwareConcurrency > 0
? Math.max(2, Math.min(4, Math.trunc(hardwareConcurrency)))
: 2
: 1;
const bridgeThreadPoolSize =
Number.isFinite(configuredThreadPoolSize) && configuredThreadPoolSize > 0
? Math.max(1, Math.trunc(configuredThreadPoolSize))
: inferredThreadPoolSize;
window.__llamadartBridgeThreadPoolSize = bridgeThreadPoolSize;
function bridgeBootstrapLog(level, ...args) {
if (!bridgeBootstrapVerbose || typeof console === 'undefined') {
return;
}
const fn = console[level];
if (typeof fn === 'function') {
fn.apply(console, args);
}
}
async function resolveCoreModuleUrl(moduleUrl) {
const bridgeUrl = new URL(moduleUrl, window.location.href);
const coreUrl = new URL('./llama_webgpu_core.js', bridgeUrl);
const version = bridgeUrl.searchParams.get('v');
if (version != null && version.length > 0) {
coreUrl.searchParams.set('v', version);
}
const coreUrlString = coreUrl.toString();
window.__llamadartBridgeCoreModuleUrl = coreUrlString;
return coreUrlString;
}
function deriveCoreWasmUrl(moduleUrl, memory64 = false) {
try {
const url = new URL(moduleUrl, window.location.href);
url.pathname = url.pathname.replace(
/\/llama_webgpu_bridge\.js$/,
memory64 ? '/llama_webgpu_core_mem64.wasm' : '/llama_webgpu_core.wasm',
);
return url.toString();
} catch (_) {
return null;
}
}
const bridgeAssetsRepo =
typeof configuredRepo === 'string' && configuredRepo.length > 0
? configuredRepo
: 'leehack/llama-web-bridge-assets';
const defaultBridgeAssetsTag = 'v0.1.82';
const defaultBridgeLlamaCppTag = 'v0.8.0';
const bridgeAssetsTag =
typeof configuredTag === 'string' && configuredTag.length > 0
? configuredTag
: defaultBridgeAssetsTag;
function bridgeTagSupportsSpeechToText(tag) {
const match = /^v(\d+)\.(\d+)\.(\d+)(?:$|-)/.exec(String(tag || ''));
if (!match) return false;
const major = Number(match[1]);
const minor = Number(match[2]);
const patch = Number(match[3]);
return major > 0 || minor > 1 || (minor === 1 && patch >= 30);
}
const isOfficialBridgeAssetsRepo =
bridgeAssetsRepo === 'leehack/llama-web-bridge-assets';
window.__llamadartBridgeSpeechToTextSupported =
typeof window.__llamadartBridgeSpeechToTextSupported === 'boolean'
? window.__llamadartBridgeSpeechToTextSupported
: isOfficialBridgeAssetsRepo &&
bridgeTagSupportsSpeechToText(bridgeAssetsTag);
window.__llamadartLiteRtLmModuleUrl =
typeof window.__llamadartLiteRtLmModuleUrl === 'string' &&
window.__llamadartLiteRtLmModuleUrl.length > 0
? window.__llamadartLiteRtLmModuleUrl
: 'https://cdn.jsdelivr.net/npm/@litert-lm/core@0.18.0/+esm';
const localBridgeVersion =
`${defaultBridgeAssetsTag}-local-${defaultBridgeLlamaCppTag}`;
window.__llamadartBridgeLocalVersion = localBridgeVersion;
const localBridgeUrl = `./webgpu_bridge/llama_webgpu_bridge.js?v=${localBridgeVersion}`;
const cdnBridgeUrl = `https://cdn.jsdelivr.net/gh/${bridgeAssetsRepo}@${bridgeAssetsTag}/llama_webgpu_bridge.js`;
const preferLocalBridgeRuntime =
window.__llamadartPreferLocalBridgeRuntime === true ||
window.location.hostname === '127.0.0.1' ||
window.location.hostname === 'localhost';
function deriveWorkerModuleUrl(moduleUrl) {
try {
const url = new URL(moduleUrl, window.location.href);
url.pathname = url.pathname.replace(
/\/llama_webgpu_bridge\.js$/,
'/llama_webgpu_bridge_worker.js',
);
return url.toString();
} catch (_) {
return null;
}
}
function deriveMem64CoreModuleUrl(moduleUrl) {
try {
const url = new URL(moduleUrl, window.location.href);
url.pathname = url.pathname.replace(
/\/llama_webgpu_bridge\.js$/,
'/llama_webgpu_core_mem64.js',
);
return url.toString();
} catch (_) {
return null;
}
}
function createMockBridgeClass({ scenario, responseText, delayMs }) {
const encoder = new TextEncoder();
const normalizedScenario = String(scenario || 'echo').trim().toLowerCase();
const perChunkDelayMs = Number.isFinite(delayMs)
? Math.max(0, Math.min(1000, Math.trunc(delayMs)))
: 0;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function extractLastUserMessage(prompt) {
if (typeof prompt !== 'string' || prompt.length === 0) {
return 'hi';
}
const qwenMatches = [...prompt.matchAll(/<\|im_start\|>user\n([\s\S]*?)<\|im_end\|>/g)];
if (qwenMatches.length > 0) {
return String(qwenMatches[qwenMatches.length - 1][1] || '').trim() || 'hi';
}
const genericMatches = [...prompt.matchAll(/^user:\s*(.+)$/gm)];
if (genericMatches.length > 0) {
return String(genericMatches[genericMatches.length - 1][1] || '').trim() || 'hi';
}
return prompt.trim().split(/\s+/).slice(-8).join(' ') || 'hi';
}
function resolveResponse(prompt) {
if (typeof responseText === 'string' && responseText.length > 0) {
return responseText;
}
const lastUser = extractLastUserMessage(prompt);
switch (normalizedScenario) {
case 'qwen-weird':
case 'weird':
return 'ЯМ В В K K K K K K';
case 'markdown':
return '- mock bridge\n- browser smoke\n- deterministic output';
case 'echo':
default:
return `Mock bridge reply: ${lastUser}`;
}
}
function chunkText(text) {
const symbols = Array.from(String(text || ''));
const chunks = [];
for (let i = 0; i < symbols.length; i += 3) {
chunks.push(symbols.slice(i, i + 3).join(''));
}
return chunks.length > 0 ? chunks : [''];
}
return class MockLlamaWebGpuBridge {
static supportsSafariAdaptiveGpu = false;
constructor(_config = {}) {
this._abortRequested = false;
this._contextSize = 4096;
this._modelUrl = null;
this._metadata = {
'general.name': 'Mock WebGPU Bridge',
'llamadart.webgpu.runtime_notes': `mock_bridge:${normalizedScenario}`,
'llamadart.webgpu.model_source': 'mock',
'llamadart.webgpu.model_cache_state': 'disabled',
'llamadart.webgpu.execution': 'mock',
};
}
async loadModelFromUrl(url, options = {}) {
this._abortRequested = false;
this._modelUrl = String(url || '');
const parsedContext = Number(options?.nCtx);
this._contextSize = Number.isFinite(parsedContext) && parsedContext > 0
? Math.trunc(parsedContext)
: 4096;
window.__llamadartMockBridgeLastLoad = {
url: this._modelUrl,
options: { ...options },
scenario: normalizedScenario,
};
return null;
}
async prefetchModelToCache(_url, _options = {}) {
return null;
}
async evictModelFromCache(_url, _options = {}) {
return null;
}
async createCompletion(prompt, options = {}) {
const output = resolveResponse(prompt);
const chunks = chunkText(output);
const emitCurrentTextOnToken = options?.emitCurrentTextOnToken === true;
const tokenEventEncoding = typeof options?.tokenEventEncoding === 'string'
? options.tokenEventEncoding
: 'bytes';
const onToken = typeof options?.onToken === 'function'
? options.onToken
: null;
this._abortRequested = false;
window.__llamadartMockBridgeLastPrompt = String(prompt || '');
window.__llamadartMockBridgeLastResponse = output;
let currentText = '';
for (const chunk of chunks) {
if (this._abortRequested) {
break;
}
currentText += chunk;
if (onToken) {
const payload = tokenEventEncoding === 'text'
? chunk
: encoder.encode(chunk);
onToken(payload, emitCurrentTextOnToken ? currentText : null);
}
if (perChunkDelayMs > 0) {
await sleep(perChunkDelayMs);
}
}
return currentText;
}
async loadMultimodalProjector(_url) {
return null;
}
async unloadMultimodalProjector() {
return null;
}
supportsVision() {
return false;
}
supportsAudio() {
return false;
}
async tokenize(text, _addSpecial = true) {
return Array.from(encoder.encode(String(text || '')));
}
async detokenize(tokens, _special = false) {
return new TextDecoder().decode(Uint8Array.from(tokens || []));
}
async embed(_text, _options = {}) {
return [0.1, 0.2, 0.3];
}
async embedBatch(texts, _options = {}) {
return Array.from(texts || []).map(() => [0.1, 0.2, 0.3]);
}
getModelMetadata() {
return { ...this._metadata };
}
getContextSize() {
return this._contextSize;
}
isGpuActive() {
return true;
}
getBackendName() {
return 'WEBGPU (Mock)';
}
setLogLevel(_level) {
return null;
}
cancel() {
this._abortRequested = true;
}
async dispose() {
this._abortRequested = true;
return null;
}
async applyChatTemplate(messages, addAssistant = true, _customTemplate = null) {
const lines = [];
for (const msg of messages || []) {
lines.push(`${String(msg?.role || 'user')}: ${String(msg?.content || '')}`);
}
if (addAssistant) {
lines.push('assistant: ');
}
return lines.join('\n');
}
};
}
// Deterministic bridge-readiness signal awaited by the Dart app so it never
// races the asynchronous bridge import. Previously the app could observe a
// missing `LlamaWebGpuBridge` global and silently fall back to a fake
// "download" that cached nothing.
let __resolveBridgeReady;
let __rejectBridgeReady;
window.__llamadartBridgeReady = false;
window.__llamadartBridgeReadyPromise = new Promise((resolve, reject) => {
__resolveBridgeReady = resolve;
__rejectBridgeReady = reject;
});
// Attach a no-op rejection handler so a failure/timeout before the Dart
// side awaits the promise does not surface as an unhandled rejection in the
// console. The failure is still observable via __llamadartBridgeLoadError
// and by awaiting __llamadartBridgeReadyPromise.
window.__llamadartBridgeReadyPromise.catch(function () {});
function markBridgeReady() {
window.__llamadartBridgeReady = true;
if (__resolveBridgeReady) {
__resolveBridgeReady();
}
}
function markBridgeFailed(detail) {
window.__llamadartBridgeReady = false;
window.__llamadartBridgeLoadError = detail;
if (__rejectBridgeReady) {
__rejectBridgeReady(new Error(detail));
}
}
// Safety timeout so the Dart side never awaits readiness forever.
setTimeout(function () {
if (!window.__llamadartBridgeReady && !window.__llamadartBridgeLoadError) {
markBridgeFailed('Bridge runtime did not become ready within 30s.');
}
}, 30000);
async function loadBridge(moduleUrl, source) {
const runtimeAssetModuleUrl = source === 'cdn' ? localBridgeUrl : moduleUrl;
const coreModuleUrl = await resolveCoreModuleUrl(runtimeAssetModuleUrl);
const mod = await import(moduleUrl);
if (!mod?.LlamaWebGpuBridge) {
throw new Error('Bridge module loaded but LlamaWebGpuBridge export was missing.');
}
const supportsDedicatedWorkerEntry =
typeof mod.enableBridgeWorkerHost === 'function';
const workerModuleUrl = supportsDedicatedWorkerEntry
? deriveWorkerModuleUrl(runtimeAssetModuleUrl)
: null;
const coreModuleUrlMem64 = deriveMem64CoreModuleUrl(runtimeAssetModuleUrl);
const wasmUrl = deriveCoreWasmUrl(runtimeAssetModuleUrl);
const wasmUrlMem64 = deriveCoreWasmUrl(runtimeAssetModuleUrl, true);
const preferMemory64 = window.__llamadartBridgeEnableMem64 === true;
window.LlamaWebGpuBridge = mod.LlamaWebGpuBridge;
window.__llamadartBridgeAdaptiveSafariGpu =
mod.LlamaWebGpuBridge?.supportsSafariAdaptiveGpu === true;
window.__llamadartBridgeAssetSource = source;
window.__llamadartBridgeModuleUrl = moduleUrl;
window.__llamadartBridgeCoreModuleUrl = coreModuleUrl;
window.__llamadartBridgeCoreModuleUrlMem64 = coreModuleUrlMem64;
window.__llamadartBridgeWasmUrl = wasmUrl;
window.__llamadartBridgeWasmUrlMem64 = wasmUrlMem64;
window.__llamadartBridgeWorkerUrl = workerModuleUrl;
window.__llamadartBridgePreferMemory64 = preferMemory64;
window.__llamadartBridgeWorkerFallbackReason = null;
window.__llamadartBridgeLoadError = null;
markBridgeReady();
bridgeBootstrapLog(
'info',
`llamadart: Web bridge runtime loaded (${source}), runtimeAssets=${source === 'cdn' ? 'local' : source}, worker=${workerModuleUrl ?? 'auto'}, mem64=${coreModuleUrlMem64 ?? 'none'}, preferMem64=${preferMemory64}, threadPool=${bridgeThreadPoolSize}`,
);
}
if (configuredMockBridgeScenario) {
window.LlamaWebGpuBridge = createMockBridgeClass({
scenario: configuredMockBridgeScenario,
responseText: configuredMockBridgeResponse,
delayMs: configuredMockBridgeDelayMs,
});
window.__llamadartBridgeAdaptiveSafariGpu = false;
window.__llamadartBridgeAssetSource = 'mock';
window.__llamadartBridgeModuleUrl = null;
window.__llamadartBridgeCoreModuleUrl = null;
window.__llamadartBridgeCoreModuleUrlMem64 = null;
window.__llamadartBridgeWasmUrl = null;
window.__llamadartBridgeWasmUrlMem64 = null;
window.__llamadartBridgeWorkerUrl = null;
window.__llamadartBridgePreferMemory64 = false;
window.__llamadartBridgeWorkerFallbackReason = null;
window.__llamadartBridgeLoadError = null;
markBridgeReady();
bridgeBootstrapLog(
'info',
`llamadart: mock bridge runtime loaded (${configuredMockBridgeScenario})`,
);
} else {
const primaryUrl = preferLocalBridgeRuntime ? localBridgeUrl : cdnBridgeUrl;
const primarySource = preferLocalBridgeRuntime ? 'local' : 'cdn';
const fallbackUrl = preferLocalBridgeRuntime ? cdnBridgeUrl : localBridgeUrl;
const fallbackSource = preferLocalBridgeRuntime ? 'cdn' : 'local';
let primaryError = null;
loadBridge(primaryUrl, primarySource)
.catch((err) => {
primaryError = err;
bridgeBootstrapLog(
'warn',
`llamadart: ${primarySource.toUpperCase()} bridge load failed, falling back to ${fallbackSource}`,
err,
);
return loadBridge(fallbackUrl, fallbackSource);
})
.catch((fallbackErr) => {
const detail =
`${primarySource.toUpperCase()} load failed: ${String(primaryError)} | ` +
`${fallbackSource.toUpperCase()} load failed: ${String(fallbackErr)}`;
markBridgeFailed(detail);
bridgeBootstrapLog('error', 'llamadart: failed to load bridge runtime', detail);
});
}
</script>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
|