feat(tweet-card): tweet_card template wired end-to-end
This commit is contained in:
parent
0d4f3b90d7
commit
9d8f772a3e
6 changed files with 143 additions and 0 deletions
|
|
@ -51,6 +51,11 @@ public function instructions(): string
|
|||
|
||||
$budget = $this->platformCopyBudget($this->platformContext);
|
||||
|
||||
if ($this->template?->generatorFormat() === 'tweet_card') {
|
||||
$budget['hard_max_chars'] = 560;
|
||||
$budget['target_chars'] = 280;
|
||||
}
|
||||
|
||||
$view = $this->template?->promptView() ?? 'prompts.post_content.generator';
|
||||
|
||||
return view($view, [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class AiTemplateRegistry
|
|||
private const TEMPLATES = [
|
||||
ImageCardTemplate::class,
|
||||
CarouselTemplate::class,
|
||||
TweetCardTemplate::class,
|
||||
];
|
||||
|
||||
/** @return array<int, AiContentTemplate> */
|
||||
|
|
|
|||
95
app/Ai/Templates/TweetCardTemplate.php
Normal file
95
app/Ai/Templates/TweetCardTemplate.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Ai\Templates;
|
||||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Services\Image\PostImagePipeline;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
|
||||
class TweetCardTemplate implements AiContentTemplate
|
||||
{
|
||||
public function key(): string
|
||||
{
|
||||
return 'tweet_card';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'posts.ai.templates.tweet_card.name';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'posts.ai.templates.tweet_card.description';
|
||||
}
|
||||
|
||||
public function previewAsset(): string
|
||||
{
|
||||
return '/images/ai-templates/tweet-card.png';
|
||||
}
|
||||
|
||||
public function needsAccount(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
public function supportedFormats(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function generatorFormat(): string
|
||||
{
|
||||
return 'tweet_card';
|
||||
}
|
||||
|
||||
public function promptView(): string
|
||||
{
|
||||
return 'prompts.post_content.tweet_card';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function schema(JsonSchema $schema, TemplateContext $context): array
|
||||
{
|
||||
return [
|
||||
'tweet_text' => $schema->string()
|
||||
->description('The post body, written as a punchy first-person X/Twitter-style take. Paragraph breaks (\\n\\n) allowed. Max ~560 characters.')
|
||||
->required(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function humanizableFields(): array
|
||||
{
|
||||
return ['tweet_text' => 'tweet_text'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $structured
|
||||
*/
|
||||
public function assemble(array $structured, TemplateContext $context): GeneratedPost
|
||||
{
|
||||
$text = (string) data_get($structured, 'tweet_text', '');
|
||||
|
||||
$media = [];
|
||||
|
||||
if ($context->socialAccount) {
|
||||
$media = app(PostImagePipeline::class)->forTweetCard(
|
||||
workspace: $context->workspace,
|
||||
account: $context->socialAccount,
|
||||
tweetText: $text,
|
||||
);
|
||||
}
|
||||
|
||||
return new GeneratedPost(
|
||||
content: $text,
|
||||
media: $media,
|
||||
contentType: ContentType::tryFrom($context->format),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -109,6 +109,10 @@ public function handle(): void
|
|||
*/
|
||||
private function humanize(Workspace $workspace, array $structured, string $format): array
|
||||
{
|
||||
if ($format === 'tweet_card') {
|
||||
return $structured;
|
||||
}
|
||||
|
||||
try {
|
||||
$input = $format === 'carousel'
|
||||
? [
|
||||
|
|
|
|||
|
|
@ -90,6 +90,31 @@ function runStreamPostCreation(string $format, SocialAccount $account, int $imag
|
|||
expect($platform->content_type)->toBe(ContentType::InstagramFeed);
|
||||
});
|
||||
|
||||
test('tweet_card template stores the tweet_text as post content and attaches a media item', function () {
|
||||
PostContentGenerator::fake([[
|
||||
'tweet_text' => 'Hello world\n\nSecond para.',
|
||||
]]);
|
||||
|
||||
$minimalPng = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
|
||||
Image::fake([base64_encode($minimalPng)]);
|
||||
|
||||
(new StreamPostCreation(
|
||||
userId: $this->user->id,
|
||||
creationId: (string) Str::uuid(),
|
||||
workspaceId: $this->workspace->id,
|
||||
format: 'x_twitter',
|
||||
socialAccountId: $this->account->id,
|
||||
imageCount: 1,
|
||||
prompt: 'A punchy take on productivity',
|
||||
template: 'tweet_card',
|
||||
))->handle();
|
||||
|
||||
$post = $this->user->currentWorkspace->posts()->latest()->first();
|
||||
|
||||
expect($post->content)->toBe('Hello world\n\nSecond para.')
|
||||
->and($post->media)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('the humanizer is given the same platform context as the generator so the rewrite honours the character cap', function () {
|
||||
PostContentGenerator::fake([[
|
||||
'content' => 'A single productivity tip',
|
||||
|
|
|
|||
13
tests/Feature/Ai/Templates/TweetCardTemplateTest.php
Normal file
13
tests/Feature/Ai/Templates/TweetCardTemplateTest.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Ai\Templates\TweetCardTemplate;
|
||||
|
||||
test('tweet card template identity', function () {
|
||||
$t = new TweetCardTemplate;
|
||||
expect($t->key())->toBe('tweet_card')
|
||||
->and($t->needsAccount())->toBeTrue()
|
||||
->and($t->generatorFormat())->toBe('tweet_card')
|
||||
->and($t->promptView())->toBe('prompts.post_content.tweet_card');
|
||||
});
|
||||
Loading…
Reference in a new issue