Commit graph

61 commits

Author SHA1 Message Date
Paulo Castellano
d336f79059 feat(social): reschedule publish on PlatformUnavailable instead of failing
Before: a scheduled post hitting a platform outage was marked Failed —
user had to manually retry. Now the job reschedules itself for 10
minutes later and the PostPlatform shows status "Retrying". Loops
indefinitely until the platform accepts the post.

- Adds PostPlatformStatus::Retrying (existing string column, no migration)
- PublishToSocialPlatform: PlatformUnavailable catch now calls
  rescheduleForRetry() which (a) updates the row to Retrying with
  retry_count + next_attempt_at in error_context, and (b) dispatches
  itself with a 10-minute delay. updatePostStatus() naturally leaves
  the parent Post in Publishing because Retrying is neither Published
  nor Failed.
- Same treatment for the retry-refresh edge case (publisher throws
  TokenExpired, refresh subsequently fails with PlatformUnavailable).
- i18n + frontend status config updated (en, pt-BR, es) for both
  posts.status.retrying and posts.edit.status.retrying.
- Tests: 3 new tests covering the dispatched job, the edge case path,
  and retry_count increment across attempts.
2026-05-19 10:03:30 -03:00
Paulo Castellano
a96136925c fix(posts): drop redundant 'publishing' toast on publish
Show.vue already renders a full-screen overlay with spinner + the same
'post is being published' messaging while post.status === 'publishing'.
The flash toast was saying the same thing transiently — duplicate UX
that also contributed to the visual noise as Echo events triggered
partial reloads.

- Remove session()->flash() for the Publishing action in PostController
- Drop the now-orphan 'flash.publishing' key from en/pt-BR/es

Scheduled-action flash kept (Show.vue has no equivalent overlay for it).
2026-05-15 12:23:52 -03:00
Paulo Castellano
44d891ef08 fix(pinterest): restore board picker + require board_id in validation
The post editor lost the Pinterest board picker during a UI rewrite,
causing scheduled posts to fail in production with 'Pinterest board_id
is required'. This restores the picker and locks the contract with
validation + tests so the regression cannot happen silently again.

Backend:
- PostController: pinterestBoards is now Record<account_id, Board[]>
  (mirrors the TikTok creator-info pattern); supports multi-account.
- UpdatePostRequest: 'platforms.*.meta.board_id' rule + after-validator
  rejects Publishing/Scheduling Pinterest posts without board_id.

Frontend:
- PinterestSettings.vue: Combobox board picker with empty-state warning;
  emits update:meta with board_id.
- ScheduleTab / PostEditorSidebar / Edit pass pinterestBoards down by
  social_account_id.

Tests (6 new):
- UpdatePostRequestTest: rejects publishing/scheduling without board_id
  across pin/carousel/video pin; allows draft without board_id;
  pinterest error doesn't block sibling platforms in multi-platform.
- PinterestPublisherTest: publisher throws for carousel + video pin
  when no board_id (existing image-pin case kept).

1542 tests passing.
2026-05-15 11:51:20 -03:00
Paulo Castellano
c29198caef feat(subscribe): direct subscription, no trial on /subscribe
Trial now exists exclusively at signup (no-card generic trial). The
/subscribe page is reached only after the trial has been consumed (or to
upgrade plans), so it should never offer another trial — that would be a
double-trial loophole.

- BillingController::subscribe drops trialDays prop
- BillingController::checkout drops ->trialDays() call (direct charge)
- Subscribe.vue drops trialDays prop and :days placeholders
- i18n (en/pt-BR/es): new subscribe-focused copy, remove start_trial/trial_info
2026-05-14 20:05:39 -03:00
Paulo Castellano
af96cb0a0e feat(posts): multi-select label filter on the posts list
Adds a combobox-style filter to the posts index toolbar so users can
narrow All / Scheduled / Posted / Drafts views by one or more labels.

- `PostController::index` accepts `?labels[]=<id>` and applies
  `whereHas('labels', whereIn(...))` (OR semantics across selected labels).
  Workspace labels are exposed to the page (sorted by name) and the
  selected set comes back under `filters.labels`.
- New `LabelFilter.vue` component reuses the existing Popover + Command
  pattern (matching `FontPicker` in the Brand settings page). Trigger
  renders the selected `LabelBadge`s inline (mirroring how each post row
  already displays its labels): 1-3 shown directly, 4+ shown as the
  first three plus a "+N" overflow indicator. Clear button has a
  tooltip and `cursor-pointer`, and stops `click`/`pointerdown`/
  `mousedown` so it doesn't reopen the Popover.
- Existing search debounce is shared with the new label watcher via a
  single `buildFilterUrl` helper. URL is updated with `preserveState +
  replace` so the back stack stays clean.
- i18n in en / pt-BR / es: `filter_by_label`, `label_search_placeholder`,
  `no_labels`, `clear_label_filter`.

Tests: 4 new index tests covering the labels prop exposure, single-label
filter, multi-label OR filter, and blank-id sanitization. Full suite:
1509 passed, 2 skipped, 0 failed.
2026-05-14 09:57:55 -03:00
Paulo Castellano
a37eb6ae9e fix(social): unify status lock + i18n notification strings
Two follow-up fixes from the code review:

1. **Unified lock key for markAsDisconnected / markAsTokenExpired.**
   Both methods now use `social_account_status:{id}` instead of
   different keys. Prevents the race where `markAsDisconnected` and
   `markAsTokenExpired` could run concurrently on the same account
   (publish-time vs verify-batch-time), causing overlapping updates and
   duplicate notifications.

2. **i18n for notification title/body in markAsTokenExpired and
   markAsDisconnected.** Strings were previously hardcoded in English.
   Added `notifications.account_disconnected.{title,body}` and
   `notifications.account_token_expired.{title,body}` in en, pt-BR, es.
   Follows the project convention (e.g. `Mail/PostPublished`) of
   concatenating the `@` prefix in PHP before passing the username to
   the translation placeholder, instead of putting `@:account` in the
   lang file.
2026-05-12 19:11:23 -03:00
Paulo Castellano
620d23187e fix(social): handle TokenExpired status fail-fast and notify user
Three related fixes for the failure mode where a scheduled post errors out
as 'An unknown X error occurred.' when a social account's refresh_token
was already invalidated by the provider:

1. **PublishToSocialPlatform**: fail-fast when account status is
   `TokenExpired`. Previously the job tried to publish, the publisher
   internally tried to refresh, the provider rejected the rotated
   refresh_token, and the failure surfaced as a generic 'unknown' error
   instead of a clear 'reconnect your account' signal.

2. **XPublisher::refreshToken**: when the OAuth endpoint rejects the
   refresh_token (typically because it was rotated/revoked at X), log the
   raw response and throw `TokenExpiredException` instead of falling
   through to `XPublishException::fromApiResponse` which expects the
   tweet-API response shape (`type`/`title`/`detail`) and treats
   OAuth-style responses (`error`/`error_description`) as 'Unknown'.

3. **SocialAccount::markAsTokenExpired**: dispatch an in-app + email
   notification (`Type::AccountDisconnected`) when an account
   transitions from `Connected` → `TokenExpired`, mirroring the
   existing pattern in `markAsDisconnected`. Wrapped in a lock to
   prevent duplicate notifications on concurrent transitions. Accepts an
   optional `notify: false` so the batch verifier
   (`VerifyWorkspaceConnections`) can suppress per-account
   notifications and rely on its summary email.
2026-05-12 18:46:06 -03:00
Paulo Castellano
b9636c6f49 feat(posts): lock scheduled posts behind explicit unschedule + extract PostEditorHeader
Scheduled posts were silently editable: auto-save fired on every keystroke
with status='scheduled', and the backend either silently failed (per-platform
content-length, our new rule) or accepted invalid changes that blew up at
publish time. Editing a queued post is also a race with the publisher job.

UX change: scheduled state now reads-only the editor and exposes one
explicit escape — Unschedule to edit. The post drops to draft, user edits,
re-schedules. Matches Buffer/Hootsuite/Later/Postiz.

Implementation:
- New isLocked computed (= isReadOnly || isScheduled) gates save(),
  triggerAutosave(), debouncedSave(), togglePlatform(). submit() stays on
  isReadOnly so unschedulePost('draft') can still transition out.
- Editor body wraps in pointer-events-none + opacity-60 when isScheduled,
  visually dimming inputs without per-input :disabled plumbing.
- Header swaps to a violet "This post is scheduled / Unschedule to edit"
  banner replacing the entire normal action bar. Delete is hidden in
  this state — only escape is Unschedule, then act on the draft.

Refactor: extracted the entire header into PostEditorHeader.vue (matches
the PostEditorComposer / PostEditorSidebar family). Single <header> root
with v-if/v-else branches by status. ~85 lines out of Edit.vue.

Cleanup: dropped orphan posts.edit.scheduled_for translation key from
en/pt-BR/es — replaced by the new scheduled_overlay_* keys.
2026-05-11 20:57:17 -03:00
Paulo Castellano
45b5b41923 feat: add thread title support to multi-language files and post edit interface 2026-05-11 20:47:53 -03:00
Paulo Castellano
b10f25f94d refactor(posts): tighten content-length caps and simplify contentOverflow
- Platform::contentOverflow returns int (0 = fits) via max(), drops the
  nullable-int + ternary pattern; HasSocialHttpClient and the validation
  rule updated to compare against 0 instead of null.
- Drop the hardcoded 63206-char limit to 10000 across all entry points
  (Platform enum, MCP CreatePostTool/UpdatePostTool, FacebookRules, all
  three FormRequests). Facebook's API accepts up to 63206 but nobody
  writes 63k-char posts and emoji-heavy content risks overflowing the
  TEXT column's 65535-byte ceiling.
- Compiled i18n JSON regenerated with the content_exceeds_platform key.
2026-05-11 20:06:44 -03:00
Paulo Castellano
d15450d2f0 feat: lightbox supports navigation, video, and click-outside-to-close
- Refactor ImagePreviewDialog to expose open(url, type) / openCollection(items, idx)
  via defineExpose; drop external props/state. GalleryBrowser and
  PostEditorComposer now call lightbox.open()/openCollection() instead of
  managing previewItem/previewIndex locally.
- Lightbox closes when clicking the empty space inside the wrapper (between
  image and arrows) using @click.self. Image click also closes; video click
  triggers play/pause without bubbling to close. Arrow buttons keep
  @click.stop so navigation never closes.
- Assets gallery (uploads / Unsplash / Giphy) now opens the lightbox in
  collection mode so the user can navigate the full tab list with arrows
  or keyboard.
- Add 'delete' typed-confirmation on the post-delete dialog (Index +
  Edit pages) and on the asset-delete dialog using the new
  common.confirm_modal.delete_keyword translation (en: 'delete',
  pt-BR: 'deletar', es: 'eliminar').
2026-05-08 19:16:38 -03:00
Paulo Castellano
ff1cc63d9b refactor: introduce VideoPreview component and standardize media handling across post previews 2026-05-08 18:30:11 -03:00
Paulo Castellano
148a2f432f 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 13:38:30 -03:00
Paulo Castellano
cbea8394fc feat: localize OAuth popup callback messages across 12 controllers
User reported the social-account OAuth callback popup ('Threads account
connected!') stayed in English regardless of the active locale. The
hardcoded message was wired through SocialController and 11 platform
controllers (Bluesky, Facebook, Instagram, InstagramFacebook, LinkedIn,
LinkedInPage, Mastodon, Pinterest, Threads, TikTok, YouTube), plus the
Blade view that the popup renders.

Adds an accounts.popup_callback i18n block (en/pt-BR/es) covering:
- The popup chrome (title, closing/close-now status text).
- Generic success/reconnect messages (one shared 'Account connected!'
  / 'Account reconnected!' line — the popup already shows a checkmark
  and lives for ~2s so platform-specific wording wasn't pulling weight).
- Error variants (account/page/channel) and contextual edge cases
  (page not found, no Facebook pages, no YouTube channels, etc.).

Updates every popupCallback() callsite to read from these keys, plus
the Blade view's title and submessage. Regenerates the JSON locale
bundle so laravel-vue-i18n stays in sync.
2026-05-07 14:03:52 -03:00
Paulo Castellano
3e642ef520 refactor: redesign billing settings + upgrade dialog with indies aesthetic
- Billing settings page restructured around the design system: dropped
  the 280px-label / 1fr-content split for stacked HeadingSmall sections;
  current plan rendered as a hero card with display-font name, price,
  amber sticker tile, and an inline primary "Change plan" CTA; payment
  method consolidated into a single sticker row with a violet card-icon
  tile and the manage CTA on the same line; empty payment-method state
  handled. Invoices keep the sticker-row treatment.
- Upgrade dialog mirrors Subscribe.vue end-to-end: planTones backgrounds
  per slug,  POPULAR / CURRENT badges absolute-positioned, ink-bordered
  rounded-full CTA pill, sticker check icons, "Everything in {plan}"
  copy, IconInfoCircle tooltip on credits, yearly/monthly toggle pill
  with rotating amber save-months badge. While a request is in flight
  every plan button is disabled and the active one shows IconLoader2
  spinner.
- Account model gains `displayablePaymentMethod()` returning the
  card array used by the UI. Resolves the customer-level default first,
  falls back to the first attached payment method (Stripe Checkout trials
  anchor the card to the subscription rather than the customer, so the
  customer-level lookup returns null even when a card exists).
- i18n: added `billing.subscription.expires_on` and `no_payment_method`
  in en/pt-BR/es.
2026-05-06 17:00:25 -03:00
Paulo Castellano
d83ac0612f feat: implement workspace management and update localization strings across multiple languages 2026-05-06 16:41:27 -03:00
Paulo Castellano
7733d573da refactor: in-page page headers, indies tabs, and post details polish
- Replace global AppHeader slots with in-page headers on Calendar, Edit,
  and Show pages — full-width canvas with the page-specific controls
  living in their own ink-bordered bar.
- Remove sidebar collapse mechanism: drop Cmd+B keyboard shortcut, force
  defaultOpen=true (ignore persisted cookie), make toggleSidebar a no-op
  on desktop. Sidebar is permanent on desktop, sheet behavior preserved
  on mobile.
- Redesign Tabs to match the design system: each TabsTrigger renders as
  a standalone sticker button (border-2 + rounded-md + shadow-xs), with
  amber-200 active state mirroring the "POST" badge from post-templates.
- Show page layout: cap preview Card to max-w-xl, preserve natural
  aspect for single-image posts (object-contain, max-h-[480px]), drop
  redundant stats grid, swap header sides (Back button left, status +
  date right with date first).
- Assets: bump GalleryBrowser search inputs to h-12 for comfortable
  browsing across uploads, Unsplash, and Giphy tabs.
- Fix PostPlatformMetrics loading-forever bug: hoist useHttp out of the
  onMounted callback into setup, and read the response array directly
  (backend returns the array at root, not under .data).
- i18n: rename posts.show.back_to_posts → posts.show.back; remove unused
  posts.show.summary.* keys.
2026-05-06 16:22:55 -03:00
Paulo Castellano
3ae4a3653a refactor: localize analytics metrics and overhaul UI components and dashboard layouts 2026-05-06 15:39:10 -03:00
Paulo Castellano
acdc78f8bd refactor: standardize UI components, modernize localization structure, and remove legacy appearance management middleware. 2026-05-06 14:26:18 -03:00
Paulo Castellano
563f752866 chore: remove unused language files and update project configuration files 2026-05-04 14:04:03 -03:00
Paulo Castellano
6f3bdb2caa feat: add duplicate post functionality and migrate LinkedIn analytics to the /rest/ API. 2026-05-03 22:37:51 -03:00
Paulo Castellano
137cfb4f6e refactor: standardize MCP tool responses using API resources and enforce strict structural validation in feature tests 2026-05-03 21:34:20 -03:00
Paulo Castellano
ec23928908 refactor: replace Inertia-based OAuth authorization view with a static Blade template and remove unused language files. 2026-05-03 20:26:09 -03:00
Paulo Castellano
afc47e9532 feat: transition AI usage from feature-based limits to a centralized monthly credit system with token tracking. 2026-05-03 19:36:26 -03:00
Paulo Castellano
34272a573f feat: implement consolidated authentication settings including session management and social provider integration 2026-05-03 17:42:44 -03:00
Paulo Castellano
0f385e1b59 refactor: reorganize billing language keys and implement AccountPolicy for subscription management 2026-05-03 17:26:55 -03:00
Paulo Castellano
488d4a41e5 feat: implement feature gating with persistent usage tracking and a reactive upgrade dialog for plan management. 2026-05-03 16:52:28 -03:00
Paulo Castellano
fe0d432f05 feat: implement settings hub dashboard and update usage metrics to support unlimited plans 2026-05-03 15:54:26 -03:00
Paulo Castellano
b47f2488d0 refactor: replace hashtags functionality with reusable signatures feature 2026-05-03 15:23:30 -03:00
Paulo Castellano
d58af18e82 feat: implement workspace creation limits based on subscription plans and remove unused data retention feature 2026-05-03 14:38:31 -03:00
Paulo Castellano
e1961374c0 chore: remove redundant validation language keys from localization files 2026-05-03 14:09:39 -03:00
Paulo Castellano
01a220a95a refactor: remove hardcoded plan prices from models and database in favor of localized translation files 2026-05-03 14:03:31 -03:00
Paulo Castellano
d39124e90b refactor: reorganize settings UI, migrate post templates to a file-based registry, and remove legacy video generation features 2026-05-03 13:44:13 -03:00
Paulo Castellano
e6efa8410e refactor: optimize platform enum and enhance AI content generator prompts with localization updates 2026-05-03 10:17:01 -03:00
Paulo Castellano
028fe1fefd feat: add image title and body fields to AI post creation and register PostTemplateSeeder in local environments 2026-05-03 10:06:02 -03:00
Paulo Castellano
1e1519876d feat: replace legacy AI assistant with modular post content generation, review, and template management system 2026-05-03 09:36:50 -03:00
Paulo Castellano
531bb23315 feat: implement platform-specific media validation and content length constraints in PostEditor 2026-05-02 15:32:55 -03:00
Paulo Castellano
02f45e31e7 feat: implement post editor sidebar with tabbed navigation and add support for LinkedIn and Pinterest platform settings 2026-05-02 14:50:36 -03:00
Paulo Castellano
17ca1f7b0e feat: implement paginated social accounts table with search functionality and updated UI components 2026-05-02 14:15:41 -03:00
Paulo Castellano
8ba5877bdf feat: implement automated brand color extraction from web metadata, CSS, and logos with supporting database schema updates 2026-05-02 13:30:59 -03:00
Paulo Castellano
f3605717c7 refactor: unify social analytics, reorganize workspace settings, and implement content validation rules 2026-05-02 12:22:42 -03:00
Paulo Castellano
3c3b170b21 feat: @mentions in comments, AI Action layer + MCP tools, settings tabs
Mentions in post comments
- @mention autocomplete (workspace members, current user excluded) with
  marker syntax @[uuid] persisted, display names rendered via CommentBody
  chips; live edit replaces markers with names and converts back on save.
- NotifyMentions action with workspace-scoped membership check, dedupes
  same user, only newly-added mentions on update.
- Email + in-app via SendNotification job, respecting per-user
  notification_preferences.mentioned_in_comment.
- Heartbeat-based presence (Cache, 60s TTL, 30s ping) so online recipients
  get only the in-app notification — no email noise.
- Real-time bell on workspace.{id}.user.{id} private channel
  (NotificationCreated event), scoped channel name avoids client-side
  filtering and lays out a convention for future workspace channels.
- Mailable localized via lang/{en,es,pt-BR}/mail.php; Maizzle source
  template for the email is committed and built into resources/views/mail.

AI generation refactor (Action layer + MCP)
- Extracted Actions/Ai/Generate{Image,Video} with QuotaExhaustedException
  so agent tools and MCP tools share a single domain entry point.
- Mcp/Tools/Ai/Generate{Image,Video}Tool registered in TryPostServer; both
  return MediaResource payloads.
- Orientation::imageApiSize maps non-OpenAI ratios to 1:1/2:3/3:2.
- config/ai.php is now the single source of truth driven by env, removing
  the trypost.ai shim. Default text/image providers flipped to OpenAI.

Settings/UX
- /settings/workspace split into shadcn Tabs (Workspace / Brand / Users)
  with three components.
- /assets and the in-editor MediaPicker open the ImagePreviewDialog
  lightbox on image click while preserving action button behaviour.
- Comments tab landed via ?tab=comments&comment=<id> from notification
  click (scroll-to + temporary highlight).
- Mention autocomplete popover flips above when near the viewport bottom.
- Real social platform PNGs replace Tabler brand glyphs in schedule
  pills and post list, with hover tooltip carrying display_name + handle.

Bug fixes
- AcceptInvite: controller now passes workspace + role payload that the
  Vue page expects; login/register CTAs preselect the invite email.
- WorkspaceInvite mailable: stopped referencing nonexistent
  $invite->workspace and $invite->role; column added to the migration,
  Invite model casts role to WorkspaceRole, CreateInvite persists it.
- PostCommentCreated: added broadcastAs so .PostCommentCreated actually
  matches the Echo listener; payload now includes mentioned_users so
  receivers render the chip correctly without a refetch.
- Preview components for X/Pinterest/Threads/Bluesky/LinkedIn/Mastodon/
  TikTok/YouTube switched from item.type === 'image' to
  !isVideoMedia(item) so media without a persisted type still renders.
- UpdatePostRequest now accepts media.*.{type,mime_type,size,...} so the
  posts.media JSON keeps the metadata that the previews need.
- Removed throttle:6,1 from social connect routes (was 429ing legitimate
  OAuth retries).
- Used MediaType enum cases instead of literal 'image'/'video' strings
  when creating media rows.

Tests
- MentionParser unit tests, NotifyMentions feature tests including
  online/offline channel selection and preference gating, MCP AI tool
  happy paths, MentionedInComment mailable rendering, AcceptInvite +
  search-members + index mentioned_users path. 1229 passing.
2026-05-01 20:59:03 -03:00
Paulo Castellano
dafdd5da43 feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
  GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
  post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
  EmojiPicker (full Unicode set, search, categories, recently-used,
  light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
  content_types map) so the user can switch Feed/Reel/Story etc. and have
  it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
  types); replaces inline maps across 5 components, fixes
  instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
  platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
  on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
  (was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 14:53:49 -03:00
Paulo Castellano
35646bbaf6 chore: working 2026-04-23 13:23:24 -03:00
Paulo Castellano
b3b59b4d13 refactor: remove onboarding flow, implement brand analysis services, and replace setup middleware with account readiness checks 2026-04-16 23:05:51 -03:00
Paulo Castellano
aaa4db1dbb 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 10:37:03 -03:00
Paulo Castellano
5f7768ca3a fix: apply code review — enforce quota server-side, project conventions
Critical fixes:
- Quota enforcement regressed when the regex flow was replaced with
  tools. The LLM was trusted to respect [Session state] quota hints,
  which is vulnerable to prompt injection / hallucination. Each tool
  now checks Pennant feature limits at the top of handle() and returns
  a short quota-exhausted string instead of calling the provider.
  Audio shares the video quota (there is no AiAudioLimit by design).
- N+1 on aiMessages: combined the two separate ->get() scans that
  produced imagesInThread and videosInThread into a single query.
- postPlatforms lazy load: controller now loadMissing('postPlatforms')
  before constructing SocialMediaAssistant, which accesses the relation
  in activePlatformRules().

Project conventions:
- \RuntimeException and \Throwable are now imported at the top of the
  controller instead of inlined per CLAUDE.md.
- Tool handle() methods use data_get($request, 'prompt') instead of
  direct array access, matching the data_get convention for service
  classes.
- enrichContent() pluralizes attachment types via Str::plural so
  '2 images' reads naturally instead of '2 image'.

Tests:
- Added Storage::fake('public') in tool tests so runs don't pollute
  the local disk with fake-generated files.
- Restored quota enforcement coverage (deleted when the flow changed)
  as three new tests — one per tool — that fill the monthly bucket
  and assert the tool refuses and nothing is generated.
- Removed inline \App\ references in tests in favor of imports.
2026-04-16 10:22:55 -03:00
Paulo Castellano
b2bf5c2059 feat: complete AI assistant with custom services and brand config
Baseline snapshot of custom AI implementation before Laravel AI SDK migration.

Includes:
- Custom services: GeminiTextGenerationService, TextGenerationService (OpenAI), ImageGenerationService, AudioGenerationService, VideoGenerationService
- IntentDetector for content moderation via keyword matching
- AI enums: Intent, Orientation, UsageType
- Blade prompt templates: system.blade.php, image.blade.php, video.blade.php
- AiMessage with content_html accessor (markdown rendering)
- AiUsageLog for monthly quota tracking per account
- PostAssistantController with regex-based [GENERATE_*] parsing
- WritingAssistantTab with markdown rendering, add-to-post, attachments
- Workspace brand fields (name, description, tone, voice_notes) in system prompt
- Session state block injected into prompts (thread counts, quota remaining)
- AttachmentCollector pattern will replace the regex approach in Phase 2
- Post comments with replies, emoji reactions, real-time via Echo
- Assets page with Unsplash + Giphy integrations
2026-04-16 09:25:08 -03:00
Paulo Castellano
b27f3fbd1e feat: add multi-language support and update comments tab UI 2026-04-15 20:36:06 -03:00
Paulo Castellano
0f6ae9a4e6 feat: add PostCommentCreated broadcast event 2026-04-15 20:11:36 -03:00