@if ($enabled)
{ window.aiWidgetOpen = v; }); {{-- Hiding sticks for the whole browsing session. Two carriers on purpose: sessionStorage survives a refresh, and the window flag survives wire:navigate even when storage is unavailable, so the bubble can never reappear a page later. --}} this.dismissed = window.aiWidgetDismissed === true || this.recall('sessionStorage', 'ai_widget_dismissed') === '1'; window.aiWidgetDismissed = this.dismissed; this.$watch('dismissed', v => { window.aiWidgetDismissed = v; this.remember('sessionStorage', 'ai_widget_dismissed', v ? '1' : '0'); }); try { const p = JSON.parse(localStorage.getItem('ai_widget_pos')); if (p && p.left != null) this.pos = p; } catch (e) {} this.$nextTick(() => this.clamp()); window.addEventListener('resize', () => this.clamp()); }, hide() { this.open = false; this.historyOpen = false; this.dismissed = true; this.$dispatch('banner-message', { style: 'info', message: @js(__('Ask is hidden. Reload BillPro to bring it back.')) }); }, launcherStyle() { let s = 'touch-action:none;'; if (this.pos.left != null) s += 'left:' + this.pos.left + 'px;top:' + this.pos.top + 'px;right:auto;bottom:auto;'; return s; }, startDrag(e) { this.el = e.currentTarget; this.drag.active = true; this.drag.moved = false; this.drag.sx = e.clientX; this.drag.sy = e.clientY; const r = this.el.getBoundingClientRect(); this.drag.ox = e.clientX - r.left; this.drag.oy = e.clientY - r.top; {{-- pointercancel matters as much as pointerup: a phone that decides mid-press that the gesture belongs to the browser fires cancel and no up. Without this the move listener stayed attached for the life of the page, so the bubble crawled after every later touch and drag.moved stayed true, which silently ate the next tap on the close button. --}} const move = ev => this.onMove(ev); const up = ev => { this.onUp(ev); window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); window.removeEventListener('pointercancel', up); }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', up); window.addEventListener('pointercancel', up); }, onMove(e) { if (!this.drag.active) return; if (!this.drag.moved && Math.hypot(e.clientX - this.drag.sx, e.clientY - this.drag.sy) < 5) return; this.drag.moved = true; this.pos = { left: e.clientX - this.drag.ox, top: e.clientY - this.drag.oy }; this.clamp(); }, onUp() { if (!this.drag.active) return; this.drag.active = false; if (this.drag.moved) this.remember('localStorage', 'ai_widget_pos', JSON.stringify(this.pos)); }, onClick() { if (this.drag.moved) { this.drag.moved = false; return; } this.open = true; }, clamp() { if (this.pos.left == null) return; const w = this.el ? this.el.offsetWidth : 120; const h = this.el ? this.el.offsetHeight : 56; this.pos.left = Math.min(Math.max(8, this.pos.left), Math.max(8, window.innerWidth - w - 8)); this.pos.top = Math.min(Math.max(8, this.pos.top), Math.max(8, window.innerHeight - h - 8)); }, }" > {{-- Launcher bubble --}}
{{-- The hide control used to be a 20px dot that only appeared on hover, sitting half on top of the pill: most attempts to hide Ask landed on the pill instead and opened the chat. It is now always visible, a real 28px button with an invisible collar around it, and it stops the pointer event so the pill's drag handler can never claim the press. --}}
{{-- Chat panel --}}
{{-- Header --}}
Semita

{{ __('Ask') }}

{{ __('Answers from your live data') }}

{{-- Four buttons on a 360px phone only fit if they sit in their own shrink-0 cluster with a tight gap; the title beside them is the part that gives way. --}}
{{-- Hiding from in here as well, so nobody has to go hunting for the small x on the bubble to make Ask go away. --}}
{{-- Conversation history --}}
@forelse ($conversations as $c)
@empty

{{ __('No past conversations yet.') }}

@endforelse
@unless ($configured)
{{ __('Assistant not configured, add an API key for the selected provider.') }}
@endunless {{-- Messages --}}
@forelse ($messages as $message) @if ($message->role === 'user')
{!! nl2br(e($message->content)) !!}
@elseif ($message->role === 'error')
{{ $message->content }}
@else
Semita
{!! nl2br(e($message->content)) !!}
@if (! empty($message->tools))
@foreach ($message->tools as $tool) {{ str_replace('_', ' ', $tool) }} @endforeach
@endif
@endif @empty
Semita

{{ __('Ask about finances, jobs, sales, collections or client feedback, answered from your live data.') }}

@foreach ($suggestions as $suggestion) @endforeach
@endforelse @if ($pending)
Semita
@endif
{{-- Composer --}}
@endif