127 lines
5.7 KiB
PHP
127 lines
5.7 KiB
PHP
<?php
|
|
|
|
use App\Actions\Embeddings\RecordEmbeddingUsageEvent;
|
|
use App\Concerns\InteractsWithEmbeddingWorkbench;
|
|
use App\Models\EmbeddingEntry;
|
|
use App\Services\EmbeddingVector;
|
|
use App\Services\EmbeddingWorkbench;
|
|
use Flux\Flux;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
new #[Title('Embed')] class extends Component
|
|
{
|
|
use InteractsWithEmbeddingWorkbench;
|
|
|
|
public string $embeddingInput = '';
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $embeddingResults = [];
|
|
|
|
public function mount(EmbeddingWorkbench $workbench): void
|
|
{
|
|
$this->initializeWorkbench($workbench);
|
|
}
|
|
|
|
public function generateEmbeddings(EmbeddingWorkbench $workbench, RecordEmbeddingUsageEvent $recordUsageEvent): void
|
|
{
|
|
if (! $this->ensureWorkbenchAvailable($workbench, 'embeddingInput')) {
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'embeddingInput' => ['required', 'string', 'max:12000'],
|
|
'embeddingModel' => ['required', 'string'],
|
|
]);
|
|
|
|
$phrases = EmbeddingVector::phrasesFromText($this->embeddingInput);
|
|
|
|
if ($phrases === []) {
|
|
$this->addError('embeddingInput', __('Enter at least one phrase.'));
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$entries = $workbench->embed($phrases, $this->embeddingModel);
|
|
$tokens = (int) $entries->sum(fn (EmbeddingEntry $entry): int => (int) ($entry->tokens ?? 0));
|
|
|
|
$this->embeddingResults = $entries
|
|
->map(fn (EmbeddingEntry $entry): array => $workbench->presentEntry($entry))
|
|
->all();
|
|
|
|
$recordUsageEvent->handle(
|
|
user: auth()->user(),
|
|
workbench: $workbench,
|
|
selectedModel: $this->embeddingModel,
|
|
tool: 'embed',
|
|
inputCount: count($phrases),
|
|
resultCount: $entries->count(),
|
|
tokens: $tokens > 0 ? $tokens : null,
|
|
);
|
|
|
|
$this->refreshSummary($workbench);
|
|
|
|
Flux::toast(variant: 'success', text: __('Embeddings generated.'));
|
|
} catch (\Throwable $exception) {
|
|
$this->addError('embeddingModel', $exception->getMessage());
|
|
}
|
|
}
|
|
};
|
|
?>
|
|
|
|
<section class="flex w-full flex-col gap-6">
|
|
@include('pages.embeddings.partials.navigation', ['heading' => __('Embed')])
|
|
|
|
<section class="rounded-lg border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
|
|
<form wire:submit="generateEmbeddings" class="flex flex-col gap-4">
|
|
<flux:field>
|
|
<flux:label>{{ __('Phrases') }}</flux:label>
|
|
<flux:textarea wire:model="embeddingInput" rows="8" placeholder="{{ __('Laravel vectors') }}" />
|
|
<flux:error name="embeddingInput" />
|
|
</flux:field>
|
|
|
|
<div>
|
|
<flux:button type="submit" variant="primary" icon="sparkles" :disabled="! $databaseReady || ! $ollamaAvailable" class="data-loading:opacity-60">
|
|
<span wire:loading.remove wire:target="generateEmbeddings">{{ __('Generate') }}</span>
|
|
<span wire:loading wire:target="generateEmbeddings">{{ __('Generating') }}</span>
|
|
</flux:button>
|
|
</div>
|
|
|
|
@if ($embeddingResults !== [])
|
|
<div class="overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
|
<thead class="bg-zinc-50 text-start text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
|
|
<tr>
|
|
<th class="px-3 py-2 text-start font-medium">{{ __('Phrase') }}</th>
|
|
<th class="px-3 py-2 text-start font-medium">{{ __('Vector') }}</th>
|
|
<th class="px-3 py-2 text-start font-medium">{{ __('Meta') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
@foreach ($embeddingResults as $result)
|
|
<tr wire:key="embedding-result-{{ $result['id'] }}">
|
|
<td class="max-w-xs px-3 py-3 align-top font-medium text-zinc-900 dark:text-zinc-100">
|
|
{{ $result['source_text'] }}
|
|
</td>
|
|
<td class="px-3 py-3 align-top">
|
|
<code class="block max-w-md truncate text-xs text-zinc-700 dark:text-zinc-300">[{{ implode(', ', $result['vector_preview']) }}...]</code>
|
|
<details class="mt-2">
|
|
<summary class="cursor-pointer text-xs text-zinc-500 dark:text-zinc-400">{{ __('Full vector') }}</summary>
|
|
<pre class="mt-2 max-h-52 overflow-auto rounded-md bg-zinc-950 p-3 text-xs text-zinc-100">{{ $result['vector_json'] }}</pre>
|
|
</details>
|
|
</td>
|
|
<td class="px-3 py-3 align-top text-zinc-600 dark:text-zinc-300">
|
|
<div>{{ $result['dimensions'] }}d</div>
|
|
<div class="text-xs">{{ $result['provider'] }} / {{ $result['model'] }}</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
</form>
|
|
</section>
|
|
</section>
|