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
26 changes: 26 additions & 0 deletions apps/api/app/Domain/Ai/CodexPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,32 @@ public static function adBrief(string $prompt, ?string $product, ?array $site, s
$user."\n\n".$languageRule);
}

/**
* What the account list calls a prototype drawn as a picture, which has no <title> of its own:
* the website's name, else its address, else the customer's first sentence. "Prototyp" nine
* times over told the customer nothing (2026-09-26).
*/
public static function title(string $prompt, ?array $site): string
{
$name = trim((string) ($site['name'] ?? ''));
if ($name !== '') {
// "Küstenpatent Kroatien | Boat Skipper B ..." keeps its first part.
$name = trim((string) preg_split('~\s+[|\x{2013}\x{2014}-]\s+~u', $name, 2)[0]);
}
if ($name === '' && isset($site['url'])) {
$name = preg_replace('~^www\.~', '', (string) parse_url((string) $site['url'], PHP_URL_HOST)) ?? '';
}
if ($name === '') {
$name = trim((string) preg_split('~(?<=[.!?])\s|\n~u', trim($prompt), 2)[0]);
$name = rtrim($name, '.!? ');
}
if ($name === '') {
return 'Prototyp';
}

return mb_strlen($name) > 70 ? rtrim(mb_substr($name, 0, 67)).'…' : $name;
}

/** One answer from Claude on the tool-less chat agent. */
private static function ask(string $system, string $user): string
{
Expand Down
7 changes: 5 additions & 2 deletions apps/api/app/Domain/Ai/ProductPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function read(string $domain): ?array
return $page ?: null;
}

/** @return array{url:string,text:string,lang?:?string}|false false so a failed read is cached too, briefly enough */
/** @return array{url:string,text:string,lang?:?string,name?:?string}|false false so a failed read is cached too, briefly enough */
private function fetch(string $domain): array|false
{
$url = "https://{$domain}/";
Expand Down Expand Up @@ -119,7 +119,10 @@ private function fetch(string $domain): array|false
// The language the business speaks to its customers in, from <html lang>.
$lang = preg_match('~<html[^>]*\slang="([a-zA-Z]{2,3})~i', $html, $l) === 1 ? strtolower($l[1]) : null;

return ['url' => $url, 'text' => $text, 'images' => $this->bigEnough($images, $domain), 'logo' => $logo, 'lang' => $lang];
// The site's own name, for the title of a prototype drawn as a picture (CodexPage).
$name = preg_match('~<title[^>]*>(.*?)</title>~is', $html, $t) === 1 ? trim(html_entity_decode(strip_tags($t[1]), ENT_QUOTES | ENT_HTML5)) : null;

return ['url' => $url, 'text' => $text, 'images' => $this->bigEnough($images, $domain), 'logo' => $logo, 'lang' => $lang, 'name' => $name ?: null];
}
} catch (\Throwable $e) {
Log::info('product page: not read', ['domain' => $domain, 'error' => mb_substr($e->getMessage(), 0, 160)]);
Expand Down
2 changes: 1 addition & 1 deletion apps/api/app/Domain/Ai/PrototypeWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public function build(string $prompt, string $kind = 'site', ?DesignRefs $refs =
$lap('brief+render');
$timing['total'] = round(array_sum($timing), 1);

return ['title' => 'Prototyp', 'html' => $out['html'], 'qa' => [
return ['title' => CodexPage::title($prompt, $site), 'html' => $out['html'], 'qa' => [
'ok' => null, 'findings' => [], 'writer' => 'codex', 'mockup' => true, 'brief' => $out['brief'],
'refs' => count($looks), 'timing' => $timing,
'product' => isset($site['url']) ? ['url' => $site['url'], 'brief' => $product] : null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use App\Domain\Ai\CodexPage;
use App\Models\Prototype;
use Illuminate\Database\Migrations\Migration;

/**
* The prototypes Codex drew before 2026-09-26 were all titled "Prototyp". They get the name the
* writer gives them now: the website's address when one was read, else the first sentence.
*/
return new class extends Migration
{
public function up(): void
{
Prototype::where('title', 'Prototyp')->each(function (Prototype $p): void {
$url = $p->qa['product']['url'] ?? null;
$title = CodexPage::title((string) $p->prompt, is_string($url) ? ['url' => $url] : null);
if ($title !== 'Prototyp') {
$p->forceFill(['title' => $title])->saveQuietly();
}
});
}

public function down(): void {}
};
29 changes: 29 additions & 0 deletions apps/api/tests/Feature/MockupTitleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Tests\Feature;

use App\Domain\Ai\CodexPage;
use Tests\TestCase;

/** A prototype drawn as a picture is named after the business, not "Prototyp" (2026-09-26). */
class MockupTitleTest extends TestCase
{
public function test_the_website_name_comes_first_without_its_tagline(): void
{
$this->assertSame('Küstenpatent Kroatien', CodexPage::title('x', ['url' => 'https://www.kuestenpatent-kroatien.at/', 'name' => 'Küstenpatent Kroatien | Boat Skipper B in einem Tag']));
}

public function test_then_the_address_then_the_first_sentence(): void
{
$this->assertSame('kuestenpatent-kroatien.at', CodexPage::title('x', ['url' => 'https://www.kuestenpatent-kroatien.at/']));
$this->assertSame('Eine Website für ein Friseurstudio in Wien', CodexPage::title("Eine Website für ein Friseurstudio in Wien. Mit Preisliste.", null));
$this->assertSame('Prototyp', CodexPage::title(' ', null));
}

public function test_a_long_sentence_is_cut(): void
{
$title = CodexPage::title(str_repeat('Bäckerei Lang ', 12), null);
$this->assertLessThanOrEqual(70, mb_strlen($title));
$this->assertStringEndsWith('…', $title);
}
}
Loading