Skip to content

Commit 9d38930

Browse files
committed
Added an custom form type for model selection with autocompletition
1 parent 67cb6fb commit 9d38930

5 files changed

Lines changed: 252 additions & 1 deletion

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
3+
*
4+
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import {Controller} from "@hotwired/stimulus";
21+
22+
import "tom-select/dist/css/tom-select.bootstrap5.css";
23+
import '../../css/components/tom-select_extensions.css';
24+
import TomSelect from "tom-select";
25+
26+
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
27+
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
28+
29+
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
30+
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
31+
32+
export default class extends Controller {
33+
_tomSelect;
34+
35+
_platformSelector;
36+
37+
connect() {
38+
39+
let dropdownParent = "body";
40+
if (this.element.closest('.modal')) {
41+
dropdownParent = null
42+
}
43+
44+
//Try to find the platform selector
45+
const platformSelector = document.querySelector("select[data-platform-selector-label='" + this.element.dataset.platformSelector + "']");
46+
//Clear tomselect options, if the platform selector changes
47+
if (platformSelector) {
48+
this.platformSelector = platformSelector;
49+
platformSelector.addEventListener('change', () => {
50+
//Force reload of options by clearing the cache and options of TomSelect and triggering a search with an empty string
51+
this._tomSelect.clearOptions();
52+
this._tomSelect.clearCache();
53+
this._tomSelect.load('');
54+
});
55+
}
56+
57+
let settings = {
58+
persistent: false,
59+
create: true,
60+
maxItems: 1,
61+
preload: 'focus',
62+
createOnBlur: true,
63+
selectOnTab: true,
64+
clearAfterSelect: true,
65+
shouldLoad: ((query) => true),
66+
maxOptions: null,
67+
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
68+
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
69+
dropdownParent: dropdownParent,
70+
render: {
71+
item: (data, escape) => {
72+
return '<span>' + escape(data.label) + '</span>';
73+
},
74+
option: (data, escape) => {
75+
if (data.image) {
76+
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>"
77+
}
78+
return '<div>' + escape(data.label) + '</div>';
79+
}
80+
},
81+
plugins: {
82+
'autoselect_typed': {},
83+
'click_to_edit': {},
84+
'clear_button': {},
85+
"restore_on_backspace": {}
86+
}
87+
};
88+
89+
if(this.element.dataset.urlTemplate) {
90+
const base_url = this.element.dataset.urlTemplate;
91+
settings.searchField = "label";
92+
settings.sortField = "label";
93+
settings.valueField = "label";
94+
settings.load = (query, callback) => {
95+
96+
97+
if (!this.platformSelector) {
98+
console.error("Platform selector not found for AI model autocomplete");
99+
callback();
100+
return;
101+
}
102+
103+
//Platform is the selected option
104+
const platform = this.platformSelector.value;
105+
if (!platform) {
106+
callback();
107+
return;
108+
}
109+
110+
const self = this;
111+
112+
//Only fetch each platform once
113+
if(self.platformLoaded === platform) {
114+
callback();
115+
}
116+
117+
118+
const url = base_url.replace('__PLATFORM__', encodeURIComponent(platform));
119+
120+
fetch(url)
121+
.then(response => response.json())
122+
.then(json => {
123+
124+
self.platformLoaded = platform;
125+
126+
var data = [];
127+
128+
for (const name in json) {
129+
data.push({
130+
"label": name,
131+
"capabilities": json[name].capabilities,
132+
});
133+
}
134+
135+
callback(data);
136+
}).catch(()=>{
137+
callback();
138+
});
139+
};
140+
}
141+
this._tomSelect = new TomSelect(this.element, settings);
142+
}
143+
144+
disconnect() {
145+
super.disconnect();
146+
//Destroy the TomSelect instance
147+
this._tomSelect.destroy();
148+
}
149+
150+
}
151+
152+

src/Controller/TypeaheadController.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
namespace App\Controller;
2424

2525
use App\Entity\Parameters\AbstractParameter;
26+
use App\Services\AI\AIPlatformRegistry;
27+
use App\Services\AI\AIPlatforms;
2628
use App\Settings\MiscSettings\IpnSuggestSettings;
29+
use Symfony\Component\Cache\Adapter\AdapterInterface;
2730
use Symfony\Component\HttpFoundation\Response;
2831
use App\Entity\Attachments\Attachment;
2932
use App\Entity\Parts\Category;
@@ -54,6 +57,8 @@
5457
use Symfony\Component\Serializer\Encoder\JsonEncoder;
5558
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
5659
use Symfony\Component\Serializer\Serializer;
60+
use Symfony\Contracts\Cache\CacheInterface;
61+
use Symfony\Contracts\Cache\ItemInterface;
5762

5863
/**
5964
* In this controller the endpoints for the typeaheads are collected.
@@ -223,4 +228,21 @@ public function ipnSuggestions(
223228

224229
return new JsonResponse($ipnSuggestions);
225230
}
231+
232+
#[Route(path: '/ai/{platform}/models', name: 'typeahead_ai_models', requirements: ['platform' => '.+'])]
233+
public function aiModels(
234+
AIPlatforms $platform,
235+
AIPlatformRegistry $platformRegistry,
236+
CacheInterface $cache,
237+
): JsonResponse {
238+
239+
$this->denyAccessUnlessGranted('@config.change_system_settings');
240+
241+
$models = $cache->get('ai_models_'.$platform->value, function(ItemInterface $item) use ($platformRegistry, $platform) {
242+
$item->expiresAfter(3600); //Cache for 1 hour
243+
return $platformRegistry->getPlatform($platform)->getModelCatalog()->getModels();
244+
});
245+
246+
return new JsonResponse($models);
247+
}
226248
}

src/Form/Settings/AiModelsType.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
24+
namespace App\Form\Settings;
25+
26+
use Symfony\Component\Form\AbstractType;
27+
use Symfony\Component\Form\Extension\Core\Type\TextType;
28+
use Symfony\Component\Form\FormInterface;
29+
use Symfony\Component\Form\FormView;
30+
use Symfony\Component\OptionsResolver\OptionsResolver;
31+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
32+
33+
/**
34+
* An text input with autocomplete for AI models from the given platform.
35+
* 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.
36+
*/
37+
final class AiModelsType extends AbstractType
38+
{
39+
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
40+
{
41+
}
42+
43+
public function getParent(): string
44+
{
45+
return TextType::class;
46+
}
47+
48+
public function configureOptions(OptionsResolver $resolver): void
49+
{
50+
//The target label of the platform select, which is used to filter the models for the selected platform.
51+
$resolver->setRequired('platform_selector');
52+
$resolver->setAllowedTypes('platform_selector', 'string');
53+
}
54+
55+
public function finishView(FormView $view, FormInterface $form, array $options): void
56+
{
57+
$view->vars['attr']['data-url-template'] = $this->urlGenerator->generate('typeahead_ai_models', ['platform' => '__PLATFORM__']);
58+
$view->vars['attr']['data-controller'] = 'elements--ai-model-autocomplete';
59+
60+
$view->vars['attr']['data-platform-selector'] = $options['platform_selector'];
61+
}
62+
}

src/Form/Settings/AiPlatformChoiceType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@
2828
use Symfony\Component\Form\AbstractType;
2929
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
3030
use Symfony\Component\Form\Extension\Core\Type\EnumType;
31+
use Symfony\Component\Form\FormInterface;
32+
use Symfony\Component\Form\FormView;
3133
use Symfony\Component\OptionsResolver\OptionsResolver;
3234

35+
/**
36+
* 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.
37+
*/
3338
final class AiPlatformChoiceType extends AbstractType
3439
{
3540
public function __construct(private readonly AIPlatformRegistry $platformRegistry)
@@ -49,6 +54,12 @@ public function configureOptions(OptionsResolver $resolver): void
4954
'class' => AIPlatforms::class,
5055
'choices' => $choices,
5156
'required' => false,
57+
'platform_selector_label' => null
5258
]);
5359
}
60+
61+
public function finishView(FormView $view, FormInterface $form, array $options): void
62+
{
63+
$view->vars['attr']['data-platform-selector-label'] = $options['platform_selector_label'] ?? $view->vars['id'].'_label';
64+
}
5465
}

src/Settings/InfoProviderSystem/AIExtractorSettings.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
namespace App\Settings\InfoProviderSystem;
2525

26+
use App\Form\Settings\AiModelsType;
2627
use App\Form\Settings\AiPlatformChoiceType;
2728
use App\Services\AI\AIPlatforms;
2829
use App\Settings\SettingsIcon;
@@ -36,15 +37,18 @@
3637
#[SettingsIcon("fa-robot")]
3738
class AIExtractorSettings
3839
{
40+
private const MODEL_SELECTOR_LABEL = 'ai_extractor';
41+
3942
use SettingsTrait;
4043

4144
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), description: new TM("settings.ips.ai_extractor.ai_platform.help"),
42-
formType: AiPlatformChoiceType::class,
45+
formType: AiPlatformChoiceType::class, formOptions: ['platform_selector_label' => self::MODEL_SELECTOR_LABEL],
4346
envVar: "string:PROVIDER_AI_EXTRACTOR_API_KEY", envVarMode: EnvVarMode::OVERWRITE
4447
)]
4548
public ?AIPlatforms $platform = null;
4649

4750
#[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.description"),
51+
formType: AiModelsType::class, formOptions: ['platform_selector' => self::MODEL_SELECTOR_LABEL],
4852
envVar: "string:PROVIDER_AI_EXTRACTOR_MODEL", envVarMode: EnvVarMode::OVERWRITE
4953
)]
5054
public string $model = 'z-ai/glm-4.7';

0 commit comments

Comments
 (0)