kv-tube/static/js/webai.js
2026-01-10 14:35:08 +07:00

144 lines
4.9 KiB
JavaScript

/**
* KV-Tube WebAI Service
* Local AI chatbot for transcript Q&A using WebLLM
*
* Runs entirely in-browser, no server required after model download
*/
// WebLLM CDN import (lazy loaded)
var WEBLLM_CDN = 'https://esm.run/@mlc-ai/web-llm';
// Model options - using verified WebLLM model IDs
var AI_MODELS = {
small: { id: 'Qwen2-0.5B-Instruct-q4f16_1-MLC', name: 'Qwen2 (0.5B)', size: '350MB' },
medium: { id: 'Qwen2-1.5B-Instruct-q4f16_1-MLC', name: 'Qwen2 (1.5B)', size: '1GB' },
};
// Default to small model
var DEFAULT_MODEL = AI_MODELS.small;
if (typeof TranscriptAI === 'undefined') {
window.TranscriptAI = class TranscriptAI {
constructor() {
this.engine = null;
this.isLoading = false;
this.isReady = false;
this.transcript = '';
this.onProgressCallback = null;
this.onReadyCallback = null;
}
setTranscript(text) {
this.transcript = text.slice(0, 8000); // Limit context size
}
setCallbacks({ onProgress, onReady }) {
this.onProgressCallback = onProgress;
this.onReadyCallback = onReady;
}
async init() {
if (this.isReady || this.isLoading) return;
this.isLoading = true;
try {
// Dynamic import WebLLM
const { CreateMLCEngine } = await import(WEBLLM_CDN);
// Initialize engine with progress callback
this.engine = await CreateMLCEngine(DEFAULT_MODEL.id, {
initProgressCallback: (report) => {
if (this.onProgressCallback) {
this.onProgressCallback(report);
}
console.log('AI Load Progress:', report.text);
}
});
this.isReady = true;
this.isLoading = false;
if (this.onReadyCallback) {
this.onReadyCallback();
}
console.log('TranscriptAI ready with model:', DEFAULT_MODEL.name);
} catch (err) {
this.isLoading = false;
console.error('Failed to load AI model:', err);
throw err;
}
}
async ask(question) {
if (!this.isReady) {
throw new Error('AI not initialized');
}
const systemPrompt = this.transcript
? `You are a helpful AI assistant analyzing a video transcript. Answer the user's question based ONLY on the transcript content below. Be concise and direct. If the answer is not in the transcript, say so.\n\nTRANSCRIPT:\n${this.transcript}`
: `You are a helpful AI assistant for KV-Tube, a lightweight YouTube client. You can help the user with general questions, explain features of the app, or chat casually. Be concise and helpful.`;
try {
const response = await this.engine.chat.completions.create({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
max_tokens: 256,
temperature: 0.7,
});
return response.choices[0].message.content;
} catch (err) {
console.error('AI response error:', err);
throw err;
}
}
async *askStreaming(question) {
if (!this.isReady) {
throw new Error('AI not initialized');
}
const systemPrompt = this.transcript
? `You are a helpful AI assistant analyzing a video transcript. Answer the user's question based ONLY on the transcript content below. Be concise and direct. If the answer is not in the transcript, say so.\n\nTRANSCRIPT:\n${this.transcript}`
: `You are a helpful AI assistant for KV-Tube, a lightweight YouTube client. You can help the user with general questions, explain features of the app, or chat casually. Be concise and helpful.`;
const chunks = await this.engine.chat.completions.create({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
max_tokens: 256,
temperature: 0.7,
stream: true,
});
for await (const chunk of chunks) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
yield delta;
}
}
}
getModelInfo() {
return DEFAULT_MODEL;
}
isModelReady() {
return this.isReady;
}
isModelLoading() {
return this.isLoading;
}
}
// Global instance
window.transcriptAI = new TranscriptAI();
}