Implement new JSON:API Resource for the api responses.
Implement Authentication States, show logout options in auth store and services. Make DB and controllers to make messages grouped by chats. Refactor backend code to use DTO.
This commit is contained in:
parent
66e858e386
commit
8e2ced8bed
16
backend/app/Actions/Chats/CreateChatAction.php
Normal file
16
backend/app/Actions/Chats/CreateChatAction.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Chats;
|
||||||
|
|
||||||
|
use App\Data\ChatResponseDto;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class CreateChatAction
|
||||||
|
{
|
||||||
|
public function create(User $user, ?string $title = null)
|
||||||
|
{
|
||||||
|
$chat = $user->chats()->create(['title' => $title]);
|
||||||
|
|
||||||
|
return ChatResponseDto::fromModel($chat);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/app/Actions/Chats/StoreChatMessageAction.php
Normal file
18
backend/app/Actions/Chats/StoreChatMessageAction.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Chats;
|
||||||
|
|
||||||
|
use App\Enums\Chats\ChatRoles;
|
||||||
|
use App\Models\Chat;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
final readonly class StoreChatMessageAction
|
||||||
|
{
|
||||||
|
public function store(Chat $chat, ChatRoles $role, string $message): Model
|
||||||
|
{
|
||||||
|
return $chat->messages()->create([
|
||||||
|
'role' => $role->value,
|
||||||
|
'content' => $message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/app/Data/ChatResponseDto.php
Normal file
28
backend/app/Data/ChatResponseDto.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Data;
|
||||||
|
|
||||||
|
use App\Models\Chat;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
|
||||||
|
final readonly class ChatResponseDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id,
|
||||||
|
public CarbonInterface $createdAt,
|
||||||
|
public CarbonInterface $updatedAt,
|
||||||
|
public string $userId,
|
||||||
|
public ?string $title = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromModel(Chat $chat)
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
id: $chat->id,
|
||||||
|
createdAt: $chat->created_at,
|
||||||
|
updatedAt: $chat->updated_at,
|
||||||
|
userId: $chat->user_id,
|
||||||
|
title: $chat->title ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Data;
|
namespace App\Data;
|
||||||
|
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
|
||||||
class SocialMediaPostResponseDto
|
class SocialMediaPostResponseDto
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
public string $id,
|
||||||
public string $post,
|
public string $post,
|
||||||
public string $image,
|
public string $image,
|
||||||
|
public CarbonInterface $createdAt
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
9
backend/app/Enums/Chats/ChatRoles.php
Normal file
9
backend/app/Enums/Chats/ChatRoles.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums\Chats;
|
||||||
|
|
||||||
|
enum ChatRoles: string
|
||||||
|
{
|
||||||
|
case USER = 'user';
|
||||||
|
case AI = 'ai';
|
||||||
|
}
|
||||||
20
backend/app/Http/Controllers/ChatController.php
Normal file
20
backend/app/Http/Controllers/ChatController.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Actions\Chats\CreateChatAction;
|
||||||
|
use App\Http\Requests\CreateChatRequest;
|
||||||
|
use App\Http\Resources\ChatResponseResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ChatController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request) {}
|
||||||
|
|
||||||
|
public function store(CreateChatRequest $request, CreateChatAction $createChatAction)
|
||||||
|
{
|
||||||
|
return new ChatResponseResource(
|
||||||
|
$createChatAction->create($request->user(), $request->input('title', null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/app/Http/Controllers/ChatMessageController.php
Normal file
19
backend/app/Http/Controllers/ChatMessageController.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\GeneratePostRequest;
|
||||||
|
use App\Http\Resources\GeneratedPostResource;
|
||||||
|
use App\Models\Chat;
|
||||||
|
use App\Services\SocialMediaService;
|
||||||
|
|
||||||
|
class ChatMessageController extends Controller
|
||||||
|
{
|
||||||
|
public function store(GeneratePostRequest $request, Chat $chat, SocialMediaService $socialMediaService)
|
||||||
|
{
|
||||||
|
return new GeneratedPostResource(
|
||||||
|
$socialMediaService
|
||||||
|
->generatePostWithImage($request->input('prompt'), $chat)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\SocialMediaPostRequest;
|
|
||||||
use App\Services\SocialMediaService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Routing\Attributes\Controllers\Middleware;
|
|
||||||
|
|
||||||
#[Middleware('auth:sanctum')]
|
|
||||||
class SocialMediaPostController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Generate a social media post and a matching image from the given prompt.
|
|
||||||
*/
|
|
||||||
public function generate(
|
|
||||||
SocialMediaPostRequest $request,
|
|
||||||
SocialMediaService $socialMediaService,
|
|
||||||
): JsonResponse {
|
|
||||||
$prompt = $request->input('prompt');
|
|
||||||
$response = $socialMediaService->generatePostWithImage($prompt);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'post' => $response->post,
|
|
||||||
'image_prompt' => $response->image,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
backend/app/Http/Requests/CreateChatRequest.php
Normal file
20
backend/app/Http/Requests/CreateChatRequest.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CreateChatRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'nullable|string|max:255',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class SocialMediaPostRequest extends FormRequest
|
class GeneratePostRequest extends FormRequest
|
||||||
{
|
{
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
32
backend/app/Http/Resources/ChatResponseResource.php
Normal file
32
backend/app/Http/Resources/ChatResponseResource.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Data\ChatResponseDto;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property ChatResponseDto $resource
|
||||||
|
*/
|
||||||
|
class ChatResponseResource extends JsonApiResource
|
||||||
|
{
|
||||||
|
public function type(): string
|
||||||
|
{
|
||||||
|
return 'chats';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveResourceIdentifier($request): string
|
||||||
|
{
|
||||||
|
return (string) $this->resource->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toAttributes(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $this->resource->title,
|
||||||
|
'createdAt' => $this->resource->createdAt->toIso8601String(),
|
||||||
|
'updatedAt' => $this->resource->updatedAt->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/app/Http/Resources/GeneratedPostResource.php
Normal file
32
backend/app/Http/Resources/GeneratedPostResource.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Data\SocialMediaPostResponseDto;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property SocialMediaPostResponseDto $resource
|
||||||
|
*/
|
||||||
|
class GeneratedPostResource extends JsonApiResource
|
||||||
|
{
|
||||||
|
public function type(): string
|
||||||
|
{
|
||||||
|
return 'generated-posts';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveResourceIdentifier($request): string
|
||||||
|
{
|
||||||
|
return (string) $this->resource->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toAttributes(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'post' => $this->resource->post,
|
||||||
|
'image' => $this->resource->image,
|
||||||
|
'createdAt' => $this->resource->createdAt->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/app/Models/Chat.php
Normal file
22
backend/app/Models/Chat.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
#[Fillable(['user_id', 'title'])]
|
||||||
|
class Chat extends Model
|
||||||
|
{
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ChatMessage::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/app/Models/ChatMessage.php
Normal file
16
backend/app/Models/ChatMessage.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable(['chat_id', 'role', 'content', 'user_id'])]
|
||||||
|
class ChatMessage extends Model
|
||||||
|
{
|
||||||
|
public function chat(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Chat::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
@ -30,4 +31,9 @@ protected function casts(): array
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function chats(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Chat::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,25 +2,40 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Actions\Chats\StoreChatMessageAction;
|
||||||
use App\Ai\Agents\ContentWriterAgent;
|
use App\Ai\Agents\ContentWriterAgent;
|
||||||
use App\Ai\Agents\CreativeDirectorAgent;
|
use App\Ai\Agents\CreativeDirectorAgent;
|
||||||
use App\Data\SocialMediaPostResponseDto;
|
use App\Data\SocialMediaPostResponseDto;
|
||||||
|
use App\Enums\Chats\ChatRoles;
|
||||||
|
use App\Models\Chat;
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
|
||||||
class SocialMediaService
|
readonly class SocialMediaService
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ContentWriterAgent $contentWriterAgent,
|
private ContentWriterAgent $contentWriterAgent,
|
||||||
private readonly CreativeDirectorAgent $creativeDirectorAgent,
|
private CreativeDirectorAgent $creativeDirectorAgent,
|
||||||
|
private StoreChatMessageAction $chatMessage,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function generatePostWithImage(string $prompt): SocialMediaPostResponseDto
|
/**
|
||||||
|
* Generate a social media post with an image.
|
||||||
|
* We are not using database transactions here because we do not want to delete the user prompt
|
||||||
|
* if any of the ai agents fails.
|
||||||
|
*/
|
||||||
|
public function generatePostWithImage(string $prompt, Chat $chat): SocialMediaPostResponseDto
|
||||||
{
|
{
|
||||||
|
$this->chatMessage->store($chat, ChatRoles::USER, $prompt);
|
||||||
|
|
||||||
$socialMediaResponse = $this->contentWriterAgent->prompt($prompt);
|
$socialMediaResponse = $this->contentWriterAgent->prompt($prompt);
|
||||||
$postText = $socialMediaResponse->text;
|
$postText = $socialMediaResponse->text;
|
||||||
|
|
||||||
|
/* @var ChatMessage $aiChat */
|
||||||
|
$aiChat = $this->chatMessage->store($chat, ChatRoles::AI, $postText);
|
||||||
|
|
||||||
$imagePromptResponse = $this->creativeDirectorAgent->prompt($postText);
|
$imagePromptResponse = $this->creativeDirectorAgent->prompt($postText);
|
||||||
$imagePrompt = $imagePromptResponse->text;
|
$imagePrompt = $imagePromptResponse->text;
|
||||||
|
|
||||||
return new SocialMediaPostResponseDto($postText, $imagePrompt);
|
return new SocialMediaPostResponseDto($aiChat->id, $postText, $imagePrompt, now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('chats', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Chat;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('chat_messages', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\SocialMediaPostController;
|
use App\Http\Controllers\ChatController;
|
||||||
|
use App\Http\Controllers\ChatMessageController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -8,4 +9,5 @@
|
|||||||
return $request->user();
|
return $request->user();
|
||||||
})->middleware('auth:sanctum');
|
})->middleware('auth:sanctum');
|
||||||
|
|
||||||
Route::post('/social-media/generate', [SocialMediaPostController::class, 'generate']);
|
Route::apiResource('chats', ChatController::class);
|
||||||
|
Route::apiResource('chats.messages', ChatMessageController::class);
|
||||||
|
|||||||
16
frontend/src/app/chat/chat-service.spec.ts
Normal file
16
frontend/src/app/chat/chat-service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChatService } from './chat-service';
|
||||||
|
|
||||||
|
describe('ChatService', () => {
|
||||||
|
let service: ChatService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ChatService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
22
frontend/src/app/chat/chat-service.ts
Normal file
22
frontend/src/app/chat/chat-service.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { API_URL } from '../core/tokens/api-urls';
|
||||||
|
import { MessageResponse, NewChatResponse } from './chat.types';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ChatService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private apiUrl = inject(API_URL);
|
||||||
|
|
||||||
|
public newChat() {
|
||||||
|
return this.http.post<NewChatResponse>(`${this.apiUrl}/chats`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMessage(id: string, message: string) {
|
||||||
|
return this.http.post<MessageResponse>(`${this.apiUrl}/chats/${id}/messages`, {
|
||||||
|
prompt: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,41 +1,62 @@
|
|||||||
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
|
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
export type Message = {
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
id: string;
|
import { ChatService } from './chat-service';
|
||||||
role: 'user' | 'ai';
|
import { ChatState, Message } from './chat.types';
|
||||||
content: string;
|
import { lastValueFrom } from 'rxjs';
|
||||||
timestamp: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChatState = {
|
|
||||||
messages: Message[];
|
|
||||||
isLoading: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState: ChatState = {
|
const initialState: ChatState = {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
id: 'welcome',
|
id: 'welcome',
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
content: "What\'s you want to post today ?",
|
content: "What's you want to post today?",
|
||||||
timestamp: new Date()
|
timestamp: new Date(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
id: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { lastValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
export const ChatStore = signalStore(
|
export const ChatStore = signalStore(
|
||||||
{ providedIn: 'root' },
|
{ providedIn: 'root' },
|
||||||
withState(initialState),
|
withState(initialState),
|
||||||
withMethods((store) => {
|
withMethods((store) => {
|
||||||
const http = inject(HttpClient);
|
const chatService = inject(ChatService);
|
||||||
|
|
||||||
|
const newChat = async () => {
|
||||||
|
try {
|
||||||
|
const response = await lastValueFrom(chatService.newChat());
|
||||||
|
patchState(store, { id: response.data.id });
|
||||||
|
return response.data.id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create a new chat:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
newChat,
|
||||||
|
|
||||||
sendMessage: async (content: string) => {
|
sendMessage: async (content: string) => {
|
||||||
|
if (!store.id()) {
|
||||||
|
try {
|
||||||
|
await newChat();
|
||||||
|
} catch (error) {
|
||||||
|
// If chat creation fails, append an error message and stop execution
|
||||||
|
const errorMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: 'ai',
|
||||||
|
content: 'Failed to initialize a new chat session. Please try again.',
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
patchState(store, (state) => ({
|
||||||
|
messages: [...state.messages, errorMessage],
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add user message
|
// Add user message
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
@ -46,24 +67,23 @@ export const ChatStore = signalStore(
|
|||||||
|
|
||||||
patchState(store, (state) => ({
|
patchState(store, (state) => ({
|
||||||
messages: [...state.messages, userMessage],
|
messages: [...state.messages, userMessage],
|
||||||
isLoading: true
|
isLoading: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
http.post<{post: string, imagePrompt?: string}>('http://localhost:8000/api/social-media/generate', { prompt: content })
|
chatService.sendMessage(<string>store.id(), content),
|
||||||
);
|
);
|
||||||
|
|
||||||
const aiMessage: Message = {
|
const aiMessage: Message = {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
content: response.post,
|
content: response.data.attributes.post,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(response.data.attributes.createdAt),
|
||||||
};
|
};
|
||||||
|
|
||||||
patchState(store, (state) => ({
|
patchState(store, (state) => ({
|
||||||
messages: [...state.messages, aiMessage],
|
messages: [...state.messages, aiMessage],
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
}));
|
}));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
let errorText = 'Sorry, I encountered an error while communicating with the server.';
|
let errorText = 'Sorry, I encountered an error while communicating with the server.';
|
||||||
@ -89,10 +109,10 @@ export const ChatStore = signalStore(
|
|||||||
|
|
||||||
patchState(store, (state) => ({
|
patchState(store, (state) => ({
|
||||||
messages: [...state.messages, errorMessage],
|
messages: [...state.messages, errorMessage],
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
30
frontend/src/app/chat/chat.types.ts
Normal file
30
frontend/src/app/chat/chat.types.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { JsonApiDocument, JsonApiResource } from '../core/types/api';
|
||||||
|
|
||||||
|
export interface NewChatAttributes {
|
||||||
|
id: string;
|
||||||
|
title: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewChatResponse = JsonApiDocument<NewChatAttributes>;
|
||||||
|
|
||||||
|
export interface MessageAttributes {
|
||||||
|
post: string;
|
||||||
|
image: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
export type MessageResponse = JsonApiDocument<MessageAttributes>;
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'ai';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatState = {
|
||||||
|
messages: Message[];
|
||||||
|
isLoading: boolean;
|
||||||
|
id: string | null;
|
||||||
|
};
|
||||||
@ -20,18 +20,22 @@
|
|||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/user/register"
|
<a routerLink="/user/register"
|
||||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-0.5">
|
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-linear-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-0.5">
|
||||||
Register
|
Register
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@else{
|
@else{
|
||||||
<div class="flex font-normal items-center space-x-3">
|
<div class="flex font-normal items-center space-x-6">
|
||||||
|
<button (click)="chatStore.newChat()"
|
||||||
|
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-linear-to-r from-blue-600 to-indigo-600 shadow-md hover:shadow-lg transition-all duration-200 transform hover:-translate-y-0.5">
|
||||||
|
New Chat
|
||||||
|
</button>
|
||||||
<span class="text-sm text-gray-200 dark:text-gray-300">
|
<span class="text-sm text-gray-200 dark:text-gray-300">
|
||||||
{{ authStore.user()?.name }}
|
{{ authStore.user()?.name }}
|
||||||
</span>
|
</span>
|
||||||
<button (click)="authStore.logout()"
|
<button (click)="authStore.logout()"
|
||||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-gray-400 bg-gray-200/9 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-0.5">
|
class="text-sm font-semibold rounded-lg text-gray-400 transition-all duration-200 transform hover:text-red-400">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Component, inject} from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import {AuthStore} from '../../../auth/auth.store';
|
import { AuthStore } from '../../../auth/auth.store';
|
||||||
|
import { ChatStore } from '../../../chat/chat.store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
@ -10,5 +11,5 @@ import {AuthStore} from '../../../auth/auth.store';
|
|||||||
})
|
})
|
||||||
export class Header {
|
export class Header {
|
||||||
protected readonly authStore = inject(AuthStore);
|
protected readonly authStore = inject(AuthStore);
|
||||||
|
protected readonly chatStore = inject(ChatStore);
|
||||||
}
|
}
|
||||||
|
|||||||
9
frontend/src/app/core/types/api.ts
Normal file
9
frontend/src/app/core/types/api.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface JsonApiResource<T> {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
attributes: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonApiDocument<T> {
|
||||||
|
data: JsonApiResource<T>;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user