refactor(core): replace SocialMediaService with GeneratePostService, add message fetching and JSON:API resources
- Removed `SocialMediaService` and migrated core post generation logic to `GeneratePostService`. - Added `GetAllChatMessagesAction` for fetching chat history. - Introduced `MessageDto`, `MessageResource`, and `MessageCollection` for consistent backend API responses. - Updated frontend state and services to support JSON:API-compliant chat messages and history retrieval. - Improved typings and casting for chat message data.
This commit is contained in:
parent
cde80dbf08
commit
20a56d4adc
1
.idea/laravel-idea.xml
generated
1
.idea/laravel-idea.xml
generated
@ -12,6 +12,7 @@
|
|||||||
<map>
|
<map>
|
||||||
<entry key="createEloquentScope:namespace" value="Models\Scopes" />
|
<entry key="createEloquentScope:namespace" value="Models\Scopes" />
|
||||||
<entry key="createFormRequestDto:namespace" value="Data" />
|
<entry key="createFormRequestDto:namespace" value="Data" />
|
||||||
|
<entry key="createJsonResource:classSuffix" value="Resource" />
|
||||||
<entry key="createModel:namespace" value="Models" />
|
<entry key="createModel:namespace" value="Models" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
18
backend/app/Actions/Chats/GetAllChatMessagesAction.php
Normal file
18
backend/app/Actions/Chats/GetAllChatMessagesAction.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Chats;
|
||||||
|
|
||||||
|
use App\Data\Chats\MessageDto;
|
||||||
|
use App\Models\Chat;
|
||||||
|
use Illuminate\Pagination\AbstractPaginator;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class GetAllChatMessagesAction
|
||||||
|
{
|
||||||
|
public function messages(Chat $chat): AbstractPaginator|LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$messages = $chat->messages()->oldest()->paginate();
|
||||||
|
|
||||||
|
return $messages->through(fn ($m) => MessageDto::fromModel($m));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<?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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
67
backend/app/Data/Chats/MessageDto.php
Normal file
67
backend/app/Data/Chats/MessageDto.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Data\Chats;
|
||||||
|
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Laravel\Ai\Messages\AssistantMessage;
|
||||||
|
use Laravel\Ai\Responses\AgentResponse;
|
||||||
|
use Laravel\Ai\Responses\TextResponse;
|
||||||
|
|
||||||
|
final readonly class MessageDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id,
|
||||||
|
public string $conversationId,
|
||||||
|
public string $role,
|
||||||
|
public string $content,
|
||||||
|
public string $agent,
|
||||||
|
public array $attachments,
|
||||||
|
public array $toolCalls,
|
||||||
|
public array $toolResults,
|
||||||
|
public array $usage,
|
||||||
|
public array $meta,
|
||||||
|
public CarbonInterface $createdAt,
|
||||||
|
public CarbonInterface $updatedAt,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromModel(ChatMessage|Model $message): MessageDto
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
id: $message->id,
|
||||||
|
conversationId: $message->conversation_id,
|
||||||
|
role: $message->role,
|
||||||
|
content: $message->content,
|
||||||
|
agent: $message->agent,
|
||||||
|
attachments: $message->attachments,
|
||||||
|
toolCalls: $message->tool_calls,
|
||||||
|
toolResults: $message->tool_results,
|
||||||
|
usage: $message->usage,
|
||||||
|
meta: $message->meta,
|
||||||
|
createdAt: $message->created_at,
|
||||||
|
updatedAt: $message->updated_at,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromAgentResponse(AgentResponse|TextResponse $response, string $agent): MessageDto
|
||||||
|
{
|
||||||
|
/** @var AssistantMessage $message */
|
||||||
|
$message = $response->messages->first();
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
id: $response->invocationId,
|
||||||
|
conversationId: $response->conversationId,
|
||||||
|
role: $message->role->value,
|
||||||
|
content: $message->content,
|
||||||
|
agent: $agent,
|
||||||
|
attachments: [],
|
||||||
|
toolCalls: $response->toolCalls->toArray(),
|
||||||
|
toolResults: $response->toolResults->toArray(),
|
||||||
|
usage: $response->usage->toArray(),
|
||||||
|
meta: $response->meta->toArray(),
|
||||||
|
createdAt: now(),
|
||||||
|
updatedAt: now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
class ChatController extends Controller
|
class ChatController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request) {}
|
public function index(Chat $chat) {}
|
||||||
|
|
||||||
public function store(CreateChatRequest $request, CreateChatAction $createChatAction)
|
public function store(CreateChatRequest $request, CreateChatAction $createChatAction)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,22 +2,30 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Chats;
|
namespace App\Http\Controllers\Chats;
|
||||||
|
|
||||||
|
use App\Actions\Chats\GetAllChatMessagesAction;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Chats\GeneratePostRequest;
|
use App\Http\Requests\Chats\GeneratePostRequest;
|
||||||
use App\Http\Resources\Chats\GeneratedPostResource;
|
use App\Http\Resources\Chats\MessageResource;
|
||||||
use App\Models\Chat;
|
use App\Models\Chat;
|
||||||
use App\Services\SocialMediaService;
|
use App\Services\GeneratePostService;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Routing\Attributes\Controllers\Authorize;
|
use Illuminate\Routing\Attributes\Controllers\Authorize;
|
||||||
|
use Illuminate\Routing\Attributes\Controllers\Middleware;
|
||||||
|
|
||||||
|
#[Middleware('auth:sanctum')]
|
||||||
class ChatMessageController extends Controller
|
class ChatMessageController extends Controller
|
||||||
{
|
{
|
||||||
#[Authorize('update', 'chat')]
|
/**
|
||||||
public function store(GeneratePostRequest $request, Chat $chat, SocialMediaService $socialMediaService)
|
* Get Chat History of a Chat
|
||||||
|
*/
|
||||||
|
#[Authorize('view', 'chat')]
|
||||||
|
public function index(Chat $chat, GetAllChatMessagesAction $getAllMessages)
|
||||||
{
|
{
|
||||||
return new GeneratedPostResource(
|
return MessageResource::collection($getAllMessages->messages($chat));
|
||||||
$socialMediaService
|
}
|
||||||
->generatePostWithImage($request->input('prompt'), $chat)
|
|
||||||
);
|
#[Authorize('update', 'chat')]
|
||||||
|
public function store(GeneratePostRequest $request, Chat $chat, GeneratePostService $socialMediaService)
|
||||||
|
{
|
||||||
|
return new MessageResource($socialMediaService->generate($request->input('prompt'), $chat));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
backend/app/Http/Resources/Chats/MessageCollection.php
Normal file
18
backend/app/Http/Resources/Chats/MessageCollection.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Chats;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Attributes\Collects;
|
||||||
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
|
#[Collects(MessageResource::class)]
|
||||||
|
class MessageCollection extends ResourceCollection
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'data' => $this->collection,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
backend/app/Http/Resources/Chats/MessageResource.php
Normal file
33
backend/app/Http/Resources/Chats/MessageResource.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Chats;
|
||||||
|
|
||||||
|
use App\Data\Chats\MessageDto;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property MessageDto $resource
|
||||||
|
*/
|
||||||
|
class MessageResource extends JsonApiResource
|
||||||
|
{
|
||||||
|
public function type(): string
|
||||||
|
{
|
||||||
|
return 'messages';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveResourceIdentifier($request): string
|
||||||
|
{
|
||||||
|
return $this->resource->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toAttributes(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'role' => $this->resource->role,
|
||||||
|
'content' => $this->resource->content,
|
||||||
|
'attachments' => $this->resource->attachments,
|
||||||
|
'createdAt' => $this->resource->createdAt->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
#[Fillable(['conversation_id', 'agent_id', 'role', 'content', 'attachment', 'tool_calls', 'tool_results', 'usage', 'meta', 'content', 'user_id'])]
|
#[Fillable(['conversation_id', 'agent_id', 'role', 'content', 'attachments', 'tool_calls', 'tool_results', 'usage', 'meta', 'content', 'user_id'])]
|
||||||
#[Table('agent_conversation_messages')]
|
#[Table('agent_conversation_messages')]
|
||||||
class ChatMessage extends Model
|
class ChatMessage extends Model
|
||||||
{
|
{
|
||||||
@ -18,4 +18,12 @@ public function chat(): BelongsTo
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Chat::class, 'conversation_id', 'id');
|
return $this->belongsTo(Chat::class, 'conversation_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'attachments' => 'array',
|
||||||
|
'meta' => 'array',
|
||||||
|
'usage' => 'array',
|
||||||
|
'tool_calls' => 'array',
|
||||||
|
'tool_results' => 'array',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
28
backend/app/Services/GeneratePostService.php
Normal file
28
backend/app/Services/GeneratePostService.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Ai\Agents\ContentWriterAgent;
|
||||||
|
use App\Data\Chats\MessageDto;
|
||||||
|
use App\Models\Chat;
|
||||||
|
|
||||||
|
class GeneratePostService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ContentWriterAgent $contentWriterAgent,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 generate(string $prompt, Chat $chat): MessageDto
|
||||||
|
{
|
||||||
|
$socialMediaResponse = $this->contentWriterAgent
|
||||||
|
->continue($chat->id, $chat->user)
|
||||||
|
->prompt($prompt);
|
||||||
|
|
||||||
|
return MessageDto::fromAgentResponse($socialMediaResponse, ContentWriterAgent::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Actions\Chats\StoreChatMessageAction;
|
|
||||||
use App\Ai\Agents\ContentWriterAgent;
|
|
||||||
use App\Ai\Agents\CreativeDirectorAgent;
|
|
||||||
use App\Data\Chats\SocialMediaPostResponseDto;
|
|
||||||
use App\Models\Chat;
|
|
||||||
|
|
||||||
readonly class SocialMediaService
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private ContentWriterAgent $contentWriterAgent,
|
|
||||||
private CreativeDirectorAgent $creativeDirectorAgent,
|
|
||||||
private StoreChatMessageAction $chatMessage,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
{
|
|
||||||
$socialMediaResponse = $this->contentWriterAgent->forUser($chat->user)->prompt($prompt);
|
|
||||||
$postText = $socialMediaResponse->text;
|
|
||||||
|
|
||||||
// Generate image prompt via creative director agent
|
|
||||||
$imagePromptResponse = $this->creativeDirectorAgent->prompt($postText);
|
|
||||||
$imagePrompt = $imagePromptResponse->text;
|
|
||||||
|
|
||||||
return new SocialMediaPostResponseDto($socialMediaResponse->conversationId, $postText, $imagePrompt, now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { API_URL } from '../core/tokens/api-urls';
|
import { API_URL } from '../core/tokens/api-urls';
|
||||||
import { MessageResponse, NewChatResponse } from './chat.types';
|
import { MessageCollection, MessageResponse, NewChatResponse } from './chat.types';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -19,4 +19,8 @@ export class ChatService {
|
|||||||
prompt: message,
|
prompt: message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMessages(id: string) {
|
||||||
|
return this.http.get<MessageCollection>(`${this.apiUrl}/chats/${id}/messages`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,16 +11,16 @@
|
|||||||
@for (msg of chatStore.messages(); track msg.id) {
|
@for (msg of chatStore.messages(); track msg.id) {
|
||||||
<div
|
<div
|
||||||
class="flex flex-col max-w-[80%] animate-[messageAppear_0.5s_cubic-bezier(0.16,1,0.3,1)]"
|
class="flex flex-col max-w-[80%] animate-[messageAppear_0.5s_cubic-bezier(0.16,1,0.3,1)]"
|
||||||
[ngClass]="msg.role === 'user' ? 'self-end items-end' : 'self-start items-start'"
|
[ngClass]="msg.attributes.role === 'user' ? 'self-end items-end' : 'self-start items-start'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="px-5 py-4 rounded-2xl text-base leading-relaxed shadow-[0_4px_15px_rgba(0,0,0,0.1)] relative whitespace-pre-wrap"
|
class="px-5 py-4 rounded-2xl text-base leading-relaxed shadow-[0_4px_15px_rgba(0,0,0,0.1)] relative whitespace-pre-wrap"
|
||||||
[ngClass]="msg.role === 'user' ? 'bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-br-sm' : 'bg-white/10 border border-white/5 text-slate-200 rounded-bl-sm backdrop-blur-md'"
|
[ngClass]="msg.attributes.role === 'user' ? 'bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-br-sm' : 'bg-white/10 border border-white/5 text-slate-200 rounded-bl-sm backdrop-blur-md'"
|
||||||
>
|
>
|
||||||
{{ msg.content }}
|
{{ msg.attributes.content }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-slate-500 mt-2 px-1">
|
<div class="text-xs text-slate-500 mt-2 px-1">
|
||||||
{{ msg.timestamp | date:'shortTime' }}
|
{{ msg.attributes.createdAt | date:'shortTime' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
|
import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { ChatService } from './chat-service';
|
import { ChatService } from './chat-service';
|
||||||
import { ChatState, Message } from './chat.types';
|
import { ChatState, Message, MessageResponse } from './chat.types';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { JsonApiResource } from '../core/types/api';
|
||||||
|
|
||||||
const initialState: ChatState = {
|
const initialState: ChatState = {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
id: 'welcome',
|
id: 'null',
|
||||||
role: 'ai',
|
type: 'messages',
|
||||||
content: "What's you want to post today?",
|
attributes: {
|
||||||
timestamp: new Date(),
|
role: 'assistant',
|
||||||
|
content: "What's you want to post today?",
|
||||||
|
attachments: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -39,6 +44,21 @@ export const ChatStore = signalStore(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const setErrorMessage = (message: string, role: 'assistant' | 'user' = 'user') => {
|
||||||
|
const errorMessage: JsonApiResource<Message> = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'messages',
|
||||||
|
attributes: {
|
||||||
|
role: role,
|
||||||
|
content: message,
|
||||||
|
attachments: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
patchState(store, (state) => ({
|
||||||
|
messages: [...state.messages, errorMessage],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newChat,
|
newChat,
|
||||||
@ -51,25 +71,24 @@ export const ChatStore = signalStore(
|
|||||||
await newChat();
|
await newChat();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If chat creation fails, append an error message and stop execution
|
// If chat creation fails, append an error message and stop execution
|
||||||
const errorMessage: Message = {
|
setErrorMessage(
|
||||||
id: Date.now().toString(),
|
'Failed to initialize a new chat session. Please try again.',
|
||||||
role: 'ai',
|
'assistant',
|
||||||
content: 'Failed to initialize a new chat session. Please try again.',
|
);
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
patchState(store, (state) => ({
|
|
||||||
messages: [...state.messages, errorMessage],
|
|
||||||
}));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user message
|
// Add user message
|
||||||
const userMessage: Message = {
|
const userMessage: JsonApiResource<Message> = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
role: 'user',
|
type: 'messages',
|
||||||
content,
|
attributes: {
|
||||||
timestamp: new Date(),
|
role: 'user',
|
||||||
|
content: content,
|
||||||
|
attachments: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
patchState(store, (state) => ({
|
patchState(store, (state) => ({
|
||||||
@ -77,19 +96,12 @@ export const ChatStore = signalStore(
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
}));
|
}));
|
||||||
try {
|
try {
|
||||||
const response = await lastValueFrom(
|
const aiMessage: MessageResponse = await lastValueFrom(
|
||||||
chatService.sendMessage(<string>store.id(), content),
|
chatService.sendMessage(<string>store.id(), content),
|
||||||
);
|
);
|
||||||
|
|
||||||
const aiMessage: Message = {
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
role: 'ai',
|
|
||||||
content: response.data.attributes.post,
|
|
||||||
timestamp: new Date(response.data.attributes.createdAt),
|
|
||||||
};
|
|
||||||
|
|
||||||
patchState(store, (state) => ({
|
patchState(store, (state) => ({
|
||||||
messages: [...state.messages, aiMessage],
|
messages: [...state.messages, aiMessage.data],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
}));
|
}));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -106,20 +118,40 @@ export const ChatStore = signalStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setErrorMessage(errorText, 'assistant');
|
||||||
const errorMessage: Message = {
|
patchState(store, () => ({
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
role: 'ai',
|
|
||||||
content: errorText,
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
patchState(store, (state) => ({
|
|
||||||
messages: [...state.messages, errorMessage],
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fetchChatHistory: async () => {
|
||||||
|
if (!store.id()) {
|
||||||
|
setErrorMessage('Please create a new chat session first.', 'assistant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchState(store, { isLoading: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatHistory = await lastValueFrom(chatService.getMessages(<string>store.id()));
|
||||||
|
patchState(store, { messages: chatHistory.data, isLoading: false });
|
||||||
|
} catch (error: any) {
|
||||||
|
let errorText = 'Failed to load chat history. Please try again.';
|
||||||
|
|
||||||
|
if (error instanceof HttpErrorResponse && error.status === 422) {
|
||||||
|
errorText = error.error?.message || 'Validation error.';
|
||||||
|
|
||||||
|
if (error.error?.errors) {
|
||||||
|
const firstErrorKey = Object.keys(error.error.errors)[0];
|
||||||
|
if (firstErrorKey && error.error.errors[firstErrorKey].length > 0) {
|
||||||
|
errorText = error.error.errors[firstErrorKey][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(errorText, 'assistant');
|
||||||
|
patchState(store, { isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export class Chat {
|
|||||||
if (urlId) {
|
if (urlId) {
|
||||||
// If an ID exists in the URL, populate it in the store
|
// If an ID exists in the URL, populate it in the store
|
||||||
this.chatStore.setChatId(urlId);
|
this.chatStore.setChatId(urlId);
|
||||||
|
this.chatStore.fetchChatHistory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { JsonApiDocument, JsonApiResource } from '../core/types/api';
|
import { JsonApiCollection, JsonApiDocument, JsonApiResource } from '../core/types/api';
|
||||||
|
|
||||||
export interface NewChatAttributes {
|
export interface NewChatAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -9,22 +9,18 @@ export interface NewChatAttributes {
|
|||||||
|
|
||||||
export type NewChatResponse = JsonApiDocument<NewChatAttributes>;
|
export type NewChatResponse = JsonApiDocument<NewChatAttributes>;
|
||||||
|
|
||||||
export interface MessageAttributes {
|
export interface Message {
|
||||||
post: string;
|
role: 'assistant' | 'user';
|
||||||
image: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
export type MessageResponse = JsonApiDocument<MessageAttributes>;
|
|
||||||
|
|
||||||
export type Message = {
|
|
||||||
id: string;
|
|
||||||
role: 'user' | 'ai';
|
|
||||||
content: string;
|
content: string;
|
||||||
timestamp: Date;
|
attachments: string[] | null;
|
||||||
};
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageResponse = JsonApiDocument<Message>;
|
||||||
|
export type MessageCollection = JsonApiCollection<Message>;
|
||||||
|
|
||||||
export type ChatState = {
|
export type ChatState = {
|
||||||
messages: Message[];
|
messages: JsonApiResource<Message>[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,3 +7,30 @@ export interface JsonApiResource<T> {
|
|||||||
export interface JsonApiDocument<T> {
|
export interface JsonApiDocument<T> {
|
||||||
data: JsonApiResource<T>;
|
data: JsonApiResource<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MetaLinks {
|
||||||
|
url: string | null;
|
||||||
|
label: string;
|
||||||
|
page: number | null;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonApiCollection<T> {
|
||||||
|
data: JsonApiResource<T>[];
|
||||||
|
links: {
|
||||||
|
first: string;
|
||||||
|
last: string;
|
||||||
|
prev: string | null;
|
||||||
|
next: string | null;
|
||||||
|
};
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
from: number;
|
||||||
|
last_page: number;
|
||||||
|
links: MetaLinks[];
|
||||||
|
path: string;
|
||||||
|
per_page: number;
|
||||||
|
to: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user