refactor model profile logic by moving rules to modelProfiles.js and updating relevant references

This commit is contained in:
2026-09-08 23:53:13 +02:00
parent c471a4794d
commit da51bd88e6
5 changed files with 104 additions and 48 deletions
-5
View File
@@ -14,8 +14,3 @@ export const themes = {
"beta-ai": {name: "Beta AI", short: "BA"}, "beta-ai": {name: "Beta AI", short: "BA"},
"confusity": {name: "Confusity", short: "CF"} "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."
};
+89
View File
@@ -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)}`
};
}
+1 -1
View File
@@ -424,6 +424,6 @@
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script>
<script type="module" src="script.js?v=20260908-model-controls"></script> <script type="module" src="script.js?v=20260908-model-descriptions"></script>
</body> </body>
</html> </html>
+12 -41
View File
@@ -1,6 +1,7 @@
import {del, get, post, put, stream} from "./app/api.js"; import {del, get, post, put, stream} from "./app/api.js";
import {closeChatDrawer, openChatDrawer} from "./app/chatDrawer.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 {applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, readStoredAppearance} from "./app/themeController.js";
import { import {
activeChatTitle, activeChatTitle,
@@ -129,6 +130,7 @@ let onboardingStep = 1, onboardingIntent = "ask";
let chatLoadingDepth = 0; let chatLoadingDepth = 0;
let chatActivityDepth = 0; let chatActivityDepth = 0;
let messageRetryState = new Map(); let messageRetryState = new Map();
let availableModelsById = new Map();
function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; } function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; }
@@ -918,41 +920,6 @@ function renderModelSection() {
return modelSelect?.selectedOptions[0]?.textContent || ""; 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() { function updateModelHelp() {
if (!modelSelect || !modelHelpTitle || !modelHelpBadge || !modelHelpSummary) return; if (!modelSelect || !modelHelpTitle || !modelHelpBadge || !modelHelpSummary) return;
const selectedOption = modelSelect.selectedOptions[0]; const selectedOption = modelSelect.selectedOptions[0];
@@ -972,7 +939,7 @@ function updateModelHelp() {
} }
const modelId = selectedOption.value; const modelId = selectedOption.value;
const modelName = selectedOption.textContent || modelId; 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 (modelBadgeName) modelBadgeName.textContent = modelName;
if (headerModelName) headerModelName.textContent = modelName; if (headerModelName) headerModelName.textContent = modelName;
modelHelpTitle.textContent = modelName; modelHelpTitle.textContent = modelName;
@@ -1810,22 +1777,26 @@ async function displayModels() {
const res = await get("/models"); const res = await get("/models");
const models = Array.isArray(res.models) ? res.models : null; const models = Array.isArray(res.models) ? res.models : null;
if (res.error || !models) { if (res.error || !models) {
availableModelsById = new Map();
modelSelect.innerHTML = "<option>Error loading models</option>"; modelSelect.innerHTML = "<option>Error loading models</option>";
renderModelSection(); renderModelSection();
return setNotice("Models could not be loaded.", "error"); return setNotice("Models could not be loaded.", "error");
} }
modelSelect.innerHTML = ""; modelSelect.innerHTML = "";
availableModelsById = new Map(models.map((model) => [String(model.model || model.name || ""), model]));
const preferredModelId = "mistral:latest"; 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) => { models.forEach((model, index) => {
const modelId = String(model.model || model.name || "");
const option = document.createElement("option"); const option = document.createElement("option");
option.value = model.model; option.value = modelId;
option.textContent = model.name; option.textContent = model.name || modelId;
option.title = getModelProfile(model).summary;
option.selected = preferredIndex >= 0 ? index === preferredIndex : index === 0; option.selected = preferredIndex >= 0 ? index === preferredIndex : index === 0;
modelSelect.appendChild(option); modelSelect.appendChild(option);
}); });
const activeChat = getChatById(activeChatId); 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; modelSelect.value = activeChat.preferred_model;
} }
renderModelSection(); renderModelSection();
+2 -1
View File
@@ -24,7 +24,8 @@ const files = [
"public/app/api.js", "public/app/api.js",
"public/app/chatDrawer.js", "public/app/chatDrawer.js",
"public/app/constants.js", "public/app/constants.js",
"public/app/dom.js" "public/app/dom.js",
"public/app/modelProfiles.js"
]; ];
for (const file of files) { for (const file of files) {