Trying another one in chat.html
Browse files- chat.html +38 -0
- chat.js +27 -0
- index.html +64 -82
chat.html
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="uk">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Локальний Чат — каркас</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { color-scheme: light dark; }
|
| 9 |
+
* { box-sizing: border-box; }
|
| 10 |
+
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial; }
|
| 11 |
+
header { position: sticky; top: 0; padding: 12px 16px; border-bottom: 1px solid rgba(127,127,127,.25); backdrop-filter: blur(6px); }
|
| 12 |
+
main { padding: 16px; }
|
| 13 |
+
.row { display: flex; gap: 8px; }
|
| 14 |
+
input[type="text"] { flex: 1; padding: 10px 12px; border: 1px solid rgba(127,127,127,.35); border-radius: 10px; background: transparent; color: inherit; }
|
| 15 |
+
button { padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(127,127,127,.35); background: transparent; color: inherit; cursor: pointer; }
|
| 16 |
+
button:disabled { opacity: .6; cursor: not-allowed; }
|
| 17 |
+
#messages { min-height: 40vh; padding-bottom: 12px; }
|
| 18 |
+
.msg { padding: 10px 12px; margin: 8px 0; border: 1px solid rgba(127,127,127,.25); border-radius: 12px; max-width: 70ch; }
|
| 19 |
+
.me { background: color-mix(in oklab, Canvas 92%, transparent); }
|
| 20 |
+
.bot { background: color-mix(in oklab, Canvas 85%, transparent); }
|
| 21 |
+
</style>
|
| 22 |
+
</head>
|
| 23 |
+
<body>
|
| 24 |
+
<header>
|
| 25 |
+
<strong>Локальний Чат (каркас)</strong>
|
| 26 |
+
<div style="opacity:.75">Цей файл підключає <code>chat.js</code> для логіки.</div>
|
| 27 |
+
</header>
|
| 28 |
+
<main>
|
| 29 |
+
<div id="messages"></div>
|
| 30 |
+
<div class="row">
|
| 31 |
+
<input id="prompt" type="text" placeholder="Напишіть повідомлення..." />
|
| 32 |
+
<button id="send">Надіслати</button>
|
| 33 |
+
</div>
|
| 34 |
+
</main>
|
| 35 |
+
|
| 36 |
+
<script src="./chat.js"></script>
|
| 37 |
+
</body>
|
| 38 |
+
</html>
|
chat.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function(){
|
| 2 |
+
const $ = (sel) => document.querySelector(sel);
|
| 3 |
+
const messages = $('#messages');
|
| 4 |
+
const input = $('#prompt');
|
| 5 |
+
const sendBtn = $('#send');
|
| 6 |
+
|
| 7 |
+
function push(role, text){
|
| 8 |
+
const div = document.createElement('div');
|
| 9 |
+
div.className = `msg ${role}`;
|
| 10 |
+
div.textContent = text;
|
| 11 |
+
messages.appendChild(div);
|
| 12 |
+
messages.scrollTop = messages.scrollHeight;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
sendBtn?.addEventListener('click', () => {
|
| 16 |
+
const text = input.value.trim();
|
| 17 |
+
if (!text) return;
|
| 18 |
+
push('me', text);
|
| 19 |
+
input.value = '';
|
| 20 |
+
// Placeholder response to show wiring works.
|
| 21 |
+
setTimeout(() => push('bot', 'Це заглушка відповіді з chat.js. (Модель ще не підключено)'), 300);
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
input?.addEventListener('keydown', (e) => {
|
| 25 |
+
if (e.key === 'Enter') sendBtn?.click();
|
| 26 |
+
});
|
| 27 |
+
})();
|
index.html
CHANGED
|
@@ -890,87 +890,68 @@ If you can answer the question directly with your existing knowledge or after us
|
|
| 890 |
|
| 891 |
// --- Dynamic Trial Models Discovery (tokenless) ---
|
| 892 |
async function discoverOpenSmallModels(maxModels = 6) {
|
| 893 |
-
|
| 894 |
-
const
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
`${env.remoteURL}/api/models?search=distil&limit=25`,
|
| 900 |
-
`${env.remoteURL}/api/models?search=tinyllama&limit=25`,
|
| 901 |
-
`${env.remoteURL}/api/models?search=phi-2&limit=25`,
|
| 902 |
-
`${env.remoteURL}/api/models?search=qwen2.5-0.5b&limit=25`,
|
| 903 |
-
`${env.remoteURL}/api/models?search=smol&limit=25`
|
| 904 |
];
|
| 905 |
-
const
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
/smol/i,
|
| 912 |
-
/mini[-_]?llama/i
|
| 913 |
];
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
function quickHeuristicIsSmall(modelInfo) {
|
| 918 |
-
// Basic gates first.
|
| 919 |
-
if (modelInfo.private || modelInfo.gated || modelInfo.disabled) return false;
|
| 920 |
-
const id = modelInfo.id || '';
|
| 921 |
-
if (nameLooksSmall(id)) return true;
|
| 922 |
-
// Try tags heuristics.
|
| 923 |
-
const tags = modelInfo.tags || [];
|
| 924 |
-
if (tags.some(t => /tiny|micro|mini|small|distil/.test(t))) return true;
|
| 925 |
-
return false;
|
| 926 |
-
}
|
| 927 |
-
async function fetchJSON(url) {
|
| 928 |
try {
|
| 929 |
-
const
|
| 930 |
-
if (!
|
| 931 |
-
return await
|
| 932 |
} catch (e) {
|
| 933 |
-
appendDiagnostic('
|
| 934 |
return null;
|
| 935 |
}
|
| 936 |
}
|
| 937 |
-
for (const
|
| 938 |
-
if (
|
| 939 |
-
const
|
| 940 |
-
if (!
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 949 |
}
|
|
|
|
| 950 |
}
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
return ordered.slice(0, maxModels);
|
| 957 |
}
|
| 958 |
|
| 959 |
trialModelsBtn.addEventListener('click', async () => {
|
| 960 |
-
const trialResultsDiv = document.getElementById('trial-results');
|
| 961 |
-
trialResultsDiv.style.display = 'block';
|
| 962 |
-
trialResultsDiv.innerHTML = '<b>Discovering open small models (no token)...</b>';
|
| 963 |
-
const TRIAL_PROMPT = 'Do planes fly higher than bees?';
|
| 964 |
trialModelsBtn.disabled = true;
|
| 965 |
-
const
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
};
|
| 974 |
const yieldUI = async () => new Promise(r=>requestAnimationFrame(r));
|
| 975 |
function withTimeout(promise, ms, label) {
|
| 976 |
return Promise.race([
|
|
@@ -984,24 +965,24 @@ If you can answer the question directly with your existing knowledge or after us
|
|
| 984 |
discovered = await discoverOpenSmallModels(10);
|
| 985 |
} catch(e) {
|
| 986 |
appendDiagnostic('Discovery error: ' + e.message);
|
|
|
|
| 987 |
}
|
| 988 |
if (!discovered.length) {
|
| 989 |
-
|
| 990 |
discovered = ['Xenova/gpt2','Xenova/distilgpt2'];
|
| 991 |
}
|
| 992 |
-
// Ensure baseline (gpt2 + distilgpt2) attempted first regardless of discovery order
|
| 993 |
const baseline = ['Xenova/gpt2','Xenova/distilgpt2'];
|
| 994 |
const ordered = baseline.concat(discovered.filter(m=>!baseline.includes(m)));
|
| 995 |
-
// Limit total trials for responsiveness
|
| 996 |
const MODELS = ordered.slice(0,6);
|
| 997 |
-
|
| 998 |
appendDiagnostic('Trial: Models -> ' + discovered.join(', '));
|
| 999 |
const collected = [];
|
| 1000 |
try {
|
| 1001 |
for (const modelId of MODELS) {
|
| 1002 |
let loadTime='-', genTime='-', snippet='', error=null;
|
| 1003 |
-
|
| 1004 |
-
|
|
|
|
| 1005 |
try {
|
| 1006 |
const pipe = await withTimeout(pipeline('text-generation', modelId, { quantized: true }), 20000, 'load');
|
| 1007 |
const t1 = performance.now();
|
|
@@ -1010,11 +991,12 @@ If you can answer the question directly with your existing knowledge or after us
|
|
| 1010 |
loadTime = ((t1-t0)/1000).toFixed(2)+'s';
|
| 1011 |
genTime = ((t2-t1)/1000).toFixed(2)+'s';
|
| 1012 |
const full = Array.isArray(out) ? (out[0]?.generated_text||'') : (out.generated_text||'');
|
| 1013 |
-
snippet = full.trim().slice(0,
|
| 1014 |
-
|
|
|
|
| 1015 |
} catch(e) {
|
| 1016 |
error = e?.message || String(e);
|
| 1017 |
-
|
| 1018 |
appendDiagnostic('Trial error '+modelId+': '+error);
|
| 1019 |
}
|
| 1020 |
collected.push({ model:modelId, loadTime, genTime, snippet, error });
|
|
@@ -1023,16 +1005,16 @@ If you can answer the question directly with your existing knowledge or after us
|
|
| 1023 |
} finally {
|
| 1024 |
trialModelsBtn.disabled = false;
|
| 1025 |
}
|
| 1026 |
-
|
|
|
|
| 1027 |
for (const r of collected) {
|
| 1028 |
if (r.error) {
|
| 1029 |
-
|
| 1030 |
} else {
|
| 1031 |
-
|
| 1032 |
}
|
| 1033 |
}
|
| 1034 |
-
|
| 1035 |
-
appendDiagnostic('Trial: dynamic markdown summary appended to chat.');
|
| 1036 |
});
|
| 1037 |
|
| 1038 |
// Event Listeners
|
|
|
|
| 890 |
|
| 891 |
// --- Dynamic Trial Models Discovery (tokenless) ---
|
| 892 |
async function discoverOpenSmallModels(maxModels = 6) {
|
| 893 |
+
// Curated base list of realistically loadable tokenless models published with ONNX/TFJS weights.
|
| 894 |
+
const CURATED = [
|
| 895 |
+
'Xenova/gpt2',
|
| 896 |
+
'Xenova/distilgpt2',
|
| 897 |
+
'Xenova/phi-2',
|
| 898 |
+
'Xenova/TinyLlama-1.1B-Chat-v1.0'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
];
|
| 900 |
+
const archWhitelist = [
|
| 901 |
+
'GPT2LMHeadModel',
|
| 902 |
+
'PhiForCausalLM',
|
| 903 |
+
'LlamaForCausalLM',
|
| 904 |
+
'MistralForCausalLM',
|
| 905 |
+
'TinyLlamaForCausalLM'
|
|
|
|
|
|
|
| 906 |
];
|
| 907 |
+
const accepted = [];
|
| 908 |
+
async function fetchConfig(modelId) {
|
| 909 |
+
const url = `${env.remoteURL}/${modelId}/resolve/main/config.json`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
try {
|
| 911 |
+
const resp = await fetch(url, { headers:{ 'Accept':'application/json' } });
|
| 912 |
+
if (!resp.ok) throw new Error(resp.status+ ' ' + resp.statusText);
|
| 913 |
+
return await resp.json();
|
| 914 |
} catch (e) {
|
| 915 |
+
appendDiagnostic('Config fail '+modelId+': '+e.message);
|
| 916 |
return null;
|
| 917 |
}
|
| 918 |
}
|
| 919 |
+
for (const m of CURATED) {
|
| 920 |
+
if (accepted.length >= maxModels) break;
|
| 921 |
+
const cfg = await fetchConfig(m);
|
| 922 |
+
if (!cfg) continue;
|
| 923 |
+
const archs = cfg.architectures || [];
|
| 924 |
+
const ok = archs.some(a => archWhitelist.includes(a));
|
| 925 |
+
if (!ok) {
|
| 926 |
+
appendDiagnostic('Skip '+m+' (arch '+archs.join('/')+' not whitelisted)');
|
| 927 |
+
continue;
|
| 928 |
+
}
|
| 929 |
+
// Rough size gating: reject if hidden_size * n_layer heuristic too large (> ~4B tokens weight proxy)
|
| 930 |
+
const hs = cfg.hidden_size || cfg.n_embd || 0;
|
| 931 |
+
const nl = cfg.num_hidden_layers || cfg.n_layer || 0;
|
| 932 |
+
if (hs && nl && hs * nl > 20000) { // heuristic threshold
|
| 933 |
+
appendDiagnostic('Skip '+m+' (heuristic size too large hs*nl='+hs*nl+')');
|
| 934 |
+
continue;
|
| 935 |
}
|
| 936 |
+
accepted.push(m);
|
| 937 |
}
|
| 938 |
+
if (accepted.length === 0) {
|
| 939 |
+
appendDiagnostic('Discovery empty; using minimal fallback list.');
|
| 940 |
+
accepted.push('Xenova/gpt2','Xenova/distilgpt2');
|
| 941 |
+
}
|
| 942 |
+
return accepted.slice(0, maxModels);
|
|
|
|
| 943 |
}
|
| 944 |
|
| 945 |
trialModelsBtn.addEventListener('click', async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 946 |
trialModelsBtn.disabled = true;
|
| 947 |
+
const TRIAL_PROMPT = 'Do planes fly higher than bees?';
|
| 948 |
+
// Create a live-updating system message
|
| 949 |
+
const liveHeader = '### Model Trials (live, no token)';
|
| 950 |
+
appendMessage({ role: 'system', content: liveHeader + '\nStarting discovery...' });
|
| 951 |
+
const liveEl = chatBox.lastElementChild; // system container
|
| 952 |
+
const lines = [liveHeader, 'Starting discovery...'];
|
| 953 |
+
const flush = () => { liveEl.textContent = lines.join('\n'); };
|
| 954 |
+
const addLine = (l) => { lines.push(l); flush(); };
|
|
|
|
| 955 |
const yieldUI = async () => new Promise(r=>requestAnimationFrame(r));
|
| 956 |
function withTimeout(promise, ms, label) {
|
| 957 |
return Promise.race([
|
|
|
|
| 965 |
discovered = await discoverOpenSmallModels(10);
|
| 966 |
} catch(e) {
|
| 967 |
appendDiagnostic('Discovery error: ' + e.message);
|
| 968 |
+
addLine('Discovery error: ' + e.message);
|
| 969 |
}
|
| 970 |
if (!discovered.length) {
|
| 971 |
+
addLine('No models discovered dynamically. Using static fallbacks.');
|
| 972 |
discovered = ['Xenova/gpt2','Xenova/distilgpt2'];
|
| 973 |
}
|
|
|
|
| 974 |
const baseline = ['Xenova/gpt2','Xenova/distilgpt2'];
|
| 975 |
const ordered = baseline.concat(discovered.filter(m=>!baseline.includes(m)));
|
|
|
|
| 976 |
const MODELS = ordered.slice(0,6);
|
| 977 |
+
addLine('Models to try: ' + MODELS.join(', '));
|
| 978 |
appendDiagnostic('Trial: Models -> ' + discovered.join(', '));
|
| 979 |
const collected = [];
|
| 980 |
try {
|
| 981 |
for (const modelId of MODELS) {
|
| 982 |
let loadTime='-', genTime='-', snippet='', error=null;
|
| 983 |
+
const t0 = performance.now();
|
| 984 |
+
addLine(`Loading ${modelId} ...`);
|
| 985 |
+
flush();
|
| 986 |
try {
|
| 987 |
const pipe = await withTimeout(pipeline('text-generation', modelId, { quantized: true }), 20000, 'load');
|
| 988 |
const t1 = performance.now();
|
|
|
|
| 991 |
loadTime = ((t1-t0)/1000).toFixed(2)+'s';
|
| 992 |
genTime = ((t2-t1)/1000).toFixed(2)+'s';
|
| 993 |
const full = Array.isArray(out) ? (out[0]?.generated_text||'') : (out.generated_text||'');
|
| 994 |
+
snippet = full.trim().slice(0,200).replace(/\n+/g,' ') || '(empty)';
|
| 995 |
+
addLine(`${modelId} ✓ load ${loadTime} gen ${genTime}`);
|
| 996 |
+
addLine(` → ${snippet}`);
|
| 997 |
} catch(e) {
|
| 998 |
error = e?.message || String(e);
|
| 999 |
+
addLine(`${modelId} ✗ ${error}`);
|
| 1000 |
appendDiagnostic('Trial error '+modelId+': '+error);
|
| 1001 |
}
|
| 1002 |
collected.push({ model:modelId, loadTime, genTime, snippet, error });
|
|
|
|
| 1005 |
} finally {
|
| 1006 |
trialModelsBtn.disabled = false;
|
| 1007 |
}
|
| 1008 |
+
addLine('');
|
| 1009 |
+
addLine('### Trial Summary');
|
| 1010 |
for (const r of collected) {
|
| 1011 |
if (r.error) {
|
| 1012 |
+
addLine(`- ${r.model}: ERROR ${r.error}`);
|
| 1013 |
} else {
|
| 1014 |
+
addLine(`- ${r.model} (Load ${r.loadTime} / Gen ${r.genTime})`);
|
| 1015 |
}
|
| 1016 |
}
|
| 1017 |
+
appendDiagnostic('Trial: progress & summary streamed into chat message.');
|
|
|
|
| 1018 |
});
|
| 1019 |
|
| 1020 |
// Event Listeners
|