Skip to content

Commit 71a1d7a

Browse files
authored
Merge pull request #95 from netresearch/feat/v2-modernization
feat: add vision, translation, and template endpoints
2 parents 72df2b5 + 0666875 commit 71a1d7a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+5096
-48
lines changed

AGENTS.md

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,34 +58,58 @@ ddev install-v14
5858

5959
```
6060
t3_cowriter/
61-
├── Classes/Controller/ # PHP AJAX endpoints (nr-llm integration)
62-
├── Configuration/ # TYPO3 configuration
63-
│ ├── Backend/AjaxRoutes.php # AJAX route definitions
64-
│ ├── RTE/Cowriter.yaml # CKEditor plugin registration
65-
│ └── Services.yaml # DI container
66-
├── Resources/Public/JavaScript/ # CKEditor plugin
67-
│ └── Ckeditor/
68-
│ ├── AIService.js # Frontend API client
69-
│ └── cowriter.js # CKEditor integration
70-
└── Documentation/ # RST documentation
61+
├── Classes/
62+
│ ├── Controller/
63+
│ │ ├── AjaxController.php # Main chat/complete/stream endpoints
64+
│ │ ├── VisionController.php # Image analysis (alt text)
65+
│ │ ├── TranslationController.php # Content translation
66+
│ │ ├── TemplateController.php # Prompt template listing
67+
│ │ └── ToolController.php # LLM function calling
68+
│ ├── Domain/DTO/ # Request/response DTOs
69+
│ ├── Tools/ContentQueryTool.php # Tool definition for TYPO3 queries
70+
│ └── Service/ # Rate limiting, context assembly
71+
├── Configuration/
72+
│ ├── Backend/AjaxRoutes.php # 11 AJAX route definitions
73+
│ ├── RTE/Cowriter.yaml # CKEditor toolbar configuration
74+
│ └── Services.yaml # DI container
75+
├── Resources/Public/JavaScript/Ckeditor/
76+
│ ├── AIService.js # Frontend API client (all endpoints)
77+
│ ├── cowriter.js # CKEditor plugin (4 toolbar items)
78+
│ ├── CowriterDialog.js # Task dialog UI
79+
│ └── UrlLoader.js # CSP-compliant URL injection
80+
└── Documentation/ # RST documentation
7181
```
7282

7383
### Data Flow
7484

7585
```
76-
CKEditor → AIService.js → TYPO3 AJAX → AjaxController → nr-llm → AI Provider
86+
CKEditor Toolbar
87+
├─ cowriter → CowriterDialog → AIService.js → AjaxController
88+
├─ cowriterVision → AIService.js ─────────────────→ VisionController
89+
├─ cowriterTranslate→ AIService.js ─────────────────→ TranslationController
90+
├─ cowriterTemplates→ AIService.js ─────────────────→ TemplateController
91+
└─ (tool calling) → AIService.js ─────────────────→ ToolController
7792
78-
CKEditor ← Response ← TYPO3 AJAX ← AjaxController ← nr-llm ← Response
93+
LlmServiceManagerInterface
94+
95+
nr-llm Provider → AI API
7996
```
8097

8198
### AJAX Routes
8299

83-
| Route | Path | Method | Purpose |
84-
|-------|------|--------|---------|
85-
| `tx_cowriter_chat` | `/cowriter/chat` | `chatAction` | Chat completion with message array (stateless) |
86-
| `tx_cowriter_complete` | `/cowriter/complete` | `completeAction` | Single prompt completion |
87-
| `tx_cowriter_stream` | `/cowriter/stream` | `streamAction` | Streaming completion via SSE |
88-
| `tx_cowriter_configurations` | `/cowriter/configurations` | `getConfigurationsAction` | List available LLM configurations |
100+
| Route | Path | Controller | Purpose |
101+
|-------|------|------------|---------|
102+
| `tx_cowriter_chat` | `/cowriter/chat` | `AjaxController::chatAction` | Chat completion (stateless) |
103+
| `tx_cowriter_complete` | `/cowriter/complete` | `AjaxController::completeAction` | Single prompt completion |
104+
| `tx_cowriter_stream` | `/cowriter/stream` | `AjaxController::streamAction` | Streaming via SSE |
105+
| `tx_cowriter_configurations` | `/cowriter/configurations` | `AjaxController::getConfigurationsAction` | List LLM configurations |
106+
| `tx_cowriter_tasks` | `/cowriter/tasks` | `AjaxController::getTasksAction` | List available tasks |
107+
| `tx_cowriter_task_execute` | `/cowriter/task-execute` | `AjaxController::executeTaskAction` | Execute a task |
108+
| `tx_cowriter_context` | `/cowriter/context` | `AjaxController::getContextAction` | Context preview |
109+
| `tx_cowriter_vision` | `/cowriter/vision` | `VisionController::analyzeAction` | Image alt text generation |
110+
| `tx_cowriter_translate` | `/cowriter/translate` | `TranslationController::translateAction` | Content translation |
111+
| `tx_cowriter_templates` | `/cowriter/templates` | `TemplateController::listAction` | Prompt template listing |
112+
| `tx_cowriter_tools` | `/cowriter/tools` | `ToolController::executeAction` | LLM tool/function calling |
89113

90114
### Key Dependencies
91115

Classes/Controller/AjaxController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
use Psr\Http\Message\ServerRequestInterface;
2828
use Psr\Log\LoggerInterface;
2929
use Throwable;
30-
use TYPO3\CMS\Backend\Attribute\AsController;
3130
use TYPO3\CMS\Core\Context\Context;
3231
use TYPO3\CMS\Core\Http\JsonResponse;
3332
use TYPO3\CMS\Core\Http\Response;
@@ -42,7 +41,6 @@
4241
* Returns raw data in JSON responses — no server-side HTML escaping.
4342
* The frontend sanitizes content via DOMParser before DOM insertion.
4443
*/
45-
#[AsController]
4644
final readonly class AjaxController
4745
{
4846
/**
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) 2025-2026 Netresearch DTT GmbH
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Netresearch\T3Cowriter\Controller;
11+
12+
use Netresearch\NrLlm\Domain\Model\PromptTemplate;
13+
use Netresearch\NrLlm\Domain\Repository\PromptTemplateRepository;
14+
use Netresearch\T3Cowriter\Service\RateLimiterInterface;
15+
use Netresearch\T3Cowriter\Service\RateLimitResult;
16+
use Psr\Http\Message\ResponseInterface;
17+
use Psr\Http\Message\ServerRequestInterface;
18+
use Psr\Log\LoggerInterface;
19+
use Throwable;
20+
use TYPO3\CMS\Core\Context\Context;
21+
use TYPO3\CMS\Core\Http\JsonResponse;
22+
23+
/**
24+
* AJAX controller for prompt template management.
25+
*
26+
* Exposes available prompt templates for the CKEditor cowriter dialog.
27+
*
28+
* @internal
29+
*/
30+
final readonly class TemplateController
31+
{
32+
public function __construct(
33+
private PromptTemplateRepository $templateRepository,
34+
private RateLimiterInterface $rateLimiter,
35+
private Context $context,
36+
private LoggerInterface $logger,
37+
) {}
38+
39+
public function listAction(ServerRequestInterface $request): ResponseInterface
40+
{
41+
/** @var int|string $userId */
42+
$userId = $this->context->getPropertyFromAspect('backend.user', 'id', 0);
43+
$rateLimitResult = $this->rateLimiter->checkLimit((string) $userId);
44+
45+
if (!$rateLimitResult->allowed) {
46+
return $this->rateLimitedResponse($rateLimitResult);
47+
}
48+
49+
try {
50+
$templates = $this->templateRepository->findActive();
51+
$result = [];
52+
53+
foreach ($templates as $template) {
54+
if (!$template instanceof PromptTemplate) {
55+
continue;
56+
}
57+
58+
$result[] = [
59+
'identifier' => $template->getIdentifier(),
60+
'name' => $template->getTitle(),
61+
'description' => $template->getDescription() ?? '',
62+
'category' => $template->getFeature(),
63+
];
64+
}
65+
66+
return $this->jsonResponseWithRateLimitHeaders([
67+
'success' => true,
68+
'templates' => $result,
69+
], $rateLimitResult);
70+
} catch (Throwable $e) {
71+
$this->logger->error('Failed to list prompt templates', [
72+
'exception' => $e->getMessage(),
73+
]);
74+
75+
return $this->jsonResponseWithRateLimitHeaders(
76+
['success' => false, 'error' => 'Failed to load templates.'],
77+
$rateLimitResult,
78+
500,
79+
);
80+
}
81+
}
82+
83+
/**
84+
* Create JSON response with rate limit headers.
85+
*
86+
* @param array<string, mixed> $data
87+
*/
88+
private function jsonResponseWithRateLimitHeaders(
89+
array $data,
90+
RateLimitResult $rateLimitResult,
91+
int $statusCode = 200,
92+
): JsonResponse {
93+
$response = new JsonResponse($data, $statusCode);
94+
95+
foreach ($rateLimitResult->getHeaders() as $name => $value) {
96+
$response = $response->withAddedHeader($name, $value);
97+
}
98+
99+
return $response;
100+
}
101+
102+
private function rateLimitedResponse(RateLimitResult $result): JsonResponse
103+
{
104+
$response = new JsonResponse(
105+
['success' => false, 'error' => 'Rate limit exceeded. Please try again later.'],
106+
429,
107+
);
108+
109+
foreach ($result->getHeaders() as $name => $value) {
110+
$response = $response->withAddedHeader($name, $value);
111+
}
112+
113+
return $response->withAddedHeader('Retry-After', (string) $result->getRetryAfter());
114+
}
115+
}

0 commit comments

Comments
 (0)