| 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"); |
| } |
| }; |
| }); |
|
|