2024-10-31 12:09:06 -05:00
< ? php
2026-03-13 15:21:22 -05:00
use App\Ai\Agents\ChatWithHoldingAgent ;
use App\Ai\Agents\ChatWithPortfolioAgent ;
use App\Ai\Agents\ChatWithSuggestedPromptsAgent ;
use App\Models\ChatWithConversation ;
use App\Models\Holding ;
use App\Models\Portfolio ;
2024-10-31 15:19:59 -05:00
use Illuminate\Database\Eloquent\Model ;
2026-03-13 15:21:22 -05:00
use Illuminate\Support\Facades\RateLimiter ;
use Laravel\Ai\Contracts\Agent ;
use Laravel\Ai\Streaming\Events\TextDelta ;
use Livewire\Attributes\Async ;
2024-10-31 15:19:59 -05:00
use Livewire\Volt\Component ;
2025-09-26 17:41:28 -05:00
new class extends Component
{
2024-10-31 15:19:59 -05:00
// props
public Model $chatable ;
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
public array $suggested_prompts = [];
public array $messages = [];
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
public ? string $prompt = null ;
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
public ? string $answer = null ;
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
public bool $streaming = false ;
2025-09-26 17:41:28 -05:00
2026-03-13 15:21:22 -05:00
public ? string $agent_conversation_id = null ;
2024-10-31 15:19:59 -05:00
// methods
2026-03-13 15:21:22 -05:00
public function mount () : void
2024-10-31 15:19:59 -05:00
{
2026-03-13 15:21:22 -05:00
$chatWith = ChatWithConversation :: firstOrCreate (
[
'chatable_type' => $this -> chatable :: class ,
'chatable_id' => $this -> chatable -> id ,
'user_id' => auth () -> id (),
],
[ 'title' => 'Chat with investments' ]
);
$this -> agent_conversation_id = $chatWith -> id ;
$this -> messages = $chatWith -> messages ()
-> orderBy ( 'id' , 'desc' )
-> limit ( 20 )
-> get ([ 'role' , 'content' , 'created_at' ])
-> map ( fn ( $m ) => [ 'role' => $m -> role , 'content' => $m -> content , 'created_at' => $m -> created_at ])
-> reverse ()
-> values ()
-> toArray ();
2024-10-31 15:19:59 -05:00
}
2026-03-13 15:21:22 -05:00
public function startCompletion ( ? string $suggestedPrompt = null ) : void
2024-10-31 15:19:59 -05:00
{
2026-03-13 15:21:22 -05:00
if ( $this -> streaming ) {
return ;
}
2024-10-31 15:19:59 -05:00
// prevent spam
2026-03-13 15:21:22 -05:00
if ( $this -> isRateLimited ()) {
2024-10-31 15:19:59 -05:00
array_push ( $this -> messages , [
2025-09-26 17:41:28 -05:00
'role' => 'assistant' ,
'content' => __ ( 'Hang on! You\'re doing that too much.' ),
2026-03-13 15:21:22 -05:00
'created_at' => now (),
2024-10-31 15:19:59 -05:00
]);
$this -> js ( 'scrollChatWindow(250)' );
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
return ;
2024-10-31 12:09:06 -05:00
}
2024-10-31 15:19:59 -05:00
if ( $suggestedPrompt ) {
$this -> prompt = $suggestedPrompt ;
2026-03-13 15:21:22 -05:00
$this -> suggested_prompts = [];
2024-10-31 15:19:59 -05:00
}
2024-10-31 12:09:06 -05:00
2026-03-13 15:21:22 -05:00
if ( empty ( trim ( $this -> prompt ? ? '' ))) {
2024-10-31 15:19:59 -05:00
$this -> resetPrompt ();
2024-10-31 12:09:06 -05:00
2026-03-13 15:21:22 -05:00
array_push ( $this -> messages , [ 'role' => 'assistant' , 'content' => __ ( 'Feel free to ask me a question!' ), 'created_at' => now ()]);
2024-10-31 12:09:06 -05:00
$this -> js ( 'scrollChatWindow(250)' );
2024-10-31 15:19:59 -05:00
return ;
2025-09-26 17:41:28 -05:00
}
2024-10-31 15:19:59 -05:00
2026-03-13 15:21:22 -05:00
array_push ( $this -> messages , [ 'role' => 'user' , 'content' => $this -> prompt , 'created_at' => now ()]);
2024-10-31 15:19:59 -05:00
$this -> js ( 'scrollChatWindow(250)' );
$this -> resetPrompt ();
$this -> streaming = true ;
2024-11-01 23:18:22 -05:00
$this -> js ( '$wire.generateCompletion()' );
2024-10-31 15:19:59 -05:00
}
2024-11-01 23:18:22 -05:00
public function generateCompletion () : void
2024-10-31 15:19:59 -05:00
{
2026-03-13 15:21:22 -05:00
$userPrompt = end ( $this -> messages )[ 'content' ] ? ? '' ;
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
try {
2026-03-13 15:21:22 -05:00
$agent = $this -> makeAgent () -> continue ( $this -> agent_conversation_id , auth () -> user ());
$stream = $agent -> stream ( $userPrompt );
} catch ( Exception $e ) {
array_push ( $this -> messages , [ 'role' => 'assistant' , 'content' => $e -> getMessage (), 'created_at' => now ()]);
2024-10-31 12:09:06 -05:00
$this -> resetPrompt ();
2025-09-26 17:41:28 -05:00
2024-10-31 15:19:59 -05:00
return ;
2024-10-31 12:09:06 -05:00
}
2025-09-26 17:41:28 -05:00
$this -> stream ( to : 'answer' , content : '' , replace : true );
2026-03-13 15:21:22 -05:00
foreach ( $stream as $event ) {
if ( $event instanceof TextDelta ) {
$this -> stream ( to : 'answer' , content : $event -> delta , replace : false );
$this -> answer .= $event -> delta ;
2024-10-31 12:09:06 -05:00
}
2024-10-31 15:19:59 -05:00
$this -> js ( 'scrollChatWindow()' );
2024-10-31 12:09:06 -05:00
}
2026-03-13 15:21:22 -05:00
array_push ( $this -> messages , [ 'role' => 'assistant' , 'content' => $this -> answer , 'created_at' => now ()]);
2024-10-31 15:19:59 -05:00
$this -> resetPrompt ();
2024-11-01 23:18:22 -05:00
$this -> js ( '$wire.generateSuggestedPrompts()' );
}
2026-03-13 15:21:22 -05:00
#[Async]
2024-11-01 23:18:22 -05:00
public function generateSuggestedPrompts () : void
{
2026-03-13 15:21:22 -05:00
try {
$response = ChatWithSuggestedPromptsAgent :: make ( messages : array_slice ( $this -> messages , - 3 )) -> prompt ( '' );
2024-11-01 23:18:22 -05:00
2026-03-13 15:21:22 -05:00
$this -> suggested_prompts = $response -> toArray ()[ 'suggested_prompts' ] ? ? [];
} catch ( Exception $e ) {
2024-11-01 23:18:22 -05:00
$this -> suggested_prompts = [];
$this -> error ( $e -> getMessage ());
}
2024-10-31 15:19:59 -05:00
}
public function resetPrompt () : void
{
$this -> answer = null ;
$this -> prompt = null ;
$this -> streaming = false ;
}
public function isRateLimited () : bool
{
2025-09-26 17:41:28 -05:00
$rateLimitKey = auth () -> id () . '/' . $this -> chatable -> id ;
2024-10-31 15:19:59 -05:00
if ( RateLimiter :: tooManyAttempts ( $rateLimitKey , 20 )) {
return true ;
2024-10-31 12:09:06 -05:00
}
2024-10-31 15:19:59 -05:00
RateLimiter :: hit ( $rateLimitKey , 60 );
2024-10-31 12:09:06 -05:00
2024-10-31 15:19:59 -05:00
return false ;
}
2024-10-31 12:09:06 -05:00
2026-03-13 15:21:22 -05:00
private function makeAgent () : Agent
2024-12-06 16:01:53 -06:00
{
2026-03-13 15:21:22 -05:00
return match ( true ) {
$this -> chatable instanceof Portfolio => new ChatWithPortfolioAgent ( $this -> chatable ),
$this -> chatable instanceof Holding => new ChatWithHoldingAgent ( $this -> chatable ),
};
2024-12-06 16:01:53 -06:00
}
2024-10-31 15:19:59 -05:00
}; ?>
2026-03-13 15:21:22 -05:00
<div
x-data="{
open: false,
async scrollChatWindow(delay = 0) {
await new Promise(resolve => setTimeout(resolve, delay));
this.$refs.chatWindow.scrollBy({
top: this.$refs.chatWindow.scrollHeight,
behavior: 'smooth'
});
}
}"
2024-11-01 22:13:40 -05:00
class="fixed z-50 bottom-8 right-8"
2024-10-31 15:19:59 -05:00
>
2024-11-01 22:13:40 -05:00
{{-- toggle button --}}
2026-03-13 15:21:22 -05:00
<x-ui.button
2024-11-01 22:13:40 -05:00
x-show="!open"
2024-10-31 15:19:59 -05:00
@click="$dispatch('toggle-ai-chat')"
2025-09-26 17:41:28 -05:00
@keyup.escape.window="open = false"
2026-03-13 15:21:22 -05:00
class="flex btn btn-circle md:btn-lg btn-primary"
2024-10-31 15:19:59 -05:00
>
<x-slot:label>
2025-09-26 17:41:28 -05:00
<x-ui.icon name="o-sparkles" class="w-6 h-6 md:w-8 md:h-8"></x-ui.icon>
2024-10-31 15:19:59 -05:00
</x-slot:label>
2025-09-26 17:41:28 -05:00
</x-ui.button>
2024-10-31 12:09:06 -05:00
2024-11-01 22:13:40 -05:00
{{-- popup --}}
2026-03-13 15:21:22 -05:00
<div
2024-10-31 15:19:59 -05:00
x-on:toggle-ai-chat.window="open = !open"
x-show="open"
2026-03-13 15:21:22 -05:00
x-trap="open"
2024-10-31 15:19:59 -05:00
x-bind:inert="!open"
2024-11-01 22:13:40 -05:00
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-full"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform translate-y-full"
2024-10-31 15:19:59 -05:00
x-cloak
2026-03-13 15:21:22 -05:00
key="ai-chat"
2025-09-26 17:41:28 -05:00
class="fixed bg-base-300 shadow-2xl rounded-none md:rounded-lg
2026-03-13 15:21:22 -05:00
inset-0 h-screen w-full md:inset-auto md:right-6
2025-09-26 17:41:28 -05:00
md:bottom-6 md:w-[32rem] md:h-[46rem]"
2024-10-31 12:09:06 -05:00
>
2026-03-13 15:21:22 -05:00
<div
class="absolute inset-0 flex flex-col overflow-hidden p-4"
2024-11-01 22:13:40 -05:00
x-intersect="scrollChatWindow()"
>
<div class="flex grow-0 justify-between items-center pb-4 ">
2025-09-26 17:41:28 -05:00
<h2 class="text-lg text-bold select-none">{{ __('AI Chat') }}</h2>
2026-03-13 15:21:22 -05:00
<x-ui.button
icon="o-x-mark"
class="absolute top-5 right-4 btn-ghost btn-circle btn-sm"
2024-11-01 22:13:40 -05:00
title="{{ __('Close') }}"
2026-03-13 15:21:22 -05:00
@click="open = false"
2024-11-01 22:13:40 -05:00
/>
</div>
2024-10-31 15:19:59 -05:00
{{-- chat window --}}
2024-11-01 22:13:40 -05:00
<div class="grow overflow-hidden overflow-y-scroll ai-chat" x-ref="chatWindow">
2024-10-31 15:19:59 -05:00
<div class="flex gap-3 mb-5 flex-1">
<span class="
flex
rounded-full
w-10 h-10
border border-gray-600
dark:border-gray-400
text-gray-600
dark:text-gray-400
bg-slate-200
dark:bg-slate-800
">
2025-09-26 17:41:28 -05:00
<x-ui.icon name="o-sparkles" class="h-auto p-1 w-10" />
2024-10-31 15:19:59 -05:00
</span>
<p class="leading-relaxed w-full">
<span class="block font-bold">AI</span> {{ __('Hi, how can I help?') }}
2026-03-13 15:21:22 -05:00
2024-10-31 15:19:59 -05:00
</p>
</div>
2026-03-13 15:21:22 -05:00
@foreach($messages as $message)
<div class="flex gap-3 mb-5 flex-1">
2024-10-31 15:19:59 -05:00
@if ($message['role'] == 'user')
2026-03-13 15:21:22 -05:00
<span class="relative flex shrink-0 overflow-hidden rounded-full w-10 h-10">
<x-ui.avatar :image="auth()->user()->profile_photo_url" class="!w-10" />
</span>
<p class="leading-relaxed">
<span class="block font-bold" title="{{ $message['created_at'] }}">{{ __('You') }} </span> {{ $message['content'] }}
</p>
2024-10-31 15:19:59 -05:00
@else
2026-03-13 15:21:22 -05:00
<span class="
flex
rounded-full
w-10 h-10
border border-gray-600
dark:border-gray-400
text-gray-600
dark:text-gray-400
bg-slate-200
dark:bg-slate-800
">
<x-ui.icon name="o-sparkles" class="h-auto p-1 w-10" />
</span>
<div class="leading-relaxed" >
<span class="block font-bold" title="{{ $message['created_at'] }}">AI </span> {!! Str::markdown($message['content']) !!}
2024-10-31 15:19:59 -05:00
</div>
2026-03-13 15:21:22 -05:00
2024-10-31 15:19:59 -05:00
@endif
2026-03-13 15:21:22 -05:00
</div>
2024-10-31 15:19:59 -05:00
@endforeach
2026-03-13 15:21:22 -05:00
2024-10-31 15:19:59 -05:00
@if($streaming)
<div class="flex gap-3 mb-10 flex-1">
2024-10-31 12:09:06 -05:00
<span class="
flex
rounded-full
w-10 h-10
border border-gray-600
dark:border-gray-400
text-gray-600
dark:text-gray-400
bg-slate-200
dark:bg-slate-800
2026-03-13 15:21:22 -05:00
">
2025-09-26 17:41:28 -05:00
<x-ui.icon name="o-sparkles" class="h-auto p-1 w-10" />
2024-10-31 12:09:06 -05:00
</span>
2024-10-31 15:19:59 -05:00
<p class="leading-relaxed" >
<span class="block font-bold ">AI </span> <span wire:stream="answer">{{ $answer }}</span>
2024-10-31 12:09:06 -05:00
</p>
</div>
2024-10-31 15:19:59 -05:00
@endif
</div>
2026-03-13 15:21:22 -05:00
2024-10-31 15:19:59 -05:00
{{-- prompt input --}}
2024-11-01 22:13:40 -05:00
<div class="mt-3 grow-0">
2026-03-13 15:21:22 -05:00
<form wire:submit.prevent>
2024-11-01 22:13:40 -05:00
<div class="">
@foreach($suggested_prompts as $prompt)
2026-03-13 15:21:22 -05:00
<x-ui.button
class="btn-xs btn-primary btn-outline mr-1 mb-2"
wire:click="startCompletion('{{ addslashes($prompt['value']) }}')"
2025-09-26 17:41:28 -05:00
>{{ $prompt['text'] }}</x-ui.button>
2024-11-01 22:13:40 -05:00
@endforeach
2026-03-13 15:21:22 -05:00
2024-10-31 12:09:06 -05:00
</div>
2026-03-13 15:21:22 -05:00
2024-11-01 22:13:40 -05:00
<div class="flex justify-between align-bottom space-x-2 mt-1">
2026-03-13 15:21:22 -05:00
<div class="w-full">
2025-09-26 17:41:28 -05:00
<x-ui.textarea
2024-11-01 22:13:40 -05:00
wire:model="prompt"
2025-09-26 17:41:28 -05:00
class="h-18 resize-none bg-base-200"
2024-11-01 22:13:40 -05:00
placeholder="{{ __('Have a question? AI might be able to help...') }}"
wire:keydown.enter.prevent="startCompletion"
autofocus
2025-09-26 17:41:28 -05:00
@toggle-ai-chat.window="setTimeout(() => $el.focus(), 250)"
2026-03-13 15:21:22 -05:00
x-trap="true"
2025-09-26 17:41:28 -05:00
></x-ui.textarea>
2026-03-13 15:21:22 -05:00
2024-11-01 22:13:40 -05:00
</div>
2025-09-26 17:41:28 -05:00
<x-ui.button
2026-03-13 15:21:22 -05:00
spinner="startCompletion, generateCompletion"
wire:click.prevent="startCompletion"
2025-09-26 17:41:28 -05:00
class="btn btn-ghost h-32"
2024-11-01 22:13:40 -05:00
icon="o-paper-airplane"
2025-09-26 17:41:28 -05:00
></x-ui.button>
2026-03-13 15:21:22 -05:00
2024-11-01 22:13:40 -05:00
</div>
2026-03-13 15:21:22 -05:00
2024-11-01 22:13:40 -05:00
<div class="w-full mt-2">
2025-09-26 17:41:28 -05:00
<p class="text-xs text-secondary leading-tight select-none">{{ __('Advice generated by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }} </p>
2024-11-01 22:13:40 -05:00
</div>
</form>
</div>
</div>
2024-10-31 12:09:06 -05:00
</div>
2026-03-13 15:21:22 -05:00
</div>