trypost/app/Models/Workspace.php

122 lines
3 KiB
PHP
Raw Normal View History

2026-01-15 01:13:44 +00:00
<?php
declare(strict_types=1);
2026-01-15 01:13:44 +00:00
namespace App\Models;
feat: AI image generation pipeline and post creation overhaul Core changes: - Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK. New AiImageClient builds prompts from a Blade template seeded by the workspace's ImageStyle enum, content language, brand color (mapped to a human-readable name via HexColorName helper) and brand description. - Drop Template B from TemplateImageGenerator: every slide now renders as Template A (full-bleed photo + bottom gradient + white/grey overlay). Removes renderTemplateB, roundCorners, blendHex, ensureContrast and the closing-slide pipeline. - StreamPostCreation creates the Post directly and dispatches PostCreationReady with post_id; the wizard kills its preview step and redirects straight to the post editor on completion. Finalize endpoint removed. - New Workspace.image_style enum field with an 8-option visual picker shared by /workspaces/create and /settings/workspace/brand via a single BrandForm component (autofill is a prop). 8 sample webp thumbs ship under public/images/branding/image-styles/. - Media items gain optional source ('ai'|'unsplash'|'giphy') and source_meta (recipe needed to regenerate AI images later); the gallery picker tags Unsplash/Giphy attachments. - Brand-color autofill: new CssColorFrequencyExtractor parses every hex/rgb/hsl value in the homepage CSS, clusters perceptually similar shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the most frequent cluster. Solves Tailwind/utility-CSS sites where no semantic --primary variable is exposed. - Credits: gpt-image-2 metered at 15 credits/image (low quality default). - Layout: AuthSplitLayout right column is sticky/h-svh so the form textarea growth no longer stretches the marketing slider. - i18n cleanup: localized labels follow the no-em-dash convention.
2026-05-08 16:38:30 +00:00
use App\Enums\Workspace\ImageStyle;
use App\Models\Traits\HasMedia;
use Database\Factories\WorkspaceFactory;
use Illuminate\Database\Eloquent\Collection;
2026-01-15 01:13:44 +00:00
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Workspace extends Model
{
/** @use HasFactory<WorkspaceFactory> */
use HasFactory, HasMedia, HasUuids;
2026-01-15 01:13:44 +00:00
protected $fillable = [
'account_id',
2026-01-15 01:13:44 +00:00
'user_id',
'name',
'brand_website',
'brand_description',
'brand_voice_traits',
'brand_color',
'background_color',
'text_color',
'brand_font',
feat: AI image generation pipeline and post creation overhaul Core changes: - Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK. New AiImageClient builds prompts from a Blade template seeded by the workspace's ImageStyle enum, content language, brand color (mapped to a human-readable name via HexColorName helper) and brand description. - Drop Template B from TemplateImageGenerator: every slide now renders as Template A (full-bleed photo + bottom gradient + white/grey overlay). Removes renderTemplateB, roundCorners, blendHex, ensureContrast and the closing-slide pipeline. - StreamPostCreation creates the Post directly and dispatches PostCreationReady with post_id; the wizard kills its preview step and redirects straight to the post editor on completion. Finalize endpoint removed. - New Workspace.image_style enum field with an 8-option visual picker shared by /workspaces/create and /settings/workspace/brand via a single BrandForm component (autofill is a prop). 8 sample webp thumbs ship under public/images/branding/image-styles/. - Media items gain optional source ('ai'|'unsplash'|'giphy') and source_meta (recipe needed to regenerate AI images later); the gallery picker tags Unsplash/Giphy attachments. - Brand-color autofill: new CssColorFrequencyExtractor parses every hex/rgb/hsl value in the homepage CSS, clusters perceptually similar shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the most frequent cluster. Solves Tailwind/utility-CSS sites where no semantic --primary variable is exposed. - Credits: gpt-image-2 metered at 15 credits/image (low quality default). - Layout: AuthSplitLayout right column is sticky/h-svh so the form textarea growth no longer stretches the marketing slider. - i18n cleanup: localized labels follow the no-em-dash convention.
2026-05-08 16:38:30 +00:00
'image_style',
feat: add workspace content_language setting for AI generation Users can now pick the language AI uses for captions, hashtags, descriptions, AND on-image / on-screen text — independent of the app UI locale. A user with pt-BR UI who writes for an English audience gets English content, and vice versa. - New content_language column on workspaces (added to the existing create_workspaces_table migration since we're pre-production), defaulting to 'en'. Fillable on the model. - Dropdown in Settings → Workspace → Brand, side-by-side with Tone. Options: English, Português (Brasil), Español. Short helper copy explains what the setting controls. - Validation: sometimes|string|in:en,pt-BR,es — optional on update so existing callers that don't post the field keep the current value (DB default handles fresh workspaces). - System prompt now uses $content_language (sourced from the workspace) instead of app()->getLocale(). The rule was also strengthened: the AI must always write in this language regardless of what language the user types instructions in, and must instruct image/video tools to render on-media text in this language too. - Image and video Blade prompts received a new line forcing any text rendered inside the generated media to use $content_language. - GenerateImage tool and VideoGenerationService pass the workspace's content_language into their respective prompt templates. Previously the caption would be in Portuguese (since it followed the user's input language) but the image could render with English text because there was no explicit constraint. Now the whole pipeline is aligned to one per-workspace language.
2026-04-16 13:37:03 +00:00
'content_language',
2026-01-15 01:13:44 +00:00
];
feat: AI image generation pipeline and post creation overhaul Core changes: - Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK. New AiImageClient builds prompts from a Blade template seeded by the workspace's ImageStyle enum, content language, brand color (mapped to a human-readable name via HexColorName helper) and brand description. - Drop Template B from TemplateImageGenerator: every slide now renders as Template A (full-bleed photo + bottom gradient + white/grey overlay). Removes renderTemplateB, roundCorners, blendHex, ensureContrast and the closing-slide pipeline. - StreamPostCreation creates the Post directly and dispatches PostCreationReady with post_id; the wizard kills its preview step and redirects straight to the post editor on completion. Finalize endpoint removed. - New Workspace.image_style enum field with an 8-option visual picker shared by /workspaces/create and /settings/workspace/brand via a single BrandForm component (autofill is a prop). 8 sample webp thumbs ship under public/images/branding/image-styles/. - Media items gain optional source ('ai'|'unsplash'|'giphy') and source_meta (recipe needed to regenerate AI images later); the gallery picker tags Unsplash/Giphy attachments. - Brand-color autofill: new CssColorFrequencyExtractor parses every hex/rgb/hsl value in the homepage CSS, clusters perceptually similar shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the most frequent cluster. Solves Tailwind/utility-CSS sites where no semantic --primary variable is exposed. - Credits: gpt-image-2 metered at 15 credits/image (low quality default). - Layout: AuthSplitLayout right column is sticky/h-svh so the form textarea growth no longer stretches the marketing slider. - i18n cleanup: localized labels follow the no-em-dash convention.
2026-05-08 16:38:30 +00:00
protected function casts(): array
{
return [
'image_style' => ImageStyle::class,
'brand_voice_traits' => 'array',
feat: AI image generation pipeline and post creation overhaul Core changes: - Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK. New AiImageClient builds prompts from a Blade template seeded by the workspace's ImageStyle enum, content language, brand color (mapped to a human-readable name via HexColorName helper) and brand description. - Drop Template B from TemplateImageGenerator: every slide now renders as Template A (full-bleed photo + bottom gradient + white/grey overlay). Removes renderTemplateB, roundCorners, blendHex, ensureContrast and the closing-slide pipeline. - StreamPostCreation creates the Post directly and dispatches PostCreationReady with post_id; the wizard kills its preview step and redirects straight to the post editor on completion. Finalize endpoint removed. - New Workspace.image_style enum field with an 8-option visual picker shared by /workspaces/create and /settings/workspace/brand via a single BrandForm component (autofill is a prop). 8 sample webp thumbs ship under public/images/branding/image-styles/. - Media items gain optional source ('ai'|'unsplash'|'giphy') and source_meta (recipe needed to regenerate AI images later); the gallery picker tags Unsplash/Giphy attachments. - Brand-color autofill: new CssColorFrequencyExtractor parses every hex/rgb/hsl value in the homepage CSS, clusters perceptually similar shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the most frequent cluster. Solves Tailwind/utility-CSS sites where no semantic --primary variable is exposed. - Credits: gpt-image-2 metered at 15 credits/image (low quality default). - Layout: AuthSplitLayout right column is sticky/h-svh so the form textarea growth no longer stretches the marketing slider. - i18n cleanup: localized labels follow the no-em-dash convention.
2026-05-08 16:38:30 +00:00
];
}
protected $appends = ['has_logo', 'logo_url'];
public function getHasLogoAttribute(): bool
{
return $this->getFirstMedia('logo') !== null;
}
public function getLogoUrlAttribute(): ?string
{
return $this->getFirstMediaUrl('logo') ?: null;
}
public function account(): BelongsTo
2026-01-15 01:13:44 +00:00
{
return $this->belongsTo(Account::class);
2026-01-15 01:13:44 +00:00
}
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
2026-01-15 01:13:44 +00:00
public function members(): BelongsToMany
{
return $this->belongsToMany(User::class)
2026-01-15 01:13:44 +00:00
->withPivot('role')
->withTimestamps();
}
public function socialAccounts(): HasMany
{
return $this->hasMany(SocialAccount::class);
}
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function signatures(): HasMany
{
return $this->hasMany(WorkspaceSignature::class);
}
public function labels(): HasMany
{
return $this->hasMany(WorkspaceLabel::class);
}
/**
* Get invites for this workspace (invites from the same account that include this workspace).
*
* @return Collection<int, Invite>
*/
public function invites()
{
return Invite::where('account_id', $this->account_id)
->whereJsonContains('workspaces', $this->id)
->whereNull('accepted_at');
}
2026-01-15 01:13:44 +00:00
public function hasMember(User $user): bool
{
return $this->account?->owner_id === $user->id || $this->members()->where('user_id', $user->id)->exists();
2026-01-15 01:13:44 +00:00
}
public function hasConnectedPlatform(string $platform): bool
{
return $this->socialAccounts()->where('platform', $platform)->exists();
}
public function getSocialAccount(string $platform): ?SocialAccount
{
return $this->socialAccounts()->where('platform', $platform)->first();
}
}