src/Twig/AppRuntime.php line 166
<?php
namespace App\Twig;
use App\Entity\News;
use App\Repository\EventRepository;
use App\Repository\NewsRepository;
use App\Repository\UseCaseRepository;
use App\Service\GalileaLargeLanguageModels\GalileaLargeLanguageModelsProviderInterface;
use DateTime;
use Sulu\Bundle\MediaBundle\Api\Media;
use Sulu\Component\SmartContent\ArrayAccessItem;
use Sulu\Component\Webspace\Analyzer\Attributes\RequestAttributes;
use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;
use Sulu\Component\Webspace\Webspace;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Intl\Currencies;
use Twig\Extension\RuntimeExtensionInterface;
class AppRuntime implements RuntimeExtensionInterface
{
// Defines the max words count for each element in teaser-grid/news-grid
public const MAX_WORDS_COUNT = 20;
private string $flyOutNavigationPath;
public function __construct(
private GalileaLargeLanguageModelsProviderInterface $galileaLargeLanguageModelsProvider,
private readonly NewsRepository $newsRepository,
private readonly UseCaseRepository $useCaseRepository,
private readonly RequestStack $requestStack,
private readonly EventRepository $eventRepository,
string $projectDir,
private readonly bool $forceWebpThumbnails,
private readonly array $forceWebpThumbnailsTypes
)
{
$this->flyOutNavigationPath = $projectDir . '/navigation/flyOutNavigation.json';
}
/**
* @return array
*/
public function getFlyOutNavigation(): array
{
if (!file_exists($this->flyOutNavigationPath)) {
throw new FileNotFoundException('Fly-Out-Navigation JSON file does not exist!');
}
$contentJSON = file_get_contents($this->flyOutNavigationPath);
$webSpaceKey = $this->getCurrentWebspace()->getKey();
$flyoutData = json_decode($contentJSON, true);
return $flyoutData[$webSpaceKey];
}
/**
* Return all news from database.
* News are filtered by tags from news-grid.
*
* @param string|null $newsGridTags
* @return array
*/
public function getNews(?string $newsGridTags): array
{
$newsArray = [];
$news = $this->newsRepository->findAll();
if (count($news) > 0) {
/** @var News $item */
foreach ($news as $item) {
$itemTags = $item->getTags();
// news-grid has Tags
if (!empty($newsGridTags) && !empty(trim($newsGridTags))) {
// But news item does not --> Continue to next news item!
if (empty($itemTags)) {
continue;
}
if (str_contains($itemTags, ',') !== false) {
$newsItemTagsArray = explode(',', $itemTags);
$newsItemTagsArray = array_map('trim', $newsItemTagsArray);
} else {
$newsItemTagsArray = [trim($itemTags)];
}
$matched = false;
foreach ($newsItemTagsArray as $newsItemTag) {
// news item has at least one tag that matches news-grid Tags
if (str_contains(strtolower($newsGridTags), strtolower($newsItemTag)) !== false) {
$matched = true;
break;
}
}
// news item does not have any tag that matches news-grid Tags --> Continue to next news item!
if (!$matched) {
continue;
}
}
$image = json_decode($item->getImage(), true);
$dateObject = new DateTime($item->getDate());
$newsArray[] = [
'title' => $item->getTitle(),
'description' => $item->getDescription(),
'source' => $item->getSource(),
'date' => $item->getDate(),
'dateGerman' => $dateObject->format('d.m.Y'),
'image' => $image['id'],
'thumbnails' => null,
'url' => $item->getUrl(),
];
}
}
return $newsArray;
}
/**
* Return all news from database.
* News are filtered by tags from news-grid.
*
* @return array
*/
public function getUseCases(string $locale, array $tags): array
{
$useCasesArray = [];
$useCases = $tags ? $this->useCaseRepository->findByTagsWithTopics($locale, $tags) : $this->useCaseRepository->findAllWithTopics($locale);
if (count($useCases) > 0) {
foreach ($useCases as $item) {
$image = json_decode($item->getImage(), true);
$useCasesArray[] = [
'topic' => [
'title' => $item->getUseCaseTopic()->getTitle(),
'description' => $item->getUseCaseTopic()->getDescription(),
'color' => $item->getUseCaseTopic()->getColor(),
],
'title' => $item->getTitle(),
'titleMobile' => $item->getTitleMobile(),
'description' => $item->getDescription(),
'image' => $image['id'],
'tags' => $item->getTags(),
'thumbnails' => null,
];
}
}
return $useCasesArray;
}
/**
* Return matching events from database.
* Events are filtered by tags from event-list.
*
* @param string $locale
* @param array $tags
* @return array
*/
public function getEvents(string $locale, array $tags): array
{
$eventsArray = [];
$events = $tags ? $this->eventRepository->findByTagsForLocale($locale, $tags) : $this->eventRepository->findAllForLocale($locale);
if (count($events) > 0) {
foreach ($events as $item) {
$image = json_decode($item->getImage(), true);
$eventsArray[] = [
'lead' => $item->getLead(),
'title' => $item->getTitle(),
'leadMobile' => $item->getLeadMobile(),
'titleMobile' => $item->getTitleMobile(),
'description' => $item->getDescription(),
'image' => $image['id'],
'link' => $item->getLink(),
'linkLabel' => $item->getLinkLabel(),
'startAt' => $item->getStartAt(),
'location' => $item->getLocation(),
'tags' => $item->getTags(),
'thumbnails' => null,
];
}
}
return $eventsArray;
}
public function getLargeLanguageModels(string $sourceUrl): array
{
$llmCatalogue = $this->galileaLargeLanguageModelsProvider->loadModels($sourceUrl);
if(!$llmCatalogue) {
return [];
}
// build country registry
$countries = array_map(fn(\stdClass $item) => (object)[
'id' => strtolower($item->key),
'name' => $item->name,
'icon' => '/assets/galilea/llms/' . $item->icon
], $llmCatalogue->countries ?? [
(object)[
'key' => 'us',
'name' => 'USA',
'icon' => 'location/us.svg'
],
(object)[
'key' => 'de',
'name' => 'Deutschland',
'icon' => 'location/de.svg'
],
(object)[
'key' => 'eu',
'name' => 'EU',
'icon' => 'location/eu.svg'
],
(object)[
'key' => 'cn',
'name' => 'China',
'icon' => 'location/cn.svg'
],
]);
$countriesById = array_column($countries, null, 'id');
$vendors = array_map(static function(\stdClass $item) use ($countriesById) {
$countryId = strtolower($item->country);
if (!isset($countriesById[$countryId])) {
error_log("Country not found for ID: " . $countryId);
return null; // Skip this vendor if country is missing
}
$country = $countriesById[$countryId];
return [
'id' => $item->key,
'name' => $item->name,
'logo' => '/assets/galilea/llms/' . $item->icon,
'hq' => $country->name,
'hq_logo' => $country->icon,
];
}, $llmCatalogue->providers);
$hosters = array_map(static function(\stdClass $item) use ($countriesById) {
$countryId = strtolower($item->companyLocation);
if (!isset($countriesById[$countryId])) {
error_log("Country not found for ID: " . $countryId);
return null; // Skip this hoster if country is missing
}
$country = $countriesById[$countryId];
return [
'id' => $item->key,
'name' => $item->name,
'logo' => '/assets/galilea/llms/' . $item->icon,
'hq' => $country->name,
'hq_logo' => $country->icon,
];
}, $llmCatalogue->deployers);
$features = array_map(fn(\stdClass $item) => [
'id' => $item->key,
'name' => $item->name,
'icon' => '/assets/galilea/llms/' . $item->icon,
], $llmCatalogue->useCases);
$validateDate = function($date, $format = 'Y-m-d')
{
$d = DateTime::createFromFormat($format, $date);
// The Y ( 4 digits year ) returns TRUE for any integer with any number of digits so changing the comparison from == to === fixes the issue.
return $d && strtolower($d->format($format)) === strtolower($date);
};
$models = array_map(fn(\stdClass $item) => [
'name' => $item->name,
'quality' => $item->stats->quality ?: 0,
'speed' => $item->stats->speed ?: 0,
'cost' => $item->stats->costs ?: 0,
'version' => $item->modelVersion ?: '-',
'languages' => count($item->languages),
'type' => $item->type ? ucfirst($item->type) : '-',
'license' => $item->license ? ucfirst($item->license) : '-',
'summary' => $item->summary ?: '-',
'description' => $item->description ?: '-',
'location' => $item->hostingLocation->name ?: '-',
'location_logo' => strtolower($item->hostingLocation->country ?: '-'),
'knowledge_cutoff' => $item->knowledgeCutoff && $validateDate($item->knowledgeCutoff) ? $item->knowledgeCutoff : '-',
'structured_output' => $item->structuredOutput ?: false,
'function_calling' => $item->functionCalling ?: false,
'context' => $item->contextWindow ?: '-',
'token_input' => $item->pricing->inputTokenPricePerMillion ? $item->pricing->inputTokenPricePerMillion . '€/1M Tokens' : '-',
'token_output' => $item->pricing->outputTokenPricePerMillion ? $item->pricing->outputTokenPricePerMillion . '€/1M Tokens' : '-',
'vendor' => $item->provider,
'hoster' => $item->deployer,
'logo' => '/assets/galilea/llms/' . $item->icon,
'features' => $item->useCases,
'rankings' => array_map(static fn(\stdClass $ranking) => [
'text' => $ranking->name,
'direction' => $ranking->rank,
'type' => ['good' => 'green', 'neutral' => 'gray', 'bad' => 'red'][$ranking->rating],
], $item->rankings)
], $llmCatalogue->models);
// Pre-process arrays for efficient lookups by ID
$vendorsById = array_column($vendors, null, 'id');
$hostersById = array_column($hosters, null, 'id');
$featuresById = array_column($features, null, 'id');
// Group models by vendor and expand references
$data = [];
foreach ($models as $idx => $model) {
if(isset($model['enabled']) && !$model['enabled']) {
// skip disabled models
continue;
}
$vendorId = $model['vendor'];
$hosterId = $model['hoster'];
$featureIds = $model['features'];
// Look up vendor data
if (!isset($vendorsById[$vendorId])) {
error_log("Vendor not found for ID: " . $vendorId);
continue; // Skip this model if vendor is missing
}
$vendor = $vendorsById[$vendorId];
// Look up hoster data
if (!isset($hostersById[$hosterId])) {
error_log("Hoster not found for ID: " . $hosterId);
continue; // Skip this model if hoster is missing
}
$hoster = $hostersById[$hosterId];
// Look up feature data
$modelFeatures = [];
foreach ($featureIds as $featureId) {
if (isset($featuresById[$featureId])) {
$modelFeatures[] = $featuresById[$featureId];
} else {
error_log("Feature not found for ID: " . $featureId);
}
}
// Create expanded model data
$expandedModel = array_merge($model, [
'hoster' => $hoster,
'features' => $modelFeatures
]);
$countryId = strtolower($model['location_logo']);
if (!isset($countriesById[$countryId])) {
error_log("Country not found for ID: " . $countryId);
continue; // Skip this model if country is missing
}
$country = $countriesById[$countryId];
$expandedModel['location_logo'] = $country->icon;
$expandedModel['location_country_name'] = $country->name;
// Initialize vendor group if it doesn't exist
if (!isset($data[$vendorId])) {
$data[$vendorId] = [
'vendor' => $vendor,
'models' => []
];
}
// Add model to its vendor group
$data[$vendorId]['models'][] = $expandedModel;
}
return $data;
}
private function decodeDataUri(string $assetDataUri)
{
$matches = [];
if(!preg_match('/^data:(image\/svg\+xml|png|jpeg|gif|webp)(;base64)?,(.+)$/', $assetDataUri, $matches)) {
return null;
}
$mimeType = $matches[1];
$isBase64 = $matches[2] ?? false;
$assetData = $matches[3];
switch($mimeType) {
case 'image/svg+xml':
$svgData = $isBase64 ? base64_decode(strtr($assetData, '-_.', '+/=')) : $assetData;
if(preg_match('/%3Csvg/', $svgData)) {
$svgData = urldecode($svgData);
}
return $svgData;
case 'image/png':
case 'image/jpeg':
case 'image/gif':
case 'image/webp':
return $isBase64 ? base64_decode(strtr($assetData, '-_.', '+/=')) : $assetData;
default:
return null;
}
}
private function formatShortAproximateNumber($number)
{
if(!is_numeric($number)) {
return $number;
}
if($number >= 10000000000) {
return round($number / 1000000000,0) . 'B';
}
if($number >= 1000000000) {
return round($number / 1000000000, 1) . 'B';
}
if($number >= 10000000) {
return round($number / 1000000, 0) . 'M';
}
if($number >= 1000000) {
return round($number / 1000000, 1) . 'M';
}
if($number >= 10000) {
return round($number / 1000, 0) . 'k';
}
if($number >= 1000) {
return round($number / 1000, 1) . 'k';
}
if($number >= 10) {
return round($number, 0);
}
return round($number, 1);
}
/**
* - Map stories to array like news
* - Merge stories and news
* - Sort by date
*
* @param array $stories
* @param array $publications
* @param array $pages
* @param array $news
* @param bool $contentMixed
* @return array
*/
public function sortByDate(
array $stories,
array $publications,
array $pages,
array $news,
bool $contentMixed = false
): array {
$pagesArray = [];
// Get other pages only if contentMixed is true
if ($contentMixed) {
$pagesArray = array_merge($stories, $publications, $pages);
}
$newsArray = $news;
if (count($pagesArray) > 0) {
$newsArray = array_merge($pagesArray, $news);
}
// Sort descending by date
usort($newsArray, function ($a, $b) {
return $a['date'] < $b['date'];
});
return $newsArray;
}
/**
* Prepare data from smart content for news-grid.
* If description has more than 20 words, the first 20 words will be cut and ... will be added to the end.
* If alternativeText is available, replace description. Text will not be cut. Length remains unchanged.
*
* @param array $items
* @param string $locale
* @return array
*/
public function getDataFromSmartContent(array $items, string $locale): array
{
$data = [];
/** @var ArrayAccessItem $item */
foreach ($items as $item) {
/** @var Media $image */
$excerptImage = $item['excerptAlternativeImage'] ?? $item['excerptImages'] ?? null;
$thumbnails = $excerptImage instanceof Media ? $excerptImage->getThumbnails() : null;
// Teaser grid title is preferred, then title!
$title = $item['excerptTitleTeaserGrid'] ?? '';
if (empty($title)) {
$title = $item['excerptTitle'] ?? '';
}
// Custom Text is preferred, then description!
$description = $item['excerptCustomText'] ?? '';
if (empty($description)) {
$description = $item['excerptDescription'] ?? '';
if (!empty($description)) {
$descriptionArray = explode(' ', $description);
// Get the first 20 words
if (count($descriptionArray) > self::MAX_WORDS_COUNT) {
$descriptionArray = array_slice($descriptionArray, 0, self::MAX_WORDS_COUNT);
$description = implode(' ', $descriptionArray);
$description .= ' ...';
}
}
}
// Creation date from settings is date of story!
$authored = $item['authored'];
$data[] = [
'title' => $title,
'description' => $description,
'source' => $item['excerptType'] ?? '',
'date' => $authored->format('Y-m-d'),
'dateGerman' => $authored->format('d.m.Y'),
'image' => $thumbnails['2880x'] ?? null,
'thumbnails' => $thumbnails,
'url' => '/' . $locale . $item['url'], // Add locale to URL
];
}
return $data;
}
public function getThumbnailUrl($media, $size)
{
if($media instanceof Media) {
$thumbnails = $media->getThumbnails();
if(!array_key_exists($size, $thumbnails)) {
throw new \InvalidArgumentException('Unknown thumbnail size "' . $size . '".');
}
if($this->forceWebpThumbnails && in_array($media->getFileVersion()->getMimeType(), $this->forceWebpThumbnailsTypes)) {
$size .= '.webp';
}
return $thumbnails[$size];
} else if(is_array($media) && array_key_exists('thumbnails', $media)) {
$thumbnails = $media['thumbnails'];
if(!array_key_exists($size, $thumbnails)) {
throw new \InvalidArgumentException('Unknown thumbnail size "' . $size . '".');
}
$extensionMimeMap = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp'
];
$ext = strtolower(pathinfo(parse_url($media['image'], PHP_URL_PATH), PATHINFO_EXTENSION));
if($this->forceWebpThumbnails && array_key_exists($ext, $extensionMimeMap) && in_array($extensionMimeMap[$ext], $this->forceWebpThumbnailsTypes)) {
$size .= '.webp';
}
return $thumbnails[$size];
}
}
public function pricingTableFeatureGroups(array $features): array
{
// reduce((groups, feature, idx) => feature.pricingFeatureGroupName ? groups|merge([{ name: feature.pricingFeatureGroupName, features: ['feature_' ~ idx] }]) : groups|map((group, idx) => idx == groups|keys|last and group.features|merge(['feature_' ~ idx]) ? groups : groups), []),
$groups = [];
$currentGroup = null;
foreach($features as $idx => $feature) {
if(!$currentGroup || ($feature['pricingFeatureGroupName'] && $currentGroup['name'] !== $feature['pricingFeatureGroupName'])) {
if($currentGroup) {
$groups[] = $currentGroup;
}
$currentGroup = [
'name' => $feature['pricingFeatureGroupName'],
'features' => []
];
}
$currentGroup['features'][] = 'feature_' . $idx;
}
$groups[] = $currentGroup;
return $groups;
}
private function getCurrentWebspace(): ?Webspace
{
$currentRequest = $this->requestStack->getCurrentRequest();
if (!$currentRequest) {
return null;
}
$suluAttributes = $currentRequest->attributes->get('_sulu');
if (!$suluAttributes instanceof RequestAttributes) {
return null;
}
return $suluAttributes->getAttribute('webspace');
}
}