307 lines
10 KiB
JavaScript
307 lines
10 KiB
JavaScript
// This is free software for the public good of a permacomputer hosted at
|
|
// permacomputer.com, an always-on computer by the people, for the people.
|
|
// One which is durable, easy to repair, & distributed like tap water
|
|
// for machine learning intelligence.
|
|
//
|
|
// The permacomputer is community-owned infrastructure optimized around
|
|
// four values:
|
|
//
|
|
// TRUTH First principles, math & science, open source code freely distributed
|
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
|
// LOVE Be yourself without hurting others, cooperation through natural law
|
|
//
|
|
// This software contributes to that vision by making machine learning
|
|
// accessible to everyone through a free, open, embeddable chat interface.
|
|
// Code is seeds to sprout on any abandoned technology.
|
|
|
|
export const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech";
|
|
export const VOICES_API_URL = "https://speech.ai.unturf.com/v1/voices";
|
|
export const MEGAPARCE_API_URL = "https://megaparce.ai.unturf.com/v1/file";
|
|
export const CODE_EXEC_URL = "https://code.ai.unturf.com"; // Code execution sandbox
|
|
export const API_KEY = "dummy-api-key";
|
|
export const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"; // default model (always available)
|
|
export const DEFAULT_MAX_TOKENS = 8000; // default max tokens
|
|
|
|
// System message configuration - implementors can customize this
|
|
export const SYSTEM_MESSAGE_BASE =
|
|
"You are Hermes, an advanced language model from Nous Research, dynamically embedded on this webpage through uncloseai.com. You have complete awareness of the page content, including all text, links, metadata, and context. Your role is to be deeply knowledgeable about THIS specific page while maintaining your capabilities as a versatile AI assistant. You should actively demonstrate your understanding by referencing specific details, names, sections, or data points from the page when relevant. Be conversational yet precise, and always ground your responses in the actual content present on the page when answering page-related questions.";
|
|
|
|
// Optional additional system message content - can be set by implementor
|
|
export let SYSTEM_MESSAGE_APPEND = "";
|
|
|
|
// Function to set additional system message content
|
|
export function setSystemMessageAppend(appendText) {
|
|
SYSTEM_MESSAGE_APPEND = appendText;
|
|
}
|
|
|
|
// Normalize language codes to supported format
|
|
export function normalizeLanguageCode(lang) {
|
|
if (!lang) return null;
|
|
const normalized = lang.toLowerCase().trim();
|
|
|
|
// Direct matches
|
|
const supported = ["en", "zh", "hi", "es", "fr", "ar", "bn", "ru", "pt", "ur", "id", "de", "ja", "sw", "mr", "te", "tr", "zh-tw", "ko", "it", "nl", "pl", "vi", "th", "uk", "sv"];
|
|
if (supported.includes(normalized)) return normalized;
|
|
|
|
// Common aliases and variations
|
|
const aliases = {
|
|
"english": "en",
|
|
"chinese": "zh",
|
|
"mandarin": "zh",
|
|
"zh-cn": "zh",
|
|
"zh_cn": "zh",
|
|
"hindi": "hi",
|
|
"spanish": "es",
|
|
"español": "es",
|
|
"french": "fr",
|
|
"français": "fr",
|
|
"arabic": "ar",
|
|
"bengali": "bn",
|
|
"russian": "ru",
|
|
"português": "pt",
|
|
"portuguese": "pt",
|
|
"urdu": "ur",
|
|
"indonesian": "id",
|
|
"german": "de",
|
|
"deutsch": "de",
|
|
"japanese": "ja",
|
|
"swahili": "sw",
|
|
"marathi": "mr",
|
|
"telugu": "te",
|
|
"turkish": "tr",
|
|
"türkçe": "tr",
|
|
"traditional chinese": "zh-tw",
|
|
"zh_tw": "zh-tw",
|
|
"korean": "ko",
|
|
"italian": "it",
|
|
"italiano": "it",
|
|
"dutch": "nl",
|
|
"nederlands": "nl",
|
|
"polish": "pl",
|
|
"polski": "pl",
|
|
"vietnamese": "vi",
|
|
"tiếng việt": "vi",
|
|
"thai": "th",
|
|
"ภาษาไทย": "th",
|
|
"ukrainian": "uk",
|
|
"українська": "uk",
|
|
"swedish": "sv",
|
|
"svenska": "sv",
|
|
};
|
|
|
|
if (aliases[normalized]) return aliases[normalized];
|
|
|
|
console.warn("Unknown language code:", lang, "- defaulting to en");
|
|
return "en";
|
|
}
|
|
|
|
// Function to get the complete system message with language preference
|
|
export async function getSystemMessage() {
|
|
const hasCustomPrompt = typeof window !== "undefined" && window.UNCLOSEAI_SYSTEM_PROMPT;
|
|
const shouldReplace = typeof window !== "undefined" && window.UNCLOSEAI_SYSTEM_PROMPT_REPLACE === true;
|
|
|
|
// Get user's language preference
|
|
// Priority: window.UNCLOSEAI_LANGUAGE (forced by embed) > localStorage > "en"
|
|
let userLang = "en";
|
|
const rawEmbedLang = (typeof window !== "undefined" && window.UNCLOSEAI_LANGUAGE) ? window.UNCLOSEAI_LANGUAGE : null;
|
|
const embedLang = normalizeLanguageCode(rawEmbedLang);
|
|
|
|
console.log("getSystemMessage: rawEmbedLang =", rawEmbedLang, "normalized =", embedLang);
|
|
|
|
// If embed forces a language, always use it (no user override)
|
|
if (rawEmbedLang && embedLang) {
|
|
userLang = embedLang;
|
|
console.log("getSystemMessage: Using forced embed language:", userLang);
|
|
} else {
|
|
try {
|
|
userLang = localStorage.getItem("uncloseai_language") || "en";
|
|
console.log("getSystemMessage: Using localStorage language:", userLang);
|
|
} catch (error) {
|
|
console.warn("Failed to read language preference:", error);
|
|
}
|
|
}
|
|
|
|
// Add language instruction to the system message
|
|
let languageInstruction = "";
|
|
if (userLang !== "en") {
|
|
// Map language codes to full names for clarity
|
|
const langNames = {
|
|
es: "Spanish",
|
|
zh: "Chinese (Simplified)",
|
|
hi: "Hindi",
|
|
fr: "French",
|
|
ar: "Arabic",
|
|
bn: "Bengali",
|
|
ru: "Russian",
|
|
pt: "Portuguese",
|
|
ur: "Urdu",
|
|
id: "Indonesian",
|
|
de: "German",
|
|
ja: "Japanese",
|
|
sw: "Swahili",
|
|
mr: "Marathi",
|
|
te: "Telugu",
|
|
tr: "Turkish",
|
|
"zh-tw": "Chinese (Traditional)",
|
|
ko: "Korean",
|
|
it: "Italian",
|
|
nl: "Dutch",
|
|
pl: "Polish",
|
|
vi: "Vietnamese",
|
|
th: "Thai",
|
|
uk: "Ukrainian",
|
|
sv: "Swedish",
|
|
};
|
|
|
|
const langName = langNames[userLang] || userLang;
|
|
languageInstruction = `\n\nIMPORTANT: The user has set their language preference to ${langName}. Please respond in ${langName} unless the user explicitly asks for another language. Maintain natural, fluent communication in ${langName}.`;
|
|
console.log("getSystemMessage: Adding language instruction for", langName);
|
|
}
|
|
|
|
// If REPLACE flag is set with a custom prompt, use only the custom prompt
|
|
if (hasCustomPrompt && shouldReplace) {
|
|
console.log("uncloseai.js: Replacing system prompt with custom prompt from parent site");
|
|
return window.UNCLOSEAI_SYSTEM_PROMPT + languageInstruction;
|
|
}
|
|
|
|
// Combine all append sources: internal SYSTEM_MESSAGE_APPEND + parent's custom prompt
|
|
const parentPrompt = hasCustomPrompt ? window.UNCLOSEAI_SYSTEM_PROMPT : "";
|
|
const combinedAppend = [SYSTEM_MESSAGE_APPEND, parentPrompt].filter(Boolean).join("\n\n");
|
|
|
|
if (combinedAppend) {
|
|
console.log("uncloseai.js: Appending custom prompt to default system prompt");
|
|
}
|
|
|
|
const baseMessage = combinedAppend
|
|
? `${SYSTEM_MESSAGE_BASE}\n\n${combinedAppend}`
|
|
: SYSTEM_MESSAGE_BASE;
|
|
|
|
return baseMessage + languageInstruction;
|
|
}
|
|
|
|
// Dynamic Endpoints Configuration for Chat API
|
|
export const VLLM_ENDPOINTS = [
|
|
{ id: "hermes.ai.unturf.com", url: "https://hermes.ai.unturf.com/v1" },
|
|
// { id: "hermes2.ai.unturf.com", url: "https://hermes2.ai.unturf.com/v1" }, // Disabled - upstream returns 404
|
|
{
|
|
id: "qwen.ai.unturf.com",
|
|
url: "https://qwen.ai.unturf.com/v1",
|
|
// Ollama endpoints don't return max_model_len, so we specify it here
|
|
// Note: Model supports 262144 but limited by GPU VRAM (24GB)
|
|
modelContextWindows: {
|
|
'hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M': 32768,
|
|
}
|
|
},
|
|
// { id: "gpt-oss.ai.unturf.com", url: "https://gpt-oss.ai.unturf.com/v1" }, // Disabled - upstream returns 404
|
|
];
|
|
|
|
// Helper to get vault value or localStorage fallback
|
|
function getVaultOrStorage(key, defaultValue = null) {
|
|
// Try vault first if available
|
|
if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
|
|
return window.UncloseVault.get(key, defaultValue);
|
|
}
|
|
// Fallback to localStorage
|
|
const stored = localStorage.getItem(key);
|
|
if (stored === null) return defaultValue;
|
|
// Parse booleans
|
|
if (stored === 'true') return true;
|
|
if (stored === 'false') return false;
|
|
return stored;
|
|
}
|
|
|
|
// Function to get API configuration (custom or default)
|
|
export async function getAPIConfig() {
|
|
try {
|
|
const useCustomAPI = getVaultOrStorage("useCustomAPI", false) === true;
|
|
|
|
if (useCustomAPI) {
|
|
const customBaseURL = getVaultOrStorage("customBaseURL", "");
|
|
const customAPIKey = getVaultOrStorage("customAPIKey", "");
|
|
|
|
// Import model functions to get the currently selected model
|
|
const { getSelectedModel } = await import("./models.js");
|
|
const selectedModel = getSelectedModel();
|
|
|
|
// Check if the selected model has a registered endpoint
|
|
const { getSelectedModelEndpoint } = await import("./models.js");
|
|
const modelEndpoint = getSelectedModelEndpoint();
|
|
|
|
// If model endpoint matches custom endpoint, use custom API credentials
|
|
if (modelEndpoint === customBaseURL) {
|
|
console.log("🔧 Model endpoint matches custom API - using custom credentials");
|
|
return {
|
|
isCustom: true,
|
|
endpoint: customBaseURL,
|
|
apiKey: customAPIKey,
|
|
model: selectedModel,
|
|
headers: {
|
|
"Authorization": `Bearer ${customAPIKey}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
};
|
|
}
|
|
|
|
// If model has built-in endpoint (like Hermes), use that with built-in credentials
|
|
if (modelEndpoint && VLLM_ENDPOINTS.some(ep => ep.url === modelEndpoint)) {
|
|
console.log("🚀 Model has built-in endpoint - using:", modelEndpoint);
|
|
return {
|
|
isCustom: false,
|
|
endpoint: modelEndpoint,
|
|
model: selectedModel,
|
|
apiKey: API_KEY,
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
}
|
|
};
|
|
}
|
|
|
|
if (customBaseURL && customAPIKey) {
|
|
console.log("🔧 Using CUSTOM API:", {
|
|
endpoint: customBaseURL,
|
|
model: selectedModel,
|
|
apiKey: customAPIKey.substring(0, 8) + "..."
|
|
});
|
|
return {
|
|
isCustom: true,
|
|
endpoint: customBaseURL,
|
|
apiKey: customAPIKey,
|
|
model: selectedModel,
|
|
headers: {
|
|
"Authorization": `Bearer ${customAPIKey}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
};
|
|
} else {
|
|
console.log("⚠️ Custom API enabled but missing configuration:", {
|
|
baseURL: !!customBaseURL,
|
|
apiKey: !!customAPIKey
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to read custom API settings:", error);
|
|
}
|
|
|
|
// Return default configuration
|
|
console.log("🏠 Using DEFAULT Hermes API");
|
|
return {
|
|
isCustom: false,
|
|
endpoints: VLLM_ENDPOINTS,
|
|
model: MODEL,
|
|
apiKey: API_KEY,
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
}
|
|
};
|
|
}
|
|
|
|
// Global state
|
|
export let lastTTSInput = "";
|
|
export let lastTTSResult = null;
|
|
|
|
export function setLastTTS(input, result) {
|
|
lastTTSInput = input;
|
|
lastTTSResult = result;
|
|
}
|