harden information getter services and optimize user msg
This commit is contained in:
@@ -51,7 +51,9 @@ final readonly class AgentRunner
|
||||
|
||||
$shopResults = [];
|
||||
$primaryShopResults = [];
|
||||
$knowledgeChunks = [];
|
||||
$sources = [];
|
||||
$urlContent = '';
|
||||
$optimizedShopQuery = '';
|
||||
$shopSearchQuery = '';
|
||||
$shopSearchDisplayQuery = '';
|
||||
@@ -77,6 +79,18 @@ final readonly class AgentRunner
|
||||
// Additional context strategies can be added here later.
|
||||
}
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Antwort wird vorbereitet',
|
||||
ragCount: null,
|
||||
shopCount: null,
|
||||
shopCountMode: 'not_requested',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: 'Beleglage wird geprüft'
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
yield $this->systemMsg($this->agentRunnerConfig->getAnalyzeRequestMessage(), 'think');
|
||||
yield $this->systemMsg($this->agentRunnerConfig->getCheckInternetSourcesMessage(), 'think');
|
||||
|
||||
@@ -101,6 +115,18 @@ final readonly class AgentRunner
|
||||
$this->addSource($sources, $this->agentRunnerConfig->getRagKnowledgeSourceLabel());
|
||||
}
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'RAG-Wissen wurde durchsucht',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: null,
|
||||
shopCountMode: 'not_requested',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $knowledgeChunks !== [] ? 'fachlich belegt' : 'noch keine belastbaren Treffer'
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
if ($usedFollowUpRetrievalContext) {
|
||||
$this->agentLogger->info('Knowledge retrieval used follow-up context', [
|
||||
'userId' => $userId,
|
||||
@@ -111,6 +137,18 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
if ($this->isCommerceIntent($commerceIntent)) {
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Shop-Suche wird vorbereitet',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: null,
|
||||
shopCountMode: 'loading',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $knowledgeChunks !== [] ? 'fachlich belegt; Shopdaten werden geprüft' : 'Shopdaten werden geprüft'
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
yield $this->systemMsg($this->agentRunnerConfig->getOptimizeSearchMessage(), 'think');
|
||||
|
||||
$commerceHistoryContext = $this->buildCommerceHistoryContext($userId, $requestContextHint);
|
||||
@@ -145,6 +183,19 @@ final readonly class AgentRunner
|
||||
|
||||
$noConcreteShopQueryMessage = $this->agentRunnerConfig->getNoConcreteShopQueryMessage();
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Mehr Kontext nötig',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: null,
|
||||
shopCountMode: 'not_requested',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: 'mehr Kontext nötig',
|
||||
completed: true
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
yield $this->systemMsg(
|
||||
$noConcreteShopQueryMessage,
|
||||
'info'
|
||||
@@ -189,6 +240,18 @@ final readonly class AgentRunner
|
||||
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
|
||||
]);
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Shop wird durchsucht',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: null,
|
||||
shopCountMode: 'loading',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $knowledgeChunks !== [] ? 'fachlich belegt; Shopdaten werden geprüft' : 'Shopdaten werden geprüft'
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
yield $this->systemMsg(
|
||||
sprintf($this->agentRunnerConfig->getFetchSearchDataMessageTemplate(), $commerceIntent),
|
||||
'think'
|
||||
@@ -271,6 +334,24 @@ final readonly class AgentRunner
|
||||
if ($attemptedShopRepair) {
|
||||
$this->addSource($sources, $this->agentRunnerConfig->getExtendedShopSearchSourceLabel());
|
||||
}
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: $primaryShopSearchHadSystemFailure ? 'Shopdaten nicht verfügbar' : 'Shop-Suche abgeschlossen',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: $primaryShopSearchHadSystemFailure ? null : count($shopResults),
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : 'count',
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
|
||||
hasKnowledge: $knowledgeChunks !== [] || trim($urlContent) !== '',
|
||||
isCommerceIntent: true,
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
hasShopResults: $shopResults !== [],
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure
|
||||
)
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
}
|
||||
|
||||
if ($shopResults !== []) {
|
||||
@@ -318,6 +399,24 @@ final readonly class AgentRunner
|
||||
]);
|
||||
}
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Antwort wird generiert',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
|
||||
hasKnowledge: $knowledgeChunks !== [] || trim($urlContent) !== '',
|
||||
isCommerceIntent: $this->isCommerceIntent($commerceIntent),
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
hasShopResults: $shopResults !== [],
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure
|
||||
)
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
if ($sources !== []) {
|
||||
yield $this->emitSources(
|
||||
$sources,
|
||||
@@ -338,6 +437,25 @@ final readonly class AgentRunner
|
||||
|
||||
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt, $noLlmFallbackAnswer);
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Abgeschlossen',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
|
||||
hasKnowledge: $knowledgeChunks !== [] || trim($urlContent) !== '',
|
||||
isCommerceIntent: $this->isCommerceIntent($commerceIntent),
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
hasShopResults: $shopResults !== [],
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure
|
||||
),
|
||||
completed: true
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
|
||||
if ($sources !== []) {
|
||||
yield $this->emitSources(
|
||||
$sources,
|
||||
@@ -389,6 +507,18 @@ final readonly class AgentRunner
|
||||
]);
|
||||
|
||||
$userErrorMessage = $this->buildUserErrorMessage($e);
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
stageLabel: 'Antwort wurde unterbrochen',
|
||||
ragCount: count($knowledgeChunks),
|
||||
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
|
||||
sourceLabels: $sources,
|
||||
confidenceLabel: 'nicht abgeschlossen',
|
||||
completed: true
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
yield $this->systemMsg($userErrorMessage, 'err');
|
||||
|
||||
$historyResponse = $this->buildHistoryResponse('', array_merge(
|
||||
@@ -1506,6 +1636,298 @@ final readonly class AgentRunner
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $sourceLabels
|
||||
*/
|
||||
private function buildProductionUiMetaMessage(
|
||||
string $stageLabel,
|
||||
?int $ragCount,
|
||||
?int $shopCount,
|
||||
string $shopCountMode,
|
||||
array $sourceLabels,
|
||||
string $confidenceLabel,
|
||||
bool $completed = false
|
||||
): string {
|
||||
$state = $completed ? 'completed' : 'running';
|
||||
$ragLabel = $ragCount === null
|
||||
? 'RAG-Treffer: wird geprüft'
|
||||
: 'RAG-Treffer: ' . max(0, $ragCount);
|
||||
$shopLabel = match ($shopCountMode) {
|
||||
'count' => 'Shop-Treffer: ' . max(0, (int) $shopCount),
|
||||
'loading' => 'Shop-Treffer: wird geladen',
|
||||
'unavailable' => 'Shop-Treffer: nicht verfügbar',
|
||||
default => 'Shop-Treffer: nicht angefragt',
|
||||
};
|
||||
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';
|
||||
$sources = $this->formatProductionUiSourceLabels($sourceLabels);
|
||||
|
||||
$html = '<div class="retriex-meta-card retriex-run-meta" data-retriex-meta-id="run-status" data-retriex-meta-state="'
|
||||
. htmlspecialchars($state, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '">'
|
||||
. '<div class="retriex-meta-card__eyebrow">RetrieX-Status</div>'
|
||||
. '<div class="retriex-meta-card__title">' . htmlspecialchars($stageLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||
. '<div class="retriex-meta-card__body">'
|
||||
. '<span class="retriex-meta-pill retriex-meta-pill--status">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($ragLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($shopLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||
. '<span class="retriex-meta-pill retriex-meta-pill--confidence">Beleglage: ' . htmlspecialchars($confidenceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||
. '</div>';
|
||||
|
||||
if ($sources !== []) {
|
||||
$html .= '<div class="retriex-source-overview"><span>Datenbasis</span><div class="retriex-source-overview__badges">';
|
||||
|
||||
foreach ($sources as $source) {
|
||||
$html .= '<span class="retriex-source-chip">' . htmlspecialchars($source, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
||||
}
|
||||
|
||||
$html .= '</div></div>';
|
||||
} else {
|
||||
$emptySourceLabel = $completed ? 'keine belastbare Datenbasis' : 'wird geprüft';
|
||||
$html .= '<div class="retriex-source-overview"><span>Datenbasis</span><div class="retriex-source-overview__empty">'
|
||||
. htmlspecialchars($emptySourceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '</div></div>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function resolveProductionUiConfidenceLabel(
|
||||
bool $hasKnowledge,
|
||||
bool $isCommerceIntent,
|
||||
bool $shopSearchAttempted,
|
||||
bool $hasShopResults,
|
||||
bool $shopSearchHadSystemFailure
|
||||
): string {
|
||||
if ($shopSearchHadSystemFailure) {
|
||||
return $hasKnowledge ? 'fachlich belegt; Shopdaten nicht verfügbar' : 'Shopdaten nicht verfügbar';
|
||||
}
|
||||
|
||||
if ($hasKnowledge && $hasShopResults) {
|
||||
return 'RAG + Shopdaten';
|
||||
}
|
||||
|
||||
if (!$hasKnowledge && $hasShopResults) {
|
||||
return 'nur Shopdaten';
|
||||
}
|
||||
|
||||
if ($hasKnowledge && $shopSearchAttempted) {
|
||||
return 'RAG-Wissen, keine Shop-Treffer';
|
||||
}
|
||||
|
||||
if ($hasKnowledge) {
|
||||
return 'fachlich belegt';
|
||||
}
|
||||
|
||||
if ($isCommerceIntent || $shopSearchAttempted) {
|
||||
return 'keine belastbaren Daten';
|
||||
}
|
||||
|
||||
return 'noch keine belastbaren Treffer';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $sourceLabels
|
||||
* @return string[]
|
||||
*/
|
||||
private function formatProductionUiSourceLabels(array $sourceLabels): array
|
||||
{
|
||||
$labels = [];
|
||||
|
||||
foreach ($sourceLabels as $label) {
|
||||
// Source labels are stored as badge HTML for the legacy "Genutzte Quellen" line.
|
||||
// The production UI data-basis chips must render plain labels, otherwise the
|
||||
// nested badge markup is escaped and shown as visible text.
|
||||
$label = $this->plainTextFromHtml((string) $label);
|
||||
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
|
||||
$label = 'Live-Shopdaten';
|
||||
}
|
||||
|
||||
if (!in_array($label, $labels, true)) {
|
||||
$labels[] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
*/
|
||||
private function buildShopProductCardsMessage(array $shopResults, string $query, bool $usedRepair): string
|
||||
{
|
||||
$maxCards = 5;
|
||||
$visibleResults = array_slice($shopResults, 0, $maxCards);
|
||||
$totalCount = count($shopResults);
|
||||
$query = $this->normalizeOneLine($query);
|
||||
$summary = $totalCount . ' Shop-Treffer ausgewertet';
|
||||
|
||||
if ($totalCount > $maxCards) {
|
||||
$summary .= ' · Top ' . $maxCards . ' angezeigt';
|
||||
}
|
||||
|
||||
if ($usedRepair) {
|
||||
$summary .= ' · erweiterte Shopsuche genutzt';
|
||||
}
|
||||
|
||||
$html = '<div class="retriex-meta-card retriex-product-results" data-retriex-meta-id="shop-results" data-retriex-meta-state="completed">'
|
||||
. '<div class="retriex-meta-card__eyebrow">Live-Shopdaten</div>'
|
||||
. '<div class="retriex-meta-card__title">Shop-Ergebnisse</div>'
|
||||
. '<div class="retriex-product-results__summary">' . htmlspecialchars($summary, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
|
||||
|
||||
if ($query !== '') {
|
||||
$html .= '<div class="retriex-meta-query retriex-meta-query--compact"><span>Ausgewertete Suchquery</span><code>'
|
||||
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '</code></div>';
|
||||
}
|
||||
|
||||
$html .= '<div class="retriex-product-grid">';
|
||||
|
||||
foreach ($visibleResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$html .= $this->buildShopProductCard($product, $query);
|
||||
}
|
||||
|
||||
$html .= '</div></div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildShopProductCard(ShopProductResult $product, string $query): string
|
||||
{
|
||||
$name = $this->normalizeOneLine($product->name) ?: 'Unbenanntes Produkt';
|
||||
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
||||
$manufacturer = $this->normalizeOneLine((string) $product->manufacturer);
|
||||
$price = $this->normalizeOneLine((string) $product->price);
|
||||
$url = $this->normalizeOneLine((string) $product->url);
|
||||
$availability = $this->formatProductAvailability($product->available);
|
||||
$relevance = $this->buildProductRelevanceLabel($product, $query);
|
||||
|
||||
$html = '<article class="retriex-product-card">'
|
||||
. '<div class="retriex-product-card__title">';
|
||||
|
||||
if ($url !== '') {
|
||||
$html .= '<a href="' . htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '">'
|
||||
. htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '</a>';
|
||||
} else {
|
||||
$html .= htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
$html .= '</div><dl class="retriex-product-card__facts">';
|
||||
$html .= '<div><dt>Artikelnummer</dt><dd>' . htmlspecialchars($productNumber !== '' ? $productNumber : 'nicht übermittelt', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||
$html .= '<div><dt>Preis</dt><dd>' . htmlspecialchars($price !== '' ? $price : 'nicht übermittelt', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||
$html .= '<div><dt>Verfügbarkeit</dt><dd>' . htmlspecialchars($availability, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||
|
||||
if ($manufacturer !== '') {
|
||||
$html .= '<div><dt>Hersteller</dt><dd>' . htmlspecialchars($manufacturer, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||
}
|
||||
|
||||
$html .= '</dl>'
|
||||
. '<div class="retriex-product-card__relevance"><span>Relevanz</span>'
|
||||
. htmlspecialchars($relevance, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '</div>'
|
||||
. '</article>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function formatProductAvailability(?bool $available): string
|
||||
{
|
||||
return match ($available) {
|
||||
true => 'verfügbar',
|
||||
false => 'nicht verfügbar',
|
||||
default => 'Shopstatus nicht übermittelt',
|
||||
};
|
||||
}
|
||||
|
||||
private function buildProductRelevanceLabel(ShopProductResult $product, string $query): string
|
||||
{
|
||||
$matchedQueries = [];
|
||||
|
||||
foreach ($product->matchedQueries as $matchedQuery) {
|
||||
$matchedQuery = $this->normalizeOneLine((string) $matchedQuery);
|
||||
|
||||
if ($matchedQuery !== '' && !in_array($matchedQuery, $matchedQueries, true)) {
|
||||
$matchedQueries[] = $matchedQuery;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedQueries !== []) {
|
||||
return 'Gefunden über: ' . implode(', ', array_slice($matchedQueries, 0, 3));
|
||||
}
|
||||
|
||||
foreach ($product->highlights as $highlight) {
|
||||
$highlight = $this->normalizeOneLine($this->plainTextFromHtml((string) $highlight));
|
||||
|
||||
if ($highlight !== '') {
|
||||
return 'Passender Shop-Hinweis: ' . mb_substr($highlight, 0, 140, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
$matchSource = $this->normalizeOneLine((string) $product->matchSource);
|
||||
|
||||
if ($matchSource !== '') {
|
||||
return 'Trefferquelle: ' . $matchSource;
|
||||
}
|
||||
|
||||
if ($query !== '') {
|
||||
return 'Passend zur Suchquery: ' . $query;
|
||||
}
|
||||
|
||||
return 'Aus den Live-Shopdaten übernommen';
|
||||
}
|
||||
|
||||
private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string
|
||||
{
|
||||
if (!$isCommerceIntent && !$hasShopResults && !$hasKnowledge) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
|
||||
if ($isCommerceIntent || $hasShopResults) {
|
||||
$actions[] = ['Im Shop suchen', 'Suche die aktuelle Produktauswahl im Shop.'];
|
||||
$actions[] = ['Nur Zubehör anzeigen', 'Zeige aus der aktuellen Produktauswahl nur Zubehör.'];
|
||||
$actions[] = ['Nur Geräte anzeigen', 'Zeige aus der aktuellen Produktauswahl nur Geräte.'];
|
||||
$actions[] = ['Preis anzeigen', 'Zeige mir die Preise der aktuell relevanten Produkte.'];
|
||||
}
|
||||
|
||||
if ($hasKnowledge || $hasShopResults) {
|
||||
$actions[] = ['Technische Details anzeigen', 'Zeige technische Details zur aktuellen Antwort.'];
|
||||
}
|
||||
|
||||
if ($actions === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<div class="retriex-meta-card retriex-followup-actions" data-retriex-meta-id="followup-actions" data-retriex-meta-state="completed">'
|
||||
. '<div class="retriex-meta-card__eyebrow">Folgeaktionen</div>'
|
||||
. '<div class="retriex-meta-card__title">Was möchtest du als Nächstes tun?</div>'
|
||||
. '<div class="retriex-action-chip-row">';
|
||||
|
||||
foreach ($actions as [$label, $actionPrompt]) {
|
||||
$html .= '<button type="button" class="retriex-action-chip" data-retriex-action-prompt="'
|
||||
. htmlspecialchars($actionPrompt, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '">'
|
||||
. htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||
. '</button>';
|
||||
}
|
||||
|
||||
$html .= '</div></div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildShopSearchMetaMessage(
|
||||
string $query,
|
||||
string $commerceIntent,
|
||||
|
||||
Reference in New Issue
Block a user