Files
nc-talk-ai/js/educai-admin.mjs.map
T
Pascal Kienast f4d56bd354 Fix last EDUCAI UI strings → Talk AI
- AdminSettings.vue: 'bundled EDUCAI icon pair' → 'Talk AI icon pair'
- PersonalBots.vue: console log prefix [EDUCAI] → [TalkAI]
- rebuilt frontend bundles
2026-07-12 02:39:55 +02:00

1 line
136 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"version":3,"file":"educai-admin.mjs","sources":["../src/components/AdminSettings.vue","../src/admin.js"],"sourcesContent":["<template>\n\t<!-- eslint-disable vue/html-indent, vue/singleline-html-element-content-newline -->\n\t<div class=\"admin-settings\">\n\t\t<h3 class=\"settings-title\">\n\t\t\t<span class=\"icon-settings\" />\n\t\t\tAdministrator Settings\n\t\t</h3>\n\n\t\t<form class=\"settings-body\" autocomplete=\"off\" @submit.prevent=\"saveSettings\">\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('appearance') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('appearance'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('appearance')\"\n\t\t\t\t\t@click=\"toggleSection('appearance')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Appearance</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">App icon variants for settings and navigation surfaces.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ appIconSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('appearance')\"\n\t\t\t\t\t:id=\"sectionId('appearance')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"app-icon-config\">\n\t\t\t\t\t\t<div class=\"icon-mode-tabs\" aria-label=\"App icon mode\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"icon-mode-tab\"\n\t\t\t\t\t\t\t\t:class=\"{ 'icon-mode-tab--active': settings.appIconMode === 'default' }\"\n\t\t\t\t\t\t\t\t@click=\"setAppIconMode('default')\">\n\t\t\t\t\t\t\t\tDefault\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"icon-mode-tab\"\n\t\t\t\t\t\t\t\t:class=\"{ 'icon-mode-tab--active': settings.appIconMode === 'custom' }\"\n\t\t\t\t\t\t\t\t@click=\"setAppIconMode('custom')\">\n\t\t\t\t\t\t\t\tCustom\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"app-icon-preview-grid\">\n\t\t\t\t\t\t\t<div class=\"app-icon-preview-card app-icon-preview-card--light\">\n\t\t\t\t\t\t\t\t<span class=\"app-icon-preview-card__label\">Black on light</span>\n\t\t\t\t\t\t\t\t<span class=\"app-icon-preview-card__frame\">\n\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\tv-if=\"appIconBlackPreviewUrl\"\n\t\t\t\t\t\t\t\t\t\t:src=\"appIconBlackPreviewUrl\"\n\t\t\t\t\t\t\t\t\t\t:alt=\"APP_DISPLAY_NAME + ' black app icon preview'\">\n\t\t\t\t\t\t\t\t\t<span v-else class=\"app-icon-preview-card__missing\">Missing</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"app-icon-preview-card app-icon-preview-card--dark\">\n\t\t\t\t\t\t\t\t<span class=\"app-icon-preview-card__label\">White on dark</span>\n\t\t\t\t\t\t\t\t<span class=\"app-icon-preview-card__frame\">\n\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\tv-if=\"appIconWhitePreviewUrl\"\n\t\t\t\t\t\t\t\t\t\t:src=\"appIconWhitePreviewUrl\"\n\t\t\t\t\t\t\t\t\t\t:alt=\"APP_DISPLAY_NAME + ' white app icon preview'\">\n\t\t\t\t\t\t\t\t\t<span v-else class=\"app-icon-preview-card__missing\">Missing</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div v-if=\"settings.appIconMode === 'default'\" class=\"app-icon-mode-panel\">\n\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\tUses the bundled Talk AI icon pair: dark icon for light settings surfaces, white icon for app navigation.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div v-else class=\"app-icon-mode-panel\">\n\t\t\t\t\t\t\t<div class=\"form-grid\">\n\t\t\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t\t\t<label for=\"app-icon-black-url\">Black icon</label>\n\t\t\t\t\t\t\t\t\t<div class=\"app-icon-input-row\">\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"app-icon-black-url\"\n\t\t\t\t\t\t\t\t\t\t\tv-model=\"settings.appIconBlackUrl\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tname=\"educai-app-icon-black-url\"\n\t\t\t\t\t\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"https://example.org/educai-black.svg or /apps/theming/img/educai-black.svg\">\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t:disabled=\"uploadingAppIconVariant === 'black'\"\n\t\t\t\t\t\t\t\t\t\t\t@click=\"triggerAppIconUpload('black')\">\n\t\t\t\t\t\t\t\t\t\t\t{{ uploadingAppIconVariant === 'black' ? 'Uploading...' : 'Upload SVG' }}\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tref=\"appIconBlackFileInput\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"app-icon-file-input\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\t\t\t\t\t\taccept=\"image/svg+xml,.svg\"\n\t\t\t\t\t\t\t\t\t\t\t@change=\"uploadAppIconSvg('black', $event)\">\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\t\t\tUsed on light settings surfaces. Enter a URL/path or upload an SVG from your computer.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t\t\t<label for=\"app-icon-white-url\">White icon</label>\n\t\t\t\t\t\t\t\t\t<div class=\"app-icon-input-row\">\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tid=\"app-icon-white-url\"\n\t\t\t\t\t\t\t\t\t\t\tv-model=\"settings.appIconWhiteUrl\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\t\t\tname=\"educai-app-icon-white-url\"\n\t\t\t\t\t\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t\t\t\t\t\t\tplaceholder=\"https://example.org/educai-white.svg or /apps/theming/img/educai-white.svg\">\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t:disabled=\"uploadingAppIconVariant === 'white'\"\n\t\t\t\t\t\t\t\t\t\t\t@click=\"triggerAppIconUpload('white')\">\n\t\t\t\t\t\t\t\t\t\t\t{{ uploadingAppIconVariant === 'white' ? 'Uploading...' : 'Upload SVG' }}\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tref=\"appIconWhiteFileInput\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"app-icon-file-input\"\n\t\t\t\t\t\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\t\t\t\t\t\taccept=\"image/svg+xml,.svg\"\n\t\t\t\t\t\t\t\t\t\t\t@change=\"uploadAppIconSvg('white', $event)\">\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\t\t\tUsed in the dark app navigation/header context. Enter a URL/path or upload an SVG from your computer.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"button-row app-icon-actions\">\n\t\t\t\t\t\t\t<button type=\"button\" class=\"button\" @click=\"resetAppIconConfig\">\n\t\t\t\t\t\t\t\tReset to default icon\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('essentials') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('essentials'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('essentials')\"\n\t\t\t\t\t@click=\"toggleSection('essentials')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Essentials</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">Default creativity and Talk webhook secret.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ essentialsSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('essentials')\"\n\t\t\t\t\t:id=\"sectionId('essentials')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"default-temperature\">Default Temperature</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"default-temperature\"\n\t\t\t\t\t\t\tv-model.number=\"settings.defaultTemperature\"\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\t\tmax=\"1\"\n\t\t\t\t\t\t\tstep=\"0.05\"\n\t\t\t\t\t\t\tplaceholder=\"0.20\">\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tControls how deterministic or creative bots are by default.\n\t\t\t\t\t\t\tLower values are better for agentic, tool-using, RAG, and workflow bots.\n\t\t\t\t\t\t\tHigher values can be useful for creative writing or brainstorming bots.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div class=\"temperature-presets\">\n\t\t\t\t\t\t\t<button type=\"button\" class=\"button\" @click=\"applyDefaultTemperaturePreset(0.2)\">\n\t\t\t\t\t\t\t\tPrecise 0.20\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button type=\"button\" class=\"button\" @click=\"applyDefaultTemperaturePreset(0.4)\">\n\t\t\t\t\t\t\t\tBalanced 0.40\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button type=\"button\" class=\"button\" @click=\"applyDefaultTemperaturePreset(0.6)\">\n\t\t\t\t\t\t\t\tCreative 0.60\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"webhook-secret\">Webhook Secret</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"webhook-secret\"\n\t\t\t\t\t\t\tv-model=\"settings.webhookSecret\"\n\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\tname=\"educai-webhook-secret\"\n\t\t\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\t\t\tplaceholder=\"your-webhook-secret\">\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tSecret used to verify webhook requests from Nextcloud Talk.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('modelEndpoints') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('modelEndpoints'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('modelEndpoints')\"\n\t\t\t\t\t@click=\"toggleSection('modelEndpoints')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Model Endpoints</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">Primary and optional secondary OpenAI-compatible providers.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ modelEndpointsSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('modelEndpoints')\"\n\t\t\t\t\t:id=\"sectionId('modelEndpoints')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Primary Endpoint</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tUsed for existing raw model names and as the default provider.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"api-endpoint\">Primary API Endpoint</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"api-endpoint\"\n\t\t\t\t\t\t\tv-model=\"settings.apiEndpoint\"\n\t\t\t\t\t\t\ttype=\"url\"\n\t\t\t\t\t\t\tname=\"educai-api-endpoint\"\n\t\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t\t\tplaceholder=\"https://chat-ai.academiccloud.de/v1/chat/completions\"\n\t\t\t\t\t\t\trequired>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tAcademicCloud (GWDG) or OpenAI-compatible API endpoint URL.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"api-key\">Primary API Key</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"api-key\"\n\t\t\t\t\t\t\tv-model=\"settings.apiKey\"\n\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\tname=\"educai-api-key\"\n\t\t\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\t\t\tplaceholder=\"sk-...\">\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tUsed for primary endpoint requests. Stored securely and never displayed back.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Secondary Endpoint</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tOptional. When configured, bots can choose models from both endpoints.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"secondary-api-endpoint\">Secondary API Endpoint</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"secondary-api-endpoint\"\n\t\t\t\t\t\t\tv-model=\"settings.secondaryApiEndpoint\"\n\t\t\t\t\t\t\ttype=\"url\"\n\t\t\t\t\t\t\tname=\"educai-secondary-api-endpoint\"\n\t\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t\t\tplaceholder=\"https://example.org/v1/chat/completions\">\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"secondary-api-key\">Secondary API Key</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"secondary-api-key\"\n\t\t\t\t\t\t\tv-model=\"settings.secondaryApiKey\"\n\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\tname=\"educai-secondary-api-key\"\n\t\t\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\t\t\tplaceholder=\"sk-...\">\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tLeave blank to keep an already stored secondary key.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Model Selection</h4>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"default-model\">Default Model</label>\n\t\t\t\t\t\t<select\n\t\t\t\t\t\t\tv-if=\"availableModelOptions.length > 0\"\n\t\t\t\t\t\t\tid=\"default-model\"\n\t\t\t\t\t\t\tv-model=\"settings.defaultModel\"\n\t\t\t\t\t\t\t:disabled=\"settings.allowMultipleModels\">\n\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\tv-for=\"option in availableModelOptionsWithCurrent(settings.defaultModel)\"\n\t\t\t\t\t\t\t\t:key=\"option.id\"\n\t\t\t\t\t\t\t\t:value=\"option.id\">\n\t\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\tid=\"default-model\"\n\t\t\t\t\t\t\tv-model=\"settings.defaultModel\"\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t:disabled=\"settings.allowMultipleModels\"\n\t\t\t\t\t\t\tplaceholder=\"primary:llama-3.3-70b-instruct\">\n\t\t\t\t\t\t<p v-if=\"settings.allowMultipleModels\" class=\"hint\">\n\t\t\t\t\t\t\tDisabled because Multiple Models mode is enabled.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p v-else class=\"hint\">\n\t\t\t\t\t\t\tLegacy unprefixed model names continue to use the primary endpoint.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label class=\"checkbox\">\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tv-model=\"settings.allowMultipleModels\"\n\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\t@change=\"onToggleMultiple\">\n\t\t\t\t\t\t\tAllow Multiple Models (per-bot selection)\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tIf enabled, choose which models are available for bots. Users will select one when creating a bot.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div v-if=\"settings.allowMultipleModels\" class=\"form-group\">\n\t\t\t\t\t\t<label>Allowed Models</label>\n\t\t\t\t\t\t<div v-if=\"loadingModels\" class=\"hint\">\n\t\t\t\t\t\t\tLoading models…\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-else>\n\t\t\t\t\t\t\t<div v-if=\"modelLoadError\" class=\"hint\" style=\"color:var(--color-error)\">\n\t\t\t\t\t\t\t\t{{ modelLoadError }}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div v-if=\"availableModelOptions.length === 0\" class=\"hint\">\n\t\t\t\t\t\t\t\tNo models loaded.\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t\t\t@click=\"loadModels\">\n\t\t\t\t\t\t\t\t\tLoad models\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\tv-model=\"settings.allowedModels\"\n\t\t\t\t\t\t\t\tmultiple\n\t\t\t\t\t\t\t\t:size=\"modelSelectSize\">\n\t\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\t\tv-for=\"option in availableModelOptionsWithCurrent(settings.allowedModels)\"\n\t\t\t\t\t\t\t\t\t:key=\"option.id\"\n\t\t\t\t\t\t\t\t\t:value=\"option.id\">\n\t\t\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\tHold Cmd/Ctrl to select multiple. Save after selection.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('fallbackTimeouts') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('fallbackTimeouts'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('fallbackTimeouts')\"\n\t\t\t\t\t@click=\"toggleSection('fallbackTimeouts')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Fallback &amp; Timeouts</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">One retry model and LLM request time limits.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ fallbackTimeoutSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('fallbackTimeouts')\"\n\t\t\t\t\t:id=\"sectionId('fallbackTimeouts')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"fallback-model\">Fallback Model</label>\n\t\t\t\t\t\t<select\n\t\t\t\t\t\t\tv-if=\"availableModelOptions.length > 0\"\n\t\t\t\t\t\t\tid=\"fallback-model\"\n\t\t\t\t\t\t\tv-model=\"settings.fallbackModel\">\n\t\t\t\t\t\t\t<option value=\"\">No fallback</option>\n\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\tv-for=\"option in availableModelOptionsWithCurrent(settings.fallbackModel)\"\n\t\t\t\t\t\t\t\t:key=\"option.id\"\n\t\t\t\t\t\t\t\t:value=\"option.id\">\n\t\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\tid=\"fallback-model\"\n\t\t\t\t\t\t\tv-model=\"settings.fallbackModel\"\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tplaceholder=\"secondary:qwen3-coder-next\">\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tUsed once after timeout or connection failures before the request fails finally.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-grid form-grid--three\">\n\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t<label for=\"llm-chat-timeout\">Chat Timeout (seconds)</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tid=\"llm-chat-timeout\"\n\t\t\t\t\t\t\t\tv-model.number=\"settings.llmChatTimeout\"\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\t\t\tstep=\"1\">\n\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\tMaximum time to wait for a regular, non-streaming LLM response.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t<label for=\"llm-stream-timeout\">Streaming Timeout (seconds)</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tid=\"llm-stream-timeout\"\n\t\t\t\t\t\t\t\tv-model.number=\"settings.llmStreamTimeout\"\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\t\t\tstep=\"1\">\n\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\tMaximum time to keep a streaming response open while the model is generating.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t<label for=\"llm-models-timeout\">Model-List Timeout (seconds)</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tid=\"llm-models-timeout\"\n\t\t\t\t\t\t\t\tv-model.number=\"settings.llmModelsTimeout\"\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\t\t\tstep=\"1\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('rag') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('rag'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('rag')\"\n\t\t\t\t\t@click=\"toggleSection('rag')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">RAG &amp; Embeddings</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">Knowledge retrieval, embedding provider, limits, and chunking.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\" :class=\"{ 'accordion-meta--warning': hasPendingEmbeddingConfigChange }\">\n\t\t\t\t\t\t{{ ragSummary }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('rag')\"\n\t\t\t\t\t:id=\"sectionId('rag')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Retrieval-Augmented Generation</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tConfigure embeddings and chunking parameters used when bots index Nextcloud files.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t<label class=\"checkbox\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tv-model=\"settings.ragEnabled\"\n\t\t\t\t\t\ttype=\"checkbox\">\n\t\t\t\t\tEnable Retrieval-Augmented Generation\n\t\t\t\t</label>\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tWhen enabled, bots can index approved files and surface relevant snippets in conversations.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"embedding-endpoint\">Embedding API Endpoint</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"embedding-endpoint\"\n\t\t\t\t\tv-model=\"settings.embeddingApiEndpoint\"\n\t\t\t\t\ttype=\"url\"\n\t\t\t\t\tname=\"educai-embedding-endpoint\"\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tplaceholder=\"https://chat-ai.academiccloud.de/v1/embeddings\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Leave blank to reuse the chat completion endpoint.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"embedding-key\">Embedding API Key</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"embedding-key\"\n\t\t\t\t\tv-model=\"settings.embeddingApiKey\"\n\t\t\t\t\ttype=\"password\"\n\t\t\t\t\tname=\"educai-embedding-key\"\n\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\tplaceholder=\"sk-...\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Provide only if embeddings require a different credential. Stored securely and never displayed back.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"embedding-model\">Embedding Model</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"embedding-model\"\n\t\t\t\t\tv-model=\"settings.embeddingModel\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tplaceholder=\"multilingual-e5-large-instruct\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tSet the provider-specific embedding model identifier (e.g., multilingual-e5-large-instruct, e5-mistral-7b-instruct).\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"embedding-rate-limit-mode\">Embedding Rate Limit Mode</label>\n\t\t\t\t<select\n\t\t\t\t\tid=\"embedding-rate-limit-mode\"\n\t\t\t\t\tv-model=\"settings.embeddingRateLimitMode\">\n\t\t\t\t\t<option value=\"inherit\">\n\t\t\t\t\t\tInherit Chat Limits\n\t\t\t\t\t</option>\n\t\t\t\t\t<option value=\"disabled\">\n\t\t\t\t\t\tDisabled\n\t\t\t\t\t</option>\n\t\t\t\t\t<option value=\"custom\">\n\t\t\t\t\t\tCustom\n\t\t\t\t\t</option>\n\t\t\t\t</select>\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tUse <code>inherit</code> to follow chat queue limits, <code>disabled</code> for self-hosted embedding endpoints without throttling, or <code>custom</code> for dedicated embedding quotas.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"settings.embeddingRateLimitMode === 'custom'\" class=\"form-grid\">\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"embedding-rate-limit-second\">Embedding Requests per Second</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"embedding-rate-limit-second\"\n\t\t\t\t\t\tv-model.number=\"settings.embeddingRateLimitSecond\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"Optional\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tOptional. Leave blank if the embedding provider does not expose second-level headers.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"embedding-rate-limit-minute\">Embedding Requests per Minute</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"embedding-rate-limit-minute\"\n\t\t\t\t\t\tv-model.number=\"settings.embeddingRateLimitMinute\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"100\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tDefault for GWDG embeddings: 100/minute.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"embedding-rate-limit-hour\">Embedding Requests per Hour</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"embedding-rate-limit-hour\"\n\t\t\t\t\t\tv-model.number=\"settings.embeddingRateLimitHour\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"2000\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tDefault for GWDG embeddings: 2000/hour.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"embedding-rate-limit-day\">Embedding Requests per Day</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"embedding-rate-limit-day\"\n\t\t\t\t\t\tv-model.number=\"settings.embeddingRateLimitDay\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"4000\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tDefault for GWDG embeddings: 4000/day.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div v-if=\"hasPendingEmbeddingConfigChange\" class=\"embedding-warning\">\n\t\t\t\t<strong>Embedding settings changed.</strong>\n\t\t\t\t<p>\n\t\t\t\t\tSave first, then run <code>Reindex All Embeddings</code> to rebuild bot vectors for the active embedding model.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-grid\">\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"rag-chunk-size\">Chunk Size (tokens)</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"rag-chunk-size\"\n\t\t\t\t\t\tv-model.number=\"settings.ragChunkSize\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"100\"\n\t\t\t\t\t\tstep=\"10\"\n\t\t\t\t\t\tplaceholder=\"750\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tControls how much text each embedding covers. Larger chunks reduce recall but speed ingestion.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"rag-chunk-overlap\">Chunk Overlap (tokens)</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"rag-chunk-overlap\"\n\t\t\t\t\t\tv-model.number=\"settings.ragChunkOverlap\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\tstep=\"5\"\n\t\t\t\t\t\tplaceholder=\"50\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOverlap provides continuity between adjacent chunks. Increase for long-form documents.\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('docling') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('docling'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('docling')\"\n\t\t\t\t\t@click=\"toggleSection('docling')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Document Conversion</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">Docling conversion for PDF and Office ingestion.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ doclingSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('docling')\"\n\t\t\t\t\t:id=\"sectionId('docling')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Document Conversion (Docling)</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tEnable conversion of PDF, DOCX, PPTX and other binary documents to text for RAG ingestion.\n\t\t\t\t\t\t\tThis uses the Docling API to extract content from documents that cannot be read as plain text.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t<label class=\"checkbox\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tv-model=\"settings.doclingEnabled\"\n\t\t\t\t\t\ttype=\"checkbox\">\n\t\t\t\t\tEnable Document Conversion\n\t\t\t\t</label>\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tWhen enabled, PDF and Office documents attached to bots will be automatically converted to text for indexing.\n\t\t\t\t\tUses the dedicated Docling API key when configured, otherwise falls back to the main API key.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"docling-endpoint\">Docling API Endpoint</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"docling-endpoint\"\n\t\t\t\t\tv-model=\"settings.doclingApiEndpoint\"\n\t\t\t\t\ttype=\"url\"\n\t\t\t\t\tname=\"educai-docling-endpoint\"\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tplaceholder=\"https://chat-ai.academiccloud.de/v1/documents/convert\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Leave blank to use the default Academic Cloud endpoint.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"docling-key\">Docling API Key</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"docling-key\"\n\t\t\t\t\tv-model=\"settings.doclingApiKey\"\n\t\t\t\t\ttype=\"password\"\n\t\t\t\t\tname=\"educai-docling-key\"\n\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\tplaceholder=\"sk-...\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Provide only if Docling requires a different credential.\n\t\t\t\t\t{{ doclingKeyHint }}\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"button-row\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t:disabled=\"doclingTesting\"\n\t\t\t\t\t@click=\"testDoclingConnection\">\n\t\t\t\t\t{{ doclingTesting ? 'Testing…' : 'Test Connection' }}\n\t\t\t\t</button>\n\t\t\t\t<span v-if=\"doclingTestResult\" :class=\"doclingTestResult.success ? 'success-text' : 'error-text'\">\n\t\t\t\t\t{{ doclingTestResult.success ? '✓ Connection successful' : ('✗ ' + doclingTestResult.error) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\n\t\t\t<div class=\"supported-formats\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t<strong>Supported formats:</strong> PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, PNG, JPG, TIFF, BMP\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('media') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('media'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('media')\"\n\t\t\t\t\t@click=\"toggleSection('media')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Media Tools</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">Image understanding and speech-to-text models.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ mediaSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('media')\"\n\t\t\t\t\t:id=\"sectionId('media')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Image Understanding</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tConfigure a multimodal model for image attachments uploaded in Nextcloud Talk. Bots only get image understanding when the built-in image tool is enabled for that bot.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"vision-endpoint\">Vision API Endpoint</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"vision-endpoint\"\n\t\t\t\t\tv-model=\"settings.visionApiEndpoint\"\n\t\t\t\t\ttype=\"url\"\n\t\t\t\t\tname=\"educai-vision-endpoint\"\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tplaceholder=\"https://chat-ai.academiccloud.de/v1/chat/completions\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Leave blank to reuse the main chat completion endpoint.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"vision-key\">Vision API Key</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"vision-key\"\n\t\t\t\t\tv-model=\"settings.visionApiKey\"\n\t\t\t\t\ttype=\"password\"\n\t\t\t\t\tname=\"educai-vision-key\"\n\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\tplaceholder=\"sk-...\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Leave blank to reuse the main API key.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"vision-model\">Vision Model</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"vision-model\"\n\t\t\t\t\tv-model=\"settings.visionModel\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tplaceholder=\"llama-4-scout-17b-16e-instruct\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tSet a model identifier that supports image inputs.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"button-row\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t:disabled=\"visionTesting\"\n\t\t\t\t\t@click=\"testVisionConnection\">\n\t\t\t\t\t{{ visionTesting ? 'Testing…' : 'Test Vision' }}\n\t\t\t\t</button>\n\t\t\t\t<span v-if=\"visionTestResult\" :class=\"visionTestResult.success ? 'success-text' : 'error-text'\">\n\t\t\t\t\t{{ visionTestResult.success ? '✓ Vision connection successful' : ('✗ ' + visionTestResult.error) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\n\t\t\t<div class=\"section-divider\" role=\"presentation\" />\n\n\t\t\t<div class=\"section-heading\">\n\t\t\t\t<h4>Speech To Text</h4>\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tConfigure a transcription model for audio and voice-message attachments uploaded in Nextcloud Talk. Bots only get this ability when the built-in audio tool is enabled for that bot.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"speech-endpoint\">Speech API Endpoint</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"speech-endpoint\"\n\t\t\t\t\tv-model=\"settings.speechApiEndpoint\"\n\t\t\t\t\ttype=\"url\"\n\t\t\t\t\tname=\"educai-speech-endpoint\"\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tplaceholder=\"https://chat-ai.academiccloud.de/v1/audio/transcriptions\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Leave blank to derive an OpenAI-compatible transcription endpoint from the main chat endpoint.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"speech-key\">Speech API Key</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"speech-key\"\n\t\t\t\t\tv-model=\"settings.speechApiKey\"\n\t\t\t\t\ttype=\"password\"\n\t\t\t\t\tname=\"educai-speech-key\"\n\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\tplaceholder=\"sk-...\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tOptional. Leave blank to reuse the main API key.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"speech-model\">Speech Model</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"speech-model\"\n\t\t\t\t\tv-model=\"settings.speechModel\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tplaceholder=\"whisper-large-v3\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tSet the transcription model identifier, for example a Whisper-compatible model.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"button-row\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t:disabled=\"speechTesting\"\n\t\t\t\t\t@click=\"testSpeechConnection\">\n\t\t\t\t\t{{ speechTesting ? 'Testing…' : 'Test Speech' }}\n\t\t\t\t</button>\n\t\t\t\t<span v-if=\"speechTestResult\" :class=\"speechTestResult.success ? 'success-text' : 'error-text'\">\n\t\t\t\t\t{{ speechTestResult.success ? '✓ Speech connection successful' : ('✗ ' + speechTestResult.error) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('limits') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('limits'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('limits')\"\n\t\t\t\t\t@click=\"toggleSection('limits')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Rate Limits</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">LLM queue limits, live counters, and queue processing.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ rateLimitSummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('limits')\"\n\t\t\t\t\t:id=\"sectionId('limits')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>LLM Rate Limiting Queue</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tWhen enabled, requests that exceed the LLM provider's rate limit will be queued and processed automatically via background jobs.\n\t\t\t\t\t\t\tConfigure limits based on your provider's quota. For GWDG, use 30/minute, 200/hour, and 1000/day. Leave per-second empty if your provider does not expose a second-level window.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t<label class=\"checkbox\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tv-model=\"settings.rateLimitEnabled\"\n\t\t\t\t\t\ttype=\"checkbox\">\n\t\t\t\t\tEnable Rate Limit Queue\n\t\t\t\t</label>\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tWhen enabled, requests exceeding rate limits will be queued instead of failing. A background job will process queued requests as capacity becomes available.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"settings.rateLimitEnabled\" class=\"form-grid\">\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"rate-limit-second\">Requests per Second</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"rate-limit-second\"\n\t\t\t\t\t\tv-model.number=\"settings.rateLimitSecond\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"Optional\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tOptional. Leave blank for providers like GWDG that only expose minute/hour/day headers.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"rate-limit-minute\">Requests per Minute</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"rate-limit-minute\"\n\t\t\t\t\t\tv-model.number=\"settings.rateLimitMinute\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"30\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tMaximum requests per minute (GWDG default: 30)\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"rate-limit-hour\">Requests per Hour</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"rate-limit-hour\"\n\t\t\t\t\t\tv-model.number=\"settings.rateLimitHour\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"200\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tMaximum requests per hour (GWDG default: 200)\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t<label for=\"rate-limit-day\">Requests per Day</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"rate-limit-day\"\n\t\t\t\t\t\tv-model.number=\"settings.rateLimitDay\"\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\tplaceholder=\"1000\">\n\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\tMaximum requests per day (GWDG default: 1000)\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"settings.rateLimitEnabled\" class=\"form-group\">\n\t\t\t\t<label for=\"rate-limit-queue-message\">Queue Message</label>\n\t\t\t\t<textarea\n\t\t\t\t\tid=\"rate-limit-queue-message\"\n\t\t\t\t\tv-model=\"settings.rateLimitQueueMessage\"\n\t\t\t\t\trows=\"4\"\n\t\t\t\t\tplaceholder=\"⏳ Your request has been queued. Position: {position}, estimated wait: ~{wait} seconds.\" />\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tMessage shown to users when their request is queued. Use <code>{position}</code> for queue position and <code>{wait}</code> for estimated wait time in seconds. Leave empty for default message.\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"settings.rateLimitEnabled\" class=\"rate-limit-status\">\n\t\t\t\t<div class=\"button-row\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t:disabled=\"rateLimitLoading\"\n\t\t\t\t\t\t@click=\"loadRateLimitStatus\">\n\t\t\t\t\t\t{{ rateLimitLoading ? 'Loading…' : 'Refresh Status' }}\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"button primary\"\n\t\t\t\t\t\t:disabled=\"queueProcessing || !chatQueueStats.pending\"\n\t\t\t\t\t\t@click=\"processQueueNow\">\n\t\t\t\t\t\t{{ queueProcessing ? 'Processing…' : 'Process Queue Now' }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"rateLimitStatus\" class=\"status-grid\">\n\t\t\t\t\t<div class=\"status-card\">\n\t\t\t\t\t\t<div class=\"status-value\">{{ chatQueueStats.pending }}</div>\n\t\t\t\t\t\t<div class=\"status-label\">Queued</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"status-card\">\n\t\t\t\t\t\t<div class=\"status-value\">{{ chatQueueStats.processing }}</div>\n\t\t\t\t\t\t<div class=\"status-label\">Processing</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<p v-if=\"rateLimitStatus\" class=\"hint rate-limit-hint\">\n\t\t\t\t\tChat requests and embeddings are tracked separately. The cards below show the last observed provider headers per endpoint.\n\t\t\t\t</p>\n\t\t\t\t<div v-if=\"rateLimitStatus\" class=\"rate-limit-subsection\">\n\t\t\t\t\t<div class=\"rate-limit-subsection__header\">\n\t\t\t\t\t\t<h5>Chat / Queue</h5>\n\t\t\t\t\t\t<p class=\"hint\">Used for bot replies and queued requests.</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"status-grid status-grid--compact\">\n\t\t\t\t\t\t<div class=\"status-card\" :class=\"chatCanProcess ? 'status-ok' : 'status-limited'\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ chatCanProcess ? '✓' : '⏳' }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">{{ chatCanProcess ? 'Available' : 'Rate Limited' }}</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"chatRateLimitState?.limit_minute\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ chatRateLimitState.remaining_minute }}/{{ chatRateLimitState.limit_minute }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Minute</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"chatRateLimitState?.limit_second\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ chatRateLimitState.remaining_second }}/{{ chatRateLimitState.limit_second }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Second</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"chatRateLimitState?.limit_hour\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ chatRateLimitState.remaining_hour }}/{{ chatRateLimitState.limit_hour }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Hour</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"chatRateLimitState?.limit_day\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ chatRateLimitState.remaining_day }}/{{ chatRateLimitState.limit_day }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Day</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"rateLimitStatus\" class=\"rate-limit-subsection\">\n\t\t\t\t\t<div class=\"rate-limit-subsection__header\">\n\t\t\t\t\t\t<h5>Embeddings</h5>\n\t\t\t\t\t\t<p class=\"hint\">Embeddings are tracked independently from the chat queue.</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-if=\"embeddingRateLimitModeStatus === 'disabled'\" class=\"hint\">\n\t\t\t\t\t\tEmbedding rate limiting is disabled. Requests go straight to the configured embedding endpoint.\n\t\t\t\t\t</p>\n\t\t\t\t\t<p v-else-if=\"embeddingStatusSource === 'configured'\" class=\"hint\">\n\t\t\t\t\t\t{{ embeddingConfiguredHint }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p v-else-if=\"embeddingStatusSource === 'observed'\" class=\"hint\">\n\t\t\t\t\t\tShowing the last observed embedding endpoint headers.\n\t\t\t\t\t</p>\n\t\t\t\t\t<div v-if=\"embeddingRateLimitModeStatus !== 'disabled' && embeddingRateLimitState\" class=\"status-grid status-grid--compact\">\n\t\t\t\t\t\t<div class=\"status-card\" :class=\"embeddingCanProcess ? 'status-ok' : 'status-limited'\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ embeddingCanProcess ? '✓' : '⏳' }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">{{ embeddingCanProcess ? 'Available' : 'Rate Limited' }}</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"embeddingRateLimitState.limit_minute\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ embeddingRateLimitState.remaining_minute }}/{{ embeddingRateLimitState.limit_minute }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Minute</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"embeddingRateLimitState.limit_second\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ embeddingRateLimitState.remaining_second }}/{{ embeddingRateLimitState.limit_second }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Second</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"embeddingRateLimitState.limit_hour\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ embeddingRateLimitState.remaining_hour }}/{{ embeddingRateLimitState.limit_hour }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Hour</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"embeddingRateLimitState.limit_day\" class=\"status-card\">\n\t\t\t\t\t\t\t<div class=\"status-value\">{{ embeddingRateLimitState.remaining_day }}/{{ embeddingRateLimitState.limit_day }}</div>\n\t\t\t\t\t\t\t<div class=\"status-label\">Per Day</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"embeddingRateLimitModeStatus !== 'disabled'\" class=\"hint\">\n\t\t\t\t\t\tNo embedding rate-limit headers observed yet.\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('memory') }\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t\t:aria-expanded=\"String(isSectionOpen('memory'))\"\n\t\t\t\t\t:aria-controls=\"sectionId('memory')\"\n\t\t\t\t\t@click=\"toggleSection('memory')\">\n\t\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t\t<span class=\"accordion-title\">Conversation Memory</span>\n\t\t\t\t\t\t<span class=\"accordion-description\">Context history budget for bot replies.</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"accordion-meta\">{{ memorySummary }}</span>\n\t\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tv-show=\"isSectionOpen('memory')\"\n\t\t\t\t\t:id=\"sectionId('memory')\"\n\t\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t\t<h4>Conversation Memory</h4>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tConfigure how much conversation history is sent to the LLM for context.\n\t\t\t\t\t\t\tUsing token-based limits prevents context window overflow with large messages.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"conversation-context-tokens\">Context Token Limit</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"conversation-context-tokens\"\n\t\t\t\t\tv-model.number=\"settings.conversationContextTokens\"\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tmin=\"1000\"\n\t\t\t\t\tstep=\"500\"\n\t\t\t\t\tplaceholder=\"8000\">\n\t\t\t\t<p class=\"hint\">\n\t\t\t\t\tMaximum tokens of conversation history to include. Default: 8000 (safe for most models).\n\t\t\t\t\tHigher values provide more context but may exceed model limits. Common limits:\n\t\t\t\t\tLlama 3.1/3.3: 128K, Qwen 3: 32K-128K, Mistral: 32K-128K.\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t<button type=\"submit\" class=\"button primary\" :disabled=\"saving\">\n\t\t\t\t{{ saving ? 'Saving...' : 'Save Settings' }}\n\t\t\t</button>\n\t\t</form>\n\n\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('tools') }\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t:aria-expanded=\"String(isSectionOpen('tools'))\"\n\t\t\t\t:aria-controls=\"sectionId('tools')\"\n\t\t\t\t@click=\"toggleSection('tools')\">\n\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t<span class=\"accordion-title\">Agent Tools</span>\n\t\t\t\t\t<span class=\"accordion-description\">Model Context Protocol tool registry.</span>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"accordion-meta\">{{ toolsSummary }}</span>\n\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t</button>\n\t\t\t<div\n\t\t\t\tv-show=\"isSectionOpen('tools')\"\n\t\t\t\t:id=\"sectionId('tools')\"\n\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t<h4>Agent Tool Registry</h4>\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"button primary\"\n\t\t\t\t\t@click=\"openCreateTool\">\n\t\t\t\t\tRegister Tool\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<p class=\"hint section-body\">\n\t\t\t\tRegister Model Context Protocol endpoints. Enabled tools become available for bot creators to attach to their assistants.\n\t\t\t</p>\n\t\t\t<div v-if=\"loadingTools\" class=\"hint section-body\">\n\t\t\t\tLoading tools…\n\t\t\t</div>\n\t\t\t<template v-else>\n\t\t\t\t<div v-if=\"tools.length === 0\" class=\"hint section-body\">\n\t\t\t\t\tNo tools registered yet.\n\t\t\t\t</div>\n\t\t\t\t<div v-else class=\"tool-list section-body\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"tool in tools\"\n\t\t\t\t\t\t:key=\"tool.id\"\n\t\t\t\t\t\tclass=\"tool-row\"\n\t\t\t\t\t\t:class=\"{ 'tool-row--disabled': !tool.enabled }\">\n\t\t\t\t\t\t<div class=\"tool-info\">\n\t\t\t\t\t\t\t<div class=\"tool-name\">\n\t\t\t\t\t\t\t\t<span class=\"tool-name__label\">{{ tool.name }}</span>\n\t\t\t\t\t\t\t\t<span class=\"badge\" :class=\"tool.enabled ? 'badge-success' : 'badge-muted'\">\n\t\t\t\t\t\t\t\t\t{{ tool.enabled ? 'Enabled' : 'Disabled' }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p v-if=\"tool.description\" class=\"hint\">\n\t\t\t\t\t\t\t\t{{ tool.description }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\t\tEndpoint: <code>{{ tool.mcp_endpoint_url }}</code>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"tool-actions\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t\t:disabled=\"toolSaving\"\n\t\t\t\t\t\t\t\t@click=\"toggleToolEnabled(tool)\">\n\t\t\t\t\t\t\t\t{{ tool.enabled ? 'Disable' : 'Enable' }}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t\t:disabled=\"toolSaving\"\n\t\t\t\t\t\t\t\t@click=\"editTool(tool)\">\n\t\t\t\t\t\t\t\tEdit\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"button danger\"\n\t\t\t\t\t\t\t\t:disabled=\"toolSaving\"\n\t\t\t\t\t\t\t\t@click=\"deleteTool(tool)\">\n\t\t\t\t\t\t\t\tDelete\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</template>\n\n\t\t\t<div v-if=\"toolFormVisible\" class=\"tool-form\">\n\t\t\t\t<h4>{{ toolForm.id ? 'Edit Tool' : 'Register Tool' }}</h4>\n\t\t\t\t<form @submit.prevent=\"saveTool\">\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"tool-name\">Tool Name</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"tool-name\"\n\t\t\t\t\t\t\tv-model=\"toolForm.name\"\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\trequired>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"tool-endpoint\">MCP Endpoint URL</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tid=\"tool-endpoint\"\n\t\t\t\t\t\t\tv-model=\"toolForm.mcpEndpointUrl\"\n\t\t\t\t\t\t\ttype=\"url\"\n\t\t\t\t\t\t\tname=\"educai-mcp-endpoint\"\n\t\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t\t\trequired>\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tHTTP or WebSocket URL exposing the MCP server.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"tool-description\">Description</label>\n\t\t\t\t\t\t<textarea\n\t\t\t\t\t\t\tid=\"tool-description\"\n\t\t\t\t\t\t\tv-model=\"toolForm.description\"\n\t\t\t\t\t\t\trows=\"3\"\n\t\t\t\t\t\t\tplaceholder=\"Short human-readable summary\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label for=\"tool-auth\">Authentication JSON</label>\n\t\t\t\t\t\t<textarea\n\t\t\t\t\t\t\tid=\"tool-auth\"\n\t\t\t\t\t\t\tv-model=\"toolForm.authentication\"\n\t\t\t\t\t\t\trows=\"4\"\n\t\t\t\t\t\t\tplaceholder=\"{&quot;headers&quot;:{&quot;Authorization&quot;:&quot;Bearer &lt;token&gt;&quot;}}\" />\n\t\t\t\t\t\t<p class=\"hint\">\n\t\t\t\t\t\t\tProvide JSON describing auth headers or tokens. {{ toolHasStoredAuth ? 'Stored credentials are kept unless you enter new values (re-enter to test or rotate).' : 'Leave blank when no authentication is required.' }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t<label class=\"checkbox\">\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tv-model=\"toolForm.enabled\"\n\t\t\t\t\t\t\t\ttype=\"checkbox\">\n\t\t\t\t\t\t\tEnabled by default\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"button-row\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\tclass=\"button primary\"\n\t\t\t\t\t\t\t:disabled=\"toolSaving\">\n\t\t\t\t\t\t\t{{ toolForm.id ? 'Save Changes' : 'Create Tool' }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t:disabled=\"toolSaving\"\n\t\t\t\t\t\t\t@click=\"closeToolForm\">\n\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t\t:disabled=\"toolTesting\"\n\t\t\t\t\t\t\t@click=\"testToolConnection\">\n\t\t\t\t\t\t\t{{ toolTesting ? 'Testing…' : 'Test Connection' }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t</form>\n\t\t\t\t<pre v-if=\"toolTestResult\" class=\"tool-test-result\">\n\t\t\t\t\t{{ toolTestResult }}\n\t\t\t\t</pre>\n\t\t\t</div>\n\t\t\t</div>\n\t\t</section>\n\n\t\t<section class=\"accordion-section\" :class=\"{ 'accordion-section--open': isSectionOpen('bots') }\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"accordion-header\"\n\t\t\t\t:aria-expanded=\"String(isSectionOpen('bots'))\"\n\t\t\t\t:aria-controls=\"sectionId('bots')\"\n\t\t\t\t@click=\"toggleSection('bots')\">\n\t\t\t\t<span class=\"accordion-heading\">\n\t\t\t\t\t<span class=\"accordion-title\">All Bots</span>\n\t\t\t\t\t<span class=\"accordion-description\">Admin overview and direct bot maintenance.</span>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"accordion-meta\">{{ botsSummary }}</span>\n\t\t\t\t<span class=\"accordion-chevron\" aria-hidden=\"true\"></span>\n\t\t\t</button>\n\t\t\t<div\n\t\t\t\tv-show=\"isSectionOpen('bots')\"\n\t\t\t\t:id=\"sectionId('bots')\"\n\t\t\t\tclass=\"accordion-panel\">\n\t\t\t\t<div class=\"section-heading\">\n\t\t\t\t\t<h4>All Bots</h4>\n\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"button\"\n\t\t\t\t\t\t:disabled=\"loadingAllBots\"\n\t\t\t\t\t\t@click=\"loadAllBots\">\n\t\t\t\t\t\t{{ loadingAllBots ? 'Loading…' : 'Refresh' }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div v-if=\"loadingAllBots\" class=\"hint\">Loading bots…</div>\n\t\t\t<div v-else-if=\"allBots.length === 0\" class=\"hint\">No bots found.</div>\n\t\t\t<div v-else class=\"bot-table\">\n\t\t\t\t<div class=\"bot-row bot-row--header\">\n\t\t\t\t\t<span>Name</span>\n\t\t\t\t\t<span>Mention</span>\n\t\t\t\t\t<span>Owner</span>\n\t\t\t\t\t<span>Visibility</span>\n\t\t\t\t\t<span>Status</span>\n\t\t\t\t\t<span class=\"actions-col\">Actions</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-for=\"bot in allBots\" :key=\"bot.id\" class=\"bot-row\">\n\t\t\t\t\t<span>{{ bot.bot_name }}</span>\n\t\t\t\t\t<span>{{ bot.mention_name }}</span>\n\t\t\t\t\t<span>{{ bot.user_id }}</span>\n\t\t\t\t\t<span>{{ formatVisibility(bot.visibility) }}</span>\n\t\t\t\t\t<span class=\"status-pill\" :class=\"`status-${bot.approval_status || 'approved'}`\">\n\t\t\t\t\t\t{{ bot.approval_status || 'approved' }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"actions-col\">\n\t\t\t\t\t\t<button type=\"button\" class=\"button\" @click=\"editBotAdmin(bot)\">Edit</button>\n\t\t\t\t\t\t<button type=\"button\" class=\"button danger\" @click=\"deleteBotAdmin(bot)\">Delete</button>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t</div>\n\t\t</section>\n\n\t\t<BotForm\n\t\t\tv-if=\"adminEditingBot\"\n\t\t\t:bot=\"adminEditingBot\"\n\t\t\t:user-permissions=\"{ isAdmin: true, isGroupAdmin: true, isTeamAdmin: true, hasApprovalRights: true, adminGroups: [], adminTeams: [] }\"\n\t\t\t@save=\"saveBotAdmin\"\n\t\t\t@cancel=\"closeAdminEditor\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { APP_DISPLAY_NAME } from '../branding.js'\nimport { generateUrl, imagePath } from '@nextcloud/router'\nimport { showSuccess, showError } from '@nextcloud/dialogs'\nimport BotForm from './BotForm.vue'\nimport { applyEducAiRuntimeIconPayload } from '../utils/appIconRuntime.js'\n\nexport default {\n\tname: 'AdminSettings',\n\tcomponents: {\n\t\tBotForm,\n\t},\n\tdata() {\n\t\treturn {\n\t\t\tAPP_DISPLAY_NAME,\n\t\t\tsaving: false,\n\t\t\tsettings: {\n\t\t\t\tapiKey: '',\n\t\t\t\tapiProvider: 'custom',\n\t\t\t\tapiEndpoint: '',\n\t\t\t\tsecondaryApiEndpoint: '',\n\t\t\t\tsecondaryApiKey: '',\n\t\t\t\tdefaultModel: 'llama-3.3-70b-instruct',\n\t\t\t\tdefaultTemperature: 0.2,\n\t\t\t\tfallbackModel: '',\n\t\t\t\tappIconUrl: '',\n\t\t\t\tappIconMode: 'default',\n\t\t\t\tappIconBlackUrl: '',\n\t\t\t\tappIconWhiteUrl: '',\n\t\t\t\tllmChatTimeout: 90,\n\t\t\t\tllmStreamTimeout: 240,\n\t\t\t\tllmModelsTimeout: 20,\n\t\t\t\twebhookSecret: '',\n\t\t\t\tallowMultipleModels: false,\n\t\t\t\tallowedModels: [],\n\t\t\t\tembeddingApiEndpoint: '',\n\t\t\t\tembeddingApiKey: '',\n\t\t\t\tembeddingModel: '',\n\t\t\t\tembeddingRateLimitMode: 'inherit',\n\t\t\t\tembeddingRateLimitSecond: null,\n\t\t\t\tembeddingRateLimitMinute: 100,\n\t\t\t\tembeddingRateLimitHour: 2000,\n\t\t\t\tembeddingRateLimitDay: 4000,\n\t\t\t\tragChunkSize: 750,\n\t\t\t\tragChunkOverlap: 50,\n\t\t\t\tragEnabled: false,\n\t\t\t\tcatalogueEnabled: false,\n\t\t\t\tcatalogueApiEndpoint: '',\n\t\t\t\tcatalogueReindexHours: 24,\n\t\t\t\tdoclingEnabled: false,\n\t\t\t\tdoclingApiEndpoint: '',\n\t\t\t\tdoclingApiKey: '',\n\t\t\t\tvisionApiEndpoint: '',\n\t\t\t\tvisionApiKey: '',\n\t\t\t\tvisionModel: '',\n\t\t\t\tspeechApiEndpoint: '',\n\t\t\t\tspeechApiKey: '',\n\t\t\t\tspeechModel: '',\n\t\t\t\trateLimitEnabled: false,\n\t\t\t\trateLimitSecond: null,\n\t\t\t\trateLimitMinute: 30,\n\t\t\t\trateLimitHour: 200,\n\t\t\t\trateLimitDay: 1000,\n\t\t\t\trateLimitQueueMessage: '',\n\t\t\t\tconversationContextTokens: 8000,\n\t\t\t},\n\t\t\topenSections: {\n\t\t\t\tappearance: true,\n\t\t\t\tessentials: true,\n\t\t\t\tmodelEndpoints: false,\n\t\t\t\tfallbackTimeouts: false,\n\t\t\t\trag: false,\n\t\t\t\tdocling: false,\n\t\t\t\tmedia: false,\n\t\t\t\tlimits: false,\n\t\t\t\tmemory: false,\n\t\t\t\ttools: false,\n\t\t\t\tbots: false,\n\t\t\t},\n\t\t\tavailableModels: [],\n\t\t\tavailableModelOptions: [],\n\t\t\tloadingModels: false,\n\t\t\tmodelLoadError: '',\n\t\t\tmodelSelectSize: 12,\n\t\t\tloadingTools: false,\n\t\t\ttoolSaving: false,\n\t\t\ttoolTesting: false,\n\t\t\ttoolFormVisible: false,\n\t\t\ttools: [],\n\t\t\ttoolForm: {\n\t\t\t\tid: null,\n\t\t\t\tname: '',\n\t\t\t\tmcpEndpointUrl: '',\n\t\t\t\tdescription: '',\n\t\t\t\tauthentication: '',\n\t\t\t\tenabled: false,\n\t\t\t},\n\t\t\ttoolHasStoredAuth: false,\n\t\t\ttoolTestResult: '',\n\t\t\tdoclingTesting: false,\n\t\t\tdoclingTestResult: null,\n\t\t\tdoclingHasStoredApiKey: false,\n\t\t\tvisionTesting: false,\n\t\t\tvisionTestResult: null,\n\t\t\tspeechTesting: false,\n\t\t\tspeechTestResult: null,\n\t\t\trateLimitLoading: false,\n\t\t\trateLimitStatus: null,\n\t\t\tqueueProcessing: false,\n\t\t\tglobalEmbeddingReindexing: false,\n\t\t\tembeddingConfigBaseline: {\n\t\t\t\tapiProvider: 'custom',\n\t\t\t\tapiEndpoint: '',\n\t\t\t\tembeddingApiEndpoint: '',\n\t\t\t\tembeddingModel: '',\n\t\t\t\thasApiKey: false,\n\t\t\t\thasEmbeddingApiKey: false,\n\t\t\t},\n\t\t\tloadingAllBots: false,\n\t\t\tallBots: [],\n\t\t\tadminEditingBot: null,\n\t\t\tuploadingAppIconVariant: '',\n\t\t\tappIconUploadUrls: {\n\t\t\t\tblack: '',\n\t\t\t\twhite: '',\n\t\t\t},\n\t\t\tappIconPreviewUrls: {\n\t\t\t\tblack: '',\n\t\t\t\twhite: '',\n\t\t\t},\n\t\t\tappIconPreviewVersion: {\n\t\t\t\tblack: 0,\n\t\t\t\twhite: 0,\n\t\t\t},\n\t\t}\n\t},\n\tcomputed: {\n\t\tappIconSummary() {\n\t\t\tif (this.settings.appIconMode === 'custom') {\n\t\t\t\treturn 'Custom'\n\t\t\t}\n\t\t\treturn 'Default'\n\t\t},\n\t\tappIconBlackPreviewUrl() {\n\t\t\treturn this.resolveAppIconPreviewUrl('black')\n\t\t},\n\t\tappIconWhitePreviewUrl() {\n\t\t\treturn this.resolveAppIconPreviewUrl('white')\n\t\t},\n\t\thasPendingEmbeddingConfigChange() {\n\t\t\tconst payload = {\n\t\t\t\tapiKey: this.settings.apiKey || '',\n\t\t\t\tapiProvider: this.settings.apiProvider || 'custom',\n\t\t\t\tapiEndpoint: this.settings.apiEndpoint || '',\n\t\t\t\tembeddingApiEndpoint: (this.settings.embeddingApiEndpoint || '').trim(),\n\t\t\t\tembeddingApiKey: this.settings.embeddingApiKey ? this.settings.embeddingApiKey : null,\n\t\t\t\tembeddingModel: (this.settings.embeddingModel || '').trim(),\n\t\t\t}\n\t\t\treturn this.hasEmbeddingConfigChange(payload)\n\t\t},\n\t\tchatQueueStats() {\n\t\t\treturn this.rateLimitStatus?.queue_stats || {\n\t\t\t\tpending: 0,\n\t\t\t\tprocessing: 0,\n\t\t\t\tcompleted: 0,\n\t\t\t\tfailed: 0,\n\t\t\t\ttotal: 0,\n\t\t\t}\n\t\t},\n\t\tchatRateLimitState() {\n\t\t\treturn this.rateLimitStatus?.chat_status?.state || this.rateLimitStatus?.state || null\n\t\t},\n\t\tchatCanProcess() {\n\t\t\tif (typeof this.rateLimitStatus?.chat_status?.can_process === 'boolean') {\n\t\t\t\treturn this.rateLimitStatus.chat_status.can_process\n\t\t\t}\n\t\t\treturn !!this.rateLimitStatus?.can_process\n\t\t},\n\t\tembeddingRateLimitState() {\n\t\t\treturn this.rateLimitStatus?.embedding_status?.state || null\n\t\t},\n\t\tembeddingRateLimitModeStatus() {\n\t\t\treturn this.rateLimitStatus?.embedding_status?.mode || this.settings.embeddingRateLimitMode || 'inherit'\n\t\t},\n\t\tembeddingStatusSource() {\n\t\t\treturn this.rateLimitStatus?.embedding_status?.source || 'configured'\n\t\t},\n\t\tembeddingConfiguredHint() {\n\t\t\tif (this.embeddingRateLimitModeStatus === 'custom') {\n\t\t\t\treturn 'Using the configured custom embedding limits until the provider returns dedicated headers.'\n\t\t\t}\n\t\t\treturn 'Using inherited chat limits until the embedding endpoint returns its own rate-limit headers.'\n\t\t},\n\t\tembeddingCanProcess() {\n\t\t\tif (typeof this.rateLimitStatus?.embedding_status?.can_process === 'boolean') {\n\t\t\t\treturn this.rateLimitStatus.embedding_status.can_process\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t\tdoclingKeyHint() {\n\t\t\tif (this.doclingHasStoredApiKey) {\n\t\t\t\treturn 'A stored Docling key is configured and kept unless you enter a new value.'\n\t\t\t}\n\t\t\treturn 'Leave blank to reuse the main API key.'\n\t\t},\n\t\tessentialsSummary() {\n\t\t\treturn `Temp ${this.formatTemperature(this.settings.defaultTemperature)}`\n\t\t},\n\t\tmodelEndpointsSummary() {\n\t\t\tif (this.settings.allowMultipleModels) {\n\t\t\t\treturn `${this.settings.allowedModels.length} allowed model(s)`\n\t\t\t}\n\t\t\tif ((this.settings.secondaryApiEndpoint || '').trim() !== '') {\n\t\t\t\treturn 'Primary + Secondary'\n\t\t\t}\n\t\t\treturn this.formatModelLabel(this.settings.defaultModel) || 'Primary only'\n\t\t},\n\t\tfallbackTimeoutSummary() {\n\t\t\tconst fallback = this.formatModelLabel(this.settings.fallbackModel)\n\t\t\treturn fallback || `${this.settings.llmChatTimeout || 90}/${this.settings.llmStreamTimeout || 240}/${this.settings.llmModelsTimeout || 20}s`\n\t\t},\n\t\tmodelOptionMap() {\n\t\t\treturn this.availableModelOptions.reduce((map, option) => {\n\t\t\t\tmap[option.id] = option\n\t\t\t\treturn map\n\t\t\t}, {})\n\t\t},\n\t\tragSummary() {\n\t\t\tif (this.hasPendingEmbeddingConfigChange) {\n\t\t\t\treturn 'Changed - reindex needed'\n\t\t\t}\n\t\t\treturn this.settings.ragEnabled ? (this.settings.embeddingModel || 'Enabled') : 'Disabled'\n\t\t},\n\t\tdoclingSummary() {\n\t\t\treturn this.settings.doclingEnabled ? 'Enabled' : 'Disabled'\n\t\t},\n\t\tmediaSummary() {\n\t\t\tconst enabled = []\n\t\t\tif (this.settings.visionModel) {\n\t\t\t\tenabled.push('Vision')\n\t\t\t}\n\t\t\tif (this.settings.speechModel) {\n\t\t\t\tenabled.push('Speech')\n\t\t\t}\n\t\t\treturn enabled.length ? enabled.join(' + ') : 'No models'\n\t\t},\n\t\trateLimitSummary() {\n\t\t\tif (!this.settings.rateLimitEnabled) {\n\t\t\t\treturn 'Disabled'\n\t\t\t}\n\t\t\treturn `${this.chatQueueStats.pending} queued`\n\t\t},\n\t\tmemorySummary() {\n\t\t\treturn `${this.settings.conversationContextTokens || 8000} tokens`\n\t\t},\n\t\ttoolsSummary() {\n\t\t\tif (this.loadingTools) {\n\t\t\t\treturn 'Loading'\n\t\t\t}\n\t\t\treturn `${this.tools.length} registered`\n\t\t},\n\t\tbotsSummary() {\n\t\t\tif (this.loadingAllBots) {\n\t\t\t\treturn 'Loading'\n\t\t\t}\n\t\t\treturn `${this.allBots.length} bot(s)`\n\t\t},\n\t},\n\tmounted() {\n\t\tthis.loadSettings()\n\t\tthis.loadTools()\n\t\tthis.loadAllBots()\n\t},\n\tmethods: {\n\t\tisSectionOpen(key) {\n\t\t\treturn !!this.openSections[key]\n\t\t},\n\t\tsectionId(key) {\n\t\t\treturn `educai-admin-section-${key}`\n\t\t},\n\t\ttoggleSection(key) {\n\t\t\tif (Object.prototype.hasOwnProperty.call(this.openSections, key)) {\n\t\t\t\tthis.openSections[key] = !this.openSections[key]\n\t\t\t}\n\t\t},\n\t\tnormalizeTemperatureValue(value) {\n\t\t\tconst numeric = Number(value)\n\t\t\tif (!Number.isFinite(numeric) || numeric < 0 || numeric > 1) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\treturn Math.round(numeric * 100) / 100\n\t\t},\n\t\tnormalizeAppIconUrl(value) {\n\t\t\tconst url = (value || '').trim()\n\t\t\tif (url === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (url.length > 1024) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.isUploadedAppIconReference(url)) {\n\t\t\t\treturn this.isValidUploadedAppIconReference(url) ? url : null\n\t\t\t}\n\t\t\tif (/^https?:\\/\\//i.test(url)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = new URL(url)\n\t\t\t\t\treturn parsed.protocol === 'http:' || parsed.protocol === 'https:' ? url : null\n\t\t\t\t} catch {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (url.startsWith('/') && !url.startsWith('//')) {\n\t\t\t\treturn url\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\t\tisUploadedAppIconReference(value) {\n\t\t\treturn /^educai-upload:\\/\\/(black|white)$/i.test(String(value || '').trim())\n\t\t},\n\t\tisServerBackedAppIconReference(value) {\n\t\t\treturn this.isUploadedAppIconReference(value)\n\t\t},\n\t\tisValidUploadedAppIconReference(value) {\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(String(value || '').trim())\n\t\t\t\treturn parsed.protocol === 'educai-upload:' && ['black', 'white'].includes(parsed.hostname)\n\t\t\t} catch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\t\tnormalizeLoadedAppIconValue(value) {\n\t\t\tconst raw = String(value || '').trim()\n\t\t\treturn /^nextcloud-file(id)?:\\/\\//i.test(raw) ? '' : raw\n\t\t},\n\t\ttriggerAppIconUpload(variant) {\n\t\t\tconst refName = variant === 'white' ? 'appIconWhiteFileInput' : 'appIconBlackFileInput'\n\t\t\tconst input = this.$refs[refName]\n\t\t\tif (input) {\n\t\t\t\tinput.value = ''\n\t\t\t\tinput.click()\n\t\t\t}\n\t\t},\n\t\tasync uploadAppIconSvg(variant, event) {\n\t\t\tconst input = event?.target\n\t\t\tconst file = input?.files?.[0] || null\n\t\t\tif (!file) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (!this.isLocalSvgUpload(file)) {\n\t\t\t\tshowError('Please upload an SVG file up to 1 MB')\n\t\t\t\tinput.value = ''\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.uploadingAppIconVariant = variant\n\t\t\ttry {\n\t\t\t\tconst formData = new FormData()\n\t\t\t\tformData.append('icon', file)\n\t\t\t\tconst response = await axios.post(\n\t\t\t\t\tthis.resolveAppIconUploadUrl(variant),\n\t\t\t\t\tformData,\n\t\t\t\t)\n\t\t\t\tconst value = response.data?.value || `educai-upload://${variant}`\n\t\t\t\tif (variant === 'white') {\n\t\t\t\t\tthis.settings.appIconWhiteUrl = value\n\t\t\t\t} else {\n\t\t\t\t\tthis.settings.appIconBlackUrl = value\n\t\t\t\t}\n\t\t\t\tthis.appIconPreviewVersion[variant] = Date.now()\n\t\t\t\tshowSuccess('App icon SVG uploaded')\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to upload app icon SVG:', error)\n\t\t\t\tshowError(error?.response?.data?.error || 'Failed to upload app icon SVG')\n\t\t\t} finally {\n\t\t\t\tthis.uploadingAppIconVariant = ''\n\t\t\t\tinput.value = ''\n\t\t\t}\n\t\t},\n\t\tisLocalSvgUpload(file) {\n\t\t\treturn file\n\t\t\t\t&& file.size > 0\n\t\t\t\t&& file.size <= 1024 * 1024\n\t\t\t\t&& (file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg'))\n\t\t},\n\t\tresolveAppIconUploadUrl(variant) {\n\t\t\treturn this.appIconUploadUrls[variant] || generateUrl(`/apps/educai/api/v1/admin/app-icon-upload/${variant}`)\n\t\t},\n\t\tsetAppIconMode(mode) {\n\t\t\tif (!['default', 'custom'].includes(mode)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.settings.appIconMode = mode\n\t\t\tif (mode === 'default') {\n\t\t\t\tthis.settings.appIconBlackUrl = ''\n\t\t\t\tthis.settings.appIconWhiteUrl = ''\n\t\t\t}\n\t\t},\n\t\tresetAppIconConfig() {\n\t\t\tthis.settings.appIconMode = 'default'\n\t\t\tthis.settings.appIconBlackUrl = ''\n\t\t\tthis.settings.appIconWhiteUrl = ''\n\t\t\tthis.settings.appIconUrl = ''\n\t\t},\n\t\tresolveAppIconPreviewUrl(variant) {\n\t\t\tif (this.settings.appIconMode === 'default') {\n\t\t\t\treturn imagePath('educai', variant === 'white' ? 'app.svg' : 'app-dark.svg')\n\t\t\t}\n\n\t\t\tif (this.settings.appIconMode === 'custom') {\n\t\t\t\tconst url = this.normalizeAppIconUrl(\n\t\t\t\t\tvariant === 'white' ? this.settings.appIconWhiteUrl : this.settings.appIconBlackUrl,\n\t\t\t\t)\n\t\t\t\tif (this.isServerBackedAppIconReference(url)) {\n\t\t\t\t\tconst version = this.appIconPreviewVersion[variant] || 0\n\t\t\t\t\tconst previewUrl = this.appIconPreviewUrls[variant] || generateUrl(`/apps/educai/api/v1/admin/app-icon-preview/${variant}`)\n\t\t\t\t\treturn `${previewUrl}?source=${encodeURIComponent(url)}&v=${version}`\n\t\t\t\t}\n\n\t\t\t\treturn url || ''\n\t\t\t}\n\n\t\t\treturn ''\n\t\t},\n\t\tbuildAppIconPayload() {\n\t\t\tconst mode = ['default', 'custom'].includes(this.settings.appIconMode)\n\t\t\t\t? this.settings.appIconMode\n\t\t\t\t: 'default'\n\n\t\t\tif (mode === 'default') {\n\t\t\t\treturn {\n\t\t\t\t\tappIconMode: 'default',\n\t\t\t\t\tappIconBlackUrl: null,\n\t\t\t\t\tappIconWhiteUrl: null,\n\t\t\t\t\tappIconUrl: '',\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst blackUrl = this.normalizeAppIconUrl(this.settings.appIconBlackUrl)\n\t\t\tif (blackUrl === null) {\n\t\t\t\tshowError('Black app icon must be an http(s) URL, an absolute Nextcloud path, or an uploaded SVG')\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst whiteUrl = this.normalizeAppIconUrl(this.settings.appIconWhiteUrl)\n\t\t\tif (whiteUrl === null) {\n\t\t\t\tshowError('White app icon must be an http(s) URL, an absolute Nextcloud path, or an uploaded SVG')\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tif (!blackUrl || !whiteUrl) {\n\t\t\t\tshowError('Custom app icon mode requires both black and white icons')\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tappIconMode: 'custom',\n\t\t\t\tappIconBlackUrl: blackUrl,\n\t\t\t\tappIconWhiteUrl: whiteUrl,\n\t\t\t\tappIconUrl: blackUrl || '',\n\t\t\t}\n\t\t},\n\t\tformatTemperature(value) {\n\t\t\tconst temperature = this.normalizeTemperatureValue(value)\n\t\t\treturn temperature === null ? '0.20' : temperature.toFixed(2)\n\t\t},\n\t\ttoEndpointModelId(model) {\n\t\t\tconst value = (model || '').trim()\n\t\t\tif (value === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn /^(primary|secondary):/.test(value) ? value : `primary:${value}`\n\t\t},\n\t\tformatModelLabel(model) {\n\t\t\tconst id = this.toEndpointModelId(model)\n\t\t\tif (id === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.modelOptionMap[id]?.label || id.replace(/^primary:/, 'Primary · ').replace(/^secondary:/, 'Secondary · ')\n\t\t},\n\t\tnormalizeModelOptions(rawOptions, rawModels) {\n\t\t\tif (Array.isArray(rawOptions) && rawOptions.length > 0) {\n\t\t\t\treturn rawOptions\n\t\t\t\t\t.filter((option) => option && option.id && option.model)\n\t\t\t\t\t.map((option) => ({\n\t\t\t\t\t\tid: String(option.id),\n\t\t\t\t\t\tlabel: option.label || this.formatModelLabel(option.id),\n\t\t\t\t\t\tmodel: String(option.model),\n\t\t\t\t\t\tendpoint: option.endpoint === 'secondary' ? 'secondary' : 'primary',\n\t\t\t\t\t}))\n\t\t\t}\n\n\t\t\treturn Array.isArray(rawModels)\n\t\t\t\t? rawModels.map((model) => ({\n\t\t\t\t\tid: this.toEndpointModelId(String(model)),\n\t\t\t\t\tlabel: this.formatModelLabel(String(model)),\n\t\t\t\t\tmodel: String(model).replace(/^(primary|secondary):/, ''),\n\t\t\t\t\tendpoint: String(model).startsWith('secondary:') ? 'secondary' : 'primary',\n\t\t\t\t}))\n\t\t\t\t: []\n\t\t},\n\t\tavailableModelOptionsWithCurrent(current) {\n\t\t\tconst values = Array.isArray(current) ? current : [current]\n\t\t\tconst options = [...this.availableModelOptions]\n\t\t\tconst existing = new Set(options.map((option) => option.id))\n\t\t\tvalues\n\t\t\t\t.map((value) => this.toEndpointModelId(value))\n\t\t\t\t.filter((id) => id !== '' && !existing.has(id))\n\t\t\t\t.forEach((id) => {\n\t\t\t\t\tconst endpoint = id.startsWith('secondary:') ? 'secondary' : 'primary'\n\t\t\t\t\tconst model = id.replace(/^(primary|secondary):/, '')\n\t\t\t\t\toptions.push({\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tlabel: this.formatModelLabel(id),\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tendpoint,\n\t\t\t\t\t})\n\t\t\t\t\texisting.add(id)\n\t\t\t\t})\n\t\t\treturn options\n\t\t},\n\t\tapplyDefaultTemperaturePreset(value) {\n\t\t\tthis.settings.defaultTemperature = value\n\t\t},\n\t\ttoOptionalPositiveInteger(value) {\n\t\t\tconst numeric = Number(value)\n\t\t\treturn Number.isFinite(numeric) && numeric > 0 ? numeric : null\n\t\t},\n\t\ttoPositiveInteger(value, fallback) {\n\t\t\tconst numeric = Number(value)\n\t\t\treturn Number.isFinite(numeric) && numeric > 0 ? numeric : fallback\n\t\t},\n\t\tasync loadSettings() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(generateUrl('/apps/educai/api/v1/settings'))\n\t\t\t\t// Don't overwrite password fields with masked values\n\t\t\t\tconst data = response.data\n\t\t\t\tthis.settings.apiProvider = data.api_provider || 'custom'\n\t\t\t\tthis.settings.apiEndpoint = data.api_endpoint || ''\n\t\t\t\tthis.settings.secondaryApiEndpoint = data.secondary_api_endpoint || ''\n\t\t\t\tthis.settings.defaultModel = this.toEndpointModelId(data.default_model || 'llama-3.3-70b-instruct')\n\t\t\t\tthis.settings.defaultTemperature = this.normalizeTemperatureValue(data.default_temperature) ?? 0.2\n\t\t\t\tthis.settings.fallbackModel = this.toEndpointModelId(data.fallback_model || '')\n\t\t\t\tthis.appIconUploadUrls = {\n\t\t\t\t\tblack: data.app_icon_upload_urls?.black || '',\n\t\t\t\t\twhite: data.app_icon_upload_urls?.white || '',\n\t\t\t\t}\n\t\t\t\tthis.appIconPreviewUrls = {\n\t\t\t\t\tblack: data.app_icon_preview_urls?.black || '',\n\t\t\t\t\twhite: data.app_icon_preview_urls?.white || '',\n\t\t\t\t}\n\t\t\t\tthis.settings.appIconUrl = this.normalizeLoadedAppIconValue(data.app_icon_url || '')\n\t\t\t\tthis.settings.appIconMode = data.app_icon_mode === 'custom' || data.app_icon_url ? 'custom' : 'default'\n\t\t\t\tthis.settings.appIconBlackUrl = this.normalizeLoadedAppIconValue(data.app_icon_black_url || (this.settings.appIconMode === 'custom' ? data.app_icon_url || '' : ''))\n\t\t\t\tthis.settings.appIconWhiteUrl = this.normalizeLoadedAppIconValue(data.app_icon_white_url || (this.settings.appIconMode === 'custom' ? data.app_icon_url || '' : ''))\n\t\t\t\tapplyEducAiRuntimeIconPayload(data)\n\t\t\t\tthis.settings.llmChatTimeout = typeof data.llm_chat_timeout === 'number' && data.llm_chat_timeout > 0 ? data.llm_chat_timeout : 90\n\t\t\t\tthis.settings.llmStreamTimeout = typeof data.llm_stream_timeout === 'number' && data.llm_stream_timeout > 0 ? data.llm_stream_timeout : 240\n\t\t\t\tthis.settings.llmModelsTimeout = typeof data.llm_models_timeout === 'number' && data.llm_models_timeout > 0 ? data.llm_models_timeout : 20\n\t\t\t\tthis.settings.allowMultipleModels = !!data.allow_multiple_models\n\t\t\t\tthis.settings.allowedModels = Array.isArray(data.allowed_models) ? data.allowed_models.map((model) => this.toEndpointModelId(model)) : []\n\t\t\t\tthis.settings.embeddingApiEndpoint = data.embedding_api_endpoint || ''\n\t\t\t\tthis.settings.embeddingModel = data.embedding_model || ''\n\t\t\t\tthis.settings.embeddingRateLimitMode = data.embedding_rate_limit_mode || 'inherit'\n\t\t\t\tthis.settings.embeddingRateLimitSecond = typeof data.embedding_rate_limit_second === 'number' && data.embedding_rate_limit_second > 0 ? data.embedding_rate_limit_second : null\n\t\t\t\tthis.settings.embeddingRateLimitMinute = typeof data.embedding_rate_limit_minute === 'number' && data.embedding_rate_limit_minute > 0 ? data.embedding_rate_limit_minute : 100\n\t\t\t\tthis.settings.embeddingRateLimitHour = typeof data.embedding_rate_limit_hour === 'number' && data.embedding_rate_limit_hour > 0 ? data.embedding_rate_limit_hour : 2000\n\t\t\t\tthis.settings.embeddingRateLimitDay = typeof data.embedding_rate_limit_day === 'number' && data.embedding_rate_limit_day > 0 ? data.embedding_rate_limit_day : 4000\n\t\t\t\tthis.embeddingConfigBaseline = {\n\t\t\t\t\tapiProvider: this.settings.apiProvider,\n\t\t\t\t\tapiEndpoint: this.settings.apiEndpoint,\n\t\t\t\t\tembeddingApiEndpoint: this.settings.embeddingApiEndpoint,\n\t\t\t\t\tembeddingModel: this.settings.embeddingModel,\n\t\t\t\t\thasApiKey: data.api_key === '***',\n\t\t\t\t\thasEmbeddingApiKey: data.embedding_api_key === '***',\n\t\t\t\t}\n\t\t\t\tthis.settings.ragChunkSize = typeof data.rag_chunk_size === 'number' ? data.rag_chunk_size : 750\n\t\t\t\tthis.settings.ragChunkOverlap = typeof data.rag_chunk_overlap === 'number' ? data.rag_chunk_overlap : 50\n\t\t\t\tthis.settings.ragEnabled = !!data.rag_enabled\n\t\t\t\tthis.settings.catalogueEnabled = !!data.catalogue_enabled\n\t\t\t\tthis.settings.catalogueApiEndpoint = data.catalogue_api_endpoint || ''\n\t\t\t\tthis.settings.catalogueReindexHours = typeof data.catalogue_reindex_hours === 'number' ? data.catalogue_reindex_hours : 24\n\t\t\t\tthis.settings.doclingEnabled = !!data.docling_enabled\n\t\t\t\tthis.settings.doclingApiEndpoint = data.docling_api_endpoint || ''\n\t\t\t\tthis.doclingHasStoredApiKey = data.docling_api_key === '***'\n\t\t\t\tthis.settings.visionApiEndpoint = data.vision_api_endpoint || ''\n\t\t\t\tthis.settings.visionModel = data.vision_model || ''\n\t\t\t\tthis.settings.speechApiEndpoint = data.speech_api_endpoint || ''\n\t\t\t\tthis.settings.speechModel = data.speech_model || ''\n\t\t\t\tthis.settings.rateLimitEnabled = !!data.rate_limit_enabled\n\t\t\t\tthis.settings.rateLimitSecond = typeof data.rate_limit_second === 'number' && data.rate_limit_second > 0 ? data.rate_limit_second : null\n\t\t\t\tthis.settings.rateLimitMinute = typeof data.rate_limit_minute === 'number' && data.rate_limit_minute > 0 ? data.rate_limit_minute : 30\n\t\t\t\tthis.settings.rateLimitHour = typeof data.rate_limit_hour === 'number' && data.rate_limit_hour > 0 ? data.rate_limit_hour : 200\n\t\t\t\tthis.settings.rateLimitDay = typeof data.rate_limit_day === 'number' && data.rate_limit_day > 0 ? data.rate_limit_day : 1000\n\t\t\t\tthis.settings.rateLimitQueueMessage = data.rate_limit_queue_message || ''\n\t\t\t\tthis.settings.conversationContextTokens = typeof data.conversation_context_tokens === 'number' ? data.conversation_context_tokens : 8000\n\t\t\t\tif (this.settings.allowMultipleModels || this.settings.secondaryApiEndpoint) {\n\t\t\t\t\tthis.loadModels()\n\t\t\t\t}\n\t\t\t\tif (this.settings.rateLimitEnabled) {\n\t\t\t\t\tthis.loadRateLimitStatus()\n\t\t\t\t}\n\t\t\t\t// Leave apiKey and webhookSecret empty to avoid showing masked values\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to load settings:', error)\n\t\t\t}\n\t\t},\n\t\tonToggleMultiple() {\n\t\t\tif (this.settings.allowMultipleModels && this.availableModelOptions.length === 0) {\n\t\t\t\tthis.loadModels()\n\t\t\t}\n\t\t},\n\t\tasync loadModels() {\n\t\t\tthis.loadingModels = true\n\t\t\tthis.modelLoadError = ''\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.get(generateUrl('/apps/educai/api/v1/models'))\n\t\t\t\tthis.availableModelOptions = this.normalizeModelOptions(resp.data?.model_options, resp.data?.models)\n\t\t\t\tthis.availableModels = this.availableModelOptions.map((option) => option.id)\n\t\t\t\t// Adjust visible rows between 12 and 18 depending on list length\n\t\t\t\tconst n = this.availableModelOptions.length\n\t\t\t\tthis.modelSelectSize = Math.max(12, Math.min(18, n || 12))\n\t\t\t\t// If no selection yet, preselect the default model if present\n\t\t\t\tif (this.settings.allowedModels.length === 0 && this.settings.defaultModel) {\n\t\t\t\t\tif (this.availableModels.includes(this.settings.defaultModel)) {\n\t\t\t\t\t\tthis.settings.allowedModels = [this.settings.defaultModel]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error('Failed to load models', e)\n\t\t\t\tthis.modelLoadError = 'Failed to load models from provider. Check API key and endpoint.'\n\t\t\t} finally {\n\t\t\t\tthis.loadingModels = false\n\t\t\t}\n\t\t},\n\t\tasync saveSettings() {\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst defaultTemperature = this.normalizeTemperatureValue(this.settings.defaultTemperature)\n\t\t\t\tif (defaultTemperature === null) {\n\t\t\t\t\tshowError('Default temperature must be between 0.0 and 1.0')\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst appIconPayload = this.buildAppIconPayload()\n\t\t\t\tif (appIconPayload === null) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Build payload explicitly to avoid sending empty secret values\n\t\t\t\tconst payload = {\n\t\t\t\t\tapiKey: this.settings.apiKey || '',\n\t\t\t\t\tapiEndpoint: (this.settings.apiEndpoint || '').trim(),\n\t\t\t\t\tsecondaryApiEndpoint: (this.settings.secondaryApiEndpoint || '').trim(),\n\t\t\t\t\tsecondaryApiKey: this.settings.secondaryApiKey ? this.settings.secondaryApiKey : null,\n\t\t\t\t\tdefaultModel: this.toEndpointModelId(this.settings.defaultModel),\n\t\t\t\t\tdefaultTemperature,\n\t\t\t\t\tfallbackModel: this.toEndpointModelId(this.settings.fallbackModel),\n\t\t\t\t\t...appIconPayload,\n\t\t\t\t\tllmChatTimeout: this.toPositiveInteger(this.settings.llmChatTimeout, 90),\n\t\t\t\t\tllmStreamTimeout: this.toPositiveInteger(this.settings.llmStreamTimeout, 240),\n\t\t\t\t\tllmModelsTimeout: this.toPositiveInteger(this.settings.llmModelsTimeout, 20),\n\t\t\t\t\tapiProvider: this.settings.apiProvider || 'custom',\n\t\t\t\t\tallowMultipleModels: this.settings.allowMultipleModels,\n\t\t\t\t\tallowedModels: this.settings.allowMultipleModels ? this.settings.allowedModels.map((model) => this.toEndpointModelId(model)) : null,\n\t\t\t\t\tembeddingApiEndpoint: (this.settings.embeddingApiEndpoint || '').trim(),\n\t\t\t\t\tembeddingApiKey: this.settings.embeddingApiKey ? this.settings.embeddingApiKey : null,\n\t\t\t\t\tembeddingModel: (this.settings.embeddingModel || '').trim(),\n\t\t\t\t\tembeddingRateLimitMode: this.settings.embeddingRateLimitMode || 'inherit',\n\t\t\t\t\tembeddingRateLimitSecond: this.toOptionalPositiveInteger(this.settings.embeddingRateLimitSecond),\n\t\t\t\t\tembeddingRateLimitMinute: this.toPositiveInteger(this.settings.embeddingRateLimitMinute, 100),\n\t\t\t\t\tembeddingRateLimitHour: this.toPositiveInteger(this.settings.embeddingRateLimitHour, 2000),\n\t\t\t\t\tembeddingRateLimitDay: this.toPositiveInteger(this.settings.embeddingRateLimitDay, 4000),\n\t\t\t\t\tragChunkSize: Number.isFinite(this.settings.ragChunkSize) ? Number(this.settings.ragChunkSize) : null,\n\t\t\t\t\tragChunkOverlap: Number.isFinite(this.settings.ragChunkOverlap) ? Number(this.settings.ragChunkOverlap) : null,\n\t\t\t\t\tragEnabled: this.settings.ragEnabled,\n\t\t\t\t\tcatalogueEnabled: this.settings.catalogueEnabled,\n\t\t\t\t\tcatalogueApiEndpoint: (this.settings.catalogueApiEndpoint || '').trim(),\n\t\t\t\t\tcatalogueReindexHours: Number.isFinite(this.settings.catalogueReindexHours) ? Number(this.settings.catalogueReindexHours) : 24,\n\t\t\t\t\tdoclingEnabled: this.settings.doclingEnabled,\n\t\t\t\t\tdoclingApiEndpoint: (this.settings.doclingApiEndpoint || '').trim(),\n\t\t\t\t\tdoclingApiKey: this.settings.doclingApiKey ? this.settings.doclingApiKey : null,\n\t\t\t\t\tvisionApiEndpoint: (this.settings.visionApiEndpoint || '').trim(),\n\t\t\t\t\tvisionApiKey: this.settings.visionApiKey ? this.settings.visionApiKey : null,\n\t\t\t\t\tvisionModel: (this.settings.visionModel || '').trim(),\n\t\t\t\t\tspeechApiEndpoint: (this.settings.speechApiEndpoint || '').trim(),\n\t\t\t\t\tspeechApiKey: this.settings.speechApiKey ? this.settings.speechApiKey : null,\n\t\t\t\t\tspeechModel: (this.settings.speechModel || '').trim(),\n\t\t\t\t\trateLimitEnabled: this.settings.rateLimitEnabled,\n\t\t\t\t\trateLimitSecond: this.toOptionalPositiveInteger(this.settings.rateLimitSecond),\n\t\t\t\t\trateLimitMinute: this.toPositiveInteger(this.settings.rateLimitMinute, 30),\n\t\t\t\t\trateLimitHour: this.toPositiveInteger(this.settings.rateLimitHour, 200),\n\t\t\t\t\trateLimitDay: this.toPositiveInteger(this.settings.rateLimitDay, 1000),\n\t\t\t\t\trateLimitQueueMessage: (this.settings.rateLimitQueueMessage || '').trim(),\n\t\t\t\t\tconversationContextTokens: Number.isFinite(this.settings.conversationContextTokens) ? Number(this.settings.conversationContextTokens) : 8000,\n\t\t\t\t}\n\t\t\t\t// Only include webhookSecret when user provided a non-empty value\n\t\t\t\tif (this.settings.webhookSecret && this.settings.webhookSecret.trim() !== '') {\n\t\t\t\t\tpayload.webhookSecret = this.settings.webhookSecret\n\t\t\t\t}\n\n\t\t\t\tconst embeddingConfigChanged = this.hasEmbeddingConfigChange(payload)\n\t\t\t\tif (embeddingConfigChanged) {\n\t\t\t\t\tconst proceed = confirm(\n\t\t\t\t\t\t'Embedding configuration changes detected (model/endpoint/key).\\n\\n'\n\t\t\t\t\t\t+ 'You must run \"Reindex All Embeddings\" after saving, otherwise retrieval quality will be degraded.\\n\\n'\n\t\t\t\t\t\t+ 'Continue saving settings?',\n\t\t\t\t\t)\n\t\t\t\t\tif (!proceed) {\n\t\t\t\t\t\tthis.saving = false\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst response = await axios.put(\n\t\t\t\t\tgenerateUrl('/apps/educai/api/v1/settings'),\n\t\t\t\t\tpayload,\n\t\t\t\t)\n\t\t\t\tapplyEducAiRuntimeIconPayload(response.data, { refreshNavigation: true })\n\t\t\t\tif (embeddingConfigChanged) {\n\t\t\t\t\tshowSuccess('Settings saved. Run \"Reindex All Embeddings\" now to rebuild vectors for the new embedding configuration.')\n\t\t\t\t} else {\n\t\t\t\t\tshowSuccess('Settings saved successfully')\n\t\t\t\t}\n\t\t\t\t// Keep baseline in sync after successful save\n\t\t\t\tconst apiKeyUpdated = !!(payload.apiKey && payload.apiKey.trim() !== '')\n\t\t\t\tconst secondaryApiKeyUpdated = !!(payload.secondaryApiKey && payload.secondaryApiKey.trim() !== '')\n\t\t\t\tconst embeddingKeyUpdated = !!(payload.embeddingApiKey && payload.embeddingApiKey.trim() !== '')\n\t\t\t\tconst doclingKeyUpdated = !!(payload.doclingApiKey && payload.doclingApiKey.trim() !== '')\n\t\t\t\tthis.doclingHasStoredApiKey = this.doclingHasStoredApiKey || doclingKeyUpdated\n\t\t\t\tthis.clearSecretInputs()\n\t\t\t\tthis.embeddingConfigBaseline = {\n\t\t\t\t\tapiProvider: this.settings.apiProvider || 'custom',\n\t\t\t\t\tapiEndpoint: this.settings.apiEndpoint,\n\t\t\t\t\tembeddingApiEndpoint: this.settings.embeddingApiEndpoint,\n\t\t\t\t\tembeddingModel: this.settings.embeddingModel,\n\t\t\t\t\thasApiKey: this.embeddingConfigBaseline.hasApiKey || apiKeyUpdated,\n\t\t\t\t\thasEmbeddingApiKey: this.embeddingConfigBaseline.hasEmbeddingApiKey || embeddingKeyUpdated,\n\t\t\t\t}\n\t\t\t\tif (secondaryApiKeyUpdated) {\n\t\t\t\t\tthis.settings.secondaryApiKey = ''\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to save settings:', error)\n\t\t\t\tshowError('Failed to save settings')\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\t\tclearSecretInputs() {\n\t\t\tthis.settings.apiKey = ''\n\t\t\tthis.settings.secondaryApiKey = ''\n\t\t\tthis.settings.webhookSecret = ''\n\t\t\tthis.settings.embeddingApiKey = ''\n\t\t\tthis.settings.doclingApiKey = ''\n\t\t\tthis.settings.visionApiKey = ''\n\t\t\tthis.settings.speechApiKey = ''\n\t\t},\n\t\topenCreateTool() {\n\t\t\tthis.resetToolForm()\n\t\t\tthis.toolFormVisible = true\n\t\t},\n\t\tresetToolForm() {\n\t\t\tthis.toolForm = {\n\t\t\t\tid: null,\n\t\t\t\tname: '',\n\t\t\t\tmcpEndpointUrl: '',\n\t\t\t\tdescription: '',\n\t\t\t\tauthentication: '',\n\t\t\t\tenabled: false,\n\t\t\t}\n\t\t\tthis.toolHasStoredAuth = false\n\t\t\tthis.toolTestResult = ''\n\t\t},\n\t\tcloseToolForm() {\n\t\t\tthis.resetToolForm()\n\t\t\tthis.toolFormVisible = false\n\t\t},\n\t\tasync loadTools() {\n\t\t\tthis.loadingTools = true\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.get(generateUrl('/apps/educai/api/v1/admin/tools'))\n\t\t\t\tthis.tools = Array.isArray(resp.data?.tools) ? resp.data.tools : []\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to load tools', error)\n\t\t\t\tshowError('Failed to load tools')\n\t\t\t} finally {\n\t\t\t\tthis.loadingTools = false\n\t\t\t}\n\t\t},\n\t\teditTool(tool) {\n\t\t\tthis.toolForm = {\n\t\t\t\tid: tool.id,\n\t\t\t\tname: tool.name,\n\t\t\t\tmcpEndpointUrl: tool.mcp_endpoint_url,\n\t\t\t\tdescription: tool.description || '',\n\t\t\t\tauthentication: '',\n\t\t\t\tenabled: !!tool.enabled,\n\t\t\t}\n\t\t\tthis.toolHasStoredAuth = tool.authentication === '***'\n\t\t\tthis.toolFormVisible = true\n\t\t\tthis.toolTestResult = ''\n\t\t},\n\t\tasync saveTool() {\n\t\t\tif (!this.toolForm.name || !this.toolForm.mcpEndpointUrl) {\n\t\t\t\tshowError('Tool name and endpoint are required')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst authProvided = this.toolForm.authentication && this.toolForm.authentication.trim() !== ''\n\t\t\tlet authPayload = null\n\t\t\tif (authProvided) {\n\t\t\t\ttry {\n\t\t\t\t\tauthPayload = JSON.parse(this.toolForm.authentication)\n\t\t\t\t} catch (error) {\n\t\t\t\t\tshowError('Authentication must be valid JSON')\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst payload = {\n\t\t\t\tname: this.toolForm.name,\n\t\t\t\tmcpEndpointUrl: this.toolForm.mcpEndpointUrl,\n\t\t\t\tdescription: this.toolForm.description || null,\n\t\t\t\tenabled: this.toolForm.enabled,\n\t\t\t}\n\t\t\tif (authProvided) {\n\t\t\t\tpayload.authentication = authPayload\n\t\t\t} else if (!this.toolForm.id) {\n\t\t\t\tpayload.authentication = null\n\t\t\t}\n\t\t\tthis.toolSaving = true\n\t\t\ttry {\n\t\t\t\tif (this.toolForm.id) {\n\t\t\t\t\tawait axios.put(generateUrl(`/apps/educai/api/v1/admin/tools/${this.toolForm.id}`), payload)\n\t\t\t\t} else {\n\t\t\t\t\tawait axios.post(generateUrl('/apps/educai/api/v1/admin/tools'), payload)\n\t\t\t\t}\n\t\t\t\tshowSuccess('Tool saved')\n\t\t\t\tawait this.loadTools()\n\t\t\t\tthis.closeToolForm()\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to save tool', error)\n\t\t\t\tshowError('Failed to save tool')\n\t\t\t} finally {\n\t\t\t\tthis.toolSaving = false\n\t\t\t}\n\t\t},\n\t\tasync deleteTool(tool) {\n\t\t\tthis.toolSaving = true\n\t\t\ttry {\n\t\t\t\tawait axios.delete(generateUrl(`/apps/educai/api/v1/admin/tools/${tool.id}`))\n\t\t\t\tshowSuccess('Tool deleted')\n\t\t\t\tawait this.loadTools()\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to delete tool', error)\n\t\t\t\tshowError('Failed to delete tool')\n\t\t\t} finally {\n\t\t\t\tthis.toolSaving = false\n\t\t\t}\n\t\t},\n\t\tasync toggleToolEnabled(tool) {\n\t\t\tthis.toolSaving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(`/apps/educai/api/v1/admin/tools/${tool.id}`), {\n\t\t\t\t\tenabled: !tool.enabled,\n\t\t\t\t})\n\t\t\t\tawait this.loadTools()\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to toggle tool', error)\n\t\t\t\tshowError('Failed to update tool state')\n\t\t\t} finally {\n\t\t\t\tthis.toolSaving = false\n\t\t\t}\n\t\t},\n\t\tasync testToolConnection() {\n\t\t\tif (!this.toolForm.mcpEndpointUrl) {\n\t\t\t\tshowError('Endpoint URL required to test')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst authProvided = this.toolForm.authentication && this.toolForm.authentication.trim() !== ''\n\t\t\tlet authPayload = null\n\t\t\tif (authProvided) {\n\t\t\t\ttry {\n\t\t\t\t\tauthPayload = JSON.parse(this.toolForm.authentication)\n\t\t\t\t} catch (error) {\n\t\t\t\t\tshowError('Authentication must be valid JSON')\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.toolTestResult = ''\n\t\t\tthis.toolTesting = true\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.post(generateUrl('/apps/educai/api/v1/admin/tools/test'), {\n\t\t\t\t\tmcpEndpointUrl: this.toolForm.mcpEndpointUrl,\n\t\t\t\t\tauthentication: authPayload,\n\t\t\t\t})\n\t\t\t\tthis.toolTestResult = JSON.stringify(resp.data?.tools ?? [], null, 2)\n\t\t\t\tshowSuccess('Tool connection succeeded')\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Tool test failed', error)\n\t\t\t\tshowError('Failed to test tool connection')\n\t\t\t} finally {\n\t\t\t\tthis.toolTesting = false\n\t\t\t}\n\t\t},\n\t\thasEmbeddingConfigChange(payload) {\n\t\t\tconst nextProvider = (payload.apiProvider || 'custom').trim()\n\t\t\tconst nextApiEndpoint = (payload.apiEndpoint || '').trim()\n\t\t\tconst nextEmbeddingEndpoint = (payload.embeddingApiEndpoint || '').trim()\n\t\t\tconst nextEmbeddingModel = (payload.embeddingModel || '').trim()\n\t\t\tconst nextEffectiveEndpoint = this.resolveEffectiveEmbeddingEndpoint(nextApiEndpoint, nextEmbeddingEndpoint)\n\n\t\t\tconst baseProvider = (this.embeddingConfigBaseline.apiProvider || 'custom').trim()\n\t\t\tconst baseApiEndpoint = (this.embeddingConfigBaseline.apiEndpoint || '').trim()\n\t\t\tconst baseEmbeddingEndpoint = (this.embeddingConfigBaseline.embeddingApiEndpoint || '').trim()\n\t\t\tconst baseEmbeddingModel = (this.embeddingConfigBaseline.embeddingModel || '').trim()\n\t\t\tconst baseEffectiveEndpoint = this.resolveEffectiveEmbeddingEndpoint(baseApiEndpoint, baseEmbeddingEndpoint)\n\n\t\t\tconst embeddingKeyUpdated = !!(payload.embeddingApiKey && payload.embeddingApiKey.trim() !== '')\n\t\t\tconst mainKeyUpdated = !!(payload.apiKey && payload.apiKey.trim() !== '')\n\t\t\tconst usesMainKeyForEmbeddings = !this.embeddingConfigBaseline.hasEmbeddingApiKey && !embeddingKeyUpdated\n\n\t\t\treturn (\n\t\t\t\tnextProvider !== baseProvider\n\t\t\t\t|| nextEmbeddingModel !== baseEmbeddingModel\n\t\t\t\t|| nextEffectiveEndpoint !== baseEffectiveEndpoint\n\t\t\t\t|| embeddingKeyUpdated\n\t\t\t\t|| (usesMainKeyForEmbeddings && mainKeyUpdated)\n\t\t\t)\n\t\t},\n\t\tresolveEffectiveEmbeddingEndpoint(apiEndpoint, embeddingApiEndpoint) {\n\t\t\tconst embeddingEndpoint = (embeddingApiEndpoint || '').trim()\n\t\t\tif (embeddingEndpoint !== '') {\n\t\t\t\treturn embeddingEndpoint\n\t\t\t}\n\t\t\treturn (apiEndpoint || '').trim()\n\t\t},\n\t\tasync reindexAllEmbeddings() {\n\t\t\tthis.globalEmbeddingReindexing = true\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.post(generateUrl('/apps/educai/api/v1/admin/embeddings/reindex-all'))\n\t\t\t\tif (resp.data?.success) {\n\t\t\t\t\tconst catalogueJobs = resp.data?.queued_catalogue_jobs ?? 0\n\t\t\t\t\tconst ragSources = resp.data?.queued_rag_sources ?? 0\n\t\t\t\t\tshowSuccess(`Queued reindex jobs: catalogue=${catalogueJobs}, bot sources=${ragSources}`)\n\t\t\t\t\tthis.loadCatalogueStatus()\n\t\t\t\t} else {\n\t\t\t\t\tshowError(resp.data?.error || 'Failed to queue global embedding reindex')\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to queue global embedding reindex', error)\n\t\t\t\tshowError(error.response?.data?.error || 'Failed to queue global embedding reindex')\n\t\t\t} finally {\n\t\t\t\tthis.globalEmbeddingReindexing = false\n\t\t\t}\n\t\t},\n\t\tformatTimestamp(timestamp) {\n\t\t\tif (!timestamp) return 'Never'\n\t\t\tconst date = new Date(timestamp * 1000)\n\t\t\tconst now = new Date()\n\t\t\tconst diffMs = now - date\n\t\t\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60))\n\t\t\tif (diffHours < 1) {\n\t\t\t\tconst diffMins = Math.floor(diffMs / (1000 * 60))\n\t\t\t\treturn `${diffMins} min ago`\n\t\t\t}\n\t\t\tif (diffHours < 24) {\n\t\t\t\treturn `${diffHours}h ago`\n\t\t\t}\n\t\t\tconst diffDays = Math.floor(diffHours / 24)\n\t\t\treturn `${diffDays}d ago`\n\t\t},\n\t\tasync testDoclingConnection() {\n\t\t\tthis.doclingTesting = true\n\t\t\tthis.doclingTestResult = null\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.post(generateUrl('/apps/educai/api/v1/admin/docling/test'), {\n\t\t\t\t\tdoclingApiEndpoint: this.settings.doclingApiEndpoint || null,\n\t\t\t\t\tdoclingApiKey: this.settings.doclingApiKey\n\t\t\t\t\t\t|| (!this.doclingHasStoredApiKey ? this.settings.apiKey : null)\n\t\t\t\t\t\t|| null,\n\t\t\t\t})\n\t\t\t\tthis.doclingTestResult = {\n\t\t\t\t\tsuccess: resp.data?.success ?? false,\n\t\t\t\t\terror: resp.data?.error ?? null,\n\t\t\t\t}\n\t\t\t\tif (this.doclingTestResult.success) {\n\t\t\t\t\tshowSuccess('Docling connection successful')\n\t\t\t\t} else {\n\t\t\t\t\tshowError(this.doclingTestResult.error || 'Connection test failed')\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Docling test failed', error)\n\t\t\t\tthis.doclingTestResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error.response?.data?.error || error.message || 'Connection failed',\n\t\t\t\t}\n\t\t\t\tshowError('Failed to test Docling connection')\n\t\t\t} finally {\n\t\t\t\tthis.doclingTesting = false\n\t\t\t}\n\t\t},\n\t\tasync testVisionConnection() {\n\t\t\tthis.visionTesting = true\n\t\t\tthis.visionTestResult = null\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.post(generateUrl('/apps/educai/api/v1/admin/vision/test'), {\n\t\t\t\t\tvisionApiEndpoint: this.settings.visionApiEndpoint || null,\n\t\t\t\t\tvisionApiKey: this.settings.visionApiKey || null,\n\t\t\t\t\tvisionModel: (this.settings.visionModel || '').trim() || null,\n\t\t\t\t})\n\t\t\t\tthis.visionTestResult = {\n\t\t\t\t\tsuccess: resp.data?.success ?? false,\n\t\t\t\t\terror: resp.data?.error ?? null,\n\t\t\t\t}\n\t\t\t\tif (this.visionTestResult.success) {\n\t\t\t\t\tshowSuccess('Vision connection successful')\n\t\t\t\t} else {\n\t\t\t\t\tshowError(this.visionTestResult.error || 'Vision connection test failed')\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Vision test failed', error)\n\t\t\t\tthis.visionTestResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error.response?.data?.error || error.message || 'Connection failed',\n\t\t\t\t}\n\t\t\t\tshowError('Failed to test vision connection')\n\t\t\t} finally {\n\t\t\t\tthis.visionTesting = false\n\t\t\t}\n\t\t},\n\t\tasync testSpeechConnection() {\n\t\t\tthis.speechTesting = true\n\t\t\tthis.speechTestResult = null\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.post(generateUrl('/apps/educai/api/v1/admin/speech/test'), {\n\t\t\t\t\tspeechApiEndpoint: this.settings.speechApiEndpoint || null,\n\t\t\t\t\tspeechApiKey: this.settings.speechApiKey || null,\n\t\t\t\t\tspeechModel: (this.settings.speechModel || '').trim() || null,\n\t\t\t\t})\n\t\t\t\tthis.speechTestResult = {\n\t\t\t\t\tsuccess: resp.data?.success ?? false,\n\t\t\t\t\terror: resp.data?.error ?? null,\n\t\t\t\t}\n\t\t\t\tif (this.speechTestResult.success) {\n\t\t\t\t\tshowSuccess('Speech connection successful')\n\t\t\t\t} else {\n\t\t\t\t\tshowError(this.speechTestResult.error || 'Speech connection test failed')\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Speech test failed', error)\n\t\t\t\tthis.speechTestResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error.response?.data?.error || error.message || 'Connection failed',\n\t\t\t\t}\n\t\t\t\tshowError('Failed to test speech connection')\n\t\t\t} finally {\n\t\t\t\tthis.speechTesting = false\n\t\t\t}\n\t\t},\n\t\tasync loadRateLimitStatus() {\n\t\t\tthis.rateLimitLoading = true\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.get(generateUrl('/apps/educai/api/v1/admin/ratelimit/status'))\n\t\t\t\tthis.rateLimitStatus = resp.data ?? null\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to load rate limit status', error)\n\t\t\t\tthis.rateLimitStatus = null\n\t\t\t} finally {\n\t\t\t\tthis.rateLimitLoading = false\n\t\t\t}\n\t\t},\n\t\tasync processQueueNow() {\n\t\t\tthis.queueProcessing = true\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.post(generateUrl('/apps/educai/api/v1/admin/ratelimit/process'))\n\t\t\t\tif (resp.data?.success) {\n\t\t\t\t\tconst processed = resp.data.processed || 0\n\t\t\t\t\tconst remaining = resp.data.remaining || 0\n\t\t\t\t\tshowSuccess(`Processed ${processed} request(s). ${remaining} remaining.`)\n\t\t\t\t\t// Refresh status\n\t\t\t\t\tthis.loadRateLimitStatus()\n\t\t\t\t} else {\n\t\t\t\t\tshowError(resp.data?.error || 'Failed to process queue')\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to process queue', error)\n\t\t\t\tshowError(error.response?.data?.error || 'Failed to process queue')\n\t\t\t} finally {\n\t\t\t\tthis.queueProcessing = false\n\t\t\t}\n\t\t},\n\t\tasync loadAllBots() {\n\t\t\tthis.loadingAllBots = true\n\t\t\ttry {\n\t\t\t\tconst resp = await axios.get(generateUrl('/apps/educai/api/v1/admin/bots'))\n\t\t\t\tthis.allBots = Array.isArray(resp.data) ? resp.data : []\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to load all bots', error)\n\t\t\t\tshowError('Failed to load bots')\n\t\t\t} finally {\n\t\t\t\tthis.loadingAllBots = false\n\t\t\t}\n\t\t},\n\t\teditBotAdmin(bot) {\n\t\t\tthis.adminEditingBot = bot\n\t\t},\n\t\tcloseAdminEditor() {\n\t\t\tthis.adminEditingBot = null\n\t\t},\n\t\tasync saveBotAdmin(botData) {\n\t\t\tif (!botData?.id) {\n\t\t\t\tshowError('Bot id is required')\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(`/apps/educai/api/v1/admin/bots/${botData.id}`), botData)\n\t\t\t\tshowSuccess('Bot updated')\n\t\t\t\tthis.adminEditingBot = null\n\t\t\t\tthis.loadAllBots()\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to update bot as admin', error)\n\t\t\t\tshowError(error.response?.data?.error || 'Failed to update bot')\n\t\t\t}\n\t\t},\n\t\tasync deleteBotAdmin(bot) {\n\t\t\tif (!bot || !bot.id) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!confirm(`Delete bot \"${bot.bot_name}\"? This cannot be undone.`)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait axios.delete(generateUrl(`/apps/educai/api/v1/bots/${bot.id}`))\n\t\t\t\tshowSuccess('Bot deleted')\n\t\t\t\tthis.loadAllBots()\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to delete bot as admin', error)\n\t\t\t\tshowError(error.response?.data?.error || 'Failed to delete bot')\n\t\t\t}\n\t\t},\n\t\tformatVisibility(visibility) {\n\t\t\tif (visibility === 'global') {\n\t\t\t\treturn 'Global'\n\t\t\t}\n\t\t\tif (visibility === 'personal') {\n\t\t\t\treturn 'Personal'\n\t\t\t}\n\t\t\tif (visibility === 'teams') {\n\t\t\t\treturn 'Teams'\n\t\t\t}\n\t\t\treturn 'Groups'\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.admin-settings {\n\tmargin-top: 40px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 8px;\n\toverflow: hidden;\n}\n\n.settings-title {\n\tmargin: 0;\n\tpadding: 16px 20px;\n\tbackground: var(--color-background-dark);\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.settings-body {\n\tpadding: 20px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.accordion-section {\n\tborder-top: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.settings-title + .settings-body .accordion-section:first-child,\n.admin-settings > .accordion-section:first-of-type {\n\tborder-top: 0;\n}\n\n.accordion-header {\n\twidth: 100%;\n\tpadding: 16px 20px;\n\tborder: 0;\n\tbackground: transparent;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1fr) auto auto;\n\talign-items: center;\n\tgap: 16px;\n\ttext-align: left;\n\tfont: inherit;\n}\n\n.accordion-header:hover,\n.accordion-header:focus-visible {\n\tbackground: var(--color-background-hover);\n}\n\n.accordion-header:focus-visible {\n\toutline: 2px solid var(--color-primary);\n\toutline-offset: -2px;\n}\n\n.accordion-heading {\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 3px;\n}\n\n.accordion-title {\n\tfont-size: 15px;\n\tfont-weight: 700;\n}\n\n.accordion-description {\n\tfont-size: 13px;\n\tcolor: var(--color-text-lighter);\n\tline-height: 1.35;\n}\n\n.accordion-meta {\n\tmax-width: 220px;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tborder-radius: 999px;\n\tpadding: 3px 9px;\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-text-lighter);\n\tfont-size: 12px;\n\tfont-weight: 600;\n}\n\n.accordion-meta--warning {\n\tbackground: rgba(236, 167, 0, 0.15);\n\tcolor: var(--color-main-text);\n}\n\n.accordion-chevron {\n\tfont-size: 22px;\n\tline-height: 1;\n\tcolor: var(--color-text-lighter);\n\ttransform: rotate(0deg);\n\ttransition: transform 120ms ease;\n}\n\n.accordion-section--open .accordion-chevron {\n\ttransform: rotate(90deg);\n}\n\n.accordion-panel {\n\tpadding: 4px 20px 20px;\n}\n\n.settings-body .accordion-header,\n.settings-body .accordion-panel {\n\tpadding-left: 0;\n\tpadding-right: 0;\n}\n\n.settings-body .accordion-section {\n\tborder-radius: 6px;\n\tborder: 1px solid var(--color-border);\n}\n\n.settings-body > .button.primary {\n\talign-self: flex-start;\n\tmargin-top: 8px;\n}\n\n.form-group {\n\tmargin-bottom: 20px;\n}\n\n.form-group label {\n\tdisplay: block;\n\tmargin-bottom: 6px;\n\tfont-weight: 600;\n}\n\n.form-group input,\n.form-group select {\n\twidth: 100%;\n\tpadding: 10px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 3px;\n\tfont-family: inherit;\n\tfont-size: 14px;\n}\n\n.form-group input:focus,\n.form-group select:focus {\n\toutline: none;\n\tborder-color: var(--color-primary);\n}\n\n.hint {\n\tmargin: 6px 0 0 0;\n\tfont-size: 13px;\n\tcolor: var(--color-text-lighter);\n}\n\n.button.primary {\n\tbackground-color: var(--color-primary);\n\tcolor: white;\n\tborder: none;\n\tpadding: 10px 20px;\n\tborder-radius: 3px;\n\tcursor: pointer;\n\tfont-size: 14px;\n}\n\n.button.primary:hover:not(:disabled) {\n\tbackground-color: var(--color-primary-element-light);\n}\n\n.button.primary:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n\n.app-icon-config {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.icon-mode-tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n.icon-mode-tab {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 6px;\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 8px 12px;\n\tfont: inherit;\n\tfont-weight: 600;\n\tcursor: pointer;\n}\n\n.icon-mode-tab:hover,\n.icon-mode-tab:focus-visible {\n\tborder-color: var(--color-primary);\n}\n\n.icon-mode-tab--active {\n\tbackground: var(--color-primary);\n\tborder-color: var(--color-primary);\n\tcolor: #fff;\n}\n\n.app-icon-preview-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n\tgap: 12px;\n}\n\n.app-icon-preview-card {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 6px;\n\tpadding: 12px;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n}\n\n.app-icon-preview-card--light {\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n}\n\n.app-icon-preview-card--dark {\n\tbackground: #111;\n\tcolor: #fff;\n}\n\n.app-icon-preview-card__label {\n\tfont-size: 13px;\n\tfont-weight: 700;\n}\n\n.app-icon-preview-card__frame {\n\twidth: 52px;\n\theight: 52px;\n\tborder-radius: 6px;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground: rgba(127, 127, 127, 0.12);\n\tflex: 0 0 auto;\n}\n\n.app-icon-preview-card__frame img {\n\tdisplay: block;\n\tmax-width: 34px;\n\tmax-height: 34px;\n\tobject-fit: contain;\n}\n\n.app-icon-preview-card__missing {\n\tfont-size: 11px;\n\tfont-weight: 700;\n\topacity: 0.7;\n}\n\n.app-icon-mode-panel {\n\tborder-top: 1px solid var(--color-border);\n\tpadding-top: 16px;\n}\n\n.app-icon-input-row {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1fr) auto;\n\tgap: 8px;\n\talign-items: center;\n}\n\n.app-icon-file-input {\n\tdisplay: none;\n}\n\n.app-icon-actions {\n\tmargin-bottom: 0;\n}\n\n/* Align checkbox with label text and standardize size */\n.form-group label.checkbox {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n}\n.form-group label.checkbox input[type=\"checkbox\"] {\n\twidth: 16px;\n\theight: 16px;\n\tmargin: 0;\n\tvertical-align: middle;\n}\n\n/* Make multi-select taller so more models are visible */\n.form-group select[multiple] {\n\tmin-height: 240px;\n}\n\n.section-divider {\n\tborder-top: 1px solid var(--color-border);\n\tmargin: 28px 0 20px;\n}\n\n.section-heading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tflex-wrap: wrap;\n\tgap: 12px;\n\tmargin: 0 0 16px;\n}\n\n.section-heading h4 {\n\tmargin: 0;\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n.section-heading .hint {\n\tflex-basis: 100%;\n\tmargin: 0;\n}\n\n.form-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n\tgap: 20px;\n}\n\n.section-body {\n\tpadding: 0 0 20px;\n}\n\n.tool-list {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.tool-row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: flex-start;\n\tgap: 24px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 6px;\n\tpadding: 16px;\n\tbackground: var(--color-background-dark);\n}\n\n.tool-row--disabled {\n\topacity: 0.7;\n}\n\n.tool-info {\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.tool-name {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmargin-bottom: 6px;\n}\n\n.tool-name__label {\n\tfont-weight: 600;\n\tfont-size: 15px;\n}\n\n.badge {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tpadding: 2px 8px;\n\tborder-radius: 999px;\n\tfont-size: 11px;\n\tfont-weight: 600;\n}\n\n.badge-success {\n\tbackground: rgba(0, 173, 127, 0.12);\n\tcolor: #009966;\n}\n\n.badge-muted {\n\tbackground: rgba(120, 120, 120, 0.12);\n\tcolor: #555;\n}\n\n.tool-actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tmin-width: 160px;\n\talign-items: flex-end;\n}\n\n.button.danger {\n\tbackground: var(--color-error);\n\tcolor: #fff;\n}\n\n.button-row {\n\tdisplay: flex;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.tool-form {\n\tborder-top: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n\tpadding: 20px;\n}\n\n.tool-form form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.tool-form textarea {\n\tfont-family: monospace;\n}\n\n.tool-test-result {\n\tmargin: 16px 20px 20px;\n\tpadding: 12px;\n\tborder-radius: 6px;\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-text);\n\tmax-height: 240px;\n\toverflow: auto;\n\tfont-size: 12px;\n}\n\ncode {\n\tfont-family: monospace;\n\tfont-size: 12px;\n\tbackground: rgba(0, 0, 0, 0.05);\n\tpadding: 1px 4px;\n\tborder-radius: 4px;\n}\n\n.success-text {\n\tcolor: var(--color-success, #46ba61);\n\tfont-size: 13px;\n}\n\n.error-text {\n\tcolor: var(--color-error, #e53e3e);\n\tfont-size: 13px;\n}\n\n.button-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tflex-wrap: wrap;\n\tmargin-bottom: 20px;\n}\n\n.supported-formats {\n\tmargin-top: 8px;\n\tmargin-bottom: 16px;\n}\n\n.supported-formats .hint strong {\n\tcolor: var(--color-text);\n}\n\n.embedding-warning {\n\tmargin: -4px 0 20px;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-warning, #eca700);\n\tborder-radius: 6px;\n\tbackground: rgba(236, 167, 0, 0.12);\n}\n\n.embedding-warning p {\n\tmargin: 6px 0 0;\n\tfont-size: 13px;\n\tcolor: var(--color-main-text);\n}\n\n.rate-limit-status {\n\tmargin: 20px 0;\n}\n\n.rate-limit-hint {\n\tmargin-top: 16px;\n}\n\n.rate-limit-subsection {\n\tmargin-top: 20px;\n}\n\n.rate-limit-subsection__header h5 {\n\tmargin: 0 0 6px;\n\tfont-size: 14px;\n}\n\n.rate-limit-subsection__header .hint {\n\tmargin: 0;\n}\n\n.status-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\n\tgap: 16px;\n\tmargin-top: 16px;\n}\n\n.status-grid--compact {\n\tmargin-top: 12px;\n}\n\n.status-card {\n\tpadding: 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 8px;\n\ttext-align: center;\n\tbackground: var(--color-background-dark);\n}\n\n.status-card.status-ok {\n\tborder-color: var(--color-success, #46ba61);\n\tbackground: rgba(70, 186, 97, 0.1);\n}\n\n.status-card.status-limited {\n\tborder-color: var(--color-warning, #eca700);\n\tbackground: rgba(236, 167, 0, 0.1);\n}\n\n.status-value {\n\tfont-size: 24px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n\tmargin-bottom: 4px;\n}\n\n.status-label {\n\tfont-size: 12px;\n\tcolor: var(--color-text-lighter);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.5px;\n}\n\n.bot-table {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.bot-row {\n\tdisplay: grid;\n\tgrid-template-columns: 1.2fr 1fr 1fr 1fr 1fr 1fr;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: 6px;\n\tbackground: var(--color-main-background);\n}\n\n.bot-row--header {\n\tfont-weight: 700;\n\tbackground: var(--color-background-dark);\n}\n\n.actions-col {\n\tdisplay: flex;\n\tgap: 8px;\n\tjustify-content: flex-end;\n}\n\n.temperature-presets {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n\tmargin-top: 10px;\n}\n\n.status-pill {\n\tpadding: 2px 8px;\n\tborder-radius: 10px;\n\tfont-size: 12px;\n\ttext-transform: capitalize;\n\tdisplay: inline-block;\n}\n\n.status-pill.status-draft {\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-text-lighter);\n}\n\n.status-pill.status-pending {\n\tbackground: var(--color-warning);\n\tcolor: #000;\n}\n\n.status-pill.status-approved {\n\tbackground: var(--color-success);\n\tcolor: #000;\n}\n\n.status-pill.status-personal {\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-dark);\n}\n\n@media (max-width: 700px) {\n\t.accordion-header {\n\t\tgrid-template-columns: minmax(0, 1fr) auto;\n\t\tgap: 10px;\n\t}\n\n\t.accordion-meta {\n\t\tgrid-column: 1 / -1;\n\t\tmax-width: 100%;\n\t\twidth: fit-content;\n\t}\n\n\t.tool-row,\n\t.bot-row {\n\t\tgrid-template-columns: 1fr;\n\t}\n\n\t.tool-row {\n\t\tflex-direction: column;\n\t}\n\n\t.app-icon-input-row {\n\t\tgrid-template-columns: 1fr;\n\t}\n\n\t.tool-actions,\n\t.actions-col {\n\t\twidth: 100%;\n\t\talign-items: stretch;\n\t\tjustify-content: flex-start;\n\t}\n}\n</style>\n","/* eslint-disable no-multiple-empty-lines */\nimport './utils/ensureOcFilePath.js'\nimport '@nextcloud/dialogs/style.css'\nimport Vue from 'vue'\nimport AdminSettings from './components/AdminSettings.vue'\nimport { syncEducAiAppIconFromSettings } from './utils/appIconRuntime.js'\n\nconst VueApp = Vue.extend(AdminSettings)\nnew VueApp().$mount('#educai-admin-root')\nsyncEducAiAppIconFromSettings()\n"],"names":["_sfc_main","BotForm","APP_DISPLAY_NAME","payload","map","option","enabled","key","value","numeric","url","parsed","raw","variant","refName","input","event","file","showError","formData","axios","showSuccess","error","generateUrl","mode","imagePath","version","blackUrl","whiteUrl","temperature","model","id","rawOptions","rawModels","current","values","options","existing","endpoint","fallback","data","applyEducAiRuntimeIconPayload","resp","n","e","defaultTemperature","appIconPayload","embeddingConfigChanged","response","apiKeyUpdated","secondaryApiKeyUpdated","embeddingKeyUpdated","doclingKeyUpdated","tool","authProvided","authPayload","nextProvider","nextApiEndpoint","nextEmbeddingEndpoint","nextEmbeddingModel","nextEffectiveEndpoint","baseProvider","baseApiEndpoint","baseEmbeddingEndpoint","baseEmbeddingModel","baseEffectiveEndpoint","mainKeyUpdated","usesMainKeyForEmbeddings","apiEndpoint","embeddingApiEndpoint","embeddingEndpoint","catalogueJobs","ragSources","timestamp","date","diffMs","diffHours","processed","remaining","bot","botData","visibility","VueApp","Vue","AdminSettings","syncEducAiAppIconFromSettings"],"mappings":"uMAs1CA,MAAAA,EAAA,CACA,KAAA,gBACA,WAAA,CACA,QAAAC,CACA,EACA,MAAA,CACA,MAAA,CACA,iBAAAC,EACA,OAAA,GACA,SAAA,CACA,OAAA,GACA,YAAA,SACA,YAAA,GACA,qBAAA,GACA,gBAAA,GACA,aAAA,yBACA,mBAAA,GACA,cAAA,GACA,WAAA,GACA,YAAA,UACA,gBAAA,GACA,gBAAA,GACA,eAAA,GACA,iBAAA,IACA,iBAAA,GACA,cAAA,GACA,oBAAA,GACA,cAAA,CAAA,EACA,qBAAA,GACA,gBAAA,GACA,eAAA,GACA,uBAAA,UACA,yBAAA,KACA,yBAAA,IACA,uBAAA,IACA,sBAAA,IACA,aAAA,IACA,gBAAA,GACA,WAAA,GACA,iBAAA,GACA,qBAAA,GACA,sBAAA,GACA,eAAA,GACA,mBAAA,GACA,cAAA,GACA,kBAAA,GACA,aAAA,GACA,YAAA,GACA,kBAAA,GACA,aAAA,GACA,YAAA,GACA,iBAAA,GACA,gBAAA,KACA,gBAAA,GACA,cAAA,IACA,aAAA,IACA,sBAAA,GACA,0BAAA,GACA,EACA,aAAA,CACA,WAAA,GACA,WAAA,GACA,eAAA,GACA,iBAAA,GACA,IAAA,GACA,QAAA,GACA,MAAA,GACA,OAAA,GACA,OAAA,GACA,MAAA,GACA,KAAA,EACA,EACA,gBAAA,CAAA,EACA,sBAAA,CAAA,EACA,cAAA,GACA,eAAA,GACA,gBAAA,GACA,aAAA,GACA,WAAA,GACA,YAAA,GACA,gBAAA,GACA,MAAA,CAAA,EACA,SAAA,CACA,GAAA,KACA,KAAA,GACA,eAAA,GACA,YAAA,GACA,eAAA,GACA,QAAA,EACA,EACA,kBAAA,GACA,eAAA,GACA,eAAA,GACA,kBAAA,KACA,uBAAA,GACA,cAAA,GACA,iBAAA,KACA,cAAA,GACA,iBAAA,KACA,iBAAA,GACA,gBAAA,KACA,gBAAA,GACA,0BAAA,GACA,wBAAA,CACA,YAAA,SACA,YAAA,GACA,qBAAA,GACA,eAAA,GACA,UAAA,GACA,mBAAA,EACA,EACA,eAAA,GACA,QAAA,CAAA,EACA,gBAAA,K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"}