+21
-3
@@ -6,15 +6,33 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ai-chat ul {
|
||||
.ai-chat ul, .ai-chat ol, .ai-chat ol li > ul {
|
||||
margin-left: 1.1rem;
|
||||
}
|
||||
|
||||
.ai-chat ul li {
|
||||
.ai-chat ul > li {
|
||||
padding-left: 1.1rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.ai-chat ol > li {
|
||||
padding-left: 1.1rem;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.ai-chat ol li li {
|
||||
padding-left: 0rem;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.ai-chat ol li li::before {
|
||||
content: " - ";
|
||||
}
|
||||
|
||||
.ai-chat li, .ai-chat p {
|
||||
padding-bottom: .25em;
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
|
||||
.ai-chat code {
|
||||
font-size: .75rem;
|
||||
}
|
||||
@@ -60,17 +60,19 @@ new class extends Component {
|
||||
$this->resetPrompt();
|
||||
|
||||
$this->streaming = true;
|
||||
$this->js('$wire.generate()');
|
||||
$this->js('$wire.generateCompletion()');
|
||||
}
|
||||
|
||||
public function generate(): void
|
||||
public function generateCompletion(): void
|
||||
{
|
||||
|
||||
try {
|
||||
$stream = OpenAI::chat()->createStreamed([
|
||||
'model' => config('openai.model'),
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $this->system_prompt],
|
||||
['role' => 'system', 'content' => "Today's date is "
|
||||
.now()->format('Y-m-d')
|
||||
.".\n\n".$this->system_prompt],
|
||||
...array_slice($this->messages, -10)
|
||||
],
|
||||
]);
|
||||
@@ -96,6 +98,76 @@ new class extends Component {
|
||||
$this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $this->answer]));
|
||||
array_push($this->messages, ['role' => 'assistant', 'content' => $this->answer]);
|
||||
$this->resetPrompt();
|
||||
$this->js('$wire.generateSuggestedPrompts()');
|
||||
}
|
||||
|
||||
public function generateSuggestedPrompts(): void
|
||||
{
|
||||
try {
|
||||
$suggested_prompts = OpenAI::chat()->create([
|
||||
'model' => config('openai.model'),
|
||||
'response_format' => [
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => [
|
||||
'name' => 'suggested_prompts_schema',
|
||||
'strict' => true,
|
||||
'schema' => [
|
||||
"type" => "object",
|
||||
"properties" => [
|
||||
"suggested_prompts" => [
|
||||
"type" => "array",
|
||||
"items" => [
|
||||
"type" => "object",
|
||||
"properties" => [
|
||||
"text" => [
|
||||
"type" => "string",
|
||||
"description" => "The prompt question (no more than 5 words)"
|
||||
],
|
||||
"value" => [
|
||||
"type" => "string",
|
||||
"description" => "The detailed version of the question"
|
||||
]
|
||||
],
|
||||
"required" => ["text", "value"],
|
||||
"additionalProperties" => false
|
||||
]
|
||||
]
|
||||
],
|
||||
"required" => ["suggested_prompts"],
|
||||
"additionalProperties" => false
|
||||
]
|
||||
]
|
||||
],
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => "
|
||||
Your role is to assist investors in asking thoughtful questions to their investment advisors.
|
||||
|
||||
When you help investors ask good questions, you should ensure the questions are based on the
|
||||
provided context. Be sure to keep the questions short!
|
||||
|
||||
The questions you recommend might be based on natural follow up from the given context, requests
|
||||
to clarify undefined terms, common decision frameworks, possible risks or benefits, or commonly
|
||||
understood investing concepts.
|
||||
|
||||
Your response should only include valid JSON.
|
||||
"],
|
||||
['role' => 'user', 'content' => "
|
||||
Generate a list of ". rand(1,5) ." follow up questions a savvy investor might ask their advisor
|
||||
based on the following conversation:
|
||||
\n\n
|
||||
".json_encode(array_slice($this->messages, -4))
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->suggested_prompts = json_decode($suggested_prompts->choices[0]->message->content, true)['suggested_prompts'];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
$this->suggested_prompts = [];
|
||||
$this->error($e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function resetPrompt(): void
|
||||
@@ -132,41 +204,53 @@ new class extends Component {
|
||||
});
|
||||
}
|
||||
}"
|
||||
class="flex flex-col"
|
||||
class="fixed z-50 bottom-8 right-8"
|
||||
>
|
||||
{{-- toggle button --}}
|
||||
<x-button
|
||||
x-show="!open"
|
||||
@click="$dispatch('toggle-ai-chat')"
|
||||
class="btn btn-circle btn-lg btn-primary fixed bottom-10 right-10"
|
||||
class="flex btn btn-circle md:btn-lg btn-primary"
|
||||
>
|
||||
<x-slot:label>
|
||||
<x-icon name="o-sparkles" class="w-8 h-8"></x-icon>
|
||||
<x-icon name="o-sparkles" class="w-6 h-6 md:w-8 md:h-8"></x-icon>
|
||||
</x-slot:label>
|
||||
</x-button>
|
||||
|
||||
{{-- popup --}}
|
||||
<div
|
||||
x-on:toggle-ai-chat.window="open = !open"
|
||||
x-show="open"
|
||||
x-trap="open"
|
||||
x-bind:inert="!open"
|
||||
x-transition.opacity
|
||||
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"
|
||||
x-cloak
|
||||
key="ai-chat"
|
||||
class="fixed
|
||||
bottom-0 right-0 w-full h-screen
|
||||
md:bottom-[7rem] md:right-10 md:w-[35rem] md:h-auto"
|
||||
class="fixed bg-base-100 shadow-2xl rounded-none md:rounded-lg
|
||||
inset-0 h-screen w-full
|
||||
md:inset-auto md:right-6 md:bottom-6 md:w-[32rem] md:h-[46rem]"
|
||||
>
|
||||
|
||||
<x-card class="shadow-2xl" title="{{ __('AI Chat') }}" x-intersect="scrollChatWindow()">
|
||||
{{-- close button --}}
|
||||
<x-button
|
||||
icon="o-x-mark"
|
||||
class="absolute top-5 right-4 btn-ghost btn-circle btn-sm"
|
||||
title="{{ __('Close') }}"
|
||||
@click="open = false"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col overflow-hidden p-4"
|
||||
x-intersect="scrollChatWindow()"
|
||||
>
|
||||
<div class="flex grow-0 justify-between items-center pb-4 ">
|
||||
<h2 class="text-lg text-bold">{{ __('AI Chat') }}</h2>
|
||||
<x-button
|
||||
icon="o-x-mark"
|
||||
class="absolute top-5 right-4 btn-ghost btn-circle btn-sm"
|
||||
title="{{ __('Close') }}"
|
||||
@click="open = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- chat window --}}
|
||||
<div class="h-[25rem] overflow-y-scroll ai-chat" x-ref="chatWindow">
|
||||
<div class="grow overflow-hidden overflow-y-scroll ai-chat" x-ref="chatWindow">
|
||||
|
||||
<div class="flex gap-3 mb-5 flex-1">
|
||||
<span class="
|
||||
@@ -248,42 +332,44 @@ new class extends Component {
|
||||
</div>
|
||||
|
||||
{{-- prompt input --}}
|
||||
<form submit="startCompletion" class="mt-3">
|
||||
<div class="">
|
||||
@foreach($suggested_prompts as $prompt)
|
||||
<x-button
|
||||
class="btn-xs btn-primary btn-outline mr-1 mb-2"
|
||||
label="{{ $prompt['text'] }}"
|
||||
wire:click="startCompletion('{{ $prompt['value'] }}')"
|
||||
/>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between align-bottom space-x-2 mt-1">
|
||||
|
||||
<div class="w-full">
|
||||
<div class="mt-3 grow-0">
|
||||
<form submit="startCompletion" >
|
||||
<div class="">
|
||||
@foreach($suggested_prompts as $prompt)
|
||||
<x-button
|
||||
class="btn-xs btn-primary btn-outline mr-1 mb-2"
|
||||
wire:click="startCompletion('{{ addslashes($prompt['value']) }}')"
|
||||
>{{ $prompt['text'] }}</x-button>
|
||||
@endforeach
|
||||
|
||||
<x-textarea
|
||||
wire:model="prompt"
|
||||
class="h-24 resize-none "
|
||||
placeholder="{{ __('Have a question? AI might be able to help...') }}"
|
||||
wire:keydown.enter.prevent="startCompletion"
|
||||
></x-textarea>
|
||||
</div>
|
||||
<x-button
|
||||
spinner="generate"
|
||||
wire:click="startCompletion"
|
||||
class="btn btn-ghost h-24"
|
||||
icon="o-paper-airplane"
|
||||
></x-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-2">
|
||||
<p class="text-xs text-secondary leading-tight">{{ __('Advice generated by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }} </p>
|
||||
</div>
|
||||
</form>
|
||||
</x-card>
|
||||
<div class="flex justify-between align-bottom space-x-2 mt-1">
|
||||
|
||||
<div class="w-full">
|
||||
|
||||
<x-textarea
|
||||
wire:model="prompt"
|
||||
class="h-24 resize-none "
|
||||
placeholder="{{ __('Have a question? AI might be able to help...') }}"
|
||||
wire:keydown.enter.prevent="startCompletion"
|
||||
autofocus
|
||||
></x-textarea>
|
||||
</div>
|
||||
<x-button
|
||||
spinner="generateCompletion"
|
||||
wire:click="startCompletion"
|
||||
class="btn btn-ghost h-24"
|
||||
icon="o-paper-airplane"
|
||||
></x-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-2">
|
||||
<p class="text-xs text-secondary leading-tight">{{ __('Advice generateCompletiond by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }} </p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user