diff --git a/uuid-browser-2-app/src/App.tsx b/uuid-browser-2-app/src/App.tsx index 404dbbd..9513208 100644 --- a/uuid-browser-2-app/src/App.tsx +++ b/uuid-browser-2-app/src/App.tsx @@ -1,62 +1,10 @@ -import React, { useRef, useState } from "react"; -import { MLCEngine } from "@mlc-ai/web-llm"; -// Try to import the type if available -// import type { ChatCompletionMessageParam } from "@mlc-ai/web-llm"; - -// Qwen2.5-0.5B-Instruct-q4f16_1-MLC is the required model -const MODEL_ID = "Qwen2.5-0.5B-Instruct-q4f16_1-MLC"; - -const UUID_PROMPT = `Your task is to generate one random UUID version 4 (UUIDv4) and output it. Follow these rules exactly: - -1. Output only the UUIDv4 — no explanation, no steps, no formatting, no words. -2. A UUIDv4 is a 36-character string with 5 sections separated by hyphens. -3. The format is: 8-4-4-4-12 hexadecimal characters (0-9, a-f). Example shape: - xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - - The 13th character must be "4" (UUIDv4). - - The 17th character must be one of "8", "9", "a", or "b". - -Return only a valid random UUIDv4 string. Do not say anything else.`; +import React from "react"; +import { useUuidGenerator } from "./useUuidGenerator"; +import { UuidOutput } from "./UuidOutput"; +import { UuidControls } from "./UuidControls"; +import { UuidFooter } from "./UuidFooter"; // Helper: filter and format UUID as it streams -function filterAndFormatUUID(raw: string) { - // Only allow 0-9, a-f, and hyphens, lowercase - let filtered = raw.toLowerCase().replace(/[^0-9a-f-]/g, ""); - // Remove extra hyphens - filtered = filtered.replace(/-{2,}/g, "-"); - // Remove leading/trailing hyphens - filtered = filtered.replace(/^[-]+|[-]+$/g, ""); - // Remove all hyphens to reformat - const hex = filtered.replace(/-/g, ""); - // Format as 8-4-4-4-12 - let uuid = ""; - const sections = [8, 4, 4, 4, 12]; - let i = 0; - for (const len of sections) { - if (hex.length < i + len) break; - uuid += hex.slice(i, i + len); - i += len; - if (uuid.length < 36) uuid += "-"; - } - // Remove trailing hyphen if incomplete - uuid = uuid.replace(/-$/, ""); - // Is it a complete UUIDv4? - const isComplete = - uuid.length === 36 && - ["8", "9", "a", "b"].includes(uuid[19]); - return { uuid, isComplete }; -} - -function formatUUIDPartial(uuid: string) { - // Add subtle color for missing chars - const missing = 36 - uuid.length; - if (missing <= 0) return uuid; - return ( - <> - {uuid} - {"_".repeat(missing)} - - ); -} const MODERN_CARD_STYLE: React.CSSProperties = { maxWidth: 420, @@ -73,36 +21,6 @@ const MODERN_CARD_STYLE: React.CSSProperties = { backdropFilter: "blur(8px)", border: "1.5px solid rgba(100,108,255,0.10)", }; -const MODERN_BUTTON_STYLE: React.CSSProperties = { - marginTop: 32, - padding: "1em 2.5em", - fontSize: 20, - borderRadius: 12, - border: "none", - background: "linear-gradient(90deg, #646cff 0%, #7f53ff 100%)", - color: "#fff", - fontWeight: 700, - cursor: "pointer", - boxShadow: "0 2px 12px #646cff33", - transition: "background 0.2s, box-shadow 0.2s", - minWidth: 200, - letterSpacing: 1, -}; -const MODERN_OUTPUT_STYLE: React.CSSProperties = { - fontFamily: "JetBrains Mono, Fira Mono, Menlo, monospace", - fontSize: 28, - letterSpacing: 2, - margin: "2.5rem 0 0.5rem 0", - minHeight: 38, - wordBreak: "break-all", - textAlign: "center", - color: "#fff", - background: "rgba(40,40,60,0.18)", - borderRadius: 10, - padding: "0.7em 0.5em", - boxShadow: "0 1px 4px #0002", - border: "1px solid #23234a33", -}; const MODERN_PROGRESS_STYLE: React.CSSProperties = { width: "100%", margin: "1.5rem 0 0.5rem 0", @@ -119,120 +37,18 @@ const MODERN_ERROR_STYLE: React.CSSProperties = { fontWeight: 500, letterSpacing: 0.2, }; -const MODERN_FOOTER_STYLE: React.CSSProperties = { - marginTop: 40, - color: "#b3b3d1", - fontSize: 15, - textAlign: "center", - opacity: 0.85, - lineHeight: 1.7, -}; const EMOJI_HEADER = "🤖✨ UUIDv4 AI Generator ✨🔑"; -const EMOJI_FOOTER = "🧠 All AI runs 100% in your browser."; const App: React.FC = () => { - const engineRef = useRef(null); - const abortRef = useRef(null); - const [output, setOutput] = useState(""); - const [loading, setLoading] = useState(false); - const [progress, setProgress] = useState(null); - const [error, setError] = useState(null); - - // Model loader with progress - const loadModel = async () => { - setProgress("Loading model..."); - setError(null); - - if (!engineRef.current) { - engineRef.current = null; - } - engineRef.current = new MLCEngine({ - initProgressCallback: (p: any) => { - if (p && typeof p === "object" && "progress" in p) { - setProgress( - `Downloading model: ${Math.round((p.progress || 0) * 100)}%` - ); - } else { - setProgress("Initializing model..."); - } - }, - }); - try { - await engineRef.current.reload(MODEL_ID); - setProgress(null); - } catch (e: any) { - setError("Model load failed: " + (e?.message || e.toString())); - setProgress(null); - throw e; - } - }; - - // Main generation handler - const handleGenerate = async () => { - setError(null); - setOutput(""); - setLoading(true); - setProgress(null); - // Abort any previous - if (abortRef.current) abortRef.current.abort(); - abortRef.current = new AbortController(); - try { - await loadModel(); - const engine = engineRef.current!; - const messages = [ - { - role: "user", - content: UUID_PROMPT, - }, - ]; - let buffer = ""; - let done = false; - // Streaming - const chunks = await engine.chat.completions.create({ - messages: messages as any, // Type assertion to bypass linter error, structure matches docs - temperature: 1, - stream: true, - stream_options: { include_usage: false }, - }); - for await (const chunk of chunks) { - if (done) break; - const delta = chunk.choices[0]?.delta.content || ""; - buffer += delta; - const { uuid, isComplete } = filterAndFormatUUID(buffer); - setOutput(uuid); - if (isComplete) { - abortRef.current.abort(); - done = true; - setLoading(false); - setProgress(null); - } - } - } catch (e: any) { - if (abortRef.current) abortRef.current.abort(); - setLoading(false); - setProgress(null); - if (e?.name === "AbortError") { - // Ignore abort - } else { - setError("Generation failed: " + (e?.message || e.toString())); - } - } finally { - if (abortRef.current) abortRef.current.abort(); - setLoading(false); - setProgress(null); - } - if (abortRef.current) abortRef.current.abort(); - setLoading(false); - setProgress(null); - }; - - // On unmount, abort any running - React.useEffect(() => { - return () => { - if (abortRef.current) abortRef.current.abort(); - }; - }, []); + const { + output, + loading, + progress, + error, + handleGenerate, + handleAbort, + } = useUuidGenerator(); return (
{ > {EMOJI_HEADER} -
- {output ? ( - formatUUIDPartial(output) - ) : ( - - xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - - )} -
+ {progress &&
{progress}
} - + {error &&
{error}
} -
- - Powered by - {" "} - - WebLLM - - {" & Qwen2.5-0.5B-Instruct"} -
- {EMOJI_FOOTER} -
- - Source code - -
-
+
); diff --git a/uuid-browser-2-app/src/UuidControls.tsx b/uuid-browser-2-app/src/UuidControls.tsx new file mode 100644 index 0000000..4a32b2b --- /dev/null +++ b/uuid-browser-2-app/src/UuidControls.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +const MODERN_BUTTON_STYLE: React.CSSProperties = { + marginTop: 32, + padding: "1em 2.5em", + fontSize: 20, + borderRadius: 12, + border: "none", + background: "linear-gradient(90deg, #646cff 0%, #7f53ff 100%)", + color: "#fff", + fontWeight: 700, + cursor: "pointer", + boxShadow: "0 2px 12px #646cff33", + transition: "background 0.2s, box-shadow 0.2s", + minWidth: 200, + letterSpacing: 1, +}; + +export function UuidControls({ loading, onGenerate, onAbort }: { + loading: boolean; + onGenerate: () => void; + onAbort: () => void; +}) { + return ( + <> + + {loading && ( + + )} + + ); +} \ No newline at end of file diff --git a/uuid-browser-2-app/src/UuidFooter.tsx b/uuid-browser-2-app/src/UuidFooter.tsx new file mode 100644 index 0000000..7fe760e --- /dev/null +++ b/uuid-browser-2-app/src/UuidFooter.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +const MODERN_FOOTER_STYLE: React.CSSProperties = { + marginTop: 40, + color: "#b3b3d1", + fontSize: 15, + textAlign: "center", + opacity: 0.85, + lineHeight: 1.7, +}; + +const EMOJI_FOOTER = "\uD83E\uDDE0 All AI runs 100% in your browser."; + +export function UuidFooter() { + return ( +
+ + Powered by + {" "} + + WebLLM + + {" & Qwen2.5-0.5B-Instruct"} +
+ {EMOJI_FOOTER} +
+ + Source code + +
+
+ ); +} \ No newline at end of file diff --git a/uuid-browser-2-app/src/UuidOutput.tsx b/uuid-browser-2-app/src/UuidOutput.tsx new file mode 100644 index 0000000..383f2c0 --- /dev/null +++ b/uuid-browser-2-app/src/UuidOutput.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +const MODERN_OUTPUT_STYLE: React.CSSProperties = { + fontFamily: "JetBrains Mono, Fira Mono, Menlo, monospace", + fontSize: 28, + letterSpacing: 2, + margin: "2.5rem 0 0.5rem 0", + minHeight: 38, + wordBreak: "break-all", + textAlign: "center", + color: "#fff", + background: "rgba(40,40,60,0.18)", + borderRadius: 10, + padding: "0.7em 0.5em", + boxShadow: "0 1px 4px #0002", + border: "1px solid #23234a33", +}; + +export function UuidOutput({ output }: { output: string }) { + // Add subtle color for missing chars + const missing = 36 - output.length; + return ( +
+ {output ? ( + <> + {output} + {missing > 0 && {"_".repeat(missing)}} + + ) : ( + + xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + + )} +
+ ); +} \ No newline at end of file diff --git a/uuid-browser-2-app/src/useUuidGenerator.ts b/uuid-browser-2-app/src/useUuidGenerator.ts new file mode 100644 index 0000000..e0a5816 --- /dev/null +++ b/uuid-browser-2-app/src/useUuidGenerator.ts @@ -0,0 +1,149 @@ +import { useRef, useState, useEffect } from "react"; +import { MLCEngine } from "@mlc-ai/web-llm"; + +const MODEL_ID = "Qwen2.5-0.5B-Instruct-q4f16_1-MLC"; + +const UUID_PROMPT = `Your task is to generate one random UUID version 4 (UUIDv4) and output it. Follow these rules exactly: + +1. Output only the UUIDv4 — no explanation, no steps, no formatting, no words. +2. A UUIDv4 is a 36-character string with 5 sections separated by hyphens. +3. The format is: 8-4-4-4-12 hexadecimal characters (0-9, a-f). Example shape: + xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + - The 13th character must be "4" (UUIDv4). + - The 17th character must be one of "8", "9", "a", or "b". + +Return only a valid random UUIDv4 string. Do not say anything else.`; + +function filterAndFormatUUID(raw: string) { + let filtered = raw.toLowerCase().replace(/[^0-9a-f-]/g, ""); + filtered = filtered.replace(/-{2,}/g, "-"); + filtered = filtered.replace(/^[-]+|[-]+$/g, ""); + const hex = filtered.replace(/-/g, ""); + let uuid = ""; + const sections = [8, 4, 4, 4, 12]; + let i = 0; + for (const len of sections) { + if (hex.length < i + len) break; + uuid += hex.slice(i, i + len); + i += len; + if (uuid.length < 36) uuid += "-"; + } + uuid = uuid.replace(/-$/, ""); + const isComplete = + uuid.length === 36 && + ["8", "9", "a", "b"].includes(uuid[19]); + return { uuid, isComplete }; +} + +export function useUuidGenerator() { + const engineRef = useRef(null); + const abortRef = useRef(null); + const [output, setOutput] = useState(""); + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); + + const loadModel = async () => { + setProgress("Loading model..."); + setError(null); + if (!engineRef.current) { + engineRef.current = null; + } + engineRef.current = new MLCEngine({ + initProgressCallback: (p: any) => { + if (p && typeof p === "object" && "progress" in p) { + setProgress( + `Downloading model: ${Math.round((p.progress || 0) * 100)}%` + ); + } else { + setProgress("Initializing model..."); + } + }, + }); + try { + await engineRef.current.reload(MODEL_ID); + setProgress(null); + } catch (e: any) { + setError("Model load failed: " + (e?.message || e.toString())); + setProgress(null); + throw e; + } + }; + + const handleGenerate = async () => { + setError(null); + setOutput(""); + setLoading(true); + setProgress(null); + if (abortRef.current) abortRef.current.abort(); + abortRef.current = new AbortController(); + try { + await loadModel(); + const engine = engineRef.current!; + const messages = [ + { + role: "user", + content: UUID_PROMPT, + }, + ]; + let buffer = ""; + let done = false; + const chunks = await engine.chat.completions.create({ + messages: messages as any, + temperature: 1, + stream: true, + stream_options: { include_usage: false }, + }); + for await (const chunk of chunks) { + if (done) break; + const delta = chunk.choices[0]?.delta.content || ""; + buffer += delta; + const { uuid, isComplete } = filterAndFormatUUID(buffer); + setOutput(uuid); + if (isComplete) { + abortRef.current.abort(); + done = true; + setLoading(false); + setProgress(null); + } + } + } catch (e: any) { + if (abortRef.current) abortRef.current.abort(); + setLoading(false); + setProgress(null); + if (e?.name === "AbortError") { + // Ignore abort + } else { + setError("Generation failed: " + (e?.message || e.toString())); + } + } finally { + if (abortRef.current) abortRef.current.abort(); + setLoading(false); + setProgress(null); + } + if (abortRef.current) abortRef.current.abort(); + setLoading(false); + setProgress(null); + }; + + const handleAbort = () => { + if (abortRef.current) abortRef.current.abort(); + setLoading(false); + setProgress(null); + }; + + useEffect(() => { + return () => { + if (abortRef.current) abortRef.current.abort(); + }; + }, []); + + return { + output, + loading, + progress, + error, + handleGenerate, + handleAbort, + }; +} \ No newline at end of file