.. include:: /Includes.rst.txt
.. _architecture:
============
Architecture
============
This section describes the internal architecture of the AI Writer extension
for developers who want to understand, extend, or debug the extension.
Request flow
============
.. code-block:: text
Browser (CKEditor Plugin)
│
│ User triggers AI Text or AI Translate
│
▼
TYPO3 AJAX Routes
POST /typo3/ajax/ok-ai-writer/generate (AI Text)
POST /typo3/ajax/ok-ai-writer/translate (AI Translate)
Body: { messages[], siteRootPageId } (production mode)
Body: { endpoint, apikey, mode, (dev mode — optional overrides)
model, messages[], siteRootPageId }
│
▼
AiTextController
│ resolveCredentials() via ConfigurationService:
│ 1. Per-site DB config (tx_okaiwriter_configuration)
│ 2. Global extension config (fallback)
│ In devMode: client values override resolved config
│ Prepends system prompt (SEO writer or translator)
│
│ generateAction() — text generation (max 2000 tokens)
│ translateAction() — translation (max 4000 tokens)
│
├── mode=azure ──▶ Azure OpenAI API (api-key header)
│
└── mode=openai ──▶ OpenAI API (Bearer token + model in body)
│
│ Returns: choices[].message.content (HTML)
│ usage.prompt_tokens / completion_tokens
│
▼
Response flows back to CKEditor
│ AI Text: displayed in preview, added to conversation history
│ AI Translate: replaces editor content directly
▼
Editor clicks "Insert into Editor" (AI Text)
Content inserted at cursor position
.. note::
The TYPO3 backend acts as a **proxy** between the browser and the AI
API. This avoids CORS restrictions. In production mode (``devMode``
disabled), API credentials never leave the server.
Configuration resolution
========================
.. code-block:: text
ConfigurationService.getConfiguration(siteRootPageId)
│
├── siteRootPageId > 0
│ └── AiWriterConfigurationRepository.findBySiteRootPageId()
│ │ (tx_okaiwriter_configuration table)
│ ├── row found with non-empty apiUrl → return per-site config
│ └── no row or empty apiUrl → fall through
│
└── Global ExtensionConfiguration('ok_ai_writer')
(ext_conf_template.txt values from settings.php)
The ``AddLanguageLabels`` middleware resolves the current site root page ID
from the request context (page module ``id`` parameter or form editing
``edit[table][uid]`` parameter) and passes it to the ``ConfigurationService``
to load the correct credentials for injection into the frontend JavaScript.
System prompts
==============
The controller prepends a system message (identical for both providers) that
instructs the AI. Each action uses a tailored prompt:
**AI Text generation:**
- Generate well-structured, SEO-optimized HTML content
- Use semantic headings (``
``, ````, ````) and paragraphs
- **Not** include ```` tags (the page already has one)
- **Not** include markdown formatting, code fences, or explanations
- Return only clean HTML
**AI Translation:**
- Translate the provided HTML content to the requested language
- Preserve all HTML tags, structure, attributes, and formatting exactly
- Only translate the visible text content
- Return only the translated HTML
.. important::
The system prompts are defined in
:php:`AiTextController::generateAction()` and
:php:`AiTextController::translateAction()`. If you need to customize
the AI's behavior, modify the ``$systemMessage`` array in those methods.
Provider modes
==============
The controller supports two AI providers, selected via the ``mode``
extension configuration:
**Azure OpenAI** (``mode = azure``)
Authenticates via ``api-key`` HTTP header. The model is determined by
the deployment name in the endpoint URL. No ``model`` parameter is sent
in the request body.
**OpenAI / ChatGPT** (``mode = openai``)
Authenticates via ``Authorization: Bearer `` header. The ``model``
parameter (e.g. ``gpt-4o``) is included in the JSON request body.
Conversation mode
=================
The plugin supports two message modes:
**Conversation mode** (default)
The browser sends the full ``messages[]`` array (user + assistant turns).
The controller prepends the system prompt and forwards the entire history
to the AI API. This enables iterative refinement.
**Legacy single-prompt mode**
If no ``messages[]`` array is sent, the controller falls back to using
a single ``prompt`` string. This mode exists for backwards compatibility.
Key files
=========
.. code-block:: text
Classes/
├── Controller/
│ ├── AiTextController.php AJAX endpoint — proxies to AI API (generate + translate)
│ └── Backend/
│ └── ConfigurationController.php Backend module — per-site config management
├── Domain/
│ └── Repository/
│ └── AiWriterConfigurationRepository.php Per-site config DB layer (encrypted API keys)
├── Middleware/
│ └── AddLanguageLabels.php Injects XLIFF labels + site-aware config into backend JS
└── Service/
├── ConfigurationService.php Config resolution (per-site → global fallback)
└── EncryptionService.php Sodium encryption for API keys
Configuration/
├── Backend/
│ ├── AjaxRoutes.php Registers /ok-ai-writer/generate and /translate routes
│ └── Modules.php Registers Web > AI Writer backend module
├── Icons.php Extension icon registration (SvgIconProvider)
├── JavaScriptModules.php ES module import map for CKEditor plugins
├── RequestMiddlewares.php Registers AddLanguageLabels middleware
├── RTE/
│ └── AiWriter.yaml CKEditor preset importing all three plugins
└── Services.yaml Symfony DI autowiring
Resources/
├── Private/
│ ├── Language/
│ │ ├── locallang.xlf English labels (CKEditor plugins)
│ │ ├── de.locallang.xlf German labels (CKEditor plugins)
│ │ ├── locallang_be_module.xlf English labels (backend module)
│ │ └── de.locallang_be_module.xlf German labels (backend module)
│ └── Templates/Backend/Configuration/
│ └── Edit.html Fluid template for backend module form
└── Public/
├── Icons/
│ └── Extension.svg Extension icon
└── JavaScript/
├── backend/
│ └── form-dirty-check.js Unsaved changes detection for backend module
└── plugin/
├── ai-text.js CKEditor 5 AI Text plugin
├── ai-translate.js CKEditor 5 AI Translate plugin
└── lorem-ipsum.js CKEditor 5 Lorem Ipsum plugin
Component details
=================
AiTextController
----------------
:File: :file:`Classes/Controller/AiTextController.php`
:Routes: ``/ok-ai-writer/generate`` and ``/ok-ai-writer/translate`` (AJAX, POST)
Receives the conversation messages (or content to translate) from the browser.
Resolves API credentials via ``ConfigurationService`` using the
``siteRootPageId`` from the request body. Prepends the appropriate system
prompt, forwards to the configured AI provider via Guzzle HTTP client, and
returns the JSON response.
- Resolves credentials via ``ConfigurationService.getConfiguration()``
(per-site → global fallback)
- In devMode: client-sent credentials override resolved config
- Supports both Azure (``api-key`` header) and OpenAI (``Bearer`` token)
- ``generateAction``: accepts ``messages[]`` (conversation) or ``prompt``
(legacy), ``max_tokens: 2000``
- ``translateAction``: accepts ``content`` (HTML) and ``language`` (target),
``max_tokens: 4000``
- Uses ``temperature: 0.3`` for both actions
- Timeout: 120 seconds
- Returns API errors as HTTP 502 with the first 500 characters of the
error response
ConfigurationController (backend module)
-----------------------------------------
:File: :file:`Classes/Controller/Backend/ConfigurationController.php`
:Module: ``web_okaiwriter`` (Web > AI Writer)
:Access: Admin only
Provides the per-site configuration management UI. Resolves the site root
from the selected page, loads existing per-site configuration (if any), and
renders a Fluid form for editing. On save, upserts the configuration record
in the ``tx_okaiwriter_configuration`` table.
- ``editAction``: renders the configuration form with current values
- ``saveAction``: validates and saves per-site config, redirects back to edit
- Uses page tree navigation component for page selection
- Shows global fallback status when no per-site config exists
- Warns when TYPO3 encryption key is missing
ConfigurationService
--------------------
:File: :file:`Classes/Service/ConfigurationService.php`
Resolves the effective AI Writer configuration by checking per-site DB
config first, then falling back to global extension configuration.
EncryptionService
-----------------
:File: :file:`Classes/Service/EncryptionService.php`
Encrypts and decrypts API keys using Sodium (``sodium_crypto_secretbox``).
Derives the encryption key from TYPO3's ``encryptionKey`` via
``sodium_crypto_generichash``.
AiWriterConfigurationRepository
--------------------------------
:File: :file:`Classes/Domain/Repository/AiWriterConfigurationRepository.php`
:Table: ``tx_okaiwriter_configuration``
Database layer for per-site configuration. Supports find-by-site-root and
upsert operations. API keys are encrypted before storage and decrypted on
retrieval via ``EncryptionService``.
AddLanguageLabels middleware
----------------------------
:File: :file:`Classes/Middleware/AddLanguageLabels.php`
:Stack: Backend middleware
Injects the extension's XLIFF translation labels into the TYPO3 backend
page renderer so they are available via ``TYPO3.lang`` in JavaScript.
Also injects site-aware extension configuration as inline settings
(``TYPO3.settings.ok_ai_writer``) with blinded credential values.
Resolves the current site root page ID from the request context by checking
the page module ``id`` parameter or form editing ``edit[table][uid]``
parameter, then looks up the ``pid`` for non-page records.
CKEditor plugins
-----------------
All three plugins are standard CKEditor 5 plugins registered via
:file:`Configuration/JavaScriptModules.php` and imported through the RTE
preset YAML.
All three plugins detect the TYPO3 backend's dark mode setting via
the ``data-color-scheme`` attribute on ```` (``dark``, ``light``, or
``auto``). For the ``auto`` value, they fall back to the browser's
``prefers-color-scheme`` media query. A shared ``getTheme()`` function
returns the full color palette (backgrounds, text, borders, inputs, buttons,
status indicators) used to style each dialog.
**ai-text.js**
Registers the ``aiText`` toolbar button. On click, creates a maximizable
modal dialog with a chat-style interface. Handles conversation state, API
calls via ``fetch()``, token tracking, and content insertion into the editor.
**ai-translate.js**
Registers the ``aiTranslate`` toolbar button. On click, opens a compact
dialog with a language selection grid (7 languages). Translates the full
editor content via the ``/ok-ai-writer/translate`` route and replaces the
editor content on success.
**lorem-ipsum.js**
Registers the ``loremIpsum`` toolbar button. On click, opens a dialog
where the editor selects the number of paragraphs (1–20) to insert at the
cursor position.
Localization
============
The extension ships with two sets of language files:
**CKEditor plugin labels:**
================================================= ===========
File Language
================================================= ===========
``Resources/Private/Language/locallang.xlf`` English
``Resources/Private/Language/de.locallang.xlf`` German
================================================= ===========
**Backend module labels:**
========================================================== ===========
File Language
========================================================== ===========
``Resources/Private/Language/locallang_be_module.xlf`` English
``Resources/Private/Language/de.locallang_be_module.xlf`` German
========================================================== ===========
CKEditor plugin labels are loaded into the backend via the
``AddLanguageLabels`` middleware and accessed in JavaScript through
``TYPO3.lang['label.key']``.
To add a new translation, create a file named
``.locallang.xlf`` (e.g. ``fr.locallang.xlf``) following
the XLIFF 1.2 format.
License
=======
This extension is licensed under the
`GNU General Public License v2.0 or later `__.
:Author: Oliver Kroener
:Email: ok@oliver-kroener.de
:Website: `oliver-kroener.de `__