|
import { prebuiltAppConfig, CreateMLCEngine } from "@charliefruan/web-llm"; |
|
import hljs from "highlight.js"; |
|
import ace from "ace-builds"; |
|
|
|
|
|
require("ace-builds/src-noconflict/mode-javascript"); |
|
require("ace-builds/webpack-resolver"); |
|
|
|
|
|
|
|
const { Type } = require("@sinclair/typebox"); |
|
|
|
let engine = null; |
|
let useCustomGrammar = false; |
|
|
|
document.addEventListener("DOMContentLoaded", () => { |
|
|
|
const grammarSelection = document.getElementById("grammar-selection"); |
|
const ebnfContainer = document.getElementById("ebnf-grammar-container"); |
|
const schemaContainer = document.getElementById("schema-container"); |
|
const modelSelection = document.getElementById("model-selection"); |
|
const ebnfTextarea = document.getElementById("ebnf-grammar"); |
|
const promptTextarea = document.getElementById("prompt"); |
|
const outputDiv = document.getElementById("output"); |
|
const statsParagraph = document.getElementById("stats"); |
|
|
|
|
|
grammarSelection.onchange = (ev) => { |
|
console.log("Grammar selection changed:", ev.target.value); |
|
if (ev.target.value === "json") { |
|
ebnfContainer.classList.add("hidden"); |
|
schemaContainer.classList.remove("hidden"); |
|
useCustomGrammar = false; |
|
} else { |
|
ebnfContainer.classList.remove("hidden"); |
|
schemaContainer.classList.add("hidden"); |
|
useCustomGrammar = true; |
|
} |
|
}; |
|
|
|
|
|
const availableModels = prebuiltAppConfig.model_list |
|
.filter( |
|
(m) => |
|
m.model_id.startsWith("Qwen2.5-0.5B-Instruct") || |
|
m.model_id.startsWith("Qwen2.5-1.5B-Instruct") || |
|
m.model_id.startsWith("Llama-3") || |
|
m.model_id.startsWith("Hermes-2") || |
|
m.model_id.startsWith("Hermes-3") || |
|
m.model_id.startsWith("Phi-3") |
|
) |
|
.map((m) => m.model_id); |
|
|
|
let selectedModel = availableModels[0]; |
|
|
|
availableModels.forEach((modelId) => { |
|
const option = document.createElement("option"); |
|
option.value = modelId; |
|
option.textContent = modelId; |
|
modelSelection.appendChild(option); |
|
}); |
|
|
|
modelSelection.value = selectedModel; |
|
|
|
modelSelection.onchange = (e) => { |
|
selectedModel = e.target.value; |
|
engine = null; |
|
}; |
|
|
|
|
|
const jsonSchemaEditor = ace.edit("schema", { |
|
mode: "ace/mode/javascript", |
|
theme: "ace/theme/github", |
|
wrap: true, |
|
}); |
|
jsonSchemaEditor.setTheme("ace/theme/github"); |
|
jsonSchemaEditor.setValue(`Type.Object({ |
|
"name": Type.String(), |
|
"house": Type.Enum({ |
|
"Gryffindor": "Gryffindor", |
|
"Hufflepuff": "Hufflepuff", |
|
"Ravenclaw": "Ravenclaw", |
|
"Slytherin": "Slytherin", |
|
}), |
|
"blood_status": Type.Enum({ |
|
"Pure-blood": "Pure-blood", |
|
"Half-blood": "Half-blood", |
|
"Muggle-born": "Muggle-born", |
|
}), |
|
"occupation": Type.Enum({ |
|
"Student": "Student", |
|
"Professor": "Professor", |
|
"Ministry of Magic": "Ministry of Magic", |
|
"Other": "Other", |
|
}), |
|
"wand": Type.Object({ |
|
"wood": Type.String(), |
|
"core": Type.String(), |
|
"length": Type.Number(), |
|
}), |
|
"alive": Type.Boolean(), |
|
"patronus": Type.String(), |
|
})`); |
|
|
|
const grammarEditor = ace.edit("ebnf-grammar", { |
|
theme: "ace/theme/github", |
|
wrap: true, |
|
}); |
|
grammarEditor.setTheme("ace/theme/github"); |
|
grammarEditor.setValue(String.raw`main ::= basic_array | basic_object |
|
basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object |
|
basic_integer ::= ("0" | "-"? [1-9] [0-9]*) ".0"? |
|
basic_number ::= ("0" | "-"? [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)? |
|
basic_string ::= (([\"] basic_string_1 [\"])) |
|
basic_string_1 ::= "" | [^"\\\x00-\x1F] basic_string_1 | "\\" escape basic_string_1 |
|
escape ::= ["\\/bfnrt] | "u" [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9] |
|
basic_boolean ::= "true" | "false" |
|
basic_null ::= "null" |
|
basic_array ::= "[" ("" | ws basic_any (ws "," ws basic_any)*) ws "]" |
|
basic_object ::= "{" ("" | ws basic_string ws ":" ws basic_any ( ws "," ws basic_string ws ":" ws basic_any)*) ws "}" |
|
ws ::= [\n\t]*`); |
|
|
|
|
|
promptTextarea.value = `Hermione Granger is a character in Harry Potter. Please fill in the following information about this character in JSON format. |
|
Name is a string of character name. |
|
House is one of Gryffindor, Hufflepuff, Ravenclaw, Slytherin. |
|
Blood status is one of Pure-blood, Half-blood, Muggle-born. |
|
Occupation is one of Student, Professor, Ministry of Magic, Other. |
|
Wand is an object with wood, core, and length. |
|
Alive is a boolean. |
|
Patronus is a string. |
|
`; |
|
|
|
document.getElementById("generate").onclick = async () => { |
|
if (!engine) { |
|
engine = await CreateMLCEngine(selectedModel, { |
|
initProgressCallback: (progress) => { |
|
console.log(progress); |
|
outputDiv.textContent = progress.text; |
|
}, |
|
}); |
|
} |
|
let response_format = { type: "grammar", grammar: grammarEditor.getValue() }; |
|
if (!useCustomGrammar) { |
|
const schemaInput = jsonSchemaEditor.getValue(); |
|
let T; |
|
try { |
|
T = eval(schemaInput); |
|
} catch (e) { |
|
console.error("Invalid schema", e); |
|
return; |
|
} |
|
const schema = JSON.stringify(T); |
|
response_format = { type: "json_object", schema } |
|
} |
|
console.log(response_format); |
|
const request = { |
|
stream: true, |
|
stream_options: { include_usage: true }, |
|
messages: [{ role: "user", content: promptTextarea.value }], |
|
max_tokens: 512, |
|
response_format, |
|
}; |
|
|
|
let curMessage = ""; |
|
let usage = null; |
|
const generator = await engine.chatCompletion(request); |
|
|
|
for await (const chunk of generator) { |
|
const curDelta = chunk.choices[0]?.delta.content; |
|
if (curDelta) curMessage += curDelta; |
|
if (chunk.usage) usage = chunk.usage; |
|
outputDiv.textContent = curMessage; |
|
} |
|
|
|
const finalMessage = await engine.getMessage(); |
|
outputDiv.innerHTML = hljs.highlight(finalMessage, { |
|
language: "json", |
|
}).value; |
|
|
|
if (usage) { |
|
const statsTextParts = []; |
|
console.log(usage); |
|
if (usage.extra.prefill_tokens_per_s) { |
|
statsTextParts.push(`Prefill Speed: ${usage.extra.prefill_tokens_per_s.toFixed( |
|
1 |
|
)} tok/s`); |
|
} |
|
if (usage.extra.decode_tokens_per_s) { |
|
statsTextParts.push(`Decode Speed: ${usage.extra.decode_tokens_per_s.toFixed( |
|
1 |
|
)} tok/s`); |
|
} |
|
if (usage.extra.time_per_output_token_s) { |
|
statsTextParts.push(`Time Per Output Token: ${(1000 * usage.extra.time_per_output_token_s).toFixed( |
|
0 |
|
)} ms`); |
|
} |
|
if (usage.extra.time_to_first_token_s) { |
|
statsTextParts.push(`Time to First Token: ${(1000 * usage.extra.time_to_first_token_s).toFixed( |
|
0 |
|
)} ms`); |
|
} |
|
if (usage.extra.grammar_init_s) { |
|
statsTextParts.push(`Grammar Init Overhead: ${(1000 * usage.extra.grammar_init_s).toFixed( |
|
0 |
|
)} ms`); |
|
} |
|
if (usage.extra.grammar_per_token_s) { |
|
statsTextParts.push(`Grammar Per-token Overhead: ${(1000 * usage.extra.grammar_per_token_s).toFixed( |
|
2 |
|
)} ms`); |
|
} |
|
statsParagraph.textContent = statsTextParts.join(", "); |
|
statsParagraph.classList.remove("hidden"); |
|
} |
|
}; |
|
}); |
|
|