File size: 4,245 Bytes
053ee0d
 
 
92f6062
053ee0d
 
 
92f6062
053ee0d
 
 
 
92f6062
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
053ee0d
 
 
 
d256fda
053ee0d
 
 
 
f18f414
 
 
 
 
 
 
 
 
 
 
 
053ee0d
 
 
 
 
d256fda
 
 
 
053ee0d
 
 
 
d256fda
053ee0d
 
 
 
d256fda
053ee0d
 
d256fda
d4e1d86
053ee0d
 
 
 
 
 
 
 
92f6062
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4e1d86
 
f18f414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4e1d86
 
053ee0d
 
 
 
92f6062
d4e1d86
053ee0d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { pipeline, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.0/dist/transformers.min.js";

const MODEL_ID = "onnx-community/Qwen2.5-Coder-1.5B-Instruct";
const DTYPE_CANDIDATES = ["q4f16", "q4", "fp16"];

let generator = null;
let isLoaded = false;
let activeDtype = null;

export async function loadModel(onProgress) {
    if (isLoaded) return;

    const errors = [];
    for (const dtype of DTYPE_CANDIDATES) {
        try {
            onProgress?.({ status: "attempt", dtype });
            generator = await pipeline("text-generation", MODEL_ID, {
                dtype,
                device: "webgpu",
                progress_callback: (progress) => onProgress?.({ ...progress, dtype }),
            });
            activeDtype = dtype;
            isLoaded = true;
            return;
        } catch (error) {
            console.error(`Model load failed for dtype=${dtype}`, error);
            errors.push(`${dtype}: ${formatError(error)}`);
        }
    }

    throw new Error(`All WebGPU dtype attempts failed. ${errors.join(" | ")}`);
}

export async function generateCode(prompt, language, onToken, onComplete) {
    if (!generator) throw new Error("Model not loaded");
    let streamedText = "";

    const messages = [
        {
            role: "system",
            content: [
                `You are an expert ${language} programmer.`,
                "Return raw source code only.",
                "Do not use markdown fences.",
                "Do not add explanations, bullet points, headings, comments, or usage notes.",
                "Do not wrap the answer in ```.",
                "The response must be directly executable or pasteable as a source file.",
            ].join(" "),
        },
        {
            role: "user",
            content: `${prompt}\n\nReturn only the ${language} code. No markdown. No comments. No explanation.`,
        },
    ];

    const streamer = new TextStreamer(generator.tokenizer, {
        skip_prompt: true,
        callback_function: (token) => {
            streamedText += token;
            onToken(token);
        },
    });

    const result = await generator(messages, {
        max_new_tokens: 1024,
        do_sample: false,
        streamer,
    });

    const generated = result?.[0]?.generated_text;
    const resultText = Array.isArray(generated)
        ? generated.at(-1).content
        : String(generated || "");
    const fullCodeRaw = resultText && resultText.trim() ? resultText : streamedText;
    const fullCode = stripMarkdownCodeFence(fullCodeRaw);
    onComplete(fullCode);
    return fullCode;
}

export function isWebGPUSupported() {
    return Boolean(navigator.gpu);
}

export function getActiveDtype() {
    return activeDtype;
}

function formatError(error) {
    if (!error) return "unknown error";
    if (error.message) return error.message;
    if (typeof error === "string") return error;
    try {
        return JSON.stringify(error);
    } catch {
        return String(error);
    }
}

export function stripMarkdownCodeFence(text) {
    const trimmed = String(text || "").trim();
    if (!trimmed) return "";

    let code = trimmed;
    const openingFence = code.match(/^```(?:[a-zA-Z0-9_+#.-]+)?\s*\n?/);
    if (openingFence) {
        code = code.slice(openingFence[0].length);
        const closingIndex = code.indexOf("```");
        if (closingIndex >= 0) code = code.slice(0, closingIndex);
    } else {
        const firstFence = code.indexOf("```");
        if (firstFence >= 0) code = code.slice(0, firstFence);
    }

    return trimMarkdownExplanation(code);
}

function trimMarkdownExplanation(text) {
    const lines = String(text || "").split(/\r?\n/);
    const explanationPattern =
        /^\s*(?:[-*]\s+|\d+\.\s+|#{1,6}\s+|Explanation\s*:|Steps\s*:|Notes?\s*:|The code\b|This code\b)/i;

    let cutIndex = lines.length;
    for (let i = 0; i < lines.length; i += 1) {
        if (explanationPattern.test(lines[i])) {
            cutIndex = i;
            break;
        }
    }

    return lines.slice(0, cutIndex).join("\n").trim();
}

Object.assign(window, {
    loadModel,
    generateCode,
    isWebGPUSupported,
    getActiveDtype,
    stripMarkdownCodeFence,
});