diff --git a/.idea/.gitignore b/.idea/.gitignore
index 30cf57e..784d205 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -8,3 +8,5 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+/laravel-idea-personal.xml
+/jsLibraryMappings.xml
diff --git a/.idea/php.xml b/.idea/php.xml
index 4bfd474..b52ccfc 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -3,6 +3,9 @@
+
+
+
@@ -15,149 +18,159 @@
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -166,6 +179,11 @@
+
+
+
+
+
diff --git a/backend/.gitignore b/backend/.gitignore
index 83d894f..f986e84 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -24,3 +24,4 @@ _ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db
+.idea/
diff --git a/backend/app/Ai/Agents/ContentWriterAgent.php b/backend/app/Ai/Agents/ContentWriterAgent.php
index 9d2fa76..df166ed 100644
--- a/backend/app/Ai/Agents/ContentWriterAgent.php
+++ b/backend/app/Ai/Agents/ContentWriterAgent.php
@@ -3,19 +3,17 @@
namespace App\Ai\Agents;
use Laravel\Ai\Attributes\Provider;
+use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
-use Laravel\Ai\Contracts\HasTools;
-use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Enums\Lab;
-use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;
#[Provider(Lab::Groq)]
-class ContentWriterAgent implements Agent, Conversational, HasTools
+class ContentWriterAgent implements Agent, Conversational
{
- use Promptable;
+ use Promptable, RemembersConversations;
/**
* Get the instructions that the agent should follow.
@@ -53,24 +51,4 @@ public function instructions(): Stringable|string
- Do not include a platform label (e.g. "For Instagram:").
INSTRUCTIONS;
}
-
- /**
- * Get the list of messages comprising the conversation so far.
- *
- * @return Message[]
- */
- public function messages(): iterable
- {
- return [];
- }
-
- /**
- * Get the tools available to the agent.
- *
- * @return Tool[]
- */
- public function tools(): iterable
- {
- return [];
- }
}
diff --git a/backend/app/Ai/Agents/CreativeDirectorAgent.php b/backend/app/Ai/Agents/CreativeDirectorAgent.php
index b01241e..2c5425a 100644
--- a/backend/app/Ai/Agents/CreativeDirectorAgent.php
+++ b/backend/app/Ai/Agents/CreativeDirectorAgent.php
@@ -3,19 +3,17 @@
namespace App\Ai\Agents;
use Laravel\Ai\Attributes\Provider;
+use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
-use Laravel\Ai\Contracts\HasTools;
-use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Enums\Lab;
-use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;
#[Provider(Lab::Groq)]
-class CreativeDirectorAgent implements Agent, Conversational, HasTools
+class CreativeDirectorAgent implements Agent, Conversational
{
- use Promptable;
+ use Promptable, RemembersConversations;
/**
* Get the instructions that the agent should follow.
@@ -27,24 +25,4 @@ public function instructions(): Stringable|string
'AI image generator. The scene must be visually compelling and directly '.
"relevant to the post's topic. Return ONLY the image prompt — nothing else.";
}
-
- /**
- * Get the list of messages comprising the conversation so far.
- *
- * @return Message[]
- */
- public function messages(): iterable
- {
- return [];
- }
-
- /**
- * Get the tools available to the agent.
- *
- * @return Tool[]
- */
- public function tools(): iterable
- {
- return [];
- }
}
diff --git a/backend/app/Http/Controllers/ChatController.php b/backend/app/Http/Controllers/ChatController.php
index 54b0b92..2a031a6 100644
--- a/backend/app/Http/Controllers/ChatController.php
+++ b/backend/app/Http/Controllers/ChatController.php
@@ -5,7 +5,7 @@
use App\Actions\Chats\CreateChatAction;
use App\Http\Requests\CreateChatRequest;
use App\Http\Resources\ChatResponseResource;
-use Illuminate\Http\Request;
+use App\Models\Chat;
class ChatController extends Controller
{
@@ -14,7 +14,7 @@ public function index(Request $request) {}
public function store(CreateChatRequest $request, CreateChatAction $createChatAction)
{
return new ChatResponseResource(
- $createChatAction->create($request->user(), $request->input('title', null))
+ $createChatAction->create($request->user(), $request->input('title', 'New Chat'))
);
}
}
diff --git a/backend/app/Http/Controllers/ChatMessageController.php b/backend/app/Http/Controllers/ChatMessageController.php
index ca664b0..31ea45a 100644
--- a/backend/app/Http/Controllers/ChatMessageController.php
+++ b/backend/app/Http/Controllers/ChatMessageController.php
@@ -6,9 +6,12 @@
use App\Http\Resources\GeneratedPostResource;
use App\Models\Chat;
use App\Services\SocialMediaService;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Attributes\Controllers\Authorize;
class ChatMessageController extends Controller
{
+ #[Authorize('update', 'chat')]
public function store(GeneratePostRequest $request, Chat $chat, SocialMediaService $socialMediaService)
{
return new GeneratedPostResource(
diff --git a/backend/app/Models/Chat.php b/backend/app/Models/Chat.php
index cd344b1..679ab5b 100644
--- a/backend/app/Models/Chat.php
+++ b/backend/app/Models/Chat.php
@@ -3,20 +3,25 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
+use Illuminate\Database\Eloquent\Attributes\Table;
+use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['user_id', 'title'])]
+#[Table('agent_conversations')]
class Chat extends Model
{
+ use HasUuids;
+
public function user(): BelongsTo
{
- return $this->belongsTo(User::class);
+ return $this->belongsTo(User::class, 'user_id');
}
public function messages(): HasMany
{
- return $this->hasMany(ChatMessage::class);
+ return $this->hasMany(ChatMessage::class, 'conversation_id', 'id')->orderBy('created_at', 'asc');
}
}
diff --git a/backend/app/Models/ChatMessage.php b/backend/app/Models/ChatMessage.php
index 626a239..83651c7 100644
--- a/backend/app/Models/ChatMessage.php
+++ b/backend/app/Models/ChatMessage.php
@@ -3,14 +3,19 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
+use Illuminate\Database\Eloquent\Attributes\Table;
+use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
-#[Fillable(['chat_id', 'role', 'content', 'user_id'])]
+#[Fillable(['conversation_id', 'agent_id', 'role', 'content', 'attachment', 'tool_calls', 'tool_results', 'usage', 'meta', 'content', 'user_id'])]
+#[Table('agent_conversation_messages')]
class ChatMessage extends Model
{
+ use HasUuids;
+
public function chat(): BelongsTo
{
- return $this->belongsTo(Chat::class);
+ return $this->belongsTo(Chat::class, 'conversation_id', 'id');
}
}
diff --git a/backend/app/Policies/ChatPolicy.php b/backend/app/Policies/ChatPolicy.php
new file mode 100644
index 0000000..6b92286
--- /dev/null
+++ b/backend/app/Policies/ChatPolicy.php
@@ -0,0 +1,47 @@
+user()->is($user);
+ }
+
+ public function create(User $user): bool
+ {
+ return false;
+ }
+
+ public function update(User $user, Chat $chat): bool
+ {
+ return $chat->user()->is($user);
+ }
+
+ public function delete(User $user, Chat $chat): bool
+ {
+ return false;
+ }
+
+ public function restore(User $user, Chat $chat): bool
+ {
+ return false;
+ }
+
+ public function forceDelete(User $user, Chat $chat): bool
+ {
+ return false;
+ }
+}
diff --git a/backend/app/Services/SocialMediaService.php b/backend/app/Services/SocialMediaService.php
index 8149fc3..f0c10ee 100644
--- a/backend/app/Services/SocialMediaService.php
+++ b/backend/app/Services/SocialMediaService.php
@@ -6,9 +6,7 @@
use App\Ai\Agents\ContentWriterAgent;
use App\Ai\Agents\CreativeDirectorAgent;
use App\Data\SocialMediaPostResponseDto;
-use App\Enums\Chats\ChatRoles;
use App\Models\Chat;
-use App\Models\ChatMessage;
readonly class SocialMediaService
{
@@ -25,17 +23,13 @@ public function __construct(
*/
public function generatePostWithImage(string $prompt, Chat $chat): SocialMediaPostResponseDto
{
- $this->chatMessage->store($chat, ChatRoles::USER, $prompt);
-
- $socialMediaResponse = $this->contentWriterAgent->prompt($prompt);
+ $socialMediaResponse = $this->contentWriterAgent->forUser($chat->user)->prompt($prompt);
$postText = $socialMediaResponse->text;
- /* @var ChatMessage $aiChat */
- $aiChat = $this->chatMessage->store($chat, ChatRoles::AI, $postText);
-
+ // Generate image prompt via creative director agent
$imagePromptResponse = $this->creativeDirectorAgent->prompt($postText);
$imagePrompt = $imagePromptResponse->text;
- return new SocialMediaPostResponseDto($aiChat->id, $postText, $imagePrompt, now());
+ return new SocialMediaPostResponseDto($socialMediaResponse->conversationId, $postText, $imagePrompt, now());
}
}
diff --git a/backend/database/migrations/2026_04_28_055031_create_chats_table.php b/backend/database/migrations/2026_04_28_055031_create_chats_table.php
deleted file mode 100644
index 134b217..0000000
--- a/backend/database/migrations/2026_04_28_055031_create_chats_table.php
+++ /dev/null
@@ -1,30 +0,0 @@
-id();
- $table->foreignIdFor(User::class, 'user_id')->constrained()->cascadeOnDelete();
- $table->string('title')->nullable();
- $table->timestamps();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('chats');
- }
-};
diff --git a/backend/database/migrations/2026_04_28_055044_create_chat_messages_table.php b/backend/database/migrations/2026_04_28_055044_create_chat_messages_table.php
deleted file mode 100644
index 90ed1ea..0000000
--- a/backend/database/migrations/2026_04_28_055044_create_chat_messages_table.php
+++ /dev/null
@@ -1,31 +0,0 @@
-id();
- $table->foreignIdFor(Chat::class, 'chat_id')->constrained()->cascadeOnDelete();
- $table->string('role');
- $table->text('content');
- $table->timestamps();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('chat_messages');
- }
-};
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index 4c598f2..62e28a3 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -3,10 +3,14 @@ import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
- loadComponent: () => import('./chat/chat').then(m => m.Chat)
+ loadComponent: () => import('./chat/chat').then((m) => m.Chat),
+ },
+ {
+ path: ':id',
+ loadComponent: () => import('./chat/chat').then((m) => m.Chat),
},
{
path: 'user',
- loadChildren: () => import('./auth/auth.routes').then(m => m.authRoutes)
- }
+ loadChildren: () => import('./auth/auth.routes').then((m) => m.authRoutes),
+ },
];
diff --git a/frontend/src/app/chat/chat.store.ts b/frontend/src/app/chat/chat.store.ts
index d9129fb..d47020c 100644
--- a/frontend/src/app/chat/chat.store.ts
+++ b/frontend/src/app/chat/chat.store.ts
@@ -4,6 +4,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { ChatService } from './chat-service';
import { ChatState, Message } from './chat.types';
import { lastValueFrom } from 'rxjs';
+import { Router } from '@angular/router';
const initialState: ChatState = {
messages: [
@@ -23,12 +24,16 @@ export const ChatStore = signalStore(
withState(initialState),
withMethods((store) => {
const chatService = inject(ChatService);
+ const router = inject(Router);
const newChat = async () => {
try {
const response = await lastValueFrom(chatService.newChat());
- patchState(store, { id: response.data.id });
- return response.data.id;
+ const chatId = response.data.id;
+
+ patchState(store, { id: chatId });
+ await router.navigate(['/', chatId], { replaceUrl: true });
+ return chatId;
} catch (error) {
console.error('Failed to create a new chat:', error);
throw error;
@@ -37,7 +42,9 @@ export const ChatStore = signalStore(
return {
newChat,
-
+ setChatId: (chatId: string) => {
+ patchState(store, { id: chatId });
+ },
sendMessage: async (content: string) => {
if (!store.id()) {
try {
diff --git a/frontend/src/app/chat/chat.ts b/frontend/src/app/chat/chat.ts
index c917dfb..34fa180 100644
--- a/frontend/src/app/chat/chat.ts
+++ b/frontend/src/app/chat/chat.ts
@@ -2,16 +2,18 @@ import { Component, ElementRef, ViewChild, effect, inject } from '@angular/core'
import { CommonModule } from '@angular/common';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ChatStore } from './chat.store';
+import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './chat.html',
- styleUrl: './chat.css'
+ styleUrl: './chat.css',
})
export class Chat {
readonly chatStore = inject(ChatStore);
+ private route = inject(ActivatedRoute);
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;
@@ -30,6 +32,16 @@ export class Chat {
});
}
+ ngOnInit() {
+ this.route.paramMap.subscribe((params) => {
+ const urlId = params.get('id');
+ if (urlId) {
+ // If an ID exists in the URL, populate it in the store
+ this.chatStore.setChatId(urlId);
+ }
+ });
+ }
+
errorMessage = '';
sendMessage() {
@@ -50,6 +62,6 @@ export class Chat {
try {
const el = this.scrollContainer.nativeElement;
el.scrollTop = el.scrollHeight;
- } catch(err) { }
+ } catch (err) {}
}
}