AI & workflows
Ai Loop Goal code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/ai-loop-goal.blade.php
@props([
'action' => '#',
'method' => 'POST',
'goal' => 'Ship the AI prompter with tests and showcase coverage.',
'goalName' => 'goal',
'loopId' => null,
'loopIdName' => 'loop_id',
'showLoopId' => true,
'status' => 'running',
'iteration' => 1,
'maxIterations' => null,
'criteria' => [],
'runTimeSeconds' => 0,
'showRunTime' => true,
'models' => [
'gpt-5.5-medium' => '5.5 Medium',
'gpt-5.5-high' => '5.5 High',
'gpt-5-mini' => '5 Mini',
],
'selectedModel' => 'gpt-5.5-medium',
'modelName' => 'model',
'showModel' => true,
'editable' => true,
'showCriteria' => true,
'submitLabel' => 'Save for next run',
'idleSubmitLabel' => 'Save goal',
'stopAction' => null,
'stopMethod' => 'POST',
'stopLabel' => 'Stop loop',
'showStop' => null,
'contained' => true,
])
@php
/*
| Goal tracker for recurring AI agent loops. Edits are queued for the next
| iteration while a loop is running; use the stop action to interrupt the
| current run when an immediate change is required.
|
| Usage:
| <x-ui.ai-loop-goal
| goal="Keep CI green on main"
| status="running"
| :iteration="4"
| max-iterations="10"
| :run-time-seconds="272"
| selected-model="gpt-5.5-high"
| :criteria="[
| ['label' => 'Workflow tests pass', 'complete' => true],
| ['label' => 'Showcase updated', 'complete' => false],
| ]"
| />
*/
$httpMethod = strtoupper($method);
$formMethod = in_array($httpMethod, ['GET', 'POST'], true) ? $httpMethod : 'POST';
$usesMethodSpoofing = ! in_array($httpMethod, ['GET', 'POST'], true);
$normalizedCriteria = collect($criteria)
->map(function ($item, $key) {
if (is_array($item)) {
return [
'label' => $item['label'] ?? (is_string($key) ? $key : ''),
'complete' => (bool) ($item['complete'] ?? false),
'active' => (bool) ($item['active'] ?? false),
];
}
return [
'label' => is_string($key) ? $key : (string) $item,
'complete' => is_string($key) ? (bool) $item : false,
'active' => false,
];
})
->filter(fn (array $item) => $item['label'] !== '')
->values()
->all();
$hasExplicitActive = collect($normalizedCriteria)->contains(fn (array $item) => $item['active']);
if ($status === 'running' && ! $hasExplicitActive) {
$activeAssigned = false;
$normalizedCriteria = collect($normalizedCriteria)
->map(function (array $item) use (&$activeAssigned) {
if (! $item['complete'] && ! $activeAssigned) {
$item['active'] = true;
$activeAssigned = true;
}
return $item;
})
->all();
}
$completedCriteria = collect($normalizedCriteria)->where('complete', true)->count();
$totalCriteria = count($normalizedCriteria);
$criteriaProgress = $totalCriteria > 0 ? (int) round(($completedCriteria / $totalCriteria) * 100) : 0;
$iterationLabel = $maxIterations
? 'Loop '.$iteration.' / '.$maxIterations
: 'Loop '.$iteration;
$formatRunTime = function (int $seconds): string {
$hours = intdiv($seconds, 3600);
$minutes = intdiv($seconds % 3600, 60);
$remainingSeconds = $seconds % 60;
if ($hours > 0) {
return $hours.'h '.$minutes.'m';
}
if ($minutes > 0) {
return $minutes.'m '.$remainingSeconds.'s';
}
return $remainingSeconds.'s';
};
$formattedRunTime = $formatRunTime((int) $runTimeSeconds);
$loopIdentifier = $loopId ?: (string) \Illuminate\Support\Str::uuid();
$isQueuedIteration = in_array($status, ['running', 'paused'], true);
$showStopButton = $showStop ?? $isQueuedIteration;
$resolvedStopAction = $stopAction ?? rtrim((string) $action, '/').'/stop';
$resolvedSubmitLabel = $isQueuedIteration ? $submitLabel : $idleSubmitLabel;
$stopHttpMethod = strtolower($stopMethod) === 'get' ? 'get' : 'post';
$footerHelp = match ($status) {
'running' => 'Goal, model, and criteria changes apply to the next iteration. Stop the loop to interrupt the current run.',
'paused' => 'The loop is paused. Save changes for the next run, or stop the loop to cancel it entirely.',
'completed' => 'Goal completed. Save a new objective to start another loop.',
default => 'Set a clear objective and criteria before starting the loop.',
};
@endphp
<form
method="{{ $formMethod }}"
action="{{ $action }}"
{{ $attributes->merge(['class' => $contained ? 'mx-auto w-full max-w-4xl' : 'w-full']) }}
>
@if ($usesMethodSpoofing)
@method($httpMethod)
@endif
@if ($formMethod !== 'GET')
@csrf
@endif
@if ($showLoopId)
<input type="hidden" name="{{ $loopIdName }}" value="{{ $loopIdentifier }}">
@endif
<div class="min-w-0 overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900">
<div class="flex flex-col gap-3 border-b border-gray-200 px-4 py-4 dark:border-gray-800 sm:flex-row sm:items-center sm:justify-between sm:px-5">
<div class="flex min-w-0 items-center gap-3">
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-900 dark:text-white">Loop goal</p>
@if ($showLoopId)
<p class="truncate font-mono text-xs text-gray-400 dark:text-gray-500" title="{{ $loopIdentifier }}">{{ $loopIdentifier }}</p>
@endif
<p class="truncate text-xs text-gray-500 dark:text-gray-400">{{ $iterationLabel }}</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
@if ($status === 'running')
<span class="inline-flex items-center gap-1.5 rounded-full bg-accent-50 px-2.5 py-1 text-xs font-medium text-accent-700 dark:bg-accent-950/60 dark:text-accent-300">
<span
class="inline-block w-3.5 text-left font-bold leading-none"
x-data="{
step: 0,
interval: null,
dots() {
return '.'.repeat((this.step % 3) + 1);
},
init() {
this.interval = setInterval(() => this.step++, 400);
},
destroy() {
if (this.interval) {
clearInterval(this.interval);
}
},
}"
x-text="dots()"
aria-hidden="true"
>.</span>
Running
</span>
@else
<x-ui.ai-loop-status-badge :status="$status" size="base" />
@endif
@if ($showRunTime)
<span
class="inline-flex items-center gap-1.5 rounded-full border border-gray-300 px-2.5 py-1 text-xs font-medium text-gray-600 dark:border-gray-700 dark:text-gray-300"
title="Current run time"
>
<x-ui.icon name="clock" size="sm" class="text-gray-400" />
<span
class="tabular-nums"
@if ($status === 'running')
x-data="{
seconds: @js((int) $runTimeSeconds),
interval: null,
format() {
const hours = Math.floor(this.seconds / 3600);
const minutes = Math.floor((this.seconds % 3600) / 60);
const remainingSeconds = this.seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
if (minutes > 0) {
return `${minutes}m ${remainingSeconds}s`;
}
return `${remainingSeconds}s`;
},
init() {
this.interval = setInterval(() => this.seconds++, 1000);
},
destroy() {
if (this.interval) {
clearInterval(this.interval);
}
},
}"
x-text="format()"
@endif
>{{ $formattedRunTime }}</span>
</span>
@endif
@if ($totalCriteria > 0)
<x-ui.badge variant="outline">
{{ $completedCriteria }} / {{ $totalCriteria }} criteria
</x-ui.badge>
@endif
</div>
</div>
<div class="space-y-4 p-4 sm:p-5">
<div>
<label for="{{ $goalName }}" class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Objective
</label>
@if ($editable)
<textarea
id="{{ $goalName }}"
name="{{ $goalName }}"
rows="3"
class="block w-full resize-none rounded-xl border border-gray-300 bg-white px-3.5 py-3 text-sm leading-6 text-gray-900 shadow-sm transition focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
>{{ $goal }}</textarea>
@else
<p class="rounded-xl border border-gray-200 bg-gray-50 px-3.5 py-3 text-sm leading-6 text-gray-700 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-300">
{{ $goal }}
</p>
@endif
</div>
@if ($showCriteria && $totalCriteria > 0)
<div>
<div class="mb-2 flex items-center justify-between gap-3">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Success criteria</p>
<span class="text-xs tabular-nums text-gray-500 dark:text-gray-400">{{ $criteriaProgress }}%</span>
</div>
<x-ui.progress :value="$criteriaProgress" size="sm" class="mb-3" />
<ul class="space-y-2" role="list">
@foreach ($normalizedCriteria as $item)
<li @class([
'flex items-start gap-3 rounded-xl border px-3 py-2.5',
'border-brand-300 bg-brand-50/60 dark:border-brand-800 dark:bg-brand-950/25' => $item['active'],
'border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950' => ! $item['active'],
])>
<span @class([
'mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full',
'bg-brand-600 text-white' => $item['complete'],
'border-2 border-brand-500 bg-brand-100 dark:border-brand-400 dark:bg-brand-950/60' => $item['active'],
'border border-gray-300 text-transparent dark:border-gray-600' => ! $item['complete'] && ! $item['active'],
])>
@if ($item['complete'])
<x-ui.icon name="check" weight="bold" size="xs" />
@elseif ($item['active'])
<span class="h-2 w-2 rounded-full bg-brand-500 animate-pulse"></span>
@endif
</span>
<span @class([
'min-w-0 flex-1 break-words text-sm leading-6',
'font-bold animate-pulse text-brand-700 dark:text-brand-300' => $item['active'],
'font-medium text-gray-900 dark:text-gray-100' => $item['complete'],
'text-gray-600 dark:text-gray-400' => ! $item['complete'] && ! $item['active'],
])>
{{ $item['label'] }}
</span>
</li>
@endforeach
</ul>
</div>
@endif
{{ $slot }}
</div>
@if (($showModel && count($models) > 0) || $editable || $showStopButton)
<div class="flex flex-col gap-4 bg-white px-4 py-3 dark:bg-gray-900 sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:px-5">
@if ($showModel && count($models) > 0)
<label class="relative w-full min-w-0 sm:max-w-xs sm:flex-1 lg:w-56 lg:flex-none">
<span class="sr-only">Model</span>
<select
name="{{ $modelName }}"
@disabled(! $editable)
class="h-9 w-full appearance-none rounded-lg border border-gray-200 bg-gray-50 pe-8 ps-3 text-sm font-medium text-gray-600 transition hover:bg-gray-100 focus:border-brand-500 focus:ring-1 focus:ring-brand-500 disabled:cursor-default disabled:opacity-100 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-300 dark:hover:bg-gray-800"
>
@foreach ($models as $value => $label)
<option value="{{ $value }}" @selected($selectedModel === $value)>{{ $label }}</option>
@endforeach
</select>
<x-ui.icon name="caret-down" weight="bold" size="sm" class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400" />
</label>
@endif
@if ($editable || $showStopButton)
<div class="flex w-full min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:gap-4 {{ ($showModel && count($models) > 0) ? 'sm:ms-auto sm:flex-1' : 'sm:justify-between' }}">
@if ($editable)
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
{{ $footerHelp }}
</p>
@endif
<div @class([
'flex shrink-0 flex-row flex-wrap items-center gap-2',
'ms-auto' => ! $editable,
])>
@if ($showStopButton)
<x-ui.button
type="submit"
variant="danger"
size="sm"
formaction="{{ $resolvedStopAction }}"
:formmethod="$stopHttpMethod"
>
{{ $stopLabel }}
</x-ui.button>
@endif
@if ($editable)
<x-ui.button type="submit" size="sm">
{{ $resolvedSubmitLabel }}
</x-ui.button>
@endif
</div>
</div>
@endif
</div>
@endif
</div>
</form>