mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2025-04-08 03:14:17 +00:00
upgraded and fixed bugs
This commit is contained in:
parent
90dd5bf9ab
commit
9ccfbdc757
26
web/dist/assets/index-B8c5eDLy.css
vendored
Normal file
26
web/dist/assets/index-B8c5eDLy.css
vendored
Normal file
File diff suppressed because one or more lines are too long
26
web/dist/assets/index-Dbs1V7LG.css
vendored
26
web/dist/assets/index-Dbs1V7LG.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
web/dist/index.html
vendored
4
web/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LoLLMS WebUI</title>
|
||||
<script type="module" crossorigin src="/assets/index-Clbu1NEV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dbs1V7LG.css">
|
||||
<script type="module" crossorigin src="/assets/index-pZeqJdLq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B8c5eDLy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@ -757,12 +757,11 @@ export default {
|
||||
this.$emit('continueMessage', this.message.id, this.message.content)
|
||||
},
|
||||
getImgUrl() {
|
||||
if (this.avatar && this.avatar.startsWith('http')) {
|
||||
console.log("this.avatar")
|
||||
console.log(this.avatar)
|
||||
if (this.avatar) {
|
||||
return this.avatar; // If avatar is a full URL
|
||||
} else if (this.avatar) {
|
||||
// Assuming avatar is a relative path on the server
|
||||
return (this.host || '') + '/' + this.avatar.split('/').pop(); // Use host + filename
|
||||
}
|
||||
}
|
||||
//console.log("No valid avatar found, using placeholder.")
|
||||
return botImgPlaceholder;
|
||||
},
|
||||
|
@ -2,51 +2,51 @@
|
||||
<div class="user-settings-panel space-y-6 p-4 md:p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-blue-300 dark:border-blue-600 pb-3 mb-4">
|
||||
<h2 class="text-xl font-semibold text-blue-800 dark:text-blue-200 mb-2 sm:mb-0"> <!-- Changed h2 styling, kept font size -->
|
||||
<h2 class="text-xl font-semibold text-blue-800 dark:text-blue-200 mb-2 sm:mb-0">
|
||||
Binding Zoo
|
||||
</h2>
|
||||
<!-- Current Binding Display -->
|
||||
<!-- Use props.config directly -->
|
||||
<div v-if="currentBindingInfo" class="flex items-center gap-2 text-sm font-medium p-2 bg-blue-200 dark:bg-blue-800 rounded-md border border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-200">
|
||||
<div v-if="currentBindingInfo" class="flex items-center gap-2 text-sm font-medium p-2 bg-blue-100 dark:bg-blue-900/50 rounded-md border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-200">
|
||||
<img :src="getIconPath(currentBindingInfo.icon)" @error="imgPlaceholder" class="w-6 h-6 rounded-full object-cover flex-shrink-0" alt="Current Binding Icon">
|
||||
<span>Active: <span class="font-semibold">{{ currentBindingInfo.name }}</span></span>
|
||||
|
||||
<button @click="handleSettings($store.state.config.binding_name)" :disabled="isLoadingAction || loading" class="svg-button ml-2 disabled:opacity-50 disabled:cursor-not-allowed" title="Configure Active Binding">
|
||||
<button @click="handleSettings(effectiveConfig.binding_name)" :disabled="isLoadingAction || loading || hasPendingChanges" class="svg-button ml-2 disabled:opacity-50 disabled:cursor-not-allowed" title="Configure Active Binding">
|
||||
<i data-feather="settings" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button @click="handleReload($store.state.config.binding_name)" :disabled="isLoadingAction || loading" class="svg-button ml-1 disabled:opacity-50 disabled:cursor-not-allowed" title="Reload Active Binding">
|
||||
<button @click="handleReload(effectiveConfig.binding_name)" :disabled="isLoadingAction || loading || hasPendingChanges" class="svg-button ml-1 disabled:opacity-50 disabled:cursor-not-allowed" title="Reload Active Binding">
|
||||
<i data-feather="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="text-sm font-medium text-red-600 dark:text-red-400 p-2 bg-red-100 dark:bg-red-900/30 rounded-md border border-red-300 dark:border-red-600"> <!-- Adjusted red colors slightly -->
|
||||
<div v-else class="text-sm font-medium text-red-600 dark:text-red-400 p-2 bg-red-100 dark:bg-red-900/30 rounded-md border border-red-300 dark:border-red-600">
|
||||
No binding selected!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-blue-600 dark:text-blue-400"> <!-- Use theme paragraph styling, adjusted size -->
|
||||
<p class="text-sm text-blue-600 dark:text-blue-400">
|
||||
Bindings are the engines that run the AI models. Select an installed binding to enable model selection and generation.
|
||||
</p>
|
||||
<div v-if="hasPendingChanges" class="p-3 bg-yellow-100 dark:bg-yellow-900/30 border border-yellow-300 dark:border-yellow-700 rounded-lg text-center text-sm text-yellow-700 dark:text-yellow-300">
|
||||
<i data-feather="alert-circle" class="inline-block w-4 h-4 mr-1 align-middle"></i>
|
||||
Apply main settings changes to use binding actions (settings, reload).
|
||||
</div>
|
||||
|
||||
<!-- Search and Sort Controls -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 mb-4">
|
||||
<!-- Search Input -->
|
||||
<div class="relative flex-grow">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-feather="search" class="w-5 h-5 text-blue-400 dark:text-blue-500"></i> <!-- Adjusted icon color -->
|
||||
<i data-feather="search" class="w-5 h-5 text-blue-400 dark:text-blue-500"></i>
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
v-model="searchTerm"
|
||||
placeholder="Search bindings by name or author..."
|
||||
class="search-input pl-10 w-full"
|
||||
placeholder="Search bindings..."
|
||||
class="input search-input pl-10 w-full"
|
||||
aria-label="Search bindings"
|
||||
:disabled="isLoadingBindings || loading"
|
||||
/>
|
||||
</div>
|
||||
<!-- Sort Select -->
|
||||
<div class="flex-shrink-0">
|
||||
<label for="binding-sort" class="sr-only">Sort bindings by</label>
|
||||
<select id="binding-sort" v-model="sortOption" class="input w-full sm:w-auto" aria-label="Sort bindings by" :disabled="isLoadingBindings || loading"> <!-- Use input class -->
|
||||
<select id="binding-sort" v-model="sortOption" class="input w-full sm:w-auto" aria-label="Sort bindings by" :disabled="isLoadingBindings || loading">
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="author">Sort by Author</option>
|
||||
<option value="status">Sort by Status</option>
|
||||
@ -56,20 +56,18 @@
|
||||
|
||||
<!-- Bindings Grid -->
|
||||
<div v-if="isLoadingBindings" class="flex justify-center items-center p-10">
|
||||
<!-- Spinner SVG -->
|
||||
<svg aria-hidden="true" role="status" class="w-8 h-8 text-blue-300 animate-spin dark:text-blue-600 fill-blue-600 dark:fill-blue-400" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> <!-- Adjusted spinner colors -->
|
||||
<svg aria-hidden="true" role="status" class="w-8 h-8 text-blue-300 animate-spin dark:text-blue-600 fill-blue-600 dark:fill-blue-400" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
||||
</svg>
|
||||
<span class="ml-2 text-loading">Loading bindings...</span> <!-- Use text-loading class -->
|
||||
<span class="ml-2 text-loading">Loading bindings...</span>
|
||||
</div>
|
||||
<div v-else-if="sortedBindings.length === 0" class="text-center text-blue-500 dark:text-blue-400 py-10"> <!-- Adjusted text color -->
|
||||
<div v-else-if="sortedBindings.length === 0" class="text-center text-blue-500 dark:text-blue-400 py-10">
|
||||
No bindings found{{ searchTerm ? ' matching "' + searchTerm + '"' : '' }}.
|
||||
</div>
|
||||
<!-- Grid rendering -->
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 scrollbar"> <!-- Added scrollbar class for potential overflow -->
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 scrollbar">
|
||||
<BindingEntry
|
||||
v-for="binding_item in bindingsToDisplay"
|
||||
v-for="binding_item in sortedBindings"
|
||||
:key="binding_item.folder"
|
||||
:binding="binding_item"
|
||||
:selected="isBindingSelected(binding_item)"
|
||||
@ -82,67 +80,55 @@
|
||||
@reload-binding="handleReloadFromEntry"
|
||||
/>
|
||||
</div>
|
||||
<!-- Save/Cancel Footer REMOVED - Parent handles this -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nextTick } from 'vue';
|
||||
import feather from 'feather-icons';
|
||||
import BindingEntry from '@/components/BindingEntry.vue'; // Ensure this path is correct
|
||||
import defaultBindingIcon from "@/assets/default_binding.png"; // Ensure this path is correct
|
||||
|
||||
import { mapState } from 'vuex'; // Example using Vuex
|
||||
import BindingEntry from '@/components/BindingEntry.vue';
|
||||
import defaultBindingIcon from "@/assets/default_binding.png";
|
||||
|
||||
export default {
|
||||
name: 'BindingZooSettings', // Changed name to avoid conflict if used elsewhere
|
||||
name: 'BindingZooSettings',
|
||||
components: {
|
||||
BindingEntry
|
||||
},
|
||||
props: {
|
||||
loading: { type: Boolean, default: false }, // Parent loading state
|
||||
// settings-changed prop removed as it's handled by emits now
|
||||
config: { type: Object, required: true }, // The editable config from parent
|
||||
loading: { type: Boolean, default: false },
|
||||
api_get_req: { type: Function, required: true },
|
||||
api_post_req: { type: Function, required: true },
|
||||
refresh_config: { type: Function, required: true },
|
||||
show_toast: { type: Function, required: true },
|
||||
show_yes_no_dialog: { type: Function, required: true },
|
||||
show_message_box: { type: Function, required: true }, // Added based on parent call
|
||||
client_id: { type: String, required: true },
|
||||
show_universal_form: { type: Function, required: true },
|
||||
client_id: { type: String, required: true },
|
||||
},
|
||||
emits: ['settings-changed'], // Declare emitted events
|
||||
emits: ['setting-updated'], // Emits updates for parent
|
||||
|
||||
data() {
|
||||
return {
|
||||
bindings: [], // Holds the list of binding objects
|
||||
isLoadingBindings: false, // Loading state specifically for fetching bindings
|
||||
isLoadingAction: false, // Loading state for actions like install, uninstall, settings, reload
|
||||
sortOption: 'name', // Default sort option
|
||||
searchTerm: '', // Search term input
|
||||
bindings: [], // Local cache of binding list from store/API
|
||||
isLoadingBindings: false,
|
||||
isLoadingAction: false,
|
||||
sortOption: 'name',
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
// Assumes your bindings are in the store like this
|
||||
allBindings: state => state.bindingsZoo || [],
|
||||
// Assumes selected binding name is stored like this
|
||||
selectedBindingName: state => state.config.binding_name,
|
||||
// Assumes you have a way to track processing state per binding
|
||||
processingBindings: state => state.processingBindingFolders || {} // e.g., { 'binding-folder': true }
|
||||
}),
|
||||
// Use sortedBindings for display
|
||||
bindingsToDisplay() {
|
||||
return this.sortedBindings;
|
||||
// Use store state for applied config checks
|
||||
effectiveConfig() {
|
||||
return this.$store.state.config || {};
|
||||
},
|
||||
hasPendingChanges() {
|
||||
return this.$store.state.settingsChanged;
|
||||
},
|
||||
|
||||
// Find the current binding info based on the APPLIED config
|
||||
currentBindingInfo() {
|
||||
// Use prop config
|
||||
if (!this.$store.state.config || !this.$store.state.config.binding_name || this.bindings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const current = this.bindings.find(b => b.folder === this.$store.state.config.binding_name);
|
||||
return current;
|
||||
if (!this.effectiveConfig.binding_name || this.bindings.length === 0) return null;
|
||||
return this.bindings.find(b => b.folder === this.effectiveConfig.binding_name);
|
||||
},
|
||||
|
||||
sortedBindings() {
|
||||
@ -156,28 +142,34 @@ export default {
|
||||
filtered = filtered.filter(b =>
|
||||
b.name?.toLowerCase().includes(lowerSearch) ||
|
||||
b.author?.toLowerCase().includes(lowerSearch) ||
|
||||
b.description?.toLowerCase().includes(lowerSearch) ||
|
||||
b.folder?.toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
const isASelected = a.folder === this.selectedBindingName;
|
||||
const isBSelected = b.folder === this.selectedBindingName;
|
||||
// --- Primary Sorting Rule: Active binding first ---
|
||||
const isASelected = a.folder === this.effectiveConfig.binding_name;
|
||||
const isBSelected = b.folder === this.effectiveConfig.binding_name;
|
||||
|
||||
// Rule 1: Selected binding comes first
|
||||
if (isASelected && !isBSelected) return -1;
|
||||
if (!isASelected && isBSelected) return 1;
|
||||
if (isASelected && !isBSelected) return -1; // a comes first
|
||||
if (!isASelected && isBSelected) return 1; // b comes first
|
||||
// If both are selected or neither are selected, proceed to secondary sort
|
||||
|
||||
// --- Secondary Sorting Rules ---
|
||||
switch (this.sortOption) {
|
||||
case 'status':
|
||||
// Sort by installed status secondarily
|
||||
if (a.installed && !b.installed) return -1;
|
||||
if (!a.installed && b.installed) return 1;
|
||||
// If status is the same, sort by name
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
case 'author':
|
||||
// Sort by author secondarily
|
||||
return (a.author || '').localeCompare(b.author || '');
|
||||
case 'name':
|
||||
default:
|
||||
// Default sort by name secondarily
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
}
|
||||
});
|
||||
@ -186,383 +178,173 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
isBindingSelected(binding) {
|
||||
return binding.folder === this.selectedBindingName;
|
||||
},
|
||||
isBindingProcessing(binding) {
|
||||
return !!this.processingBindings[binding.folder];
|
||||
},
|
||||
// Helper to construct full icon path
|
||||
getIconPath(iconRelativePath) {
|
||||
console.log(`iconRelativePath: ${iconRelativePath}`)
|
||||
if (!iconRelativePath) return defaultBindingIcon;
|
||||
// Ensure bUrl is correctly prepended only if needed (check if iconRelativePath already has http)
|
||||
if (iconRelativePath.startsWith('http')) {
|
||||
return iconRelativePath;
|
||||
}
|
||||
const icon = "/"+iconRelativePath.replace(/\\/g, '/');
|
||||
console.log(`icon: ${icon}`)
|
||||
return icon;
|
||||
},
|
||||
watch: {
|
||||
// Watch store changes for bindings (if managed by store)
|
||||
// This assumes bindings might be updated externally
|
||||
'$store.state.bindingsZoo': {
|
||||
handler(newBindings) {
|
||||
if (newBindings) {
|
||||
this.bindings = (newBindings || []).map(b => ({
|
||||
...b,
|
||||
// Preserve existing processing state if possible
|
||||
isProcessing: this.bindings.find(ob => ob.folder === b.folder)?.isProcessing || false
|
||||
}));
|
||||
this.replaceFeatherIcons();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // Also run on component load if store might already have data
|
||||
}
|
||||
},
|
||||
|
||||
// Method for the image error handler
|
||||
methods: {
|
||||
// Check selection based on the EDITABLE config prop
|
||||
isBindingSelected(binding) {
|
||||
return binding.folder === this.config.binding_name;
|
||||
},
|
||||
getIconPath(iconRelativePath) {
|
||||
if (!iconRelativePath) return defaultBindingIcon;
|
||||
if (iconRelativePath.startsWith('http')) return iconRelativePath;
|
||||
// Assuming icons are served relative to the base URL or root
|
||||
const icon = iconRelativePath.startsWith('/') ? iconRelativePath : `/${iconRelativePath}`;
|
||||
return icon.replace(/\\/g, '/'); // Normalize path separators
|
||||
},
|
||||
imgPlaceholder(event) {
|
||||
event.target.src = defaultBindingIcon;
|
||||
},
|
||||
|
||||
// Use prop function for API GET requests
|
||||
async internal_api_get_req(endpoint, params = {}) {
|
||||
try {
|
||||
// Add client_id if not present, assuming the prop function doesn't do it automatically
|
||||
const fullParams = { client_id: this.client_id, ...params };
|
||||
// Await the promise returned by the prop function
|
||||
return await this.api_get_req(endpoint, fullParams);
|
||||
} catch (error) {
|
||||
console.error(`API GET error for ${endpoint}:`, error);
|
||||
this.show_toast(`API Error: ${error.message || 'Failed to fetch data'}`, 4, false);
|
||||
throw error; // Re-throw to allow caller handling if needed
|
||||
}
|
||||
},
|
||||
|
||||
// Use prop function for API POST requests
|
||||
async internal_api_post_req(endpoint, data = {}) {
|
||||
try {
|
||||
// Add client_id if not present, assuming the prop function doesn't do it automatically
|
||||
const fullData = { client_id: this.client_id, ...data };
|
||||
// Await the promise returned by the prop function
|
||||
return await this.api_post_req(endpoint, fullData);
|
||||
} catch (error) {
|
||||
console.error(`API POST error for ${endpoint}:`, error);
|
||||
this.show_toast(`API Error: ${error.message || 'Action failed'}`, 4, false);
|
||||
throw error; // Re-throw to allow caller handling if needed
|
||||
}
|
||||
},
|
||||
|
||||
async fetchBindings() {
|
||||
this.isLoadingBindings = true;
|
||||
try {
|
||||
// Use internal wrapper which uses the prop function
|
||||
const response = await this.internal_api_get_req('list_bindings');
|
||||
// Assume response is the array directly now
|
||||
const response = await this.api_get_req('list_bindings');
|
||||
// Update local state, potentially overwriting store-watched state if fetch is primary source
|
||||
this.bindings = (response || []).map(b => ({ ...b, isProcessing: false }));
|
||||
} catch (error) {
|
||||
// Error already shown by internal_api_get_req
|
||||
this.bindings = [];
|
||||
} finally {
|
||||
this.isLoadingBindings = false;
|
||||
nextTick(feather.replace);
|
||||
}
|
||||
// Or commit to store if store is the source of truth:
|
||||
// this.$store.commit('setBindingsZoo', (response || []).map(b => ({ ...b, isProcessing: false })));
|
||||
} catch (error) { this.bindings = []; } finally { this.isLoadingBindings = false; this.replaceFeatherIcons(); }
|
||||
},
|
||||
|
||||
setBindingProcessing(folder, state) {
|
||||
const index = this.bindings.findIndex(b => b.folder === folder);
|
||||
if (index !== -1) {
|
||||
// Vue 2 reactivity might need $set, Vue 3 handles this automatically
|
||||
this.bindings[index].isProcessing = state;
|
||||
if (index !== -1) this.bindings[index].isProcessing = state;
|
||||
},
|
||||
handleSelect(binding) {
|
||||
if (!binding?.installed) { this.show_toast(`Binding "${binding.name}" is not installed.`, 3, false); return; }
|
||||
if (this.config.binding_name !== binding.folder) {
|
||||
this.$emit('setting-updated', { key: 'binding_name', value: binding.folder });
|
||||
this.$emit('setting-updated', { key: 'model_name', value: null });
|
||||
this.show_toast(`Selected binding: ${binding.name}. Apply changes.`, 3, true);
|
||||
}
|
||||
},
|
||||
|
||||
async handleSelect(binding) {
|
||||
console.log("received selection of binding")
|
||||
console.log(binding)
|
||||
if (!binding || !binding.folder) {
|
||||
console.error("Invalid binding data received in handleSelect:", binding);
|
||||
this.show_toast("Internal error: Invalid binding data.", 4, false);
|
||||
return;
|
||||
}
|
||||
if (!binding.installed) {
|
||||
this.show_toast(`Binding "${binding.name}" is not installed.`, 3, false);
|
||||
return;
|
||||
}
|
||||
// Use prop config
|
||||
if (this.$store.state.config.binding_name !== binding.folder) {
|
||||
this.$store.state.config.binding_name = binding.folder
|
||||
this.$store.state.config.model_name = null
|
||||
await this.$store.dispatch('refreshModelsZoo');
|
||||
await this.$store.dispatch('refreshModels');
|
||||
|
||||
// Emit events for parent to handle state update
|
||||
this.$emit('settings-changed', true); // Inform parent about the change
|
||||
this.show_toast(`Selected binding: ${binding.name}`, 3, true);
|
||||
}
|
||||
},
|
||||
|
||||
async handleInstall(binding) {
|
||||
if (!binding || !binding.folder) {
|
||||
console.error("Invalid binding data received in handleInstall:", binding);
|
||||
this.show_toast("Internal error: Invalid binding data.", 4, false);
|
||||
return;
|
||||
}
|
||||
let proceed = true;
|
||||
if (binding.disclaimer) {
|
||||
// Use prop function
|
||||
proceed = await this.show_yes_no_dialog(`Disclaimer for ${binding.name}:\n\n${binding.disclaimer}\n\nProceed with installation?`, 'Proceed', 'Cancel');
|
||||
}
|
||||
if (!proceed) return;
|
||||
|
||||
this.setBindingProcessing(binding.folder, true);
|
||||
this.isLoadingAction = true;
|
||||
try {
|
||||
// Use internal wrapper
|
||||
const response = await this.internal_api_post_req('install_binding', { name: binding.folder });
|
||||
if (response && response.status) {
|
||||
// Use prop function
|
||||
this.show_toast(`Binding "${binding.name}" installed successfully! Reload recommended.`, 5, true);
|
||||
await this.fetchBindings(); // Refresh internal list
|
||||
} else {
|
||||
this.show_toast(`Failed to install "${binding.name}": ${response?.error || 'Unknown error'}`, 4, false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error handled by internal_api_post_req
|
||||
} finally {
|
||||
this.setBindingProcessing(binding.folder, false);
|
||||
this.isLoadingAction = false;
|
||||
nextTick(feather.replace);
|
||||
}
|
||||
let proceed = true;
|
||||
if (binding.disclaimer) {
|
||||
proceed = await this.show_yes_no_dialog(`Disclaimer for ${binding.name}:\n${binding.disclaimer}\nProceed?`, 'Proceed', 'Cancel');
|
||||
}
|
||||
if (!proceed) return;
|
||||
this.setBindingProcessing(binding.folder, true); this.isLoadingAction = true;
|
||||
try {
|
||||
const response = await this.api_post_req('install_binding', { name: binding.folder });
|
||||
if (response?.status) {
|
||||
this.show_toast(`"${binding.name}" installed! Reload recommended.`, 5, true);
|
||||
await this.fetchBindings();
|
||||
} else { this.show_toast(`Install failed: ${response?.error || 'Error'}`, 4, false); }
|
||||
} catch (error) { /* Handled */ }
|
||||
finally { this.setBindingProcessing(binding.folder, false); this.isLoadingAction = false; this.replaceFeatherIcons(); }
|
||||
},
|
||||
|
||||
async handleUninstall(binding) {
|
||||
if (!binding || !binding.folder) {
|
||||
console.error("Invalid binding data received in handleUninstall:", binding);
|
||||
this.show_toast("Internal error: Invalid binding data.", 4, false);
|
||||
return;
|
||||
}
|
||||
// Use prop function
|
||||
const yes = await this.show_yes_no_dialog(`Uninstall "${binding.name}"?\nThis removes its files.`, 'Uninstall', 'Cancel');
|
||||
if (!yes) return;
|
||||
|
||||
this.setBindingProcessing(binding.folder, true);
|
||||
this.isLoadingAction = true;
|
||||
try {
|
||||
// Use internal wrapper - ensure correct endpoint name
|
||||
const response = await this.internal_api_post_req('uninstall_binding', { name: binding.folder });
|
||||
if (response && response.status) {
|
||||
this.show_toast(`Binding "${binding.name}" uninstalled successfully!`, 4, true);
|
||||
await this.fetchBindings(); // Refresh internal list
|
||||
// If active binding was uninstalled, inform parent
|
||||
if (this.$store.state.config.binding_name === binding.folder) {
|
||||
this.$store.state.config.binding_name = null
|
||||
this.$store.state.config.model_name = null
|
||||
this.$emit('settings-changed', true);
|
||||
this.refresh_config(); // Trigger parent refresh
|
||||
}
|
||||
} else {
|
||||
this.show_toast(`Failed to uninstall "${binding.name}": ${response?.error || 'Unknown error'}`, 4, false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error handled by internal_api_post_req
|
||||
} finally {
|
||||
this.setBindingProcessing(binding.folder, false);
|
||||
this.isLoadingAction = false;
|
||||
nextTick(feather.replace);
|
||||
}
|
||||
const yes = await this.show_yes_no_dialog(`Uninstall "${binding.name}"?`, 'Uninstall', 'Cancel');
|
||||
if (!yes) return;
|
||||
this.setBindingProcessing(binding.folder, true); this.isLoadingAction = true;
|
||||
try {
|
||||
const response = await this.api_post_req('uninstall_binding', { name: binding.folder });
|
||||
if (response?.status) {
|
||||
this.show_toast(`"${binding.name}" uninstalled.`, 4, true);
|
||||
await this.fetchBindings();
|
||||
if (this.config.binding_name === binding.folder) {
|
||||
this.$emit('setting-updated', { key: 'binding_name', value: null });
|
||||
this.$emit('setting-updated', { key: 'model_name', value: null });
|
||||
}
|
||||
} else { this.show_toast(`Uninstall failed: ${response?.error || 'Error'}`, 4, false); }
|
||||
} catch (error) { /* Handled */ }
|
||||
finally { this.setBindingProcessing(binding.folder, false); this.isLoadingAction = false; this.replaceFeatherIcons(); }
|
||||
},
|
||||
|
||||
async handleReinstall(binding) {
|
||||
if (!binding || !binding.folder) {
|
||||
console.error("Invalid binding data received in handleReinstall:", binding);
|
||||
this.show_toast("Internal error: Invalid binding data.", 4, false);
|
||||
return;
|
||||
}
|
||||
// Use prop function
|
||||
const yes = await this.show_yes_no_dialog(`Reinstall "${binding.name}"?\nThis overwrites files.`, 'Reinstall', 'Cancel');
|
||||
if (!yes) return;
|
||||
|
||||
this.setBindingProcessing(binding.folder, true);
|
||||
this.isLoadingAction = true;
|
||||
try {
|
||||
// Use internal wrapper
|
||||
const response = await this.internal_api_post_req('reinstall_binding', { name: binding.folder });
|
||||
if (response && response.status) {
|
||||
this.show_toast(`Binding "${binding.name}" reinstalled successfully! Reload recommended.`, 5, true);
|
||||
await this.fetchBindings(); // Refresh internal list
|
||||
} else {
|
||||
this.show_toast(`Failed to reinstall "${binding.name}": ${response?.error || 'Unknown error'}`, 4, false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error handled by internal_api_post_req
|
||||
} finally {
|
||||
this.setBindingProcessing(binding.folder, false);
|
||||
this.isLoadingAction = false;
|
||||
nextTick(feather.replace);
|
||||
}
|
||||
const yes = await this.show_yes_no_dialog(`Reinstall "${binding.name}"?`, 'Reinstall', 'Cancel');
|
||||
if (!yes) return;
|
||||
this.setBindingProcessing(binding.folder, true); this.isLoadingAction = true;
|
||||
try {
|
||||
const response = await this.api_post_req('reinstall_binding', { name: binding.folder });
|
||||
if (response?.status) {
|
||||
this.show_toast(`"${binding.name}" reinstalled! Reload recommended.`, 5, true);
|
||||
await this.fetchBindings();
|
||||
} else { this.show_toast(`Reinstall failed: ${response?.error || 'Error'}`, 4, false); }
|
||||
} catch (error) { /* Handled */ }
|
||||
finally { this.setBindingProcessing(binding.folder, false); this.isLoadingAction = false; this.replaceFeatherIcons(); }
|
||||
},
|
||||
|
||||
handleSettingsFromEntry(binding) {
|
||||
if (!binding || !binding.folder) {
|
||||
console.error("Invalid binding data in handleSettingsFromEntry:", binding);
|
||||
this.show_toast("Internal error: Invalid binding data.", 4, false);
|
||||
return;
|
||||
}
|
||||
this.handleSettings(binding.folder);
|
||||
},
|
||||
|
||||
handleSettingsFromEntry(binding) { this.handleSettings(binding.folder); },
|
||||
handleReloadFromEntry(binding) { this.handleReload(binding.folder); },
|
||||
async handleSettings(bindingFolder) {
|
||||
console.log("Handling settings from")
|
||||
console.log(bindingFolder)
|
||||
if (!bindingFolder) {
|
||||
this.show_toast("No binding specified.", 3, false);
|
||||
return;
|
||||
}
|
||||
if (!bindingFolder) { this.show_toast("No binding specified.", 3, false); return; }
|
||||
if (this.hasPendingChanges) { this.show_toast(`Apply settings changes first.`, 3, false); return; }
|
||||
const targetBinding = this.bindings.find(b => b.folder === bindingFolder);
|
||||
if (!targetBinding) {
|
||||
this.show_toast(`Binding "${bindingFolder}" not found.`, 4, false);
|
||||
return;
|
||||
}
|
||||
if (!targetBinding.installed) {
|
||||
this.show_toast(`"${targetBinding.name}" is not installed.`, 3, false);
|
||||
return;
|
||||
}
|
||||
// Use prop config
|
||||
if (bindingFolder !== this.$store.state.config.binding_name) {
|
||||
this.show_toast(`Select "${targetBinding.name}" first to configure it.`, 4, false);
|
||||
return;
|
||||
}
|
||||
if (!targetBinding?.installed) { this.show_toast(`Binding "${targetBinding?.name || bindingFolder}" not installed.`, 3, false); return; }
|
||||
if (bindingFolder !== this.effectiveConfig.binding_name) { this.show_toast(`Select and Apply "${targetBinding.name}" first.`, 4, false); return; }
|
||||
|
||||
this.isLoadingAction = true;
|
||||
try {
|
||||
// Use internal wrapper
|
||||
const settingsData = await this.internal_api_post_req('get_active_binding_settings');
|
||||
|
||||
if (settingsData && Object.keys(settingsData).length > 0) {
|
||||
const bindingName = targetBinding.name || bindingFolder;
|
||||
// Use prop function for form
|
||||
const result = await this.show_universal_form(settingsData, `Settings - ${bindingName}`, "Save", "Cancel");
|
||||
|
||||
if (result !== null && result !== undefined) { // Form submitted
|
||||
// Use internal wrapper
|
||||
const setResponse = await this.internal_api_post_req('set_active_binding_settings', { settings: result });
|
||||
if (setResponse && setResponse.status) {
|
||||
this.show_toast(`Settings for "${bindingName}" updated. Reloading...`, 4, true);
|
||||
// Use internal wrapper
|
||||
await this.internal_api_post_req('update_binding_settings');
|
||||
this.show_toast(`Binding "${bindingName}" reloaded with new settings.`, 4, true);
|
||||
// No need to emit 'settings-changed' here as parent likely handles config persistence globally
|
||||
this.$emit('settings-changed', true);
|
||||
this.refresh_config(); // Tell parent to refresh state
|
||||
} else {
|
||||
this.show_toast(`Failed to update settings for "${bindingName}": ${setResponse?.error || 'Unknown error'}`, 4, false);
|
||||
}
|
||||
} else { // Form cancelled
|
||||
this.show_toast(`Settings update for "${bindingName}" cancelled.`, 3, false);
|
||||
}
|
||||
} else if (settingsData && Object.keys(settingsData).length === 0) {
|
||||
this.show_toast(`"${targetBinding.name}" has no settings.`, 4, false);
|
||||
} else {
|
||||
this.show_toast(`Could not get settings for "${targetBinding.name}".`, 4, false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error handled by internal_api_post_req or show_universal_form
|
||||
} finally {
|
||||
this.isLoadingAction = false;
|
||||
nextTick(feather.replace);
|
||||
}
|
||||
const settingsData = await this.api_post_req('get_active_binding_settings');
|
||||
if (settingsData && Object.keys(settingsData).length > 0) {
|
||||
const result = await this.show_universal_form(settingsData, `Settings - ${targetBinding.name}`, "Save", "Cancel");
|
||||
if (result !== null && result !== undefined) {
|
||||
const setResponse = await this.api_post_req('set_active_binding_settings', { settings: result });
|
||||
if (setResponse?.status) {
|
||||
this.show_toast(`Settings updated for "${targetBinding.name}". Reloading...`, 4, true);
|
||||
await this.api_post_req('update_binding_settings');
|
||||
this.show_toast(`Binding "${targetBinding.name}" reloaded.`, 4, true);
|
||||
} else { this.show_toast(`Update failed: ${setResponse?.error || 'Error'}`, 4, false); }
|
||||
} else { this.show_toast(`Settings update cancelled.`, 3, false); }
|
||||
} else if (settingsData) { this.show_toast(`"${targetBinding.name}" has no settings.`, 3, false);
|
||||
} else { this.show_toast(`Could not get settings.`, 4, false); }
|
||||
} catch (error) { /* Handled */ }
|
||||
finally { this.isLoadingAction = false; this.replaceFeatherIcons(); }
|
||||
},
|
||||
|
||||
handleReloadFromEntry(binding) {
|
||||
if (!binding || !binding.folder) {
|
||||
console.error("Invalid binding data in handleReloadFromEntry:", binding);
|
||||
this.show_toast("Internal error: Invalid binding data.", 4, false);
|
||||
return;
|
||||
}
|
||||
this.handleReload(binding.folder);
|
||||
},
|
||||
|
||||
async handleReload(bindingFolder) {
|
||||
if (!bindingFolder) {
|
||||
this.show_toast("No binding specified.", 3, false);
|
||||
return;
|
||||
}
|
||||
if (!bindingFolder) { this.show_toast("No binding specified.", 3, false); return; }
|
||||
if (this.hasPendingChanges) { this.show_toast(`Apply settings changes first.`, 3, false); return; }
|
||||
const targetBinding = this.bindings.find(b => b.folder === bindingFolder);
|
||||
if (!targetBinding) {
|
||||
this.show_toast(`Binding "${bindingFolder}" not found.`, 4, false);
|
||||
return;
|
||||
}
|
||||
if (!targetBinding.installed) {
|
||||
this.show_toast(`"${targetBinding.name}" is not installed.`, 3, false);
|
||||
return;
|
||||
}
|
||||
// Use prop config
|
||||
if (bindingFolder !== this.$store.state.config.binding_name) {
|
||||
this.show_toast(`"${targetBinding.name}" is not active. Select it first.`, 3, false);
|
||||
return;
|
||||
}
|
||||
if (!targetBinding?.installed) { this.show_toast(`Binding "${targetBinding?.name || bindingFolder}" not installed.`, 3, false); return; }
|
||||
if (bindingFolder !== this.effectiveConfig.binding_name) { this.show_toast(`"${targetBinding.name}" is not the active binding.`, 3, false); return; }
|
||||
|
||||
this.isLoadingAction = true;
|
||||
this.show_toast(`Reloading "${targetBinding.name}"...`, 3, true);
|
||||
this.isLoadingAction = true; this.show_toast(`Reloading "${targetBinding.name}"...`, 3, true);
|
||||
try {
|
||||
// Use internal wrapper
|
||||
const response = await this.internal_api_post_req('reload_binding', { name: bindingFolder });
|
||||
if (response && response.status) {
|
||||
this.show_toast(`Binding "${targetBinding.name}" reloaded.`, 4, true);
|
||||
this.refresh_config(); // Refresh parent state
|
||||
} else {
|
||||
this.show_toast(`Failed to reload "${targetBinding.name}": ${response?.error || 'Unknown error'}`, 4, false);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error handled by internal_api_post_req
|
||||
} finally {
|
||||
this.isLoadingAction = false;
|
||||
nextTick(feather.replace);
|
||||
}
|
||||
const response = await this.api_post_req('reload_binding', { name: bindingFolder });
|
||||
if (response?.status) { this.show_toast(`"${targetBinding.name}" reloaded.`, 4, true); }
|
||||
else { this.show_toast(`Reload failed: ${response?.error || 'Error'}`, 4, false); }
|
||||
} catch (error) { /* Handled */ }
|
||||
finally { this.isLoadingAction = false; this.replaceFeatherIcons(); }
|
||||
},
|
||||
replaceFeatherIcons() {
|
||||
nextTick(() => { try { feather.replace(); } catch (e) {} });
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchBindings(); // Fetch data on mount
|
||||
nextTick(() => {
|
||||
feather.replace();
|
||||
});
|
||||
// Fetch initial list or rely on watcher if store is primary source
|
||||
this.fetchBindings();
|
||||
this.replaceFeatherIcons();
|
||||
},
|
||||
|
||||
// Watch for external config changes if needed, though parent should handle consistency
|
||||
// watch: {
|
||||
// config: {
|
||||
// handler(newConfig, oldConfig) {
|
||||
// // Optional: Handle external changes if necessary, e.g., refresh bindings if binding_name changes externally
|
||||
// console.log("BindingZoo detected config change from parent");
|
||||
// },
|
||||
// deep: true // Be careful with deep watchers
|
||||
// }
|
||||
// },
|
||||
|
||||
updated() {
|
||||
nextTick(() => {
|
||||
feather.replace(); // Ensure icons are updated on subsequent renders
|
||||
});
|
||||
this.replaceFeatherIcons();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styles from the original component */
|
||||
.input-field {
|
||||
@apply block w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:text-gray-200 dark:placeholder-gray-400 disabled:opacity-50;
|
||||
}
|
||||
|
||||
.binding-entry-processing {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Example Primary Color Definitions (adjust as needed) */
|
||||
.bg-primary-light {
|
||||
background-color: #e0f2fe; /* Tailwind sky-100 */
|
||||
}
|
||||
.dark .bg-primary-dark\/20 {
|
||||
background-color: rgba(59, 130, 246, 0.2); /* Tailwind blue-500 with 20% opacity */
|
||||
}
|
||||
.border-primary-dark\/30 {
|
||||
border-color: rgba(37, 99, 235, 0.3); /* Tailwind blue-700 with 30% opacity */
|
||||
}
|
||||
.focus\:ring-primary-dark\/50:focus {
|
||||
--tw-ring-color: rgba(37, 99, 235, 0.5); /* Tailwind blue-700 with 50% opacity */
|
||||
}
|
||||
.dark .fill-primary {
|
||||
fill: #3b82f6; /* Tailwind blue-500 */
|
||||
}
|
||||
.fill-primary {
|
||||
fill: #2563eb; /* Tailwind blue-600 */
|
||||
}
|
||||
.user-settings-panel { @apply bg-white dark:bg-gray-800 rounded-lg shadow; }
|
||||
.svg-button { @apply p-1 rounded-full text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 dark:focus:ring-offset-gray-800 transition-colors duration-150; }
|
||||
.input { @apply block w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-offset-gray-900 sm:text-sm disabled:opacity-50 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500; }
|
||||
.search-input { /* Inherits .input */ }
|
||||
.text-loading { @apply text-blue-600 dark:text-blue-300; }
|
||||
.scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
.scrollbar::-webkit-scrollbar-track { @apply bg-gray-100 dark:bg-gray-700 rounded-lg; }
|
||||
.scrollbar::-webkit-scrollbar-thumb { @apply bg-gray-300 dark:bg-gray-500 rounded-lg; }
|
||||
.scrollbar::-webkit-scrollbar-thumb:hover { @apply bg-gray-400 dark:bg-gray-400; }
|
||||
</style>
|
@ -8,96 +8,86 @@
|
||||
Configure how LoLLMs interacts with the internet to answer questions or find information. Requires a model capable of function calling or specific instruction following.
|
||||
</p>
|
||||
|
||||
<!-- Internet Search Toggles -->
|
||||
<section class="space-y-4 p-4 border border-blue-300 dark:border-blue-600 rounded-lg panels-color">
|
||||
<h3 class="text-lg font-medium text-blue-700 dark:text-blue-300 mb-3">Activation & Behavior</h3>
|
||||
|
||||
<!-- Activate Internet Search -->
|
||||
<div class="toggle-item">
|
||||
<label for="activate_internet_search" class="toggle-label">
|
||||
Enable Automatic Internet Search
|
||||
<span class="toggle-description">Allow the AI to decide when to search the internet based on the prompt.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="activate_internet_search" :checked="$store.state.config.activate_internet_search" @update:checked="updateBoolean('activate_internet_search', $event)" />
|
||||
<ToggleSwitch id="activate_internet_search" :checked="config.activate_internet_search" @update:checked="updateValue('activate_internet_search', $event)" />
|
||||
</div>
|
||||
|
||||
<!-- Activate Search Decision -->
|
||||
<div class="toggle-item" :class="{ 'opacity-50 pointer-events-none': !$store.state.config.activate_internet_search }">
|
||||
<div class="toggle-item" :class="{ 'opacity-50 pointer-events-none': !config.activate_internet_search }">
|
||||
<label for="internet_activate_search_decision" class="toggle-label">
|
||||
Enable Explicit Search Decision
|
||||
<span class="toggle-description">Make the AI explicitly state whether it needs to search the internet before performing the search.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="internet_activate_search_decision" :checked="$store.state.config.internet_activate_search_decision" @update:checked="updateBoolean('internet_activate_search_decision', $event)" :disabled="!$store.state.config.activate_internet_search"/>
|
||||
<ToggleSwitch id="internet_activate_search_decision" :checked="config.internet_activate_search_decision" @update:checked="updateValue('internet_activate_search_decision', $event)" :disabled="!config.activate_internet_search"/>
|
||||
</div>
|
||||
|
||||
<!-- Activate Pages Judgement -->
|
||||
<div class="toggle-item" :class="{ 'opacity-50 pointer-events-none': !$store.state.config.activate_internet_search }">
|
||||
<div class="toggle-item" :class="{ 'opacity-50 pointer-events-none': !config.activate_internet_search }">
|
||||
<label for="activate_internet_pages_judgement" class="toggle-label">
|
||||
Enable Search Result Evaluation
|
||||
<span class="toggle-description">Allow the AI to evaluate the relevance and quality of search result snippets before using them.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="activate_internet_pages_judgement" :checked="$store.state.config.activate_internet_pages_judgement" @update:checked="updateBoolean('activate_internet_pages_judgement', $event)" :disabled="!$store.state.config.activate_internet_search"/>
|
||||
<ToggleSwitch id="activate_internet_pages_judgement" :checked="config.activate_internet_pages_judgement" @update:checked="updateValue('activate_internet_pages_judgement', $event)" :disabled="!config.activate_internet_search"/>
|
||||
</div>
|
||||
|
||||
<!-- Activate Quick Search -->
|
||||
<div class="toggle-item" :class="{ 'opacity-50 pointer-events-none': !$store.state.config.activate_internet_search }">
|
||||
<div class="toggle-item" :class="{ 'opacity-50 pointer-events-none': !config.activate_internet_search }">
|
||||
<label for="internet_quick_search" class="toggle-label">
|
||||
Enable Quick Search
|
||||
<span class="toggle-description">Perform a faster search potentially using fewer results or less processing, might be less accurate.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="internet_quick_search" :checked="$store.state.config.internet_quick_search" @update:checked="updateBoolean('internet_quick_search', $event)" :disabled="!$store.state.config.activate_internet_search"/>
|
||||
<ToggleSwitch id="internet_quick_search" :checked="config.internet_quick_search" @update:checked="updateValue('internet_quick_search', $event)" :disabled="!config.activate_internet_search"/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Internet Search Parameters -->
|
||||
<section :class="['space-y-4 p-4 border border-blue-300 dark:border-blue-600 rounded-lg panels-color', !$store.state.config.activate_internet_search ? 'opacity-50 pointer-events-none' : '']">
|
||||
<section :class="['space-y-4 p-4 border border-blue-300 dark:border-blue-600 rounded-lg panels-color', !config.activate_internet_search ? 'opacity-50 pointer-events-none' : '']">
|
||||
<h3 class="text-lg font-medium text-blue-700 dark:text-blue-300 mb-3">Search Parameters</h3>
|
||||
|
||||
<!-- Number of Search Pages/Results -->
|
||||
<div class="setting-item">
|
||||
<label for="internet_nb_search_pages" class="setting-label">
|
||||
Number of Search Results
|
||||
<span class="block text-xs font-normal text-blue-500 dark:text-blue-400 mt-1">Controls how many search result snippets are initially retrieved.</span>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-4">
|
||||
<input id="internet_nb_search_pages-range" :value="$store.state.config.internet_nb_search_pages" @input="updateValue('internet_nb_search_pages', $event.target.value)" type="range" min="1" max="20" step="1" class="range-input" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_nb_search_pages-number" :value="$store.state.config.internet_nb_search_pages" @input="updateValue('internet_nb_search_pages', $event.target.value)" type="number" min="1" max="20" step="1" class="input-sm w-20 text-center" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_nb_search_pages-range" :value="config.internet_nb_search_pages" @input="handleNumberInput('internet_nb_search_pages', $event.target.value, true)" type="range" min="1" max="20" step="1" class="range-input" :disabled="!config.activate_internet_search">
|
||||
<input id="internet_nb_search_pages-number" :value="config.internet_nb_search_pages" @input="handleNumberInput('internet_nb_search_pages', $event.target.value, true)" type="number" min="1" max="20" step="1" class="input-sm w-20 text-center" :disabled="!config.activate_internet_search">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vectorization Chunk Size -->
|
||||
<div class="setting-item">
|
||||
<label for="internet_vectorization_chunk_size" class="setting-label">
|
||||
Content Chunk Size
|
||||
<span class="block text-xs font-normal text-blue-500 dark:text-blue-400 mt-1">Size of text chunks when processing content from searched web pages (if applicable).</span>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-4">
|
||||
<input id="internet_vectorization_chunk_size-range" :value="$store.state.config.internet_vectorization_chunk_size" @input="updateValue('internet_vectorization_chunk_size', $event.target.value)" type="range" min="100" max="1000" step="50" class="range-input" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_vectorization_chunk_size-number" :value="$store.state.config.internet_vectorization_chunk_size" @input="updateValue('internet_vectorization_chunk_size', $event.target.value)" type="number" min="100" max="1000" step="50" class="input-sm w-20 text-center" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_vectorization_chunk_size-range" :value="config.internet_vectorization_chunk_size" @input="handleNumberInput('internet_vectorization_chunk_size', $event.target.value, true)" type="range" min="100" max="1000" step="50" class="range-input" :disabled="!config.activate_internet_search">
|
||||
<input id="internet_vectorization_chunk_size-number" :value="config.internet_vectorization_chunk_size" @input="handleNumberInput('internet_vectorization_chunk_size', $event.target.value, true)" type="number" min="100" max="1000" step="50" class="input-sm w-20 text-center" :disabled="!config.activate_internet_search">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vectorization Overlap Size -->
|
||||
<div class="setting-item">
|
||||
<label for="internet_vectorization_overlap_size" class="setting-label">
|
||||
Content Overlap Size
|
||||
<span class="block text-xs font-normal text-blue-500 dark:text-blue-400 mt-1">Overlap between text chunks when processing web page content.</span>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-4">
|
||||
<input id="internet_vectorization_overlap_size-range" :value="$store.state.config.internet_vectorization_overlap_size" @input="updateValue('internet_vectorization_overlap_size', $event.target.value)" type="range" min="0" max="200" step="10" class="range-input" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_vectorization_overlap_size-number" :value="$store.state.config.internet_vectorization_overlap_size" @input="updateValue('internet_vectorization_overlap_size', $event.target.value)" type="number" min="0" max="200" step="10" class="input-sm w-20 text-center" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_vectorization_overlap_size-range" :value="config.internet_vectorization_overlap_size" @input="handleNumberInput('internet_vectorization_overlap_size', $event.target.value, true)" type="range" min="0" max="200" step="10" class="range-input" :disabled="!config.activate_internet_search">
|
||||
<input id="internet_vectorization_overlap_size-number" :value="config.internet_vectorization_overlap_size" @input="handleNumberInput('internet_vectorization_overlap_size', $event.target.value, true)" type="number" min="0" max="200" step="10" class="input-sm w-20 text-center" :disabled="!config.activate_internet_search">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Number of Vectorization Chunks to Use -->
|
||||
<div class="setting-item">
|
||||
<label for="internet_vectorization_nb_chunks" class="setting-label">
|
||||
Number of Content Chunks to Use
|
||||
<span class="block text-xs font-normal text-blue-500 dark:text-blue-400 mt-1">Maximum number of processed text chunks from web pages to include in the context.</span>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-4">
|
||||
<input id="internet_vectorization_nb_chunks-range" :value="$store.state.config.internet_vectorization_nb_chunks" @input="updateValue('internet_vectorization_nb_chunks', $event.target.value)" type="range" min="1" max="20" step="1" class="range-input" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_vectorization_nb_chunks-number" :value="$store.state.config.internet_vectorization_nb_chunks" @input="updateValue('internet_vectorization_nb_chunks', $event.target.value)" type="number" min="1" max="20" step="1" class="input-sm w-20 text-center" :disabled="!$store.state.config.activate_internet_search">
|
||||
<input id="internet_vectorization_nb_chunks-range" :value="config.internet_vectorization_nb_chunks" @input="handleNumberInput('internet_vectorization_nb_chunks', $event.target.value, true)" type="range" min="1" max="20" step="1" class="range-input" :disabled="!config.activate_internet_search">
|
||||
<input id="internet_vectorization_nb_chunks-number" :value="config.internet_vectorization_nb_chunks" @input="handleNumberInput('internet_vectorization_nb_chunks', $event.target.value, true)" type="number" min="1" max="20" step="1" class="input-sm w-20 text-center" :disabled="!config.activate_internet_search">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -112,149 +102,61 @@ import { nextTick } from 'vue';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch.vue';
|
||||
|
||||
export default {
|
||||
name: 'InternetSettings', // Give the component a name
|
||||
name: 'InternetSettings',
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
props: {
|
||||
loading: { // Note: loading prop is defined but not used in the template/script
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:setting'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
// No specific reactive data needed for this component's logic
|
||||
};
|
||||
config: { type: Object, required: true }, // Receive editable config copy
|
||||
loading: { type: Boolean, default: false }
|
||||
},
|
||||
emits: ['setting-updated'], // Declare the event
|
||||
|
||||
methods: {
|
||||
updateValue(key, value) {
|
||||
// Ensure numeric values from range/number inputs are parsed correctly
|
||||
const numericKeys = [
|
||||
'internet_nb_search_pages',
|
||||
'internet_vectorization_chunk_size',
|
||||
'internet_vectorization_overlap_size',
|
||||
'internet_vectorization_nb_chunks'
|
||||
];
|
||||
// Use Number() for potentially better handling of empty strings/non-numeric input
|
||||
// Default to 0 or a reasonable minimum if parsing fails
|
||||
const finalValue = numericKeys.includes(key) ? (Number(value) || 0) : value;
|
||||
|
||||
this.$emit('update:setting', { key, value: finalValue });
|
||||
// Emit update for parent component
|
||||
this.$emit('setting-updated', { key, value });
|
||||
},
|
||||
|
||||
updateBoolean(key, value) {
|
||||
this.$emit('update:setting', { key, value: Boolean(value) });
|
||||
handleNumberInput(key, value, isInt = false) {
|
||||
// Parse the value
|
||||
let parsedValue = isInt ? parseInt(value) : parseFloat(value);
|
||||
// Check for NaN after parsing
|
||||
if (isNaN(parsedValue)) {
|
||||
console.warn(`Attempted to set invalid number for ${key}:`, value);
|
||||
// Fallback to a reasonable default, like the minimum allowed value or 0
|
||||
const minVal = {
|
||||
'internet_nb_search_pages': 1,
|
||||
'internet_vectorization_chunk_size': 100,
|
||||
'internet_vectorization_overlap_size': 0,
|
||||
'internet_vectorization_nb_chunks': 1
|
||||
}[key] || 0; // Default to 0 if key not found
|
||||
parsedValue = minVal;
|
||||
// Optionally update the input field visually to the fallback value if needed
|
||||
// e.g., by finding the element and setting its value, but emitting is usually enough
|
||||
}
|
||||
this.updateValue(key, parsedValue);
|
||||
},
|
||||
|
||||
// Helper to ensure Feather icons are rendered after DOM updates
|
||||
replaceFeatherIcons() {
|
||||
nextTick(() => {
|
||||
try {
|
||||
// Check if feather is available (it might not be in all test environments)
|
||||
if (typeof feather !== 'undefined' && feather && typeof feather.replace === 'function') {
|
||||
feather.replace();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Feather icons replacement failed:", e);
|
||||
}
|
||||
});
|
||||
nextTick(() => { try { feather.replace(); } catch (e) {} });
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.replaceFeatherIcons();
|
||||
// Since feather icons are static here (no v-if toggling them),
|
||||
// calling it only in mounted might be sufficient.
|
||||
},
|
||||
updated() {
|
||||
// Optional: Re-run feather replace if the component structure might change dynamically
|
||||
// though in this specific component, it might not be strictly necessary.
|
||||
this.replaceFeatherIcons();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Root variables (if not defined globally) - Adjust colors as needed */
|
||||
:root {
|
||||
--color-primary: #3b82f6; /* Tailwind blue-500 */
|
||||
--color-primary-rgb: 59, 130, 246;
|
||||
}
|
||||
.dark:root { /* Use :root within .dark selector if needed */
|
||||
--color-primary: #60a5fa; /* Tailwind blue-400 */
|
||||
--color-primary-rgb: 96, 165, 250;
|
||||
}
|
||||
|
||||
/* Using shared styles defined in previous components or globally */
|
||||
.setting-item {
|
||||
@apply flex flex-col md:flex-row md:items-center gap-2 md:gap-4 py-2;
|
||||
}
|
||||
.setting-label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 w-full md:w-1/3 lg:w-1/4 flex-shrink-0;
|
||||
}
|
||||
.input-field-sm {
|
||||
@apply block w-full px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary disabled:opacity-50;
|
||||
}
|
||||
/* Custom styling for range input thumb and track with accent color */
|
||||
.range-input {
|
||||
@apply w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-600 disabled:opacity-50;
|
||||
accent-color: var(--color-primary); /* Modern way to color the track/thumb */
|
||||
}
|
||||
|
||||
/* Thumb base style */
|
||||
.range-input::-webkit-slider-thumb {
|
||||
@apply appearance-none w-4 h-4 bg-primary rounded-full cursor-pointer transition-colors shadow; /* Use theme primary color var */
|
||||
background-color: var(--color-primary); /* Explicitly set */
|
||||
}
|
||||
.range-input::-moz-range-thumb {
|
||||
@apply w-4 h-4 bg-primary rounded-full cursor-pointer border-none transition-colors shadow; /* Use theme primary color var */
|
||||
background-color: var(--color-primary); /* Explicitly set */
|
||||
}
|
||||
|
||||
/* Disabled thumb style */
|
||||
.range-input:disabled::-webkit-slider-thumb {
|
||||
@apply bg-gray-400 dark:bg-gray-500 cursor-not-allowed;
|
||||
}
|
||||
.range-input:disabled::-moz-range-thumb {
|
||||
@apply bg-gray-400 dark:bg-gray-500 cursor-not-allowed;
|
||||
}
|
||||
|
||||
/* --- Updated Focus Style --- */
|
||||
.range-input:focus {
|
||||
@apply outline-none; /* Remove default browser outline */
|
||||
}
|
||||
|
||||
.range-input:focus::-webkit-slider-thumb {
|
||||
/* Ring effect using box-shadow */
|
||||
/* Offset color (white in light, gray-800 in dark) + Ring color (primary with 50% alpha) */
|
||||
box-shadow: 0 0 0 2px theme('colors.white'), 0 0 0 4px rgba(var(--color-primary-rgb), 0.5);
|
||||
}
|
||||
.dark .range-input:focus::-webkit-slider-thumb {
|
||||
box-shadow: 0 0 0 2px theme('colors.gray.800'), 0 0 0 4px rgba(var(--color-primary-rgb), 0.5);
|
||||
}
|
||||
|
||||
.range-input:focus::-moz-range-thumb {
|
||||
/* Ring effect using box-shadow */
|
||||
box-shadow: 0 0 0 2px theme('colors.white'), 0 0 0 4px rgba(var(--color-primary-rgb), 0.5);
|
||||
}
|
||||
.dark .range-input:focus::-moz-range-thumb {
|
||||
box-shadow: 0 0 0 2px theme('colors.gray.800'), 0 0 0 4px rgba(var(--color-primary-rgb), 0.5);
|
||||
}
|
||||
/* --- End Updated Focus Style --- */
|
||||
|
||||
|
||||
.toggle-item {
|
||||
@apply flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors;
|
||||
}
|
||||
.toggle-label {
|
||||
@apply text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer flex-1 mr-4;
|
||||
}
|
||||
.toggle-description {
|
||||
@apply block text-xs text-gray-500 dark:text-gray-400 mt-1 font-normal;
|
||||
}
|
||||
|
||||
/* Using shared styles defined in previous components */
|
||||
.setting-item { @apply flex flex-col md:flex-row md:items-center gap-2 md:gap-4 py-2; }
|
||||
.setting-label { @apply block text-sm font-medium text-gray-700 dark:text-gray-300 w-full md:w-1/3 lg:w-1/4 flex-shrink-0; }
|
||||
.input-sm { @apply block w-full px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-offset-gray-900 sm:text-sm disabled:opacity-50 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500; }
|
||||
.range-input { @apply w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-600 accent-blue-600 dark:accent-blue-500 disabled:opacity-50; }
|
||||
.toggle-item { @apply flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors; }
|
||||
.toggle-label { @apply text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer flex-1 mr-4; }
|
||||
.toggle-description { @apply block text-xs text-gray-500 dark:text-gray-400 mt-1 font-normal; }
|
||||
.panels-color { @apply bg-white dark:bg-gray-800; }
|
||||
</style>
|
@ -262,7 +262,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Security Measures Section (New) -->
|
||||
<div class="space-y-4 p-4 border border-blue-300 dark:border-blue-600 rounded-lg panels-color">
|
||||
<h3 class="text-lg font-medium text-blue-700 dark:text-blue-300 mb-3">Security Measures</h3>
|
||||
<div class="toggle-item">
|
||||
<label for="turn_on_setting_update_validation" class="toggle-label">
|
||||
Validate Setting Updates
|
||||
<span class="toggle-description">Enable validation for changes to configuration settings to prevent unauthorized or invalid updates.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="turn_on_setting_update_validation" :checked="config.turn_on_setting_update_validation" @update:checked="updateValue('turn_on_setting_update_validation', $event)" />
|
||||
</div>
|
||||
<div class="toggle-item">
|
||||
<label for="turn_on_code_execution" class="toggle-label">
|
||||
Allow Code Execution
|
||||
<span class="toggle-description">Permit the execution of code snippets within the application (use with caution).</span>
|
||||
</label>
|
||||
<ToggleSwitch id="turn_on_code_execution" :checked="config.turn_on_code_execution" @update:checked="updateValue('turn_on_code_execution', $event)" />
|
||||
</div>
|
||||
<div class="toggle-item">
|
||||
<label for="turn_on_code_validation" class="toggle-label">
|
||||
Validate Executed Code
|
||||
<span class="toggle-description">Enable validation of code before execution to ensure safety and correctness.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="turn_on_code_validation" :checked="config.turn_on_code_validation" @update:checked="updateValue('turn_on_code_validation', $event)" />
|
||||
</div>
|
||||
<div class="toggle-item">
|
||||
<label for="turn_on_open_file_validation" class="toggle-label">
|
||||
Validate File Opening
|
||||
<span class="toggle-description">Check files before opening to prevent access to unauthorized or harmful content.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="turn_on_open_file_validation" :checked="config.turn_on_open_file_validation" @update:checked="updateValue('turn_on_open_file_validation', $event)" />
|
||||
</div>
|
||||
<div class="toggle-item">
|
||||
<label for="turn_on_send_file_validation" class="toggle-label">
|
||||
Validate File Sending
|
||||
<span class="toggle-description">Validate files before sending to ensure they meet security and format requirements.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="turn_on_send_file_validation" :checked="config.turn_on_send_file_validation" @update:checked="updateValue('turn_on_send_file_validation', $event)" />
|
||||
</div>
|
||||
<div class="toggle-item">
|
||||
<label for="turn_on_language_validation" class="toggle-label">
|
||||
Validate Language Inputs
|
||||
<span class="toggle-description">Ensure language inputs are valid and safe before processing.</span>
|
||||
</label>
|
||||
<ToggleSwitch id="turn_on_language_validation" :checked="config.turn_on_language_validation" @update:checked="updateValue('turn_on_language_validation', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Thinking Methods Section -->
|
||||
<div class="space-y-4 p-4 border border-blue-300 dark:border-blue-600 rounded-lg panels-color">
|
||||
<h3 class="text-lg font-medium text-blue-700 dark:text-blue-300 mb-3">Thinking Methods</h3>
|
||||
@ -507,7 +552,7 @@ export default {
|
||||
this.loadLocalPresets();
|
||||
try {
|
||||
// Use prop function
|
||||
const response = await this.api_post_req('/get_thinking_methods');
|
||||
const response = await this.api_post_req('get_thinking_methods');
|
||||
if (response.status === 'success') {
|
||||
this.thinkingPresets = response.thinking_methods || [];
|
||||
// Set initial selectedPresetName based on current prop value
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user