diff --git a/public/app/constants.js b/public/app/constants.js index 478d154..0b63eea 100644 --- a/public/app/constants.js +++ b/public/app/constants.js @@ -14,8 +14,3 @@ export const themes = { "beta-ai": {name: "Beta AI", short: "BA"}, "confusity": {name: "Confusity", short: "CF"} }; - -export const defaultModelProfile = { - badge: "General", - summary: "General-purpose chat model. Start here if you are unsure which model to use." -}; diff --git a/public/app/modelProfiles.js b/public/app/modelProfiles.js new file mode 100644 index 0000000..360d3e6 --- /dev/null +++ b/public/app/modelProfiles.js @@ -0,0 +1,89 @@ +export const defaultModelProfile = { + badge: "General", + summary: "Best for general questions, drafting, and everyday conversation. Use a specialized model when you need stronger coding, reasoning, or roleplay behavior." +}; + +const modelProfileRules = [ + { + matches: ["catgirl"], + badge: "Roleplay", + summary: "Best for playful catgirl character roleplay and expressive, in-character conversation. Not intended for neutral factual answers or technical work." + }, + { + matches: ["femboy"], + badge: "Roleplay", + summary: "Best for personality-led roleplay, casual conversation, and maintaining a stylized character voice. Not intended for precise research or technical tasks." + }, + { + matches: ["buenzli", "bünzli"], + badge: "Roleplay", + summary: "Best for Swiss-flavored character roleplay, local humor, and casual conversation. Choose a general model for neutral writing or factual questions." + }, + { + matches: ["deepseek-r1", "deepseek-r1-distill"], + badge: "Reasoning", + summary: "Best for problems that benefit from deliberate reasoning, such as logic, mathematics, planning, and difficult technical questions. It can be slower and more verbose for simple chat." + }, + { + matches: ["codellama", "code-llama", "deepseek-coder", "qwen2.5-coder", "qwen3-coder", "starcoder", "codestral", "coder"], + badge: "Coding", + summary: "Best for writing, explaining, reviewing, and debugging code. Choose a general model for creative conversation or broad non-technical questions." + }, + { + matches: ["dolphin"], + badge: "Creative", + summary: "Best for open-ended conversation, brainstorming, creative writing, and roleplay. Its answers can be less restrained and less dependable for factual or safety-critical topics." + }, + { + matches: ["mixtral"], + badge: "Versatile", + summary: "Best for detailed writing, summarization, multilingual tasks, and general problem-solving. It is a capable all-rounder but may respond more slowly than smaller models." + }, + { + matches: ["mistral"], + badge: "Balanced", + summary: "Best for everyday questions, concise writing, summaries, and light coding. A strong default when you want a balance of speed and answer quality." + }, + { + matches: ["gemma"], + badge: "Efficient", + summary: "Best for quick answers, rewriting, summarization, and lightweight general chat. Larger or reasoning-focused models are better for complex multi-step problems." + }, + { + matches: ["qwen"], + badge: "Versatile", + summary: "Best for multilingual conversation, structured writing, instruction following, and general technical tasks. Coder variants are the better choice for code-heavy work." + }, + { + matches: ["llama"], + badge: "General", + summary: "Best for general conversation, writing, summarization, and broad knowledge tasks. Specialized coding or reasoning models may perform better in those areas." + }, + { + matches: ["phi"], + badge: "Compact", + summary: "Best for fast, focused questions, short summaries, and lightweight assistance. Its compact size makes it less suitable for demanding reasoning or long, nuanced responses." + } +]; + +function describeBuild(model) { + const parameterSize = String(model?.details?.parameter_size || "").trim(); + const quantization = String(model?.details?.quantization_level || "").trim(); + const details = [parameterSize, quantization].filter(Boolean); + return details.length ? ` Available build: ${details.join(", ")}.` : ""; +} + +export function getModelProfile(model = {}) { + const modelId = String(model.model || model.name || ""); + const modelName = String(model.name || model.model || ""); + const family = String(model?.details?.family || ""); + const families = Array.isArray(model?.details?.families) ? model.details.families.join(" ") : ""; + const haystack = `${modelId} ${modelName} ${family} ${families}`.toLowerCase(); + const match = modelProfileRules.find((profile) => profile.matches.some((token) => haystack.includes(token))); + const profile = match || defaultModelProfile; + + return { + badge: profile.badge, + summary: `${profile.summary}${describeBuild(model)}` + }; +} diff --git a/public/index.html b/public/index.html index 385abc1..8d3532b 100644 --- a/public/index.html +++ b/public/index.html @@ -424,6 +424,6 @@ - + diff --git a/public/script.js b/public/script.js index f9cfbc4..20e00d8 100644 --- a/public/script.js +++ b/public/script.js @@ -1,6 +1,7 @@ import {del, get, post, put, stream} from "./app/api.js"; import {closeChatDrawer, openChatDrawer} from "./app/chatDrawer.js"; -import {defaultModelProfile, onboardingPrompts, requestedChatId} from "./app/constants.js"; +import {onboardingPrompts, requestedChatId} from "./app/constants.js"; +import {getModelProfile} from "./app/modelProfiles.js"; import {applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, readStoredAppearance} from "./app/themeController.js"; import { activeChatTitle, @@ -129,6 +130,7 @@ let onboardingStep = 1, onboardingIntent = "ask"; let chatLoadingDepth = 0; let chatActivityDepth = 0; let messageRetryState = new Map(); +let availableModelsById = new Map(); function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; } @@ -918,41 +920,6 @@ function renderModelSection() { return modelSelect?.selectedOptions[0]?.textContent || ""; } -function getModelProfile(modelId, modelName) { - const haystack = `${modelId || ""} ${modelName || ""}`.toLowerCase(); - if (haystack.includes("codellama") || haystack.includes("code")) { - return { - badge: "Coding", - summary: "Best for code generation, debugging, and technical explanations. Usually stronger on programming tasks than on creative chat." - }; - } - if (haystack.includes("gemma")) { - return { - badge: "Fast", - summary: "A lighter general chat model. Good for quick answers and lower-latency replies, with less depth than larger models." - }; - } - if (haystack.includes("dolphin")) { - return { - badge: "Chatty", - summary: "Instruction-following conversational model. Good for open-ended chat, brainstorming, and longer natural responses." - }; - } - if (haystack.includes("mistral")) { - return { - badge: "Balanced", - summary: "Strong default for everyday use. Usually a good balance between speed, clarity, and response quality." - }; - } - if (haystack.includes("catgirl") || haystack.includes("femboy") || haystack.includes("buenzli")) { - return { - badge: "Persona", - summary: "Specialized character-style model. Best for roleplay or stylized voice, and less reliable for factual or neutral answers." - }; - } - return defaultModelProfile; -} - function updateModelHelp() { if (!modelSelect || !modelHelpTitle || !modelHelpBadge || !modelHelpSummary) return; const selectedOption = modelSelect.selectedOptions[0]; @@ -972,7 +939,7 @@ function updateModelHelp() { } const modelId = selectedOption.value; const modelName = selectedOption.textContent || modelId; - const profile = getModelProfile(modelId, modelName); + const profile = getModelProfile(availableModelsById.get(modelId) || {model: modelId, name: modelName}); if (modelBadgeName) modelBadgeName.textContent = modelName; if (headerModelName) headerModelName.textContent = modelName; modelHelpTitle.textContent = modelName; @@ -1810,22 +1777,26 @@ async function displayModels() { const res = await get("/models"); const models = Array.isArray(res.models) ? res.models : null; if (res.error || !models) { + availableModelsById = new Map(); modelSelect.innerHTML = ""; renderModelSection(); return setNotice("Models could not be loaded.", "error"); } modelSelect.innerHTML = ""; + availableModelsById = new Map(models.map((model) => [String(model.model || model.name || ""), model])); const preferredModelId = "mistral:latest"; - const preferredIndex = models.findIndex((model) => model.model === preferredModelId); + const preferredIndex = models.findIndex((model) => (model.model || model.name) === preferredModelId); models.forEach((model, index) => { + const modelId = String(model.model || model.name || ""); const option = document.createElement("option"); - option.value = model.model; - option.textContent = model.name; + option.value = modelId; + option.textContent = model.name || modelId; + option.title = getModelProfile(model).summary; option.selected = preferredIndex >= 0 ? index === preferredIndex : index === 0; modelSelect.appendChild(option); }); const activeChat = getChatById(activeChatId); - if (activeChat?.preferred_model && models.some((model) => model.model === activeChat.preferred_model)) { + if (activeChat?.preferred_model && models.some((model) => (model.model || model.name) === activeChat.preferred_model)) { modelSelect.value = activeChat.preferred_model; } renderModelSection(); diff --git a/scripts/lint.mjs b/scripts/lint.mjs index 15b09a9..5046922 100644 --- a/scripts/lint.mjs +++ b/scripts/lint.mjs @@ -24,7 +24,8 @@ const files = [ "public/app/api.js", "public/app/chatDrawer.js", "public/app/constants.js", - "public/app/dom.js" + "public/app/dom.js", + "public/app/modelProfiles.js" ]; for (const file of files) {