From 4623cf210e21718823753edecacf1f4cd8ed6ee5 Mon Sep 17 00:00:00 2001 From: codemenschendev Date: Sat, 26 Sep 2026 15:02:53 +0700 Subject: [PATCH] fix(prototypes): name a drawn prototype after the business, not "Prototyp" Every prototype Codex drew as a picture was titled "Prototyp", so the account list showed the same word over and over. The title is now the website's own (its first part), else the site's address, else the customer's first sentence. A migration renames the ones already made. Project: Appwerk --- apps/api/app/Domain/Ai/CodexPage.php | 26 +++++++++++++++++ apps/api/app/Domain/Ai/ProductPage.php | 7 +++-- apps/api/app/Domain/Ai/PrototypeWriter.php | 2 +- ...26_09_26_120000_name_mockup_prototypes.php | 25 ++++++++++++++++ apps/api/tests/Feature/MockupTitleTest.php | 29 +++++++++++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 apps/api/database/migrations/2026_09_26_120000_name_mockup_prototypes.php create mode 100644 apps/api/tests/Feature/MockupTitleTest.php diff --git a/apps/api/app/Domain/Ai/CodexPage.php b/apps/api/app/Domain/Ai/CodexPage.php index 3d3c7be..95c2faf 100644 --- a/apps/api/app/Domain/Ai/CodexPage.php +++ b/apps/api/app/Domain/Ai/CodexPage.php @@ -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 { diff --git a/apps/api/app/Domain/Ai/ProductPage.php b/apps/api/app/Domain/Ai/ProductPage.php index c24bad8..4d416c9 100644 --- a/apps/api/app/Domain/Ai/ProductPage.php +++ b/apps/api/app/Domain/Ai/ProductPage.php @@ -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}/"; @@ -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[^>]*>(.*?)~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)]); diff --git a/apps/api/app/Domain/Ai/PrototypeWriter.php b/apps/api/app/Domain/Ai/PrototypeWriter.php index 8671443..b9cf562 100644 --- a/apps/api/app/Domain/Ai/PrototypeWriter.php +++ b/apps/api/app/Domain/Ai/PrototypeWriter.php @@ -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, diff --git a/apps/api/database/migrations/2026_09_26_120000_name_mockup_prototypes.php b/apps/api/database/migrations/2026_09_26_120000_name_mockup_prototypes.php new file mode 100644 index 0000000..f50ffa8 --- /dev/null +++ b/apps/api/database/migrations/2026_09_26_120000_name_mockup_prototypes.php @@ -0,0 +1,25 @@ +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 {} +}; diff --git a/apps/api/tests/Feature/MockupTitleTest.php b/apps/api/tests/Feature/MockupTitleTest.php new file mode 100644 index 0000000..cdb84c8 --- /dev/null +++ b/apps/api/tests/Feature/MockupTitleTest.php @@ -0,0 +1,29 @@ +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); + } +}