refactor ai stuff
This commit is contained in:
parent
bc8f56fc54
commit
f2bd344c5b
5 changed files with 300 additions and 243 deletions
|
@ -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>
|
||||
);
|
||||
|
|
52
uuid-browser-2-app/src/UuidControls.tsx
Normal file
52
uuid-browser-2-app/src/UuidControls.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
43
uuid-browser-2-app/src/UuidFooter.tsx
Normal file
43
uuid-browser-2-app/src/UuidFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
36
uuid-browser-2-app/src/UuidOutput.tsx
Normal file
36
uuid-browser-2-app/src/UuidOutput.tsx
Normal 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>
|
||||
);
|
||||
}
|
149
uuid-browser-2-app/src/useUuidGenerator.ts
Normal file
149
uuid-browser-2-app/src/useUuidGenerator.ts
Normal 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,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue