mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-16 08:21:36 +00:00
Added an custom form type for model selection with autocompletition
This commit is contained in:
parent
67cb6fb8a2
commit
9d389309fc
5 changed files with 252 additions and 1 deletions
152
assets/controllers/elements/ai_model_autocomplete_controller.js
Normal file
152
assets/controllers/elements/ai_model_autocomplete_controller.js
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||||
|
import '../../css/components/tom-select_extensions.css';
|
||||||
|
import TomSelect from "tom-select";
|
||||||
|
|
||||||
|
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||||
|
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||||
|
|
||||||
|
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||||
|
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
_tomSelect;
|
||||||
|
|
||||||
|
_platformSelector;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
//Try to find the platform selector
|
||||||
|
const platformSelector = document.querySelector("select[data-platform-selector-label='" + this.element.dataset.platformSelector + "']");
|
||||||
|
//Clear tomselect options, if the platform selector changes
|
||||||
|
if (platformSelector) {
|
||||||
|
this.platformSelector = platformSelector;
|
||||||
|
platformSelector.addEventListener('change', () => {
|
||||||
|
//Force reload of options by clearing the cache and options of TomSelect and triggering a search with an empty string
|
||||||
|
this._tomSelect.clearOptions();
|
||||||
|
this._tomSelect.clearCache();
|
||||||
|
this._tomSelect.load('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = {
|
||||||
|
persistent: false,
|
||||||
|
create: true,
|
||||||
|
maxItems: 1,
|
||||||
|
preload: 'focus',
|
||||||
|
createOnBlur: true,
|
||||||
|
selectOnTab: true,
|
||||||
|
clearAfterSelect: true,
|
||||||
|
shouldLoad: ((query) => true),
|
||||||
|
maxOptions: null,
|
||||||
|
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||||
|
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||||
|
dropdownParent: dropdownParent,
|
||||||
|
render: {
|
||||||
|
item: (data, escape) => {
|
||||||
|
return '<span>' + escape(data.label) + '</span>';
|
||||||
|
},
|
||||||
|
option: (data, escape) => {
|
||||||
|
if (data.image) {
|
||||||
|
return "<div class='row m-0'><div class='col-2 pl-0 pr-1'><img class='typeahead-image' src='" + data.image + "'/></div><div class='col-10'>" + data.label + "</div></div>"
|
||||||
|
}
|
||||||
|
return '<div>' + escape(data.label) + '</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'autoselect_typed': {},
|
||||||
|
'click_to_edit': {},
|
||||||
|
'clear_button': {},
|
||||||
|
"restore_on_backspace": {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(this.element.dataset.urlTemplate) {
|
||||||
|
const base_url = this.element.dataset.urlTemplate;
|
||||||
|
settings.searchField = "label";
|
||||||
|
settings.sortField = "label";
|
||||||
|
settings.valueField = "label";
|
||||||
|
settings.load = (query, callback) => {
|
||||||
|
|
||||||
|
|
||||||
|
if (!this.platformSelector) {
|
||||||
|
console.error("Platform selector not found for AI model autocomplete");
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Platform is the selected option
|
||||||
|
const platform = this.platformSelector.value;
|
||||||
|
if (!platform) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
//Only fetch each platform once
|
||||||
|
if(self.platformLoaded === platform) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const url = base_url.replace('__PLATFORM__', encodeURIComponent(platform));
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => {
|
||||||
|
|
||||||
|
self.platformLoaded = platform;
|
||||||
|
|
||||||
|
var data = [];
|
||||||
|
|
||||||
|
for (const name in json) {
|
||||||
|
data.push({
|
||||||
|
"label": name,
|
||||||
|
"capabilities": json[name].capabilities,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(data);
|
||||||
|
}).catch(()=>{
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this._tomSelect = new TomSelect(this.element, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
super.disconnect();
|
||||||
|
//Destroy the TomSelect instance
|
||||||
|
this._tomSelect.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,7 +23,10 @@ declare(strict_types=1);
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Parameters\AbstractParameter;
|
use App\Entity\Parameters\AbstractParameter;
|
||||||
|
use App\Services\AI\AIPlatformRegistry;
|
||||||
|
use App\Services\AI\AIPlatforms;
|
||||||
use App\Settings\MiscSettings\IpnSuggestSettings;
|
use App\Settings\MiscSettings\IpnSuggestSettings;
|
||||||
|
use Symfony\Component\Cache\Adapter\AdapterInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use App\Entity\Attachments\Attachment;
|
use App\Entity\Attachments\Attachment;
|
||||||
use App\Entity\Parts\Category;
|
use App\Entity\Parts\Category;
|
||||||
|
|
@ -54,6 +57,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||||
use Symfony\Component\Serializer\Serializer;
|
use Symfony\Component\Serializer\Serializer;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In this controller the endpoints for the typeaheads are collected.
|
* In this controller the endpoints for the typeaheads are collected.
|
||||||
|
|
@ -223,4 +228,21 @@ class TypeaheadController extends AbstractController
|
||||||
|
|
||||||
return new JsonResponse($ipnSuggestions);
|
return new JsonResponse($ipnSuggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/ai/{platform}/models', name: 'typeahead_ai_models', requirements: ['platform' => '.+'])]
|
||||||
|
public function aiModels(
|
||||||
|
AIPlatforms $platform,
|
||||||
|
AIPlatformRegistry $platformRegistry,
|
||||||
|
CacheInterface $cache,
|
||||||
|
): JsonResponse {
|
||||||
|
|
||||||
|
$this->denyAccessUnlessGranted('@config.change_system_settings');
|
||||||
|
|
||||||
|
$models = $cache->get('ai_models_'.$platform->value, function(ItemInterface $item) use ($platformRegistry, $platform) {
|
||||||
|
$item->expiresAfter(3600); //Cache for 1 hour
|
||||||
|
return $platformRegistry->getPlatform($platform)->getModelCatalog()->getModels();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new JsonResponse($models);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
src/Form/Settings/AiModelsType.php
Normal file
62
src/Form/Settings/AiModelsType.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Form\Settings;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An text input with autocomplete for AI models from the given platform.
|
||||||
|
* The platform is determined by the value of another form field, which is specified by the "platform_selector" option. This allows to filter the available models based on the selected platform.
|
||||||
|
*/
|
||||||
|
final class AiModelsType extends AbstractType
|
||||||
|
{
|
||||||
|
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParent(): string
|
||||||
|
{
|
||||||
|
return TextType::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
//The target label of the platform select, which is used to filter the models for the selected platform.
|
||||||
|
$resolver->setRequired('platform_selector');
|
||||||
|
$resolver->setAllowedTypes('platform_selector', 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function finishView(FormView $view, FormInterface $form, array $options): void
|
||||||
|
{
|
||||||
|
$view->vars['attr']['data-url-template'] = $this->urlGenerator->generate('typeahead_ai_models', ['platform' => '__PLATFORM__']);
|
||||||
|
$view->vars['attr']['data-controller'] = 'elements--ai-model-autocomplete';
|
||||||
|
|
||||||
|
$view->vars['attr']['data-platform-selector'] = $options['platform_selector'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,8 +28,13 @@ use App\Services\AI\AIPlatforms;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow to choose an AI platform from the enabled platforms in the system. This is used in the settings to choose the default platform for AI features.
|
||||||
|
*/
|
||||||
final class AiPlatformChoiceType extends AbstractType
|
final class AiPlatformChoiceType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly AIPlatformRegistry $platformRegistry)
|
public function __construct(private readonly AIPlatformRegistry $platformRegistry)
|
||||||
|
|
@ -49,6 +54,12 @@ final class AiPlatformChoiceType extends AbstractType
|
||||||
'class' => AIPlatforms::class,
|
'class' => AIPlatforms::class,
|
||||||
'choices' => $choices,
|
'choices' => $choices,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'platform_selector_label' => null
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function finishView(FormView $view, FormInterface $form, array $options): void
|
||||||
|
{
|
||||||
|
$view->vars['attr']['data-platform-selector-label'] = $options['platform_selector_label'] ?? $view->vars['id'].'_label';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Settings\InfoProviderSystem;
|
namespace App\Settings\InfoProviderSystem;
|
||||||
|
|
||||||
|
use App\Form\Settings\AiModelsType;
|
||||||
use App\Form\Settings\AiPlatformChoiceType;
|
use App\Form\Settings\AiPlatformChoiceType;
|
||||||
use App\Services\AI\AIPlatforms;
|
use App\Services\AI\AIPlatforms;
|
||||||
use App\Settings\SettingsIcon;
|
use App\Settings\SettingsIcon;
|
||||||
|
|
@ -36,15 +37,18 @@ use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||||
#[SettingsIcon("fa-robot")]
|
#[SettingsIcon("fa-robot")]
|
||||||
class AIExtractorSettings
|
class AIExtractorSettings
|
||||||
{
|
{
|
||||||
|
private const MODEL_SELECTOR_LABEL = 'ai_extractor';
|
||||||
|
|
||||||
use SettingsTrait;
|
use SettingsTrait;
|
||||||
|
|
||||||
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), description: new TM("settings.ips.ai_extractor.ai_platform.help"),
|
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), description: new TM("settings.ips.ai_extractor.ai_platform.help"),
|
||||||
formType: AiPlatformChoiceType::class,
|
formType: AiPlatformChoiceType::class, formOptions: ['platform_selector_label' => self::MODEL_SELECTOR_LABEL],
|
||||||
envVar: "string:PROVIDER_AI_EXTRACTOR_API_KEY", envVarMode: EnvVarMode::OVERWRITE
|
envVar: "string:PROVIDER_AI_EXTRACTOR_API_KEY", envVarMode: EnvVarMode::OVERWRITE
|
||||||
)]
|
)]
|
||||||
public ?AIPlatforms $platform = null;
|
public ?AIPlatforms $platform = null;
|
||||||
|
|
||||||
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.description"),
|
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.description"),
|
||||||
|
formType: AiModelsType::class, formOptions: ['platform_selector' => self::MODEL_SELECTOR_LABEL],
|
||||||
envVar: "string:PROVIDER_AI_EXTRACTOR_MODEL", envVarMode: EnvVarMode::OVERWRITE
|
envVar: "string:PROVIDER_AI_EXTRACTOR_MODEL", envVarMode: EnvVarMode::OVERWRITE
|
||||||
)]
|
)]
|
||||||
public string $model = 'z-ai/glm-4.7';
|
public string $model = 'z-ai/glm-4.7';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue