From a0bcd51cc965014243a997614cbbf3f3e059a334 Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Mon, 15 Jun 2026 08:41:02 +0200 Subject: [PATCH 1/5] feat: base assistant detail page (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds App\Controller\AssistantController with a single show action mapped at GET /assistant/{id}. Symfony's MapEntity converter resolves the route id to an Assistant (404 on unknown id). The Twig template renders the base fields the entity currently carries: title, description, framework + language model in a runtime box, and tags in a tag box when non-empty. Uses the existing Eyebrow / Box components and the Danish translation file (security pattern: keys under assistant.detail.*). Out of scope per the entity's current shape: export entry point (#22), system prompt preview, parameters, organisation / author display, back-link to a real catalogue (#15) — back currently points at the frontpage. These hook in as the underlying data lands. Tests: 38 cases, 100 assertions, 100% coverage. WebTestCase exercises the happy path with tags, the no-tags variant (asserts the tags section is omitted), and the 404 path. Depends on PR #67 (base Assistant entity) which depends on PR #59. Refs #20, #16. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 6 ++ src/Controller/AssistantController.php | 40 +++++++++ templates/assistant/show.html.twig | 52 ++++++++++++ tests/Controller/AssistantControllerTest.php | 86 ++++++++++++++++++++ translations/messages.da.yaml | 10 +++ 5 files changed, 194 insertions(+) create mode 100644 src/Controller/AssistantController.php create mode 100644 templates/assistant/show.html.twig create mode 100644 tests/Controller/AssistantControllerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0dff5..1122a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 deferred to follow-on PRs per ADR 005 ([#14](https://github.com/itk-dev/ai-lib/issues/14), [#16](https://github.com/itk-dev/ai-lib/issues/16)). +- Assistant detail page at `/assistant/{id}` rendering the base + fields (title, description, framework, language model, tags). + Export entry point, organisation / author display, system-prompt + preview, and back-link to the catalogue listing all wait for the + follow-on data and #15 / #22 + ([#20](https://github.com/itk-dev/ai-lib/issues/20)). ### Changed diff --git a/src/Controller/AssistantController.php b/src/Controller/AssistantController.php new file mode 100644 index 0000000..66228d6 --- /dev/null +++ b/src/Controller/AssistantController.php @@ -0,0 +1,40 @@ + '\d+'], methods: ['GET'])] + public function show(Assistant $assistant): Response + { + return $this->render('assistant/show.html.twig', [ + 'assistant' => $assistant, + ]); + } +} diff --git a/templates/assistant/show.html.twig b/templates/assistant/show.html.twig new file mode 100644 index 0000000..02fa234 --- /dev/null +++ b/templates/assistant/show.html.twig @@ -0,0 +1,52 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'assistant.detail.title'|trans({'%title%': assistant.title, '%brand%': brand_name}) }}{% endblock %} + +{% block body %} +
+
+

+ + {{ 'assistant.detail.back'|trans }} + +

+ {{ 'assistant.detail.eyebrow'|trans }} +

+ {{ assistant.title }} +

+

+ {{ assistant.description }} +

+
+ + +
+
+
+ {{ 'assistant.detail.framework_label'|trans }} +
+
{{ assistant.framework }}
+
+
+
+ {{ 'assistant.detail.language_model_label'|trans }} +
+
{{ assistant.languageModel }}
+
+
+
+ + {% if assistant.tags is not empty %} + +
    + {% for tag in assistant.tags %} +
  • + {{ tag }} +
  • + {% endfor %} +
+
+ {% endif %} +
+{% endblock %} diff --git a/tests/Controller/AssistantControllerTest.php b/tests/Controller/AssistantControllerTest.php new file mode 100644 index 0000000..960aeca --- /dev/null +++ b/tests/Controller/AssistantControllerTest.php @@ -0,0 +1,86 @@ +client = self::createClient(); + $container = self::getContainer(); + $em = $container->get(EntityManagerInterface::class); + self::resetSchema($em); + + $this->assistant = new Assistant( + title: 'Borgerservice-vejviser', + description: 'Hjælper sagsbehandlere med at finde den rigtige paragraf.', + languageModel: 'gpt-4o', + framework: 'openwebui', + tags: ['borgerservice', 'paragraf'], + ); + $em->persist($this->assistant); + $em->flush(); + } + + public function testRendersAssistantDetail(): void + { + $id = $this->assistant->getId(); + self::assertNotNull($id); + + $crawler = $this->client->request('GET', '/assistant/'.$id); + + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('h1', 'Borgerservice-vejviser'); + self::assertSelectorTextContains('article', 'Hjælper sagsbehandlere'); + + $runtime = $crawler->filter('article dl')->text(); + self::assertStringContainsString('openwebui', $runtime); + self::assertStringContainsString('gpt-4o', $runtime); + + $tagsText = $crawler->filter('article ul')->text(); + self::assertStringContainsString('borgerservice', $tagsText); + self::assertStringContainsString('paragraf', $tagsText); + } + + public function testOmitsTagsSectionWhenAssistantHasNone(): void + { + $bare = new Assistant('Tagless', 'No tags here.', 'gpt-4o', 'openwebui'); + $em = self::getContainer()->get(EntityManagerInterface::class); + $em->persist($bare); + $em->flush(); + $id = $bare->getId(); + self::assertNotNull($id); + + $crawler = $this->client->request('GET', '/assistant/'.$id); + + self::assertResponseIsSuccessful(); + self::assertCount(0, $crawler->filter('article ul'), 'tags