From 3f78e2e9f118383c0f00a2f45129191a02b064d5 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Mon, 9 Jun 2025 15:42:22 +0200 Subject: [PATCH] first attempt to use an ai based search --- composer.json | 4 +- composer.lock | 87 ++- config/bundles.php | 1 + config/packages/web_profiler.yaml | 11 + config/routes/web_profiler.yaml | 8 + config/services.yaml | 10 +- migrations/Version20250609005825.php | 29 + src/Application/Commands/LoadFixtures.php | 684 ++++++++++++++---- .../DataCollector/AiChatDataCollector.php | 39 + src/Domain/AI/AIClient.php | 53 +- .../ContentManagement/CarPropertyEmbedder.php | 22 +- .../Model/Battery/BatteryProperties.php | 22 - src/Domain/Model/CarPropertyCollection.php | 55 -- src/Domain/Model/CarPropertyType.php | 95 --- src/Domain/Model/{ => Cars}/Brand.php | 3 +- .../Model/{ => Cars}/BrandCollection.php | 2 +- src/Domain/Model/{ => Cars}/CarModel.php | 2 +- .../Model/{ => Cars}/CarModelCollection.php | 2 +- src/Domain/Model/{ => Cars}/CarProperty.php | 12 +- .../Model/Cars/CarPropertyCollection.php | 107 +++ .../CarPropertyValues/V1/Acceleration.php | 21 + .../V1/AverageConsumption.php | 17 + .../V1/Battery/BatteryCapacity.php | 19 + .../V1/Battery/BatteryType.php | 19 + .../V1}/Battery/CellChemistry.php | 4 +- .../CarPropertyValues/V1/CarPropertyValue.php | 15 + .../CarPropertyValues/V1/CatalogPrice.php | 17 + .../V1}/Charging/ChargeCurve.php | 8 +- .../V1/Charging/ChargeTimeProperties.php | 50 ++ .../V1/Charging/ChargingConnectivity.php | 37 + .../V1/Charging/ChargingSpeed.php | 30 + .../V1}/Charging/ConnectorType.php | 0 .../Cars/CarPropertyValues/V1/MotorPower.php | 17 + .../Cars/CarPropertyValues/V1/Production.php | 30 + .../CarPropertyValues/V1}/Range/NefzRange.php | 5 + .../CarPropertyValues/V1}/Range/RealRange.php | 0 .../CarPropertyValues/V1}/Range/WltpRange.php | 5 + .../V1/RangeSpecification.php | 35 + .../Cars/CarPropertyValues/V1/TopSpeed.php | 17 + src/Domain/Model/{ => Cars}/CarRevision.php | 2 +- .../{ => Cars}/CarRevisionCollection.php | 2 +- .../Model/Charging/ChargeTimeProperties.php | 17 - .../Model/Charging/ChargingConnectivity.php | 16 - .../Model/Charging/ChargingProperties.php | 16 - src/Domain/Model/Value/Acceleration.php | 21 - src/Domain/Model/Value/ChargingSpeed.php | 27 - src/Domain/Repository/BrandRepository.php | 6 +- src/Domain/Repository/CarModelRepository.php | 6 +- .../Repository/CarPropertyRepository.php | 10 +- .../Repository/CarRevisionRepository.php | 4 +- src/Domain/Repository/EmbeddingRepository.php | 4 +- .../Repository/Loader/FullCarLoader.php | 83 +++ src/Domain/Repository/Loader/FullCarModel.php | 40 + .../Repository/Loader/FullCarRevision.php | 45 ++ src/Domain/Search/AiTileEngine.php | 121 ++++ src/Domain/Search/Engine.php | 24 +- src/Domain/Search/TileBuilder.php | 58 -- .../TileBuilder/AccelerationTileBuilder.php | 23 + .../TileBuilder/AvailabilityTileBuilder.php | 26 + .../Search/TileBuilder/BatteryTileBuilder.php | 28 + .../TileBuilder/ChargingTileBuilder.php | 23 + .../TileBuilder/ConsumptionTileBuilder.php | 21 + .../TileBuilder/DrivetrainTileBuilder.php | 20 + .../Search/TileBuilder/PowerTileBuilder.php | 20 + .../Search/TileBuilder/PriceTileBuilder.php | 23 + .../ProductionPeriodTileBuilder.php | 30 + .../Search/TileBuilder/RangeTileBuilder.php | 20 + src/Domain/Search/TileBuilder/TileBuilder.php | 19 + .../TileBuilder/TileBuilderProvider.php | 34 + .../TileBuilder/TopSpeedTileBuilder.php | 21 + .../Search/TileBuilders/CarTileBuilder.php | 61 -- src/Domain/Search/TileCollection.php | 5 + src/Domain/Search/Tiles/AccelerationTile.php | 2 +- src/Domain/Search/Tiles/AvailabilityTile.php | 4 +- src/Domain/Search/Tiles/CarTile.php | 2 +- src/Domain/Search/Tiles/ChargingTile.php | 2 +- src/Domain/Search/Tiles/SectionTile.php | 5 +- src/Domain/Search/Tiles/SubSectionTile.php | 8 +- .../Search/View/CarRevisionComparison.php | 31 + src/Domain/Search/View/FullBrandView.php | 33 + src/Domain/Search/View/FullCarModelView.php | 93 +++ .../Search/View/FullCarRevisionView.php | 81 +++ .../Search/View/SpecificCarPropertyView.php | 72 ++ src/Domain/Search/View/View.php | 27 + src/Domain/Search/View/ViewProvider.php | 43 ++ .../BrandRepository/ModelMapper.php | 2 +- .../BrandRepository/SqlBrandRepository.php | 6 +- .../CarModelRepository/ModelMapper.php | 4 +- .../SqlCarModelRepository.php | 6 +- .../SqlCarPropertyRepository.php | 78 +- .../CarRevisionRepository/ModelMapper.php | 2 +- .../SqlCarRevisionRepository.php | 4 +- .../SqlEmbeddingRepository.php | 8 +- symfony.lock | 13 + templates/base.html.twig | 2 +- templates/profiler/ai-chat.html.twig | 39 + templates/result/tiles/car.html.twig | 4 + .../result/tiles/productionperiod.html.twig | 49 +- templates/result/tiles/section.html.twig | 4 +- templates/result/tiles/subsection.html.twig | 3 +- 100 files changed, 2406 insertions(+), 691 deletions(-) create mode 100644 config/packages/web_profiler.yaml create mode 100644 config/routes/web_profiler.yaml create mode 100644 migrations/Version20250609005825.php create mode 100644 src/Application/DataCollector/AiChatDataCollector.php delete mode 100644 src/Domain/Model/Battery/BatteryProperties.php delete mode 100644 src/Domain/Model/CarPropertyCollection.php delete mode 100644 src/Domain/Model/CarPropertyType.php rename src/Domain/Model/{ => Cars}/Brand.php (77%) rename src/Domain/Model/{ => Cars}/BrandCollection.php (89%) rename src/Domain/Model/{ => Cars}/CarModel.php (88%) rename src/Domain/Model/{ => Cars}/CarModelCollection.php (90%) rename src/Domain/Model/{ => Cars}/CarProperty.php (65%) create mode 100644 src/Domain/Model/Cars/CarPropertyCollection.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Acceleration.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/AverageConsumption.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryCapacity.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryType.php rename src/Domain/Model/{ => Cars/CarPropertyValues/V1}/Battery/CellChemistry.php (73%) create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/CarPropertyValue.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/CatalogPrice.php rename src/Domain/Model/{ => Cars/CarPropertyValues/V1}/Charging/ChargeCurve.php (53%) create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeTimeProperties.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingConnectivity.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingSpeed.php rename src/Domain/Model/{ => Cars/CarPropertyValues/V1}/Charging/ConnectorType.php (100%) create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/MotorPower.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/Production.php rename src/Domain/Model/{ => Cars/CarPropertyValues/V1}/Range/NefzRange.php (66%) rename src/Domain/Model/{ => Cars/CarPropertyValues/V1}/Range/RealRange.php (100%) rename src/Domain/Model/{ => Cars/CarPropertyValues/V1}/Range/WltpRange.php (66%) create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/RangeSpecification.php create mode 100644 src/Domain/Model/Cars/CarPropertyValues/V1/TopSpeed.php rename src/Domain/Model/{ => Cars}/CarRevision.php (91%) rename src/Domain/Model/{ => Cars}/CarRevisionCollection.php (90%) delete mode 100644 src/Domain/Model/Charging/ChargeTimeProperties.php delete mode 100644 src/Domain/Model/Charging/ChargingConnectivity.php delete mode 100644 src/Domain/Model/Charging/ChargingProperties.php delete mode 100644 src/Domain/Model/Value/Acceleration.php delete mode 100644 src/Domain/Model/Value/ChargingSpeed.php create mode 100644 src/Domain/Repository/Loader/FullCarLoader.php create mode 100644 src/Domain/Repository/Loader/FullCarModel.php create mode 100644 src/Domain/Repository/Loader/FullCarRevision.php create mode 100644 src/Domain/Search/AiTileEngine.php delete mode 100644 src/Domain/Search/TileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/AccelerationTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/AvailabilityTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/BatteryTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/ChargingTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/ConsumptionTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/DrivetrainTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/PowerTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/PriceTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/ProductionPeriodTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/RangeTileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/TileBuilder.php create mode 100644 src/Domain/Search/TileBuilder/TileBuilderProvider.php create mode 100644 src/Domain/Search/TileBuilder/TopSpeedTileBuilder.php delete mode 100644 src/Domain/Search/TileBuilders/CarTileBuilder.php create mode 100644 src/Domain/Search/View/CarRevisionComparison.php create mode 100644 src/Domain/Search/View/FullBrandView.php create mode 100644 src/Domain/Search/View/FullCarModelView.php create mode 100644 src/Domain/Search/View/FullCarRevisionView.php create mode 100644 src/Domain/Search/View/SpecificCarPropertyView.php create mode 100644 src/Domain/Search/View/View.php create mode 100644 src/Domain/Search/View/ViewProvider.php create mode 100644 templates/profiler/ai-chat.html.twig diff --git a/composer.json b/composer.json index 076cc58..2fc0bba 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,9 @@ "phpstan/phpstan-symfony": "^2.0", "symfony/debug-bundle": "7.2.*", "symfony/maker-bundle": "^1.0", - "symfony/var-dumper": "7.2.*" + "symfony/stopwatch": "^7.3", + "symfony/var-dumper": "7.2.*", + "symfony/web-profiler-bundle": "^7.3" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 7e22d66..67e5338 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aff00a8d8bd55186f046d02ac6fd7c4d", + "content-hash": "e4da23c3811aae55314b0e018026605a", "packages": [ { "name": "composer/semver", @@ -4572,6 +4572,91 @@ ], "time": "2025-05-15T09:04:05+00:00" }, + { + "name": "symfony/web-profiler-bundle", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "a22b7e4a744820a56f1bafa830f2c72a2ba0913c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/a22b7e4a744820a56f1bafa830f2c72a2ba0913c", + "reference": "a22b7e4a744820a56f1bafa830f2c72a2ba0913c", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^7.3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/form": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/serializer": "<7.2", + "symfony/workflow": "<7.3" + }, + "require-dev": { + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-02T05:30:54+00:00" + }, { "name": "symfony/yaml", "version": "v7.2.6", diff --git a/config/bundles.php b/config/bundles.php index 7d41ca9..c488ad8 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -7,4 +7,5 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml new file mode 100644 index 0000000..1e039b7 --- /dev/null +++ b/config/packages/web_profiler.yaml @@ -0,0 +1,11 @@ +when@dev: + web_profiler: + toolbar: true + + framework: + profiler: + collect_serializer_data: true + +when@test: + framework: + profiler: { collect: false } diff --git a/config/routes/web_profiler.yaml b/config/routes/web_profiler.yaml new file mode 100644 index 0000000..b3b7b4b --- /dev/null +++ b/config/routes/web_profiler.yaml @@ -0,0 +1,8 @@ +when@dev: + web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.php' + prefix: /_wdt + + web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.php' + prefix: /_profiler diff --git a/config/services.yaml b/config/services.yaml index 12e943d..b43c51c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,4 +14,12 @@ services: App\Domain\AI\AIClient: arguments: - $apiKey: '%env(OPENAI_API_KEY)%' \ No newline at end of file + $apiKey: '%env(OPENAI_API_KEY)%' + + App\Application\DataCollector\AiChatDataCollector: + arguments: + $aiClient: '@App\Domain\AI\AIClient' + tags: + - { name: data_collector, id: 'ai_chat', template: 'profiler/ai-chat.html.twig' } + + App\Domain\Search\Engine: '@App\Domain\Search\AiTileEngine' \ No newline at end of file diff --git a/migrations/Version20250609005825.php b/migrations/Version20250609005825.php new file mode 100644 index 0000000..05ca2cf --- /dev/null +++ b/migrations/Version20250609005825.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE car_properties DROP COLUMN type'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE car_properties ADD COLUMN type VARCHAR(255) NOT NULL'); + } +} diff --git a/src/Application/Commands/LoadFixtures.php b/src/Application/Commands/LoadFixtures.php index 5595f2d..2d8105c 100644 --- a/src/Application/Commands/LoadFixtures.php +++ b/src/Application/Commands/LoadFixtures.php @@ -3,9 +3,9 @@ namespace App\Application\Commands; use App\Domain\ContentManagement\CarPropertyEmbedder; -use App\Domain\Model\Brand; -use App\Domain\Model\CarModel; -use App\Domain\Model\CarRevision; +use App\Domain\Model\Cars\Brand; +use App\Domain\Model\Cars\CarModel; +use App\Domain\Model\Cars\CarRevision; use App\Domain\Model\Image; use App\Domain\Model\Value\Date; use App\Domain\Model\Value\Price; @@ -14,12 +14,23 @@ use App\Domain\Model\Id\BrandId; use App\Domain\Model\Id\CarModelId; use App\Domain\Model\Id\CarRevisionId; use App\Domain\Model\Id\CarPropertyId; -use App\Domain\Model\Battery\CellChemistry; -use App\Domain\Model\CarProperty; -use App\Domain\Model\CarPropertyType; +use App\Domain\Model\Cars\CarProperty; +use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue; +use App\Domain\Model\Cars\CarPropertyValues\V1\Production; use App\Domain\Model\Value\Energy; use App\Domain\Model\Value\Power; -use App\Domain\Model\Value\Acceleration; +use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration; +use App\Domain\Model\Cars\CarPropertyValues\V1\AverageConsumption; +use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryCapacity; +use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryType; +use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\CellChemistry; +use App\Domain\Model\Cars\CarPropertyValues\V1\CatalogPrice; +use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargingSpeed; +use App\Domain\Model\Cars\CarPropertyValues\V1\MotorPower; +use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification; +use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed; +use App\Domain\Model\Range\NefzRange; +use App\Domain\Model\Range\WltpRange; use App\Domain\Model\Value\Speed; use App\Domain\Model\Value\Consumption; use App\Domain\Model\Value\Range; @@ -145,15 +156,14 @@ class LoadFixtures extends Command $this->carRevisionRepository->save($carRevision); - foreach ($revisionFixture['properties'] as $propertyData) { + foreach ($revisionFixture['properties'] as $propertyValue) { $property = new CarProperty( CarPropertyId::generate(), $carRevision->carRevisionId, - $propertyData['type'], - $propertyData['value'] + $propertyValue ); - $this->carPropertyEmbedder->createPersistedEmbedding($property, $carRevision, $brand); + $this->carPropertyEmbedder->createPersistedEmbedding($property, $carRevision, $carModel, $brand); $this->carPropertyRepository->save($property); } @@ -170,11 +180,183 @@ class LoadFixtures extends Command } /** - * @return array}>}>}> + * @return array}>}>}> */ private function getFixtures(): array { return [ + [ + 'brand' => 'Skoda', + 'models' => [ + [ + 'model' => 'Elroq', + 'revisions' => [ + [ + 'revision' => '85', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(10, 1, 2024)), + new MotorPower(new Power(210)), // Estimated from 0-100 time and specs + new Acceleration(6.6), + new TopSpeed(new Speed(180)), // Estimated typical for this class + new AverageConsumption(new Consumption(new Energy(17.1))), + new BatteryCapacity(new Energy(77.0), new Energy(82.0)), // Usable/gross capacity + new BatteryType(CellChemistry::LithiumIronPhosphate, 'MEB Platform', 'CATL'), + new ChargingSpeed(new Power(120), new Power(120)), + new RangeSpecification(new NefzRange(new Range(493)), new WltpRange(new Range(450))), + new CatalogPrice(new Price(43900, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Mercedes-Benz', + 'models' => [ + [ + 'model' => 'CLA', + 'revisions' => [ + [ + 'revision' => '250+', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(5, 1, 2025)), + new MotorPower(new Power(200)), // Estimated from acceleration + new Acceleration(6.7), + new TopSpeed(new Speed(210)), // Estimated + new AverageConsumption(new Consumption(new Energy(15.0))), + new BatteryCapacity(new Energy(85.0), new Energy(90.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'MMA Platform', 'CATL'), + new ChargingSpeed(new Power(170), new Power(170)), + new RangeSpecification(new NefzRange(new Range(700)), new WltpRange(new Range(565))), + new CatalogPrice(new Price(55859, Currency::euro())), + ] + ] + ] + ], + [ + 'model' => 'EQS', + 'revisions' => [ + [ + 'revision' => '450+', + 'image' => new Image(externalPublicUrl: 'https://ev-database.org/img/auto/Mercedes_EQS_2024/Mercedes_EQS_2024-01@2x.jpg'), + 'properties' => [ + new Production(productionBegin: new Date(1, 1, 2021)), + new MotorPower(new Power(245)), + new Acceleration(6.2), + new TopSpeed(new Speed(210)), + new AverageConsumption(new Consumption(new Energy(15.7))), + new BatteryCapacity(new Energy(90.0), new Energy(107.8)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'EVA Platform', 'CATL'), + new ChargingSpeed(new Power(200), new Power(200)), + new RangeSpecification(new NefzRange(new Range(770)), new WltpRange(new Range(756))), + new CatalogPrice(new Price(106374, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Kia', + 'models' => [ + [ + 'model' => 'EV3', + 'revisions' => [ + [ + 'revision' => '81.4 kWh', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(8, 1, 2024)), + new MotorPower(new Power(150)), // Estimated from acceleration + new Acceleration(7.7), + new TopSpeed(new Speed(170)), // Estimated + new AverageConsumption(new Consumption(new Energy(17.1))), + new BatteryCapacity(new Energy(78.0), new Energy(81.4)), + new BatteryType(CellChemistry::LithiumIronPhosphate, 'E-GMP Platform', 'CATL'), + new ChargingSpeed(new Power(105), new Power(105)), + new RangeSpecification(new NefzRange(new Range(472)), new WltpRange(new Range(455))), + new CatalogPrice(new Price(41390, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Smart', + 'models' => [ + [ + 'model' => '#5', + 'revisions' => [ + [ + 'revision' => 'Premium', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(5, 1, 2025)), + new MotorPower(new Power(250)), // Estimated from acceleration + new Acceleration(6.5), + new TopSpeed(new Speed(180)), // Estimated + new AverageConsumption(new Consumption(new Energy(20.2))), + new BatteryCapacity(new Energy(94.0), new Energy(100.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'SEA Platform', 'CATL'), + new ChargingSpeed(new Power(230), new Power(230)), + new RangeSpecification(new NefzRange(new Range(579)), new WltpRange(new Range(465))), + new CatalogPrice(new Price(55400, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Volkswagen', + 'models' => [ + [ + 'model' => 'ID.7', + 'revisions' => [ + [ + 'revision' => 'Pro', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(8, 1, 2023)), + new MotorPower(new Power(210)), // Estimated from acceleration + new Acceleration(6.5), + new TopSpeed(new Speed(180)), // Estimated + new AverageConsumption(new Consumption(new Energy(16.2))), + new BatteryCapacity(new Energy(77.0), new Energy(82.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'MEB Platform', 'LG Energy Solution'), + new ChargingSpeed(new Power(170), new Power(170)), + new RangeSpecification(new NefzRange(new Range(621)), new WltpRange(new Range(475))), + new CatalogPrice(new Price(53995, Currency::euro())), + ] + ] + ] + ], + [ + 'model' => 'ID.4', + 'revisions' => [ + [ + 'revision' => 'Pro', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(1, 1, 2020)), + new MotorPower(new Power(150)), + new Acceleration(8.5), + new TopSpeed(new Speed(160)), + new AverageConsumption(new Consumption(new Energy(16.3))), + new BatteryCapacity(new Energy(77.0), new Energy(82.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'MEB Platform', 'LG Energy Solution'), + new ChargingSpeed(new Power(125), new Power(125)), + new RangeSpecification(new NefzRange(new Range(549)), new WltpRange(new Range(520))), + new CatalogPrice(new Price(51515, Currency::euro())), + ] + ] + ] + ] + ] + ], [ 'brand' => 'Tesla', 'models' => [ @@ -185,20 +367,16 @@ class LoadFixtures extends Command 'revision' => 'Plaid', 'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-S-Main-Hero-Desktop-LHD.jpg'), 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(750)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(2.1)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(322)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.3))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(95.0)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(100.0)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => '4680'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'Tesla'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(250)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(628)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(652)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(129990, Currency::euro())], + new Production(productionBegin: new Date(1, 1, 2021)), + new MotorPower(new Power(750)), + new Acceleration(2.1), + new TopSpeed(new Speed(322)), + new AverageConsumption(new Consumption(new Energy(19.3))), + new BatteryCapacity(new Energy(95.0), new Energy(100.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, '4680', 'Tesla'), + new ChargingSpeed(new Power(250), new Power(250)), + new RangeSpecification(new NefzRange(new Range(652)), new WltpRange(new Range(628))), + new CatalogPrice(new Price(129990, Currency::euro())), ] ] ] @@ -210,20 +388,16 @@ class LoadFixtures extends Command 'revision' => 'Long Range', 'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-3-Main-Hero-Desktop-LHD.jpg'), 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2020)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(366)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(4.4)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(233)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(14.9))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(75.0)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(82.0)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => '2170'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'Tesla'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(250)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(602)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(614)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(49990, Currency::euro())], + new Production(productionBegin: new Date(1, 1, 2020)), + new MotorPower(new Power(366)), + new Acceleration(4.4), + new TopSpeed(new Speed(233)), + new AverageConsumption(new Consumption(new Energy(14.9))), + new BatteryCapacity(new Energy(75.0), new Energy(82.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, '2170', 'Tesla'), + new ChargingSpeed(new Power(250), new Power(250)), + new RangeSpecification(new NefzRange(new Range(614)), new WltpRange(new Range(602))), + new CatalogPrice(new Price(129990, Currency::euro())), ] ] ] @@ -238,22 +412,18 @@ class LoadFixtures extends Command 'revisions' => [ [ 'revision' => 'xDrive50', - 'image' => new Image(externalPublicUrl: 'https://www.bmw.de/content/dam/bmw/common/all-models/x-series/ix/2021/highlights/bmw-ix-sp-desktop.jpg'), + 'image' => new Image(externalPublicUrl: 'https://cdn.motor1.com/images/mgl/N7Lgn/s1/bmw-ix-xdrive50-2021.jpg'), 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(385)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(4.6)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(200)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.8))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(71.2)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(76.6)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'BMW Gen5'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'CATL'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(195)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(630)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(680)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(77300, Currency::euro())], + new Production(productionBegin: new Date(1, 1, 2021)), + new MotorPower(new Power(385)), + new Acceleration(4.6), + new TopSpeed(new Speed(200)), + new AverageConsumption(new Consumption(new Energy(19.8))), + new BatteryCapacity(new Energy(71.2), new Energy(76.6)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'BMW Gen5', 'CATL'), + new ChargingSpeed(new Power(195), new Power(195)), + new RangeSpecification(new NefzRange(new Range(680)), new WltpRange(new Range(630))), + new CatalogPrice(new Price(77300, Currency::euro())), ] ] ] @@ -268,82 +438,18 @@ class LoadFixtures extends Command 'revisions' => [ [ 'revision' => 'RS', - 'image' => new Image(externalPublicUrl: 'https://www.audi.de/content/dam/nemo/models/e-tron-gt/my-2021/1920x1080-gallery/1920x1080_AudiRS_e-tron_GT_19.jpg'), + 'image' => new Image(externalPublicUrl: 'https://ev-database.org/img/auto/Audi_e-tron_GT_RS/Audi_e-tron_GT_RS-02@2x.jpg'), 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(475)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(3.3)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(250)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.6))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(83.7)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(93.4)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'PPE Platform'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(270)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(472)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(487)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(142900, Currency::euro())], - ] - ] - ] - ] - ] - ], - [ - 'brand' => 'Mercedes-Benz', - 'models' => [ - [ - 'model' => 'EQS', - 'revisions' => [ - [ - 'revision' => '450+', - 'image' => new Image(externalPublicUrl: 'https://www.mercedes-benz.de/content/germany/de/mercedes-benz/vehicles/passenger-cars/eqs/sedan/overview/_jcr_content/root/responsivegrid/simple_stage.component.damq2.3318859008536.jpg'), - 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(245)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(6.2)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(210)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(15.7))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(90.0)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(107.8)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'EVA Platform'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'CATL'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(200)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(756)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(770)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(106374, Currency::euro())], - ] - ] - ] - ] - ] - ], - [ - 'brand' => 'Volkswagen', - 'models' => [ - [ - 'model' => 'ID.4', - 'revisions' => [ - [ - 'revision' => 'Pro', - 'image' => new Image(externalPublicUrl: 'https://www.volkswagen.de/content/dam/vw-ngw/vw_pkw/importers/de/models/id-4/gallery/id4-gallery-exterior-01-16x9.jpg'), - 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2020)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(150)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(8.5)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(160)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(16.3))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(77.0)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(82.0)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'MEB Platform'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(125)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(520)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(549)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(51515, Currency::euro())], + new Production(productionBegin: new Date(1, 1, 2021)), + new MotorPower(new Power(475)), + new Acceleration(3.3), + new TopSpeed(new Speed(250)), + new AverageConsumption(new Consumption(new Energy(19.6))), + new BatteryCapacity(new Energy(83.7), new Energy(93.4)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'PPE Platform', 'LG Energy Solution'), + new ChargingSpeed(new Power(270), new Power(270)), + new RangeSpecification(new NefzRange(new Range(487)), new WltpRange(new Range(472))), + new CatalogPrice(new Price(142900, Currency::euro())), ] ] ] @@ -358,22 +464,310 @@ class LoadFixtures extends Command 'revisions' => [ [ 'revision' => 'Turbo S', - 'image' => new Image(externalPublicUrl: 'https://www.porsche.com/germany/models/taycan/taycan-models/turbo-s/_jcr_content/par/twocolumnlayout/par_left/image.transform.porsche-model-desktop-xl.jpg'), + 'image' => null, 'properties' => [ - ['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2019)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(560)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(2.8)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(260)], - ['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(23.7))], - ['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(83.7)], - ['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(93.4)], - ['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide], - ['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'J1 Platform'], - ['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'], - ['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(270)], - ['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(440)], - ['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(452)], - ['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(185456, Currency::euro())], + new Production(productionBegin: new Date(1, 1, 2019)), + new MotorPower(new Power(560)), + new Acceleration(2.8), + new TopSpeed(new Speed(260)), + new AverageConsumption(new Consumption(new Energy(23.7))), + new BatteryCapacity(new Energy(83.7), new Energy(93.4)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'J1 Platform', 'LG Energy Solution'), + new ChargingSpeed(new Power(270), new Power(270)), + new RangeSpecification(new NefzRange(new Range(452)), new WltpRange(new Range(440))), + new CatalogPrice(new Price(185456, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Ford', + 'models' => [ + [ + 'model' => 'Mustang Mach-E', + 'revisions' => [ + [ + 'revision' => 'Extended Range RWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(1, 1, 2021)), + new MotorPower(new Power(269)), + new Acceleration(7.0), + new TopSpeed(new Speed(180)), + new AverageConsumption(new Consumption(new Energy(18.7))), + new BatteryCapacity(new Energy(88.0), new Energy(98.8)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'GE2 Platform', 'LG Energy Solution'), + new ChargingSpeed(new Power(150), new Power(150)), + new RangeSpecification(new NefzRange(new Range(610)), new WltpRange(new Range(600))), + new CatalogPrice(new Price(72900, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Hyundai', + 'models' => [ + [ + 'model' => 'IONIQ 6', + 'revisions' => [ + [ + 'revision' => '77.4 kWh RWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(7, 1, 2022)), + new MotorPower(new Power(229)), + new Acceleration(7.4), + new TopSpeed(new Speed(185)), + new AverageConsumption(new Consumption(new Energy(14.3))), + new BatteryCapacity(new Energy(77.4), new Energy(84.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'E-GMP Platform', 'SK Innovation'), + new ChargingSpeed(new Power(233), new Power(233)), + new RangeSpecification(new NefzRange(new Range(614)), new WltpRange(new Range(614))), + new CatalogPrice(new Price(53900, Currency::euro())), + ] + ] + ] + ], + [ + 'model' => 'IONIQ 5', + 'revisions' => [ + [ + 'revision' => '77.4 kWh AWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(2, 1, 2021)), + new MotorPower(new Power(239)), + new Acceleration(5.2), + new TopSpeed(new Speed(185)), + new AverageConsumption(new Consumption(new Energy(17.7))), + new BatteryCapacity(new Energy(77.4), new Energy(84.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'E-GMP Platform', 'SK Innovation'), + new ChargingSpeed(new Power(233), new Power(233)), + new RangeSpecification(new NefzRange(new Range(507)), new WltpRange(new Range(507))), + new CatalogPrice(new Price(59900, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Polestar', + 'models' => [ + [ + 'model' => '2', + 'revisions' => [ + [ + 'revision' => 'Long Range Single Motor', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(1, 1, 2020)), + new MotorPower(new Power(170)), + new Acceleration(7.4), + new TopSpeed(new Speed(160)), + new AverageConsumption(new Consumption(new Energy(16.5))), + new BatteryCapacity(new Energy(78.0), new Energy(82.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'CMA Platform', 'CATL'), + new ChargingSpeed(new Power(150), new Power(150)), + new RangeSpecification(new NefzRange(new Range(540)), new WltpRange(new Range(635))), + new CatalogPrice(new Price(51400, Currency::euro())), + ] + ] + ] + ], + [ + 'model' => '3', + 'revisions' => [ + [ + 'revision' => 'Long Range RWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(10, 1, 2023)), + new MotorPower(new Power(220)), + new Acceleration(7.0), + new TopSpeed(new Speed(180)), + new AverageConsumption(new Consumption(new Energy(15.8))), + new BatteryCapacity(new Energy(78.0), new Energy(84.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'SPA2 Platform', 'CATL'), + new ChargingSpeed(new Power(250), new Power(250)), + new RangeSpecification(new NefzRange(new Range(610)), new WltpRange(new Range(610))), + new CatalogPrice(new Price(73400, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Volvo', + 'models' => [ + [ + 'model' => 'EX30', + 'revisions' => [ + [ + 'revision' => 'Extended Range Single Motor', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(6, 1, 2023)), + new MotorPower(new Power(200)), + new Acceleration(5.3), + new TopSpeed(new Speed(180)), + new AverageConsumption(new Consumption(new Energy(14.8))), + new BatteryCapacity(new Energy(69.0), new Energy(72.0)), + new BatteryType(CellChemistry::LithiumIronPhosphate, 'SEA Platform', 'CATL'), + new ChargingSpeed(new Power(153), new Power(153)), + new RangeSpecification(new NefzRange(new Range(476)), new WltpRange(new Range(476))), + new CatalogPrice(new Price(41700, Currency::euro())), + ] + ] + ] + ], + [ + 'model' => 'XC40', + 'revisions' => [ + [ + 'revision' => 'Extended Range', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(10, 1, 2021)), + new MotorPower(new Power(170)), + new Acceleration(7.3), + new TopSpeed(new Speed(160)), + new AverageConsumption(new Consumption(new Energy(18.8))), + new BatteryCapacity(new Energy(78.0), new Energy(82.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'CMA Platform', 'CATL'), + new ChargingSpeed(new Power(150), new Power(150)), + new RangeSpecification(new NefzRange(new Range(425)), new WltpRange(new Range(425))), + new CatalogPrice(new Price(52950, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Genesis', + 'models' => [ + [ + 'model' => 'GV70', + 'revisions' => [ + [ + 'revision' => 'Electrified AWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(6, 1, 2022)), + new MotorPower(new Power(360)), + new Acceleration(4.2), + new TopSpeed(new Speed(225)), + new AverageConsumption(new Consumption(new Energy(20.9))), + new BatteryCapacity(new Energy(77.4), new Energy(84.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'E-GMP Platform', 'SK Innovation'), + new ChargingSpeed(new Power(233), new Power(233)), + new RangeSpecification(new NefzRange(new Range(455)), new WltpRange(new Range(455))), + new CatalogPrice(new Price(74800, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Lucid', + 'models' => [ + [ + 'model' => 'Air', + 'revisions' => [ + [ + 'revision' => 'Dream Edition Range', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(10, 1, 2021)), + new MotorPower(new Power(696)), + new Acceleration(2.5), + new TopSpeed(new Speed(270)), + new AverageConsumption(new Consumption(new Energy(13.8))), + new BatteryCapacity(new Energy(113.0), new Energy(118.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'LCAP Platform', 'Samsung SDI'), + new ChargingSpeed(new Power(300), new Power(300)), + new RangeSpecification(new NefzRange(new Range(883)), new WltpRange(new Range(883))), + new CatalogPrice(new Price(218000, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'Nissan', + 'models' => [ + [ + 'model' => 'Ariya', + 'revisions' => [ + [ + 'revision' => '87 kWh FWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(7, 1, 2022)), + new MotorPower(new Power(178)), + new Acceleration(7.5), + new TopSpeed(new Speed(160)), + new AverageConsumption(new Consumption(new Energy(17.6))), + new BatteryCapacity(new Energy(87.0), new Energy(91.0)), + new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'CMF-EV Platform', 'Envision AESC'), + new ChargingSpeed(new Power(130), new Power(130)), + new RangeSpecification(new NefzRange(new Range(533)), new WltpRange(new Range(533))), + new CatalogPrice(new Price(59990, Currency::euro())), + ] + ] + ] + ] + ] + ], + [ + 'brand' => 'BYD', + 'models' => [ + [ + 'model' => 'Tang', + 'revisions' => [ + [ + 'revision' => '86.4 kWh AWD', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(1, 1, 2023)), + new MotorPower(new Power(380)), + new Acceleration(4.6), + new TopSpeed(new Speed(180)), + new AverageConsumption(new Consumption(new Energy(17.9))), + new BatteryCapacity(new Energy(86.4), new Energy(89.0)), + new BatteryType(CellChemistry::LithiumIronPhosphate, 'e-Platform 3.0', 'BYD'), + new ChargingSpeed(new Power(170), new Power(170)), + new RangeSpecification(new NefzRange(new Range(530)), new WltpRange(new Range(530))), + new CatalogPrice(new Price(72990, Currency::euro())), + ] + ] + ] + ], + [ + 'model' => 'Atto 3', + 'revisions' => [ + [ + 'revision' => '60.48 kWh', + 'image' => null, + 'properties' => [ + new Production(productionBegin: new Date(9, 1, 2022)), + new MotorPower(new Power(150)), + new Acceleration(7.3), + new TopSpeed(new Speed(160)), + new AverageConsumption(new Consumption(new Energy(15.4))), + new BatteryCapacity(new Energy(60.48), new Energy(64.0)), + new BatteryType(CellChemistry::LithiumIronPhosphate, 'e-Platform 3.0', 'BYD'), + new ChargingSpeed(new Power(88), new Power(88)), + new RangeSpecification(new NefzRange(new Range(420)), new WltpRange(new Range(420))), + new CatalogPrice(new Price(44490, Currency::euro())), ] ] ] diff --git a/src/Application/DataCollector/AiChatDataCollector.php b/src/Application/DataCollector/AiChatDataCollector.php new file mode 100644 index 0000000..b93f730 --- /dev/null +++ b/src/Application/DataCollector/AiChatDataCollector.php @@ -0,0 +1,39 @@ +data = $this->aiClient->getLog(); + } + + public function getName(): string + { + return 'ai_chat'; + } + + /** + * @return array + */ + public function getLog(): array + { + return $this->data; + } + + public function reset(): void + { + $this->data = []; + } +} \ No newline at end of file diff --git a/src/Domain/AI/AIClient.php b/src/Domain/AI/AIClient.php index b86c7db..9ad6088 100644 --- a/src/Domain/AI/AIClient.php +++ b/src/Domain/AI/AIClient.php @@ -11,6 +11,11 @@ class AIClient { private readonly OpenAI\Client $client; + /** + * @var array + */ + private array $log = []; + public function __construct( private readonly string $apiKey, ) { @@ -20,15 +25,48 @@ class AIClient public function generateText(string $prompt): string { $response = $this->client->chat()->create([ - 'model' => 'gpt-4o-mini', + 'model' => 'gpt-4.1-nano-2025-04-14', 'messages' => [ ['role' => 'user', 'content' => $prompt], ], ]); + $this->log[] = [ + 'prompt' => $prompt, + 'response' => $response->choices[0]->message->content ?? '', + ]; + return $response->choices[0]->message->content ?? ''; } + /** + * @return array + */ + public function generateJson(string $prompt): array + { + $response = $this->client->chat()->create([ + 'model' => 'gpt-4.1-nano', + 'messages' => [ + ['role' => 'user', 'content' => $prompt], + ], + 'response_format' => [ + 'type' => 'json_object', + ], + ]); + + $this->log[] = [ + 'prompt' => $prompt, + 'response' => $response->choices[0]->message->content ?? '', + ]; + + $result = json_decode($response->choices[0]->message->content ?? '', true) ?? []; + if (!is_array($result)) { + throw new \Exception('Invalid JSON response from AI'); + } + + return $result; + } + public function embedTextLarge(string $text): LargeEmbeddingVector { $response = $this->client->embeddings()->create([ @@ -48,4 +86,17 @@ class AIClient return new SmallEmbeddingVector(new Vector($response->embeddings[0]->embedding)); } + + /** + * @return array + */ + public function getLog(): array + { + return $this->log; + } + + public function resetLog(): void + { + $this->log = []; + } } \ No newline at end of file diff --git a/src/Domain/ContentManagement/CarPropertyEmbedder.php b/src/Domain/ContentManagement/CarPropertyEmbedder.php index 4cf98bd..2f1ff40 100644 --- a/src/Domain/ContentManagement/CarPropertyEmbedder.php +++ b/src/Domain/ContentManagement/CarPropertyEmbedder.php @@ -4,12 +4,13 @@ namespace App\Domain\ContentManagement; use App\Domain\AI\AIClient; use App\Domain\Model\Embedding\Embedding; -use App\Domain\Model\Brand; -use App\Domain\Model\CarProperty; -use App\Domain\Model\CarRevision; +use App\Domain\Model\Cars\Brand; +use App\Domain\Model\Cars\CarModel; +use App\Domain\Model\Cars\CarProperty; +use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue; +use App\Domain\Model\Cars\CarRevision; use App\Domain\Model\Id\EmbeddingId; use App\Domain\Repository\EmbeddingRepository; -use Stringable; class CarPropertyEmbedder { @@ -19,16 +20,17 @@ class CarPropertyEmbedder ) { } - public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?Brand $brand): ?Embedding + public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?CarModel $carModel, ?Brand $brand): ?Embedding { - if (!($carProperty->value instanceof Stringable)) { - return null; - } - - $text = $carProperty->type->humanReadable() . ': ' . (string) $carProperty->value; + $text = $carProperty->value->humanReadable(); if ($carRevision !== null) { $text .= ' - ' . $carRevision->name; } + + if ($carModel !== null) { + $text .= ', ' . $carModel->name; + } + if ($brand !== null) { $text .= ', ' . $brand->name; } diff --git a/src/Domain/Model/Battery/BatteryProperties.php b/src/Domain/Model/Battery/BatteryProperties.php deleted file mode 100644 index 777aa00..0000000 --- a/src/Domain/Model/Battery/BatteryProperties.php +++ /dev/null @@ -1,22 +0,0 @@ -usableCapacity->__toString(); - } -} \ No newline at end of file diff --git a/src/Domain/Model/CarPropertyCollection.php b/src/Domain/Model/CarPropertyCollection.php deleted file mode 100644 index 245655e..0000000 --- a/src/Domain/Model/CarPropertyCollection.php +++ /dev/null @@ -1,55 +0,0 @@ -properties[] = $property; - } - - public function count(): int - { - return count($this->properties); - } - - /** - * @return CarProperty[] - */ - public function array(): array - { - return $this->properties; - } - - public function get(CarPropertyType $type): CarPropertyCollection - { - return new CarPropertyCollection(array_filter($this->properties, fn(CarProperty $property) => $property->type === $type)); - } - - public function getOne(CarPropertyType $type): ?CarProperty - { - return array_values($this->get($type)->array())[0] ?? null; - } - - /** - * @param CarPropertyType[] $types - */ - public function hasTypes(array $types): bool - { - foreach ($types as $type) { - if ($this->get($type)->count() === 0) { - return false; - } - } - - return true; - } -} \ No newline at end of file diff --git a/src/Domain/Model/CarPropertyType.php b/src/Domain/Model/CarPropertyType.php deleted file mode 100644 index e2f69c8..0000000 --- a/src/Domain/Model/CarPropertyType.php +++ /dev/null @@ -1,95 +0,0 @@ - 'Name', - self::PRODUCTION_BEGIN => 'Production Begin', - self::PRODUCTION_END => 'Production End', - self::DRIVING_CHARACTERISTICS_POWER => 'Power', - self::DRIVING_CHARACTERISTICS_ACCELERATION => 'Acceleration', - self::DRIVING_CHARACTERISTICS_TOP_SPEED => 'Top Speed', - self::DRIVING_CHARACTERISTICS_CONSUMPTION => 'Consumption', - self::BATTERY_USABLE_CAPACITY => 'Usable Capacity', - self::BATTERY_TOTAL_CAPACITY => 'Total Capacity', - self::BATTERY_CELL_CHEMISTRY => 'Cell Chemistry', - self::BATTERY_MODEL => 'Battery Model', - self::BATTERY_MANUFACTURER => 'Battery Manufacturer', - self::CHARGING_TOP_CHARGING_SPEED => 'Top Charging Speed', - self::CHARGING_CHARGE_CURVE => 'Charge Curve', - self::CHARGING_CHARGE_TIME_PROPERTIES => 'Charge Time Properties', - self::CHARGING_CONNECTIVITY => 'Charging Connectivity', - self::CHARGING_CONNECTOR_TYPES => 'Connector Types', - self::CHARGING_PLUG_AND_CHARGE => 'Charging Plug and Charge', - self::RANGE_WLTP => 'WLTP Range', - self::RANGE_NEFZ => 'NEFZ Range', - self::RANGE_REAL_RANGE_TESTS => 'Real Range Tests', - self::CATALOG_PRICE => 'Catalog Price', - self::CATALOG_PRICE_CURRENCY => 'Catalog Price Currency', - self::CATALOG_PRICE_INCLUDES_VAT => 'Catalog Price Includes VAT', - self::CHARGE_TIME_0_TO_100 => 'Charge Time 0 to 100', - self::CHARGE_TIME_0_TO_70 => 'Charge Time 0 to 70', - self::CHARGE_TIME_10_TO_70 => 'Charge Time 10 to 70', - self::CHARGE_TIME_20_TO_70 => 'Charge Time 20 to 70', - self::CHARGE_TIME_10_TO_80 => 'Charge Time 10 to 80', - self::CHARGE_TIME_20_TO_80 => 'Charge Time 20 to 80', - self::CHARGE_TIME_10_TO_90 => 'Charge Time 10 to 90', - self::CHARGE_TIME_20_TO_90 => 'Charge Time 20 to 90', - self::CHARGING_IS_400V => 'Charging is 400V', - self::CHARGING_IS_800V => 'Charging is 800V', - }; - } -} \ No newline at end of file diff --git a/src/Domain/Model/Brand.php b/src/Domain/Model/Cars/Brand.php similarity index 77% rename from src/Domain/Model/Brand.php rename to src/Domain/Model/Cars/Brand.php index 0163a65..915f883 100644 --- a/src/Domain/Model/Brand.php +++ b/src/Domain/Model/Cars/Brand.php @@ -1,8 +1,9 @@ [] $properties + */ + public function __construct( + private array $properties = [], + ) {} + + /** + * @param CarProperty $property + */ + public function add(CarProperty $property): void + { + $this->properties[] = $property; + } + + public function count(): int + { + return count($this->properties); + } + + /** + * @return CarProperty[] + */ + public function array(): array + { + return $this->properties; + } + + /** + * @return CarPropertyCollection + */ + public function sortByRevision(): CarPropertyCollection + { + $properties = $this->properties; + usort($properties, fn(CarProperty $a, CarProperty $b) => $a->carRevisionId->value <=> $b->carRevisionId->value); + return new CarPropertyCollection($properties); + } + + /** + * @template T of CarPropertyValue + * + * @param class-string $type + * + * @return CarPropertyCollection + */ + public function get(string $type): CarPropertyCollection + { + /** @var CarPropertyCollection $collection */ + $collection = new CarPropertyCollection(array_filter($this->properties, fn(CarProperty $property) => $property->value instanceof $type)); + return $collection; + } + + /** + * @template T of CarPropertyValue + * + * @param class-string $type + * + * @return CarProperty|null + */ + public function getOne(string $type): ?CarProperty + { + return array_values($this->get($type)->array())[0] ?? null; + } + + /** + * @template T of CarPropertyValue + * + * @param class-string $type + * + * @return T|null + */ + public function getOneValue(string $type): mixed + { + $value = $this->getOne($type)?->value; + if ($value === null) { + return null; + } + + return $value; + } + + /** + * @param class-string[] $types + */ + public function hasTypes(array $types): bool + { + foreach ($types as $type) { + if ($this->get($type)->count() === 0) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Acceleration.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Acceleration.php new file mode 100644 index 0000000..827a4a0 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Acceleration.php @@ -0,0 +1,21 @@ +secondsFrom0To100 . ' sec (0-100 km/h)'; + } + + public function seconds(): float + { + return $this->secondsFrom0To100; + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/AverageConsumption.php b/src/Domain/Model/Cars/CarPropertyValues/V1/AverageConsumption.php new file mode 100644 index 0000000..5da711e --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/AverageConsumption.php @@ -0,0 +1,17 @@ +consumption->__toString(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryCapacity.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryCapacity.php new file mode 100644 index 0000000..ccad7a0 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryCapacity.php @@ -0,0 +1,19 @@ +usableCapacity->__toString() . ", total capacity: " . $this->totalCapacity->__toString(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryType.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryType.php new file mode 100644 index 0000000..586a357 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/BatteryType.php @@ -0,0 +1,19 @@ +chemistry->value . ' ' . $this->model . ' ' . $this->manufacturer; + } +} \ No newline at end of file diff --git a/src/Domain/Model/Battery/CellChemistry.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/CellChemistry.php similarity index 73% rename from src/Domain/Model/Battery/CellChemistry.php rename to src/Domain/Model/Cars/CarPropertyValues/V1/Battery/CellChemistry.php index 61573f9..31c47a3 100644 --- a/src/Domain/Model/Battery/CellChemistry.php +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Battery/CellChemistry.php @@ -1,8 +1,6 @@ humanReadable(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/CatalogPrice.php b/src/Domain/Model/Cars/CarPropertyValues/V1/CatalogPrice.php new file mode 100644 index 0000000..7c35a46 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/CatalogPrice.php @@ -0,0 +1,17 @@ +price->__toString(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Charging/ChargeCurve.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeCurve.php similarity index 53% rename from src/Domain/Model/Charging/ChargeCurve.php rename to src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeCurve.php index b0e51bf..ac02daf 100644 --- a/src/Domain/Model/Charging/ChargeCurve.php +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeCurve.php @@ -2,9 +2,10 @@ namespace App\Domain\Model\Charging; +use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue; use App\Domain\Model\Value\Power; -final readonly class ChargeCurve +final readonly class ChargeCurve extends CarPropertyValue { public function __construct( public ?Power $averagePowerSoc0 = null, @@ -19,4 +20,9 @@ final readonly class ChargeCurve public ?Power $averagePowerSoc90 = null, public ?Power $averagePowerSoc100 = null, ) {} + + public function humanReadable(): string + { + return 'Charge curve: ' . $this->averagePowerSoc0 . ' ' . $this->averagePowerSoc10 . ' ' . $this->averagePowerSoc20 . ' ' . $this->averagePowerSoc30 . ' ' . $this->averagePowerSoc40 . ' ' . $this->averagePowerSoc50 . ' ' . $this->averagePowerSoc60 . ' ' . $this->averagePowerSoc70 . ' ' . $this->averagePowerSoc80 . ' ' . $this->averagePowerSoc90 . ' ' . $this->averagePowerSoc100; + } } \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeTimeProperties.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeTimeProperties.php new file mode 100644 index 0000000..c135302 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargeTimeProperties.php @@ -0,0 +1,50 @@ +minutesFrom0To100 !== null) { + $properties[] = $this->minutesFrom0To100 . ' min (0-100%)'; + } + if ($this->minutesFrom0To70 !== null) { + $properties[] = $this->minutesFrom0To70 . ' min (0-70%)'; + } + if ($this->minutesFrom10To70 !== null) { + $properties[] = $this->minutesFrom10To70 . ' min (10-70%)'; + } + if ($this->minutesFrom20To70 !== null) { + $properties[] = $this->minutesFrom20To70 . ' min (20-70%)'; + } + if ($this->minutesFrom10To80 !== null) { + $properties[] = $this->minutesFrom10To80 . ' min (10-80%)'; + } + if ($this->minutesFrom20To80 !== null) { + $properties[] = $this->minutesFrom20To80 . ' min (20-80%)'; + } + if ($this->minutesFrom10To90 !== null) { + $properties[] = $this->minutesFrom10To90 . ' min (10-90%)'; + } + if ($this->minutesFrom20To90 !== null) { + $properties[] = $this->minutesFrom20To90 . ' min (20-90%)'; + } + + return 'Charge time properties: ' . implode(', ', $properties); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingConnectivity.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingConnectivity.php new file mode 100644 index 0000000..901877f --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingConnectivity.php @@ -0,0 +1,37 @@ +is400v !== null) { + $properties[] = '400V'; + } + if ($this->is800v !== null) { + $properties[] = '800V'; + } + if ($this->plugAndCharge !== null) { + $properties[] = 'Plug and Charge: ' . ($this->plugAndCharge ? 'Yes' : 'No'); + } + foreach ($this->connectorTypes as $connectorType) { + $properties[] = $connectorType->value; + } + + return 'Charging connectivity: ' . implode(', ', $properties); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingSpeed.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingSpeed.php new file mode 100644 index 0000000..79646ec --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ChargingSpeed.php @@ -0,0 +1,30 @@ +dcMax() . ' AC: ' . $this->acMax(); + } + + public function dcMax(): Power + { + return $this->dcMaxKw; + } + + public function acMax(): Power + { + return $this->acMaxKw; + } +} \ No newline at end of file diff --git a/src/Domain/Model/Charging/ConnectorType.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ConnectorType.php similarity index 100% rename from src/Domain/Model/Charging/ConnectorType.php rename to src/Domain/Model/Cars/CarPropertyValues/V1/Charging/ConnectorType.php diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/MotorPower.php b/src/Domain/Model/Cars/CarPropertyValues/V1/MotorPower.php new file mode 100644 index 0000000..182b5d6 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/MotorPower.php @@ -0,0 +1,17 @@ +power->__toString(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/Production.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Production.php new file mode 100644 index 0000000..97df37d --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Production.php @@ -0,0 +1,30 @@ +productionBegin === null && $this->productionEnd === null) { + return ''; + } + + if ($this->productionBegin === null) { + return 'Production end: ' . $this->productionEnd?->__toString(); + } + + if ($this->productionEnd === null) { + return 'Production begin: ' . $this->productionBegin->__toString(); + } + + return 'Production begin: ' . $this->productionBegin->__toString() . ' - End: ' . $this->productionEnd->__toString(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/Range/NefzRange.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Range/NefzRange.php similarity index 66% rename from src/Domain/Model/Range/NefzRange.php rename to src/Domain/Model/Cars/CarPropertyValues/V1/Range/NefzRange.php index ae57a67..388948a 100644 --- a/src/Domain/Model/Range/NefzRange.php +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Range/NefzRange.php @@ -9,4 +9,9 @@ final readonly class NefzRange public function __construct( public readonly Range $range, ) {} + + public function __toString(): string + { + return $this->range->__toString(); + } } \ No newline at end of file diff --git a/src/Domain/Model/Range/RealRange.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Range/RealRange.php similarity index 100% rename from src/Domain/Model/Range/RealRange.php rename to src/Domain/Model/Cars/CarPropertyValues/V1/Range/RealRange.php diff --git a/src/Domain/Model/Range/WltpRange.php b/src/Domain/Model/Cars/CarPropertyValues/V1/Range/WltpRange.php similarity index 66% rename from src/Domain/Model/Range/WltpRange.php rename to src/Domain/Model/Cars/CarPropertyValues/V1/Range/WltpRange.php index f6dc08a..dbd5f33 100644 --- a/src/Domain/Model/Range/WltpRange.php +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/Range/WltpRange.php @@ -9,4 +9,9 @@ final readonly class WltpRange public function __construct( public readonly Range $range, ) {} + + public function __toString(): string + { + return $this->range->__toString(); + } } \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/RangeSpecification.php b/src/Domain/Model/Cars/CarPropertyValues/V1/RangeSpecification.php new file mode 100644 index 0000000..8c99a18 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/RangeSpecification.php @@ -0,0 +1,35 @@ +nefzRange === null && $this->wltpRange === null) { + throw new \InvalidArgumentException('At least one range must be specified'); + } + } + + public function humanReadable(): string + { + if ($this->nefzRange === null && $this->wltpRange === null) { + return 'No Range specified'; + } + + if ($this->nefzRange === null) { + return 'WLTP Range: ' . $this->wltpRange?->__toString(); + } + + if ($this->wltpRange === null) { + return 'NEFZ Range: ' . $this->nefzRange->__toString(); + } + + return ''; + } +} \ No newline at end of file diff --git a/src/Domain/Model/Cars/CarPropertyValues/V1/TopSpeed.php b/src/Domain/Model/Cars/CarPropertyValues/V1/TopSpeed.php new file mode 100644 index 0000000..57afcc7 --- /dev/null +++ b/src/Domain/Model/Cars/CarPropertyValues/V1/TopSpeed.php @@ -0,0 +1,17 @@ +speed->__toString(); + } +} \ No newline at end of file diff --git a/src/Domain/Model/CarRevision.php b/src/Domain/Model/Cars/CarRevision.php similarity index 91% rename from src/Domain/Model/CarRevision.php rename to src/Domain/Model/Cars/CarRevision.php index 5597814..bd94974 100644 --- a/src/Domain/Model/CarRevision.php +++ b/src/Domain/Model/Cars/CarRevision.php @@ -1,6 +1,6 @@ secondsFrom0To100 . ' sec (0-100 km/h)'; - } - - public function seconds(): float - { - return $this->secondsFrom0To100; - } -} \ No newline at end of file diff --git a/src/Domain/Model/Value/ChargingSpeed.php b/src/Domain/Model/Value/ChargingSpeed.php deleted file mode 100644 index 30b4fb3..0000000 --- a/src/Domain/Model/Value/ChargingSpeed.php +++ /dev/null @@ -1,27 +0,0 @@ -dcMax() . ' DC / ' . $this->acMax() . ' AC'; - } - - public function dcMax(): Power - { - return $this->dcMaxKw; - } - - public function acMax(): Power - { - return $this->acMaxKw; - } -} \ No newline at end of file diff --git a/src/Domain/Repository/BrandRepository.php b/src/Domain/Repository/BrandRepository.php index cdaf6f2..d6f028c 100644 --- a/src/Domain/Repository/BrandRepository.php +++ b/src/Domain/Repository/BrandRepository.php @@ -2,9 +2,9 @@ namespace App\Domain\Repository; -use App\Domain\Model\Brand; -use App\Domain\Model\BrandCollection; -use App\Domain\Model\CarModel; +use App\Domain\Model\Cars\Brand; +use App\Domain\Model\Cars\BrandCollection; +use App\Domain\Model\Cars\CarModel; use App\Domain\Model\Id\BrandId; interface BrandRepository diff --git a/src/Domain/Repository/CarModelRepository.php b/src/Domain/Repository/CarModelRepository.php index 4cc03e1..e5ca42e 100644 --- a/src/Domain/Repository/CarModelRepository.php +++ b/src/Domain/Repository/CarModelRepository.php @@ -2,9 +2,9 @@ namespace App\Domain\Repository; -use App\Domain\Model\CarModel; -use App\Domain\Model\CarModelCollection; -use App\Domain\Model\CarRevision; +use App\Domain\Model\Cars\CarModel; +use App\Domain\Model\Cars\CarModelCollection; +use App\Domain\Model\Cars\CarRevision; use App\Domain\Model\Id\CarModelId; use App\Domain\Model\Id\BrandId; diff --git a/src/Domain/Repository/CarPropertyRepository.php b/src/Domain/Repository/CarPropertyRepository.php index acc1791..6f4d1fd 100644 --- a/src/Domain/Repository/CarPropertyRepository.php +++ b/src/Domain/Repository/CarPropertyRepository.php @@ -2,14 +2,16 @@ namespace App\Domain\Repository; -use App\Domain\Model\CarProperty; -use App\Domain\Model\CarPropertyCollection; -use App\Domain\Model\CarRevision; -use App\Domain\Model\Id\CarRevisionId; +use App\Domain\Model\Cars\CarProperty; +use App\Domain\Model\Cars\CarPropertyCollection; +use App\Domain\Model\Cars\CarRevision; +use App\Domain\Model\Id\CarPropertyId; use App\Domain\Model\Id\EmbeddingId; interface CarPropertyRepository { + public function findById(CarPropertyId $carPropertyId): ?CarProperty; + public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection; /** diff --git a/src/Domain/Repository/CarRevisionRepository.php b/src/Domain/Repository/CarRevisionRepository.php index 9171bd0..db1f826 100644 --- a/src/Domain/Repository/CarRevisionRepository.php +++ b/src/Domain/Repository/CarRevisionRepository.php @@ -2,8 +2,8 @@ namespace App\Domain\Repository; -use App\Domain\Model\CarRevision; -use App\Domain\Model\CarRevisionCollection; +use App\Domain\Model\Cars\CarRevision; +use App\Domain\Model\Cars\CarRevisionCollection; use App\Domain\Model\Id\CarRevisionId; use App\Domain\Model\Id\CarModelId; diff --git a/src/Domain/Repository/EmbeddingRepository.php b/src/Domain/Repository/EmbeddingRepository.php index ee9d71e..56d0ce7 100644 --- a/src/Domain/Repository/EmbeddingRepository.php +++ b/src/Domain/Repository/EmbeddingRepository.php @@ -19,14 +19,14 @@ interface EmbeddingRepository * @param int $limit * @return EmbeddingCollection */ - public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): EmbeddingCollection; + public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 100): EmbeddingCollection; /** * @param SmallEmbeddingVector $vector * @param int $limit * @return EmbeddingCollection */ - public function searchBySmallEmbeddingVector(SmallEmbeddingVector $vector, int $limit = 10): EmbeddingCollection; + public function searchBySmallEmbeddingVector(SmallEmbeddingVector $vector, int $limit = 100): EmbeddingCollection; /** * @param string $phrase diff --git a/src/Domain/Repository/Loader/FullCarLoader.php b/src/Domain/Repository/Loader/FullCarLoader.php new file mode 100644 index 0000000..79f15d8 --- /dev/null +++ b/src/Domain/Repository/Loader/FullCarLoader.php @@ -0,0 +1,83 @@ +carPropertyRepository->findById($carPropertyId); + if ($carProperty === null) { + throw new \InvalidArgumentException('Car property not found'); + } + + return $this->loadRevision($carProperty->carRevisionId); + } + + public function loadRevision(CarRevisionId $carRevisionId): FullCarRevision + { + $carRevision = $this->carRevisionRepository->findById($carRevisionId); + if ($carRevision === null) { + throw new \InvalidArgumentException('Car revision not found'); + } + + $carModel = $this->carModelRepository->findById($carRevision->carModelId); + if ($carModel === null) { + throw new \InvalidArgumentException('Car model not found'); + } + + $brand = $this->brandRepository->findById($carModel->brandId); + if ($brand === null) { + throw new \InvalidArgumentException('Brand not found'); + } + + $carProperties = $this->carPropertyRepository->findByCarRevision($carRevision); + + return new FullCarRevision( + $carRevision, + $carModel, + $brand, + $carProperties, + ); + } + + public function loadModel(CarModelId $carModelId): FullCarModel + { + $carModel = $this->carModelRepository->findById($carModelId); + if ($carModel === null) { + throw new \InvalidArgumentException('Car model not found'); + } + + $brand = $this->brandRepository->findById($carModel->brandId); + if ($brand === null) { + throw new \InvalidArgumentException('Brand not found'); + } + + $carRevisions = $this->carRevisionRepository->findByCarModelId($carModelId); + + /** @var FullCarRevision[] $fullCarRevisions */ + $fullCarRevisions = array_map(fn(CarRevision $rev) => $this->loadRevision($rev->carRevisionId), $carRevisions->array()); + + return new FullCarModel( + $carModel, + $brand, + $fullCarRevisions, + ); + } +} \ No newline at end of file diff --git a/src/Domain/Repository/Loader/FullCarModel.php b/src/Domain/Repository/Loader/FullCarModel.php new file mode 100644 index 0000000..889af51 --- /dev/null +++ b/src/Domain/Repository/Loader/FullCarModel.php @@ -0,0 +1,40 @@ +carModel; + } + + public function getBrand(): Brand + { + return $this->brand; + } + + /** + * @return FullCarRevision[] + */ + public function getCarRevisions(): array + { + return $this->carRevisions; + } +} \ No newline at end of file diff --git a/src/Domain/Repository/Loader/FullCarRevision.php b/src/Domain/Repository/Loader/FullCarRevision.php new file mode 100644 index 0000000..e79bfad --- /dev/null +++ b/src/Domain/Repository/Loader/FullCarRevision.php @@ -0,0 +1,45 @@ + $carPropertyCollection + */ + public function __construct( + private readonly CarRevision $carRevision, + private readonly CarModel $carModel, + private readonly Brand $brand, + private readonly CarPropertyCollection $carPropertyCollection, + ) {} + + public function getCarRevision(): CarRevision + { + return $this->carRevision; + } + + public function getCarModel(): CarModel + { + return $this->carModel; + } + + public function getBrand(): Brand + { + return $this->brand; + } + + /** + * @return CarPropertyCollection + */ + public function getCarPropertyCollection(): CarPropertyCollection + { + return $this->carPropertyCollection; + } +} \ No newline at end of file diff --git a/src/Domain/Search/AiTileEngine.php b/src/Domain/Search/AiTileEngine.php new file mode 100644 index 0000000..5b11a22 --- /dev/null +++ b/src/Domain/Search/AiTileEngine.php @@ -0,0 +1,121 @@ +embeddingRepository->searchByLargeEmbeddingVector($this->aiClient->embedTextLarge($query)); + $carProperties = $this->carPropertyRepository->findByEmbeddingPhraseHashes(array_map(fn (Embedding $embedding) => $embedding->phraseHash(), $results->array())); + $carProperties = $carProperties->sortByRevision(); + + $contextString = ""; + $currentRevisionId = null; + foreach ($carProperties->array() as $carProperty) { + $carRevision = $this->carRevisionRepository->findById($carProperty->carRevisionId); + if ($carRevision === null) { + continue; + } + + $carModel = $this->carModelRepository->findByCarRevision($carRevision); + if ($carModel === null) { + continue; + } + + $brand = $this->brandRepository->findByCarModel($carModel); + if ($brand === null) { + continue; + } + + if ($currentRevisionId !== $carProperty->carRevisionId->value) { + $currentRevisionId = $carProperty->carRevisionId->value; + $contextString .= "--------------------------------\n"; + $contextString .= "Model: " . $carModel->name . " (CarModelId: " . $carModel->carModelId->value . ")\n"; + $contextString .= "Brand: " . $brand->name . " (BrandId: " . $brand->brandId->value . ")\n"; + $contextString .= "Revision: " . $carRevision->name . " (CarRevisionId: " . $carRevision->carRevisionId->value . ")\n"; + } + + $contextString .= $carProperty->value->humanReadable() . " (CarPropertyId: " . $carProperty->carPropertyId->value . ")\n"; + } + + $views = $this->viewProvider->getAllViews(); + $viewString = ""; + foreach ($views as $view) { + $reflectionClass = new \ReflectionClass($view); + $shortClassName = $reflectionClass->getShortName(); + + $viewString .= "--------------------------------\n"; + $viewString .= "View: " . $shortClassName . "\n"; + $viewString .= "Description: " . $view->description() . "\n"; + $viewString .= "Data description: " . json_encode($view->dataDescription()) . "\n"; + } + + + $input = <<aiClient->generateJson($input); + if (!isset($response['view']) || !is_string($response['view'])) { + throw new \Exception('Invalid JSON response from AI'); + } + + $view = $this->viewProvider->getView($response['view']); + $data = $response['data'] ?? []; + + if (!is_array($data)) { + throw new \Exception('Invalid JSON response from AI'); + } + + return $view->build($data); + } +} \ No newline at end of file diff --git a/src/Domain/Search/Engine.php b/src/Domain/Search/Engine.php index 0f6ad5d..2f64940 100644 --- a/src/Domain/Search/Engine.php +++ b/src/Domain/Search/Engine.php @@ -2,27 +2,9 @@ namespace App\Domain\Search; -use App\Domain\AI\AIClient; -use App\Domain\Model\Embedding\Embedding; -use App\Domain\Repository\CarPropertyRepository; -use App\Domain\Repository\EmbeddingRepository; +use App\Domain\Search\TileCollection; -class Engine +interface Engine { - public function __construct( - private readonly EmbeddingRepository $embeddingRepository, - private readonly CarPropertyRepository $carPropertyRepository, - private readonly AIClient $aiClient, - private readonly TileBuilder $tileBuilder, - ) { - } - - public function search(string $query): TileCollection - { - $results = $this->embeddingRepository->searchByLargeEmbeddingVector($this->aiClient->embedTextLarge($query)); - - $carProperties = $this->carPropertyRepository->findByEmbeddingPhraseHashes(array_map(fn (Embedding $embedding) => $embedding->phraseHash(), $results->array())); - - return $this->tileBuilder->build($carProperties); - } + public function search(string $query): TileCollection; } \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder.php b/src/Domain/Search/TileBuilder.php deleted file mode 100644 index 5e7390c..0000000 --- a/src/Domain/Search/TileBuilder.php +++ /dev/null @@ -1,58 +0,0 @@ -carTileBuilder = new CarTileBuilder(); - } - - public function build(CarPropertyCollection $carProperties): TileCollection - { - $carRevisionGroups = []; - foreach ($carProperties->array() as $carProperty) { - $carRevisionId = $carProperty->carRevisionId->value; - if (!isset($carRevisionGroups[$carRevisionId])) { - $carRevisionGroups[$carRevisionId] = new CarPropertyCollection(); - } - $carRevisionGroups[$carRevisionId]->add($carProperty); - } - - $tiles = new TileCollection([]); - - foreach ($carRevisionGroups as $carRevisionId => $properties) { - $persistedCarRevision = $this->carRevisionRepository->findById(new CarRevisionId($carRevisionId)); - if ($persistedCarRevision === null) { - continue; - } - - $carModel = $this->carModelRepository->findByCarRevision($persistedCarRevision); - if ($carModel === null) { - continue; - } - - $brand = $this->brandRepository->findByCarModel($carModel); - if ($brand === null) { - continue; - } - - $this->carTileBuilder->build($brand, $carModel, $persistedCarRevision, $properties, $tiles); - } - - return $tiles; - } -} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/AccelerationTileBuilder.php b/src/Domain/Search/TileBuilder/AccelerationTileBuilder.php new file mode 100644 index 0000000..0401e99 --- /dev/null +++ b/src/Domain/Search/TileBuilder/AccelerationTileBuilder.php @@ -0,0 +1,23 @@ +value instanceof Acceleration) { + return null; + } + + return new TileCollection([new AccelerationTile($carProperty->value)]); + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/AvailabilityTileBuilder.php b/src/Domain/Search/TileBuilder/AvailabilityTileBuilder.php new file mode 100644 index 0000000..37b2bdb --- /dev/null +++ b/src/Domain/Search/TileBuilder/AvailabilityTileBuilder.php @@ -0,0 +1,26 @@ +value instanceof Production) { + return new TileCollection([new AvailabilityTile( + $carProperty->value->productionBegin, + $carProperty->value->productionEnd, + )]); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/BatteryTileBuilder.php b/src/Domain/Search/TileBuilder/BatteryTileBuilder.php new file mode 100644 index 0000000..b80a5d2 --- /dev/null +++ b/src/Domain/Search/TileBuilder/BatteryTileBuilder.php @@ -0,0 +1,28 @@ +value instanceof BatteryType) { + return new TileCollection([new BatteryTile(new BatteryProperties( + $carProperty->value->chemistry, + $carProperty->value->model, + $carProperty->value->manufacturer, + ))]); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/ChargingTileBuilder.php b/src/Domain/Search/TileBuilder/ChargingTileBuilder.php new file mode 100644 index 0000000..8d1c9ee --- /dev/null +++ b/src/Domain/Search/TileBuilder/ChargingTileBuilder.php @@ -0,0 +1,23 @@ +value instanceof ChargingProperties) { + return new TileCollection([new ChargingTile($carProperty->value)]); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/ConsumptionTileBuilder.php b/src/Domain/Search/TileBuilder/ConsumptionTileBuilder.php new file mode 100644 index 0000000..e292f3c --- /dev/null +++ b/src/Domain/Search/TileBuilder/ConsumptionTileBuilder.php @@ -0,0 +1,21 @@ +value instanceof CatalogPrice) { + return null; + } + + return new TileCollection([new PriceTile($carProperty->value->price)]); + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/ProductionPeriodTileBuilder.php b/src/Domain/Search/TileBuilder/ProductionPeriodTileBuilder.php new file mode 100644 index 0000000..614ba85 --- /dev/null +++ b/src/Domain/Search/TileBuilder/ProductionPeriodTileBuilder.php @@ -0,0 +1,30 @@ +value instanceof RangeSpecification) { + return new TileCollection([new RangeTile($carProperty->value->nefzRange->range)]); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/TileBuilder.php b/src/Domain/Search/TileBuilder/TileBuilder.php new file mode 100644 index 0000000..a3e2a7d --- /dev/null +++ b/src/Domain/Search/TileBuilder/TileBuilder.php @@ -0,0 +1,19 @@ + $carProperty + * + * @return TileCollection|null + */ + public function build(CarProperty $carProperty): ?TileCollection; +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/TileBuilderProvider.php b/src/Domain/Search/TileBuilder/TileBuilderProvider.php new file mode 100644 index 0000000..2d954b4 --- /dev/null +++ b/src/Domain/Search/TileBuilder/TileBuilderProvider.php @@ -0,0 +1,34 @@ + $tileBuilders + */ + public function __construct( + #[AutowireIterator('app.tile_builder')] + private iterable $tileBuilders, + ) {} + + /** + * @param CarProperty $carProperty + */ + public function build(CarProperty $carProperty): TileCollection + { + foreach ($this->tileBuilders as $tileBuilder) { + $tile = $tileBuilder->build($carProperty); + if ($tile !== null) { + return $tile; + } + } + + throw new \Exception(sprintf('No tile builder found for car property %s of type %s', $carProperty->carPropertyId->value, get_class($carProperty->value))); + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilder/TopSpeedTileBuilder.php b/src/Domain/Search/TileBuilder/TopSpeedTileBuilder.php new file mode 100644 index 0000000..efaec7e --- /dev/null +++ b/src/Domain/Search/TileBuilder/TopSpeedTileBuilder.php @@ -0,0 +1,21 @@ +value instanceof TopSpeed) { + return new TileCollection([new TopSpeedTile($carProperty->value->speed)]); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Domain/Search/TileBuilders/CarTileBuilder.php b/src/Domain/Search/TileBuilders/CarTileBuilder.php deleted file mode 100644 index 1c1d29c..0000000 --- a/src/Domain/Search/TileBuilders/CarTileBuilder.php +++ /dev/null @@ -1,61 +0,0 @@ -hasTypes([ - CarPropertyType::CATALOG_PRICE, - CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, - ])) { - $priceProperty = $carProperties->getOne(CarPropertyType::CATALOG_PRICE); - $accelerationProperty = $carProperties->getOne(CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION); - - if ($priceProperty !== null && $accelerationProperty !== null && $carRevision->image !== null) { - // Handle Price - it should already be a Price object - $priceValue = $priceProperty->value; - if (!$priceValue instanceof Price) { - return; // Skip if not a Price object - } - - // Handle Acceleration - it should already be an Acceleration object - $accelerationValue = $accelerationProperty->value; - if (!$accelerationValue instanceof Acceleration) { - return; // Skip if not an Acceleration object - } - - $subTiles->add(new CarTile( - $carRevision->image, - [ - new PriceTile($priceValue), - new AccelerationTile($accelerationValue), - ] - )); - } - } - - $tiles->add(new SectionTile( - $carRevision->name, - $subTiles->array() - )); - } -} \ No newline at end of file diff --git a/src/Domain/Search/TileCollection.php b/src/Domain/Search/TileCollection.php index 7bd17b9..3558193 100644 --- a/src/Domain/Search/TileCollection.php +++ b/src/Domain/Search/TileCollection.php @@ -23,4 +23,9 @@ final class TileCollection { $this->tiles[] = $tile; } + + public function merge(TileCollection $tileCollection): void + { + $this->tiles = array_merge($this->tiles, $tileCollection->array()); + } } \ No newline at end of file diff --git a/src/Domain/Search/Tiles/AccelerationTile.php b/src/Domain/Search/Tiles/AccelerationTile.php index 7d31ef2..a63c0f9 100644 --- a/src/Domain/Search/Tiles/AccelerationTile.php +++ b/src/Domain/Search/Tiles/AccelerationTile.php @@ -2,7 +2,7 @@ namespace App\Domain\Search\Tiles; -use App\Domain\Model\Value\Acceleration; +use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration; class AccelerationTile { diff --git a/src/Domain/Search/Tiles/AvailabilityTile.php b/src/Domain/Search/Tiles/AvailabilityTile.php index e93ded5..ef48725 100644 --- a/src/Domain/Search/Tiles/AvailabilityTile.php +++ b/src/Domain/Search/Tiles/AvailabilityTile.php @@ -7,7 +7,7 @@ use App\Domain\Model\Value\Date; class AvailabilityTile { public function __construct( - public readonly string $status, public readonly ?Date $availableSince = null, + public readonly ?Date $availableUntil = null, ) {} -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Domain/Search/Tiles/CarTile.php b/src/Domain/Search/Tiles/CarTile.php index b07342b..3344b6e 100644 --- a/src/Domain/Search/Tiles/CarTile.php +++ b/src/Domain/Search/Tiles/CarTile.php @@ -10,7 +10,7 @@ final readonly class CarTile * @param object[] $tiles */ public function __construct( - public Image $image, + public ?Image $image, public array $tiles ) { } } \ No newline at end of file diff --git a/src/Domain/Search/Tiles/ChargingTile.php b/src/Domain/Search/Tiles/ChargingTile.php index ead68d8..6983433 100644 --- a/src/Domain/Search/Tiles/ChargingTile.php +++ b/src/Domain/Search/Tiles/ChargingTile.php @@ -2,7 +2,7 @@ namespace App\Domain\Search\Tiles; -use App\Domain\Model\Value\ChargingSpeed; +use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargingSpeed; class ChargingTile { diff --git a/src/Domain/Search/Tiles/SectionTile.php b/src/Domain/Search/Tiles/SectionTile.php index 275ce0f..82a6af2 100644 --- a/src/Domain/Search/Tiles/SectionTile.php +++ b/src/Domain/Search/Tiles/SectionTile.php @@ -4,11 +4,8 @@ namespace App\Domain\Search\Tiles; class SectionTile { - /** - * @param object[] $tiles - */ + public function __construct( public readonly string $title, - public readonly array $tiles, ) {} } \ No newline at end of file diff --git a/src/Domain/Search/Tiles/SubSectionTile.php b/src/Domain/Search/Tiles/SubSectionTile.php index 70d7129..8463bbf 100644 --- a/src/Domain/Search/Tiles/SubSectionTile.php +++ b/src/Domain/Search/Tiles/SubSectionTile.php @@ -4,11 +4,7 @@ namespace App\Domain\Search\Tiles; class SubSectionTile { - /** - * @param object[] $tiles - */ public function __construct( - public readonly string $title, - public readonly array $tiles, + public readonly string $title ) {} -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Domain/Search/View/CarRevisionComparison.php b/src/Domain/Search/View/CarRevisionComparison.php new file mode 100644 index 0000000..2fcc873 --- /dev/null +++ b/src/Domain/Search/View/CarRevisionComparison.php @@ -0,0 +1,31 @@ + $data + * + * @return TileCollection + */ + public function build(array $data): TileCollection + { + return new TileCollection([]); + } + + public function dataDescription(): array + { + return [ + 'car_revision_id_1' => 'Car revision ID 1', + 'car_revision_id_2' => 'Car revision ID 2', + ]; + } + + public function description(): string + { + return 'This view shows a comparison of two car revisions.'; + } +} \ No newline at end of file diff --git a/src/Domain/Search/View/FullBrandView.php b/src/Domain/Search/View/FullBrandView.php new file mode 100644 index 0000000..ca24b3e --- /dev/null +++ b/src/Domain/Search/View/FullBrandView.php @@ -0,0 +1,33 @@ + $data + * + * @return TileCollection + */ + public function build(array $data): TileCollection + { + return new TileCollection([]); + } + + public function dataDescription(): array + { + return [ + 'brand_id' => 'Brand ID', + ]; + } + + public function description(): string + { + return <<<'EOT' + This view shows all information about a brand. It is used to display the full information about a brand if requested in the query. + E.g. a brand name is given. You should only use this view if you are really sure, that the query is about a brand and not models or revisions. + EOT; + } +} \ No newline at end of file diff --git a/src/Domain/Search/View/FullCarModelView.php b/src/Domain/Search/View/FullCarModelView.php new file mode 100644 index 0000000..09a2065 --- /dev/null +++ b/src/Domain/Search/View/FullCarModelView.php @@ -0,0 +1,93 @@ + $data + * + * @return TileCollection + */ + public function build(array $data): TileCollection + { + if (!is_string($data['car_model_id'] ?? null)) { + throw new \InvalidArgumentException('Car model ID is required'); + } + + $fullCarModel = $this->fullCarLoader->loadModel(new CarModelId($data['car_model_id'])); + $carModel = $fullCarModel->getCarModel(); + $brand = $fullCarModel->getBrand(); + $carRevisions = $fullCarModel->getCarRevisions(); + + $allTiles = []; + + // Add section title for the car model + $allTiles[] = new SectionTile($brand->name . ' ' . $carModel->name); + + // Generate a CarTile for each car revision + foreach ($carRevisions as $fullCarRevision) { + $carProperties = $fullCarRevision->getCarPropertyCollection(); + $carRevision = $fullCarRevision->getCarRevision(); + + /** @var CarProperty[] $properties */ + $properties = array_filter([ + $carProperties->getOne(Production::class), + $carProperties->getOne(TopSpeed::class), + $carProperties->getOne(Acceleration::class), + $carProperties->getOne(RangeSpecification::class), + ], static fn($value) => $value !== null); + + $tiles = new TileCollection([]); + foreach ($properties as $property) { + $tileCollection = $this->tileBuilderProvider->build($property); + $tiles->merge($tileCollection); + } + + $allTiles[] = new SubSectionTile($brand->name . ' ' . $carModel->name . ' ' . $carRevision->name); + $allTiles[] = new CarTile( + $carRevision->image, + $tiles->array(), + ); + } + + return new TileCollection($allTiles); + } + + public function dataDescription(): array + { + return [ + 'car_model_id' => 'Car model ID', + ]; + } + + public function description(): string + { + return <<<'EOT' + This view shows all information about a car model. It is used to display the full information about a car model if requested by the user. + E.g. a model name is given + EOT; + } +} \ No newline at end of file diff --git a/src/Domain/Search/View/FullCarRevisionView.php b/src/Domain/Search/View/FullCarRevisionView.php new file mode 100644 index 0000000..0d305f6 --- /dev/null +++ b/src/Domain/Search/View/FullCarRevisionView.php @@ -0,0 +1,81 @@ + $data + * + * @return TileCollection + */ + public function build(array $data): TileCollection + { + if (!is_string($data['car_revision_id'] ?? null)) { + throw new \InvalidArgumentException('Car revision ID is required'); + } + + $fullCar = $this->fullCarLoader->loadRevision(new CarRevisionId($data['car_revision_id'])); + + $carRevision = $fullCar->getCarRevision(); + $carModel = $fullCar->getCarModel(); + $brand = $fullCar->getBrand(); + $carProperties = $fullCar->getCarPropertyCollection(); + + /** @var CarProperty[] $properties */ + $properties = array_filter([ + $carProperties->getOne(Production::class), + $carProperties->getOne(TopSpeed::class), + $carProperties->getOne(Acceleration::class), + $carProperties->getOne(RangeSpecification::class), + ], static fn($value) => $value !== null); + + $tiles = new TileCollection([]); + foreach ($properties as $property) { + $tileCollection = $this->tileBuilderProvider->build($property); + $tiles->merge($tileCollection); + } + + return new TileCollection([ + new SectionTile($brand->name . ' ' . $carModel->name . ' ' . $carRevision->name), + new CarTile( + $carRevision->image, + $tiles->array(), + ), + ]); + } + + public function dataDescription(): array + { + return [ + 'car_revision_id' => 'Car revision ID', + ]; + } + + public function description(): string + { + return <<<'EOT' + This view shows all information about a car revision. It is used to display the full information about a car revision if a specific revision is requested in the query. + E.g. a model and revision name is given. You should always prefer this view over the full car model view if enough information is given in the query. + EOT; + } +} \ No newline at end of file diff --git a/src/Domain/Search/View/SpecificCarPropertyView.php b/src/Domain/Search/View/SpecificCarPropertyView.php new file mode 100644 index 0000000..a78e6c8 --- /dev/null +++ b/src/Domain/Search/View/SpecificCarPropertyView.php @@ -0,0 +1,72 @@ + $data + * + * @return TileCollection + */ + public function build(array $data): TileCollection + { + $properties = $data['properties'] ?? []; + if (!is_array($properties)) { + throw new \Exception('Properties must be an array'); + } + + $tiles = []; + foreach ($properties as $propertyId) { + if (!is_string($propertyId)) { + continue; + } + + $carProperty = $this->carPropertyRepository->findById(new CarPropertyId($propertyId)); + if ($carProperty === null) { + continue; + } + + $fullCar = $this->fullCarLoader->loadRevisionByCarPropertyId($carProperty->carPropertyId); + + $tileCollection = $this->tileBuilderProvider->build($carProperty); + + $tiles[] = new SectionTile($fullCar->getBrand()->name . ' ' . $fullCar->getCarModel()->name . ' ' . $fullCar->getCarRevision()->name); + $tiles = array_merge($tiles, $tileCollection->array()); + } + + return new TileCollection($tiles); + } + + public function dataDescription(): array + { + return [ + 'properties' => [ + 'carproperty_123', + 'carproperty_456', + ], + ]; + } + + public function description(): string + { + return 'This view shows all information about a specific car property.'; + } +} \ No newline at end of file diff --git a/src/Domain/Search/View/View.php b/src/Domain/Search/View/View.php new file mode 100644 index 0000000..5c85ad5 --- /dev/null +++ b/src/Domain/Search/View/View.php @@ -0,0 +1,27 @@ + $data + * + * @return TileCollection + */ + public function build(array $data): TileCollection; + + /** + * @return array + */ + public function dataDescription(): array; + + /** + * @return string + */ + public function description(): string; +} \ No newline at end of file diff --git a/src/Domain/Search/View/ViewProvider.php b/src/Domain/Search/View/ViewProvider.php new file mode 100644 index 0000000..e6b3334 --- /dev/null +++ b/src/Domain/Search/View/ViewProvider.php @@ -0,0 +1,43 @@ + $views + */ + public function __construct( + #[AutowireIterator('app.view')] + private iterable $views, + ) {} + + /** + * @param string $viewClass + * + * @return View + */ + public function getView(string $viewClass): View + { + foreach ($this->views as $view) { + $reflectionClass = new \ReflectionClass($view); + $shortClassName = $reflectionClass->getShortName(); + + if ($shortClassName === $viewClass) { + return $view; + } + } + + throw new \Exception(sprintf('View %s not found', $viewClass)); + } + + /** + * @return array + */ + public function getAllViews(): array + { + return iterator_to_array($this->views); + } +} \ No newline at end of file diff --git a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php index 957b20a..420c87e 100644 --- a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php +++ b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php @@ -2,7 +2,7 @@ namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository; -use App\Domain\Model\Brand; +use App\Domain\Model\Cars\Brand; use App\Domain\Model\Id\BrandId; class ModelMapper diff --git a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/SqlBrandRepository.php b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/SqlBrandRepository.php index fa47055..d97e7f2 100644 --- a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/SqlBrandRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/SqlBrandRepository.php @@ -2,9 +2,9 @@ namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository; -use App\Domain\Model\Brand; -use App\Domain\Model\BrandCollection; -use App\Domain\Model\CarModel; +use App\Domain\Model\Cars\Brand; +use App\Domain\Model\Cars\BrandCollection; +use App\Domain\Model\Cars\CarModel; use App\Domain\Model\Id\BrandId; use App\Domain\Repository\BrandRepository; use Doctrine\DBAL\Connection; diff --git a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php index 32a4597..ca8f4a0 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php @@ -2,8 +2,8 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository; -use App\Domain\Model\CarModel; -use App\Domain\Model\Brand; +use App\Domain\Model\Cars\CarModel; +use App\Domain\Model\Cars\Brand; use App\Domain\Model\Id\BrandId; use App\Domain\Model\Id\CarModelId; diff --git a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/SqlCarModelRepository.php b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/SqlCarModelRepository.php index 8e290ff..11f9e7e 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/SqlCarModelRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/SqlCarModelRepository.php @@ -2,9 +2,9 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository; -use App\Domain\Model\CarModel; -use App\Domain\Model\CarModelCollection; -use App\Domain\Model\CarRevision; +use App\Domain\Model\Cars\CarModel; +use App\Domain\Model\Cars\CarModelCollection; +use App\Domain\Model\Cars\CarRevision; use App\Domain\Model\Id\CarModelId; use App\Domain\Model\Id\BrandId; use App\Domain\Repository\CarModelRepository; diff --git a/src/Infrastructure/PostgreSQL/Repository/CarPropertyRepository/SqlCarPropertyRepository.php b/src/Infrastructure/PostgreSQL/Repository/CarPropertyRepository/SqlCarPropertyRepository.php index e65c242..8b81122 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarPropertyRepository/SqlCarPropertyRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarPropertyRepository/SqlCarPropertyRepository.php @@ -2,10 +2,10 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarPropertyRepository; -use App\Domain\Model\CarProperty; -use App\Domain\Model\CarPropertyCollection; -use App\Domain\Model\CarPropertyType; -use App\Domain\Model\CarRevision; +use App\Domain\Model\Cars\CarPropertyCollection; +use App\Domain\Model\Cars\CarProperty; +use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue; +use App\Domain\Model\Cars\CarRevision; use App\Domain\Model\Id\CarPropertyId; use App\Domain\Model\Id\CarRevisionId; use App\Domain\Repository\CarPropertyRepository; @@ -17,6 +17,21 @@ final class SqlCarPropertyRepository implements CarPropertyRepository private readonly Connection $connection, ) {} + public function findById(CarPropertyId $carPropertyId): ?CarProperty + { + $result = $this->connection->executeQuery( + 'SELECT * FROM car_properties WHERE id = ?', + [$carPropertyId->value] + ); + + $row = $result->fetchAssociative(); + if ($row === false) { + return null; + } + + return $this->mapRowToCarProperty($row); + } + public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection { $result = $this->connection->executeQuery( @@ -37,8 +52,20 @@ final class SqlCarPropertyRepository implements CarPropertyRepository public function findByEmbeddingIds(array $embeddingIds): CarPropertyCollection { - // Placeholder implementation - would need to join with embeddings table - return new CarPropertyCollection([]); + $result = $this->connection->executeQuery( + 'SELECT * FROM car_properties WHERE id IN (?)', + [$embeddingIds] + ); + + $carProperties = []; + foreach ($result->fetchAllAssociative() as $row) { + $carProperty = $this->mapRowToCarProperty($row); + if ($carProperty !== null) { + $carProperties[] = $carProperty; + } + } + + return new CarPropertyCollection($carProperties); } public function findByEmbeddingPhraseHashes(array $phraseHashes): CarPropertyCollection @@ -71,11 +98,10 @@ final class SqlCarPropertyRepository implements CarPropertyRepository { $this->connection->transactional(function (Connection $connection) use ($carProperty) { $connection->executeStatement( - 'INSERT INTO car_properties (id, car_revision_id, type, value) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET car_revision_id = EXCLUDED.car_revision_id, type = EXCLUDED.type, value = EXCLUDED.value', + 'INSERT INTO car_properties (id, car_revision_id, value) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET car_revision_id = EXCLUDED.car_revision_id, value = EXCLUDED.value', [ $carProperty->carPropertyId->value, $carProperty->carRevisionId->value, - $carProperty->type->value, serialize($carProperty->value), ] ); @@ -115,25 +141,27 @@ final class SqlCarPropertyRepository implements CarPropertyRepository */ private function mapRowToCarProperty(array $row): ?CarProperty { - try { - $id = $row['id'] ?? null; - $carRevisionId = $row['car_revision_id'] ?? null; - $type = $row['type'] ?? null; - $value = $row['value'] ?? null; + $id = $row['id'] ?? null; + $carRevisionId = $row['car_revision_id'] ?? null; + $value = $row['value'] ?? null; - if (!is_string($id) || !is_string($carRevisionId) || !is_string($type) || !is_string($value)) { - return null; - } - - return new CarProperty( - new CarPropertyId($id), - new CarRevisionId($carRevisionId), - CarPropertyType::from($type), - unserialize($value) - ); - } catch (\Exception $e) { - // Invalid data, skip this row + if (!is_string($id) || !is_string($carRevisionId) || !is_string($value)) { return null; } + + try { + $value = unserialize($value); + if (!$value instanceof CarPropertyValue) { + return null; + } + } catch (\Exception $e) { + return null; + } + + return new CarProperty( + new CarPropertyId($id), + new CarRevisionId($carRevisionId), + $value + ); } } \ No newline at end of file diff --git a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php index a6f5461..58b4ca5 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php @@ -2,7 +2,7 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository; -use App\Domain\Model\CarRevision; +use App\Domain\Model\Cars\CarRevision; use App\Domain\Model\Id\CarModelId; use App\Domain\Model\Id\CarRevisionId; use App\Domain\Model\Image; diff --git a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/SqlCarRevisionRepository.php b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/SqlCarRevisionRepository.php index 099dcbe..bf7bb6f 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/SqlCarRevisionRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/SqlCarRevisionRepository.php @@ -2,8 +2,8 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository; -use App\Domain\Model\CarRevision; -use App\Domain\Model\CarRevisionCollection; +use App\Domain\Model\Cars\CarRevision; +use App\Domain\Model\Cars\CarRevisionCollection; use App\Domain\Model\Id\CarRevisionId; use App\Domain\Model\Id\CarModelId; use App\Domain\Repository\CarRevisionRepository; diff --git a/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/SqlEmbeddingRepository.php b/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/SqlEmbeddingRepository.php index 811b0e7..3a7b348 100644 --- a/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/SqlEmbeddingRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/SqlEmbeddingRepository.php @@ -66,7 +66,7 @@ final class SqlEmbeddingRepository implements EmbeddingRepository return $this->mapRowToEmbedding($row); } - public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 100): EmbeddingCollection + public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 20): EmbeddingCollection { $result = $this->connection->executeQuery( 'SELECT *, large_embedding_vector <=> :embeddingVector AS distance @@ -82,15 +82,13 @@ final class SqlEmbeddingRepository implements EmbeddingRepository $embeddings = []; foreach ($result->fetchAllAssociative() as $row) { - if ($row['distance'] < 0.7) { - $embeddings[] = $this->mapRowToEmbedding($row); - } + $embeddings[] = $this->mapRowToEmbedding($row); } return new EmbeddingCollection($embeddings); } - public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 10): EmbeddingCollection + public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 20): EmbeddingCollection { $result = $this->connection->executeQuery( 'SELECT * diff --git a/symfony.lock b/symfony.lock index 44f95c8..c902303 100644 --- a/symfony.lock +++ b/symfony.lock @@ -173,5 +173,18 @@ "files": [ "config/packages/validator.yaml" ] + }, + "symfony/web-profiler-bundle": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.3", + "ref": "5b2b543e13942495c0003f67780cb4448af9e606" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] } } diff --git a/templates/base.html.twig b/templates/base.html.twig index f0fdd27..124724d 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -230,7 +230,7 @@ .subsection-title { font-size: 1.1rem; font-weight: 600; - padding: 0.25rem 0.5rem; + padding: 0.25rem 0rem; position: relative; border-radius: 2px 2px 0 0; border-bottom: none; diff --git a/templates/profiler/ai-chat.html.twig b/templates/profiler/ai-chat.html.twig new file mode 100644 index 0000000..eb411a2 --- /dev/null +++ b/templates/profiler/ai-chat.html.twig @@ -0,0 +1,39 @@ +{# templates/Collector/ai_chat.html.twig #} +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {# Optional: Add a toolbar icon or summary here #} +{% endblock %} + +{% block menu %} + + {{ include('@WebProfiler/Icon/logger.svg') }} + AI Chat + +{% endblock %} + +{% block panel %} +

AI Chat Log

+ {% if collector.log is empty %} +
+

No AI chat log entries.

+
+ {% else %} + + + + + + + + + {% for entry in collector.log %} + + + + + {% endfor %} + +
PromptResponse
{{ entry.prompt|e }}
{{ entry.response|e }}
+ {% endif %} +{% endblock %} diff --git a/templates/result/tiles/car.html.twig b/templates/result/tiles/car.html.twig index 1d78484..d615b24 100644 --- a/templates/result/tiles/car.html.twig +++ b/templates/result/tiles/car.html.twig @@ -1,6 +1,10 @@
{% if tile.image %} + {% else %} +
+ +
{% endif %}
{% for tile in tile.tiles %} diff --git a/templates/result/tiles/productionperiod.html.twig b/templates/result/tiles/productionperiod.html.twig index 8799cbf..ea1ca06 100644 --- a/templates/result/tiles/productionperiod.html.twig +++ b/templates/result/tiles/productionperiod.html.twig @@ -1,41 +1,24 @@
- - Produktionszeitraum -
-
{% if tile.productionBegin or tile.productionEnd %} -
- {% if tile.productionBegin %} -
-
{{ tile.productionBegin.year }}
- Start +
+ {% if tile.productionBegin %} + {{ tile.productionBegin.year }} + {% else %} + ? + {% endif %} + bis + {% if tile.productionEnd %} + {{ tile.productionEnd.year }} + {% else %} + heute + {% endif %}
- {% endif %} - - {% if tile.productionBegin and tile.productionEnd %} -
- {% elseif tile.productionBegin %} -
- {% endif %} - - {% if tile.productionEnd %} -
-
{{ tile.productionEnd.year }}
- Ende -
- {% elseif tile.productionBegin %} -
-
laufend
- aktuell -
- {% endif %} -
{% else %} -
- Zeitraum unbekannt -
+
+ Zeitraum unbekannt +
{% endif %}
- Verfügbarkeit + Produktion
\ No newline at end of file diff --git a/templates/result/tiles/section.html.twig b/templates/result/tiles/section.html.twig index e06ee11..7e0a903 100644 --- a/templates/result/tiles/section.html.twig +++ b/templates/result/tiles/section.html.twig @@ -1,3 +1 @@ -

{{ tile.title }}

- -{% include 'result/tiles/collection.html.twig' with { tiles: tile.tiles } %} \ No newline at end of file +

{{ tile.title }}

\ No newline at end of file diff --git a/templates/result/tiles/subsection.html.twig b/templates/result/tiles/subsection.html.twig index b99601f..fb907b6 100644 --- a/templates/result/tiles/subsection.html.twig +++ b/templates/result/tiles/subsection.html.twig @@ -1,2 +1 @@ -

{{ tile.title }}

-{% include 'result/tiles/collection.html.twig' with { tiles: tile.tiles } %} \ No newline at end of file +

{{ tile.title }}

\ No newline at end of file