refactor ai stuff

This commit is contained in:
lubiana 2025-07-05 15:55:17 +02:00
parent bc8f56fc54
commit f2bd344c5b
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
5 changed files with 300 additions and 243 deletions

View file

@ -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}
<span style={{ opacity: 0.3 }}>{"_".repeat(missing)}</span>
</>
);
}
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<MLCEngine | null>(null);
const abortRef = useRef<AbortController | null>(null);
const [output, setOutput] = useState("");
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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 (
<div
@ -269,54 +85,15 @@ const App: React.FC = () => {
>
{EMOJI_HEADER}
</h1>
<div style={MODERN_OUTPUT_STYLE}>
{output ? (
formatUUIDPartial(output)
) : (
<span style={{ opacity: 0.25 }}>
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
</span>
)}
</div>
<UuidOutput output={output} />
{progress && <div style={MODERN_PROGRESS_STYLE}>{progress}</div>}
<button
style={{
...MODERN_BUTTON_STYLE,
opacity: loading ? 0.6 : 1,
pointerEvents: loading ? "none" : "auto",
}}
onClick={handleGenerate}
disabled={loading}
>
{loading ? "Generating..." : "Generate UUIDv4"}
</button>
<UuidControls
loading={loading}
onGenerate={handleGenerate}
onAbort={handleAbort}
/>
{error && <div style={MODERN_ERROR_STYLE}>{error}</div>}
<div style={MODERN_FOOTER_STYLE}>
<span>
Powered by
{" "}
<a
href="https://github.com/mlc-ai/web-llm"
style={{ color: "#646cff", textDecoration: "underline dotted" }}
target="_blank"
rel="noopener noreferrer"
>
WebLLM
</a>
{" & Qwen2.5-0.5B-Instruct"}
<br />
{EMOJI_FOOTER}
<br />
<a
href="https://git.hannover.ccc.de/lubiana/uuid"
style={{ color: "#646cff", textDecoration: "underline dotted" }}
target="_blank"
rel="noopener noreferrer"
>
Source code
</a>
</span>
</div>
<UuidFooter />
</div>
</div>
);

View file

@ -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 (
<>
<button
style={{
...MODERN_BUTTON_STYLE,
opacity: loading ? 0.6 : 1,
pointerEvents: loading ? "none" : "auto",
}}
onClick={onGenerate}
disabled={loading}
>
{loading ? "Generating..." : "Generate UUIDv4"}
</button>
{loading && (
<button
style={{
...MODERN_BUTTON_STYLE,
background: "#ff4d6d",
marginTop: 12,
minWidth: 120,
}}
onClick={onAbort}
>
Abort
</button>
)}
</>
);
}

View file

@ -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 (
<div style={MODERN_FOOTER_STYLE}>
<span>
Powered by
{" "}
<a
href="https://github.com/mlc-ai/web-llm"
style={{ color: "#646cff", textDecoration: "underline dotted" }}
target="_blank"
rel="noopener noreferrer"
>
WebLLM
</a>
{" & Qwen2.5-0.5B-Instruct"}
<br />
{EMOJI_FOOTER}
<br />
<a
href="https://git.hannover.ccc.de/lubiana/uuid"
style={{ color: "#646cff", textDecoration: "underline dotted" }}
target="_blank"
rel="noopener noreferrer"
>
Source code
</a>
</span>
</div>
);
}

View file

@ -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 (
<div style={MODERN_OUTPUT_STYLE}>
{output ? (
<>
{output}
{missing > 0 && <span style={{ opacity: 0.3 }}>{"_".repeat(missing)}</span>}
</>
) : (
<span style={{ opacity: 0.25 }}>
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
</span>
)}
</div>
);
}

View file

@ -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<MLCEngine | null>(null);
const abortRef = useRef<AbortController | null>(null);
const [output, setOutput] = useState("");
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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,
};
}