Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion Classes/DataProcessing/ChatbotConfigProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@
use Moinferdi\Chatbot\Service\ConfigurationResolver;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;

final class ChatbotConfigProcessor implements DataProcessorInterface
{
private const DEFAULT_START_MESSAGE = 'LLL:EXT:chatbot/Resources/Private/Language/locallang.xlf:widget.startMessage';
private const DEFAULT_TITLE = 'LLL:EXT:chatbot/Resources/Private/Language/locallang.xlf:widget.title';

public function __construct(
private readonly ConfigurationResolver $configResolver,
private readonly LanguageServiceFactory $languageServiceFactory,
private readonly FileRepository $fileRepository,
) {}

/**
Expand Down Expand Up @@ -56,22 +60,74 @@ public function process(
$processedData['startMessage'] = $override !== ''
? $override
: $this->localizedDefaultStartMessage($request);
// Per-language title: DB override wins, otherwise fall back to the
// localized default title.
$title = (string)($config->title ?? '');
$processedData['chatTitle'] = $title !== ''
? $title
: $this->localizedDefaultTitle($request);
$processedData['colorPrimary'] = $config->colorPrimary;
$processedData['colorBackground'] = $config->colorBackground;
$processedData['colorText'] = $config->colorText;
$processedData['colorTitle'] = $config->colorTitle;
$processedData['position'] = $config->position;
$processedData['avatarUrl'] = $this->resolveAvatarUrl($request);

return $processedData;
}

private function localizedDefaultStartMessage(ServerRequestInterface $request): string
{
return $this->translate(self::DEFAULT_START_MESSAGE, $request);
}

private function localizedDefaultTitle(ServerRequestInterface $request): string
{
return $this->translate(self::DEFAULT_TITLE, $request);
}

private function translate(string $lllKey, ServerRequestInterface $request): string
{
$language = $request->getAttribute('language');
$languageService = $language instanceof SiteLanguage
? $this->languageServiceFactory->createFromSiteLanguage($language)
: $this->languageServiceFactory->create('default');

return $languageService->sL(self::DEFAULT_START_MESSAGE);
return $languageService->sL($lllKey);
}

/**
* Resolve the FAL avatar image URL from the site root page.
* Returns empty string if no avatar is configured.
*/
private function resolveAvatarUrl(ServerRequestInterface $request): string
{
/** @var Site|null $site */
$site = $request->getAttribute('site');
if (!$site) {
return '';
}

$rootPageId = $site->getRootPageId();

try {
$fileObjects = $this->fileRepository->findByRelation(
'pages',
'tx_chatbot_avatar',
$rootPageId
);
if (!empty($fileObjects)) {
$file = $fileObjects[0];
$publicUrl = $file->getPublicUrl();
if ($publicUrl !== null) {
return $publicUrl;
}
}
} catch (\Throwable) {
// FAL lookup may fail in some contexts; degrade gracefully.
}

return '';
}

private function getRequest(ContentObjectRenderer $cObj): \Psr\Http\Message\ServerRequestInterface
Expand Down
10 changes: 7 additions & 3 deletions Classes/Service/ChatbotConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,31 @@ private function __construct(
public readonly string $colorPrimary,
public readonly string $colorBackground,
public readonly string $colorText,
public readonly string $colorTitle,
public readonly string $position,
public readonly ?string $startMessage,
public readonly ?string $title,
) {}

public static function disabled(): self
{
return new self(
enabled: false, baseUrl: '', apiKey: '', model: '', everywhere: false,
colorPrimary: '', colorBackground: '', colorText: '', position: '', startMessage: null,
colorPrimary: '', colorBackground: '', colorText: '', colorTitle: '', position: '', startMessage: null, title: null,
);
}

public static function enabled(
string $baseUrl, string $apiKey, string $model, bool $everywhere,
string $colorPrimary, string $colorBackground, string $colorText,
string $position, ?string $startMessage,
string $colorTitle,
string $position, ?string $startMessage, ?string $title = null,
): self {
return new self(
enabled: true, baseUrl: $baseUrl, apiKey: $apiKey, model: $model, everywhere: $everywhere,
colorPrimary: $colorPrimary, colorBackground: $colorBackground, colorText: $colorText,
position: $position, startMessage: $startMessage,
colorTitle: $colorTitle,
position: $position, startMessage: $startMessage, title: $title,
);
}

Expand Down
5 changes: 4 additions & 1 deletion Classes/Service/ConfigurationResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ public function resolve(ServerRequestInterface $request): ChatbotConfig
$colorPrimary = ($rootPage['tx_chatbot_color_primary'] ?? '') ?: ($settings['color']['primary'] ?? '#4F9EF7');
$colorBackground = ($rootPage['tx_chatbot_color_background'] ?? '') ?: ($settings['color']['background'] ?? '#ffffff');
$colorText = ($rootPage['tx_chatbot_color_text'] ?? '') ?: ($settings['color']['text'] ?? '#1a1a1a');
$colorTitle = ($rootPage['tx_chatbot_color_title'] ?? '') ?: ($settings['color']['title'] ?? '#ffffff');
$position = ($rootPage['tx_chatbot_position'] ?? '') ?: ($settings['position'] ?? 'bottom-right');
$startMessage = ($rootPage['tx_chatbot_start_message'] ?? '') ?: null;
$title = ($rootPage['tx_chatbot_title'] ?? '') ?: null;

return ChatbotConfig::enabled(
baseUrl: $baseUrl, apiKey: $apiKey, model: $model, everywhere: $everywhere,
colorPrimary: $colorPrimary, colorBackground: $colorBackground, colorText: $colorText,
position: $position, startMessage: $startMessage,
colorTitle: $colorTitle,
position: $position, startMessage: $startMessage, title: $title,
);
}

Expand Down
34 changes: 33 additions & 1 deletion Configuration/TCA/Overrides/pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@
'default' => '#1a1a1a',
],
],
'tx_chatbot_color_title' => [
'exclude' => true,
'label' => $ll . 'pages.tx_chatbot_color_title',
'config' => [
'type' => 'color',
'size' => 10,
'default' => '#ffffff',
],
],
'tx_chatbot_position' => [
'exclude' => true,
'label' => $ll . 'pages.tx_chatbot_position',
Expand All @@ -133,6 +142,27 @@
'placeholder' => 'Hello! How can I help you today?',
],
],
'tx_chatbot_title' => [
'exclude' => true,
'label' => $ll . 'pages.tx_chatbot_title',
'description' => $ll . 'pages.tx_chatbot_title.description',
'config' => [
'type' => 'input',
'eval' => 'trim',
'size' => 40,
'max' => 255,
'placeholder' => 'Chat Assistant',
],
],
'tx_chatbot_avatar' => [
'exclude' => true,
'label' => $ll . 'pages.tx_chatbot_avatar',
'config' => [
'type' => 'file',
'allowed' => ['common-image-types'],
'maxitems' => 1,
],
],
];

foreach ($columns as $field => $config) {
Expand All @@ -148,7 +178,9 @@
'tx_chatbot_enabled', 'tx_chatbot_everywhere',
'tx_chatbot_base_url', 'tx_chatbot_model', 'tx_chatbot_api_key',
'tx_chatbot_color_primary', 'tx_chatbot_color_background', 'tx_chatbot_color_text',
'tx_chatbot_color_title',
'tx_chatbot_position', 'tx_chatbot_start_message',
'tx_chatbot_title', 'tx_chatbot_avatar',
]);

ExtensionManagementUtility::addToAllTCAtypes(
Expand All @@ -167,7 +199,7 @@
// inherit the default-language value instead of overwriting it with a blank
// (which previously could wipe out the API key in non-default languages).
foreach (array_keys($columns) as $field) {
if ($field !== 'tx_chatbot_start_message') {
if ($field !== 'tx_chatbot_start_message' && $field !== 'tx_chatbot_title') {
$GLOBALS['TCA']['pages']['columns'][$field]['l10n_mode'] = 'exclude';
}
}
Expand Down
4 changes: 4 additions & 0 deletions Resources/Private/Language/locallang_db.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
<trans-unit id="pages.tx_chatbot_color_primary" resname="pages.tx_chatbot_color_primary"><source>Primary Color</source></trans-unit>
<trans-unit id="pages.tx_chatbot_color_background" resname="pages.tx_chatbot_color_background"><source>Background Color</source></trans-unit>
<trans-unit id="pages.tx_chatbot_color_text" resname="pages.tx_chatbot_color_text"><source>Text Color</source></trans-unit>
<trans-unit id="pages.tx_chatbot_color_title" resname="pages.tx_chatbot_color_title"><source>Chat Title Color</source></trans-unit>
<trans-unit id="pages.tx_chatbot_position" resname="pages.tx_chatbot_position"><source>Widget Position</source></trans-unit>
<trans-unit id="pages.tx_chatbot_start_message" resname="pages.tx_chatbot_start_message"><source>Start Message</source></trans-unit>
<trans-unit id="pages.tx_chatbot_start_message.description" resname="pages.tx_chatbot_start_message.description"><source>Shown when the chat opens. This field is per-language: translate the root page to set a greeting for each language. Leave empty to use the built-in localized default. All other settings above are shared across languages.</source></trans-unit>
<trans-unit id="pages.tx_chatbot_title" resname="pages.tx_chatbot_title"><source>Chat Title</source></trans-unit>
<trans-unit id="pages.tx_chatbot_title.description" resname="pages.tx_chatbot_title.description"><source>Custom title shown in the chat header. This field is per-language. Leave empty to use the default "Chat Assistant".</source></trans-unit>
<trans-unit id="pages.tx_chatbot_avatar" resname="pages.tx_chatbot_avatar"><source>Profile Photo</source></trans-unit>
<trans-unit id="pages.tx_chatbot_position.bottom-right" resname="pages.tx_chatbot_position.bottom-right"><source>Bottom Right</source></trans-unit>
<trans-unit id="pages.tx_chatbot_position.bottom-left" resname="pages.tx_chatbot_position.bottom-left"><source>Bottom Left</source></trans-unit>
<trans-unit id="CType.chatbot_widget" resname="CType.chatbot_widget"><source>Chatbot Widget</source></trans-unit>
Expand Down
12 changes: 6 additions & 6 deletions Resources/Private/Partials/Chatbot/Widget.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
id="moinferdi-chatbot"
class="cb-widget cb-widget--{position}"
role="region"
aria-label="{f:translate(key:'widget.title', extensionName:'Chatbot', default:'Chat assistant')}"
aria-label="{chatTitle}"
data-endpoint="{endpointUrl}"
data-model="{model}"
data-start-message="{startMessage}"
style="--cb-primary:{colorPrimary}; --cb-bg:{colorBackground}; --cb-text:{colorText};"
data-avatar-url="{avatarUrl}"
style="--cb-primary:{colorPrimary}; --cb-bg:{colorBackground}; --cb-text:{colorText}; --cb-title-color:{colorTitle};"
>
<button
class="cb-launcher"
Expand Down Expand Up @@ -62,10 +63,9 @@
aria-modal="true"
>
<div class="cb-panel__header">
<h3 class="cb-panel__title">
{f:translate(key:'widget.title', extensionName:'Chatbot',
default:'Chat assistant')}
</h3>
<span class="cb-panel__title">
{chatTitle}
</span>
<button
class="cb-panel__close"
aria-label="{f:translate(key:'widget.close', extensionName:'Chatbot', default:'Close chat')}"
Expand Down
30 changes: 26 additions & 4 deletions Resources/Public/Css/chatbot.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
.cb-widget--open .cb-panel{display:flex}

.cb-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;background:var(--cb-primary,#4F9EF7);color:#fff;flex-shrink:0}
.cb-panel__title{margin:0;font-size:15px;font-weight:600}
.cb-panel__title{margin:0;font-size:15px;font-weight:600;color:var(--cb-title-color,#fff)}
.cb-panel__close{background:none;border:none;color:#fff;cursor:pointer;padding:4px;border-radius:4px;display:flex;align-items:center;justify-content:center}
.cb-panel__close:hover{background:rgba(255,255,255,.2)}
.cb-panel__close:focus-visible{outline:2px solid #fff;outline-offset:2px}

.cb-panel__messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;min-height:100px;max-height:380px}

.cb-message{max-width:85%;padding:10px 14px;border-radius:12px;font-size:13.5px;line-height:1.55;word-wrap:break-word;white-space:pre-wrap}
.cb-message--assistant{align-self:flex-start;background:rgba(0,0,0,.06);border-bottom-left-radius:4px}
.cb-message--user{align-self:flex-end;background:var(--cb-primary,#4F9EF7);color:#fff;border-bottom-right-radius:4px}
.cb-message{max-width:85%;border-radius:12px;font-size:13.5px;line-height:1.55;word-wrap:break-word}
.cb-message--assistant{align-self:flex-start;background:rgba(0,0,0,.06);border-bottom-left-radius:4px;position:relative;margin-bottom:0;padding:10px 14px}
.cb-message--user{align-self:flex-end;background:var(--cb-primary,#4F9EF7);color:#fff;border-bottom-right-radius:4px;padding:10px 14px}
.cb-message--system{align-self:center;font-size:12px;color:var(--cb-text,#1a1a1a);opacity:.7;max-width:90%;text-align:center;padding:6px 12px}
.cb-message--error{align-self:center;background:#FEE2E2;color:#991B1B;border-radius:8px;font-size:12.5px;padding:8px 14px}

Expand All @@ -49,6 +49,28 @@
.cb-panel__send:focus-visible{outline:3px solid var(--cb-primary,#4F9EF7);outline-offset:2px}
.cb-panel__attribution{text-align:center;font-size:11px;color:var(--cb-text,#1a1a1a);opacity:.5;padding-top:6px}

/* --- Avatar --- */
.cb-message-row{display:flex;align-items:flex-end;gap:8px;max-width:90%}
.cb-message-row--assistant{align-self:flex-start}
.cb-message-row--user{align-self:flex-end}
.cb-avatar{width:28px;height:28px;border-radius:50%;object-fit:cover;flex-shrink:0;align-self:flex-end;border:2px solid var(--cb-bg,#fff);box-shadow:0 1px 3px rgba(0,0,0,.12)}

/* --- Markdown content formatting --- */
.cb-message strong{font-weight:700}
.cb-message em{font-style:italic}
.cb-message code{font-family:'SF Mono',Monaco,'Cascadia Code',monospace;font-size:12.5px;background:rgba(0,0,0,.08);padding:1px 5px;border-radius:3px}
.cb-message pre{background:rgba(0,0,0,.08);border-radius:6px;padding:10px 12px;overflow-x:auto;margin:6px 0;font-size:12px;line-height:1.45;white-space:pre-wrap}
.cb-message pre code{background:none;padding:0;font-size:inherit}
.cb-message ul,.cb-message ol{padding-left:18px;margin:4px 0}
.cb-message li{margin-bottom:2px}
.cb-message a{color:var(--cb-primary,#4F9EF7);text-decoration:underline}
.cb-message a:hover{opacity:.8}
.cb-message blockquote{border-left:3px solid var(--cb-primary,#4F9EF7);margin:6px 0;padding:4px 10px;opacity:.85}
.cb-message h1,.cb-message h2,.cb-message h3{font-size:14px;font-weight:700;margin:6px 0 2px}
.cb-message hr{border:none;border-top:1px solid rgba(0,0,0,.12);margin:8px 0}
.cb-message p{margin:0 0 4px}
.cb-message p:last-child{margin-bottom:0}

@media(max-width:480px){
.cb-panel{width:calc(100vw - 32px);max-height:calc(100vh - 120px)}
.cb-widget--bottom-right,.cb-widget--bottom-left{bottom:12px;right:12px;left:auto}
Expand Down
Loading
Loading