Skip to main content

AI & workflows

Ai Loop Panel code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/ai-loop-panel.blade.php

@props([
    'title',
    'description' => null,
    'open' => false,
    'badge' => null,
    'badgeVariant' => 'gray',
    'mobileCollapsible' => false,
])

@php
    /*
    | Expandable panel for AI loop workspaces. Wrap embedded AI components
    | with `:embedded="true"` to avoid duplicate headers and borders.
    |
    | mobileCollapsible — collapsible below lg; always expanded on lg+ screens
    |
    | Usage:
    |   <x-ui.ai-loop-panel title="Loop history" description="2 iterations" :open="true">
    |       <x-ui.ai-loop-history :embedded="true" ... />
    |   </x-ui.ai-loop-panel>
    */
@endphp

<div
    x-data="{
        isLarge: window.matchMedia('(min-width: 1024px)').matches,
        open: @js($mobileCollapsible)
            ? (window.matchMedia('(min-width: 1024px)').matches || @js((bool) $open))
            : @js((bool) $open),
        init() {
            if (! @js($mobileCollapsible)) {
                return;
            }

            const mediaQuery = window.matchMedia('(min-width: 1024px)');

            mediaQuery.addEventListener('change', (event) => {
                this.isLarge = event.matches;

                if (event.matches) {
                    this.open = true;
                }
            });
        },
        toggle() {
            if (@js($mobileCollapsible) && this.isLarge) {
                return;
            }

            this.open = ! this.open;
        },
        isContentVisible() {
            return @js($mobileCollapsible) ? (this.isLarge || this.open) : this.open;
        },
    }"
    {{ $attributes->merge(['class' => 'min-w-0 overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900']) }}
>
    <button
        type="button"
        @click="toggle()"
        :aria-expanded="isContentVisible()"
        @class([
            'flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition',
            'hover:bg-gray-50 dark:hover:bg-gray-800/40' => ! $mobileCollapsible,
            'hover:bg-gray-50 dark:hover:bg-gray-800/40 lg:cursor-default lg:hover:bg-transparent' => $mobileCollapsible,
        ])
    >
        <div class="min-w-0 shrink-0">
            <p class="whitespace-nowrap text-sm font-semibold text-gray-900 dark:text-white">{{ $title }}</p>
            @if ($description)
                <p class="truncate text-xs text-gray-500 dark:text-gray-400">{{ $description }}</p>
            @endif
        </div>

        <div class="flex shrink-0 flex-wrap items-center justify-end gap-1.5">
            @isset($badges)
                {{ $badges }}
            @endisset
            @if ($badge !== null)
                <x-ui.badge :variant="$badgeVariant" size="sm">{{ $badge }}</x-ui.badge>
            @endif
            <x-ui.icon
                name="caret-down"
                weight="bold"
                size="base"
                ::class="open ? 'rotate-180' : ''"
                @class([
                    'shrink-0 text-gray-400 transition-transform duration-200',
                    'lg:hidden' => $mobileCollapsible,
                ])
            />
        </div>
    </button>

    <div x-show="isContentVisible()" x-collapse x-cloak>
        {{ $slot }}
    </div>
</div>