Packages
phoenix_kit
1.7.3
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/modules/ai/settings.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="{@project_title} AI Settings"
current_path={@current_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/modules")}
class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12"
>
<.icon name="hero-arrow-left" class="w-4 h-4" /> Back to Modules
</.link>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">
AI Module
</h1>
<p class="text-lg text-base-content/70">
Manage AI providers and text processing configuration
</p>
</div>
</header>
<%= if @has_accounts do %>
<%!-- Configuration State: Main Tabs --%>
<div class="tabs tabs-boxed mb-6 justify-center">
<button
class={"tab #{if @active_tab == "slots", do: "tab-active"}"}
phx-click="switch_tab"
phx-value-tab="slots"
>
<.icon name="hero-cpu-chip" class="w-4 h-4 mr-2" /> Model Configuration
</button>
<button
class={"tab #{if @active_tab == "usage", do: "tab-active"}"}
phx-click="switch_tab"
phx-value-tab="usage"
>
<.icon name="hero-chart-bar" class="w-4 h-4 mr-2" /> Usage
</button>
</div>
<%!-- Model Configuration Tab --%>
<%= if @active_tab == "slots" do %>
<%!-- Model Type Sub-tabs --%>
<div class="flex justify-center mb-6">
<div class="join">
<%= for model_type <- @model_types do %>
<button
class={"btn join-item #{if @slot_type == model_type.type, do: "btn-primary", else: "btn-ghost"}"}
phx-click="switch_slot_type"
phx-value-type={model_type.id}
>
<.icon name={model_type.icon} class="w-4 h-4 mr-1" />
{model_type.label}
</button>
<% end %>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex justify-between items-start mb-2">
<h2 class="card-title">
<%= case @slot_type do %>
<% :text -> %>
Text Processing Configuration
<% :vision -> %>
Vision Processing Configuration
<% :image_gen -> %>
Image Generation Configuration
<% :embeddings -> %>
Embeddings Configuration
<% end %>
</h2>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/ai/accounts")}
class="btn btn-ghost btn-sm"
>
<.icon name="hero-key" class="w-4 h-4 mr-1" /> Manage Accounts
</.link>
</div>
<p class="text-sm text-base-content/70 mb-6">
<%= case @slot_type do %>
<% :text -> %>
Configure up to 3 presets for text/chat completion. These can be used as selectable options or as a fallback chain.
<% :vision -> %>
Configure up to 3 presets for vision/multimodal models. These models can analyze images and provide text responses.
<% :image_gen -> %>
Configure up to 3 presets for image generation. These models create images from text prompts.
<% :embeddings -> %>
Configure up to 3 presets for text embeddings. Used for semantic search, RAG, and similarity matching.
<% end %>
</p>
<div class="space-y-6">
<%= for {slot, index} <- Enum.with_index(@slots) do %>
<div class={"card #{if slot["enabled"], do: "bg-primary/10 border-2 border-primary", else: "bg-base-200"}"}>
<div class="card-body p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold">
Slot {index + 1}: {slot["name"] || "Unnamed"}
</h3>
<label class="label cursor-pointer gap-2">
<span class="label-text text-sm">Enabled</span>
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked={slot["enabled"]}
phx-click="update_slot"
phx-value-slot-index={index}
phx-value-slot-enabled={if slot["enabled"], do: "false", else: "true"}
phx-value-slot-name={slot["name"]}
phx-value-slot-description={slot["description"]}
phx-value-slot-account_id={slot["account_id"]}
phx-value-slot-model={slot["model"]}
phx-value-slot-temperature={slot["temperature"]}
phx-value-slot-max_tokens={slot["max_tokens"]}
phx-value-slot-size={slot["size"]}
phx-value-slot-quality={slot["quality"]}
phx-value-slot-dimensions={slot["dimensions"]}
/>
</label>
</div>
<form
phx-change="update_slot"
phx-value-slot-index={index}
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<input type="hidden" name="slot[index]" value={index} />
<%!-- Name --%>
<div class="form-control">
<label class="label">
<span class="label-text">Name</span>
</label>
<input
type="text"
class="input input-bordered input-sm"
value={slot["name"]}
placeholder="e.g., Fast, Quality, Cheap"
name="slot[name]"
/>
</div>
<%!-- Account Selection --%>
<div class="form-control">
<label class="label">
<span class="label-text">Account</span>
</label>
<select
class="select select-bordered select-sm"
name="slot[account_id]"
>
<option value="">Select account...</option>
<%= for account <- @accounts do %>
<option
value={account.id}
selected={slot["account_id"] == account.id}
>
{account.name}
</option>
<% end %>
</select>
</div>
<%!-- Model Selection --%>
<% has_account = slot["account_id"] != nil %>
<% cache_key = {slot["account_id"], @slot_type} %>
<% account_cache = @models_cache[cache_key] %>
<% slot_models = (account_cache && account_cache.models) || [] %>
<% slot_models_grouped = (account_cache && account_cache.grouped) || [] %>
<% model_info = Enum.find(slot_models, fn m -> m["id"] == slot["model"] end) %>
<% has_model = slot["model"] != nil && slot["model"] != "" %>
<% is_loading =
@models_loading_for != nil && @models_loading_for == slot["account_id"] %>
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Model</span>
<%= if is_loading do %>
<span class="label-text-alt flex items-center gap-1">
<span class="loading loading-spinner loading-xs"></span>
Loading models...
</span>
<% end %>
</label>
<%= if length(slot_models) > 0 do %>
<select
class="select select-bordered select-sm w-full md:w-2/3"
name="slot[model]"
disabled={!has_account}
>
<option value="">
{if has_account,
do: "Select a model...",
else: "Select an account first"}
</option>
<%= for {provider, models} <- slot_models_grouped do %>
<optgroup label={
PhoenixKit.AI.OpenRouterClient.extract_provider(provider <> "/x")
}>
<%= for model <- models do %>
<option
value={model["id"]}
selected={slot["model"] == model["id"]}
>
{model["name"]}
</option>
<% end %>
</optgroup>
<% end %>
</select>
<%!-- Model Info Display --%>
<%= if model_info do %>
<div class="mt-2 p-3 bg-base-200 rounded-lg text-sm">
<div class="flex flex-wrap gap-3">
<div class="flex items-center gap-1">
<.icon
name="hero-document-text"
class="w-4 h-4 text-base-content/50"
/>
<span class="text-base-content/70">Context:</span>
<span class="font-medium">
{format_number(model_info["context_length"] || 0)}
</span>
</div>
<%= if model_info["max_completion_tokens"] do %>
<div class="flex items-center gap-1">
<.icon
name="hero-arrow-right-circle"
class="w-4 h-4 text-base-content/50"
/>
<span class="text-base-content/70">Max output:</span>
<span class="font-medium">
{format_number(model_info["max_completion_tokens"])}
</span>
</div>
<% end %>
<%= if model_info["pricing"] do %>
<div class="flex items-center gap-1">
<.icon
name="hero-currency-dollar"
class="w-4 h-4 text-base-content/50"
/>
<span class="text-base-content/70">Price:</span>
<span class="font-medium text-xs">
${Float.round(
(model_info["pricing"]["prompt"] || 0) * 1.0 * 1_000_000,
2
)}/M in,
${Float.round(
(model_info["pricing"]["completion"] || 0) * 1.0 *
1_000_000,
2
)}/M out
</span>
</div>
<% end %>
</div>
<%= if model_info["supported_parameters"] && length(model_info["supported_parameters"]) > 0 do %>
<div class="mt-2 flex items-center gap-1 flex-wrap">
<span class="text-base-content/50 text-xs">Supports:</span>
<%= for param <- Enum.take(model_info["supported_parameters"], 8) do %>
<span class="badge badge-xs badge-ghost">{param}</span>
<% end %>
<%= if length(model_info["supported_parameters"]) > 8 do %>
<span class="text-xs text-base-content/50">
+{length(model_info["supported_parameters"]) - 8} more
</span>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% else %>
<%!-- Fallback: show loading, text input, or prompt to select account --%>
<%= if is_loading do %>
<div class="flex items-center gap-2 h-8">
<span class="loading loading-spinner loading-sm"></span>
<span class="text-sm text-base-content/70">Loading models...</span>
</div>
<% else %>
<%= if has_account do %>
<input
type="text"
class="input input-bordered input-sm w-full md:w-2/3"
value={slot["model"]}
placeholder="e.g., anthropic/claude-3-haiku"
name="slot[model]"
/>
<%= if @models_error do %>
<label class="label">
<span class="label-text-alt text-error">{@models_error}</span>
</label>
<% end %>
<% else %>
<div class="text-sm text-base-content/50 py-2">
Select an account to load available models
</div>
<% end %>
<% end %>
<% end %>
</div>
<%!-- Type-specific parameters - only show when model is selected --%>
<%= if has_model do %>
<%= case @slot_type do %>
<% type when type in [:text, :vision] -> %>
<%!-- Collapsible Model Parameters --%>
<div class="md:col-span-2">
<details class="collapse collapse-arrow bg-base-200 rounded-lg" open>
<summary class="collapse-title font-medium py-2 min-h-0">
<.icon name="hero-adjustments-horizontal" class="w-4 h-4 mr-2" />
Model Parameters
</summary>
<div class="collapse-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<%!-- Temperature --%>
<div class="form-control">
<label class="label">
<span class="label-text">Temperature</span>
<span class="label-text-alt" id={"temp-display-#{index}"}>
{slot["temperature"]}
</span>
</label>
<input
type="range"
min="0"
max="2"
step="0.1"
value={slot["temperature"]}
class="range range-sm range-primary"
name="slot[temperature]"
oninput={"document.getElementById('temp-display-#{index}').textContent = this.value"}
/>
<div class="flex justify-between text-xs text-base-content/50 px-1">
<span>Precise</span>
<span>Creative</span>
</div>
</div>
<%!-- Max Tokens --%>
<% max_tokens_limit =
(model_info && model_info["max_completion_tokens"]) ||
128_000 %>
<% display_max_tokens =
slot["max_tokens"] ||
(model_info && model_info["max_completion_tokens"]) %>
<div class="form-control">
<label class="label">
<span class="label-text">Max Tokens</span>
<%= if model_info && model_info["max_completion_tokens"] do %>
<span class="label-text-alt text-base-content/50">
(max: {format_number(
model_info["max_completion_tokens"]
)})
</span>
<% end %>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={display_max_tokens}
min="1"
max={max_tokens_limit}
name="slot[max_tokens]"
placeholder={
if model_info && model_info["max_completion_tokens"],
do:
"#{model_info["max_completion_tokens"]} (model max)",
else: ""
}
/>
</div>
<%!-- Top P --%>
<div class="form-control">
<label class="label">
<span class="label-text">Top P</span>
<span class="label-text-alt text-base-content/50">
(nucleus sampling)
</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["top_p"]}
min="0"
max="1"
step="0.05"
placeholder="Default (1.0)"
name="slot[top_p]"
/>
</div>
<%!-- Top K --%>
<div class="form-control">
<label class="label">
<span class="label-text">Top K</span>
<span class="label-text-alt text-base-content/50">
(token choices)
</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["top_k"]}
min="1"
max="100"
placeholder="Default (disabled)"
name="slot[top_k]"
/>
</div>
<%!-- Frequency Penalty --%>
<div class="form-control">
<label class="label">
<span class="label-text">Frequency Penalty</span>
<span class="label-text-alt text-base-content/50">
(-2 to 2)
</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["frequency_penalty"]}
min="-2"
max="2"
step="0.1"
placeholder="Default (0)"
name="slot[frequency_penalty]"
/>
</div>
<%!-- Presence Penalty --%>
<div class="form-control">
<label class="label">
<span class="label-text">Presence Penalty</span>
<span class="label-text-alt text-base-content/50">
(-2 to 2)
</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["presence_penalty"]}
min="-2"
max="2"
step="0.1"
placeholder="Default (0)"
name="slot[presence_penalty]"
/>
</div>
<%!-- Repetition Penalty --%>
<div class="form-control">
<label class="label">
<span class="label-text">Repetition Penalty</span>
<span class="label-text-alt text-base-content/50">
(0 to 2)
</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["repetition_penalty"]}
min="0"
max="2"
step="0.1"
placeholder="Default (1.0)"
name="slot[repetition_penalty]"
/>
</div>
<%!-- Seed --%>
<div class="form-control">
<label class="label">
<span class="label-text">Seed</span>
<span class="label-text-alt text-base-content/50">
(reproducibility)
</span>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["seed"]}
min="0"
placeholder="Random"
name="slot[seed]"
/>
</div>
<%!-- Stop Sequences --%>
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Stop Sequences</span>
<span class="label-text-alt text-base-content/50">
(comma-separated)
</span>
</label>
<input
type="text"
class="input input-bordered input-sm w-full"
value={
if is_list(slot["stop"]),
do: Enum.join(slot["stop"], ", "),
else: slot["stop"]
}
placeholder='e.g., "\n\n", "END"'
name="slot[stop]"
/>
</div>
</div>
</div>
</details>
</div>
<% :image_gen -> %>
<%!-- Image Size --%>
<div class="form-control">
<label class="label">
<span class="label-text">Image Size</span>
</label>
<select
class="select select-bordered select-sm"
name="slot[size]"
>
<option value="1024x1024" selected={slot["size"] == "1024x1024"}>
1024×1024 (Square)
</option>
<option value="1792x1024" selected={slot["size"] == "1792x1024"}>
1792×1024 (Landscape)
</option>
<option value="1024x1792" selected={slot["size"] == "1024x1792"}>
1024×1792 (Portrait)
</option>
</select>
</div>
<%!-- Quality --%>
<div class="form-control">
<label class="label">
<span class="label-text">Quality</span>
</label>
<select
class="select select-bordered select-sm"
name="slot[quality]"
>
<option value="standard" selected={slot["quality"] == "standard"}>
Standard
</option>
<option value="hd" selected={slot["quality"] == "hd"}>
HD (Higher detail)
</option>
</select>
</div>
<% :embeddings -> %>
<%!-- Dimensions --%>
<div class="form-control">
<label class="label">
<span class="label-text">Dimensions</span>
<%= if model_info && model_info["dimensions"] do %>
<span class="label-text-alt text-base-content/50">
(default: {model_info["dimensions"]})
</span>
<% end %>
</label>
<input
type="number"
class="input input-bordered input-sm w-full"
value={slot["dimensions"]}
placeholder={
if model_info,
do: "#{model_info["dimensions"]} (default)",
else: "Model default"
}
min="1"
name="slot[dimensions]"
/>
<label class="label">
<span class="label-text-alt text-base-content/50">
Leave empty to use model default. Lower dimensions = faster, higher = more accurate.
</span>
</label>
</div>
<%!-- Empty div for grid alignment --%>
<div></div>
<% end %>
<% end %>
</form>
</div>
</div>
<% end %>
</div>
<div class="card-actions justify-end mt-6">
<button
class="btn btn-primary"
phx-click="save_slots"
disabled={@saving_slots}
>
<%= if @saving_slots do %>
<span class="loading loading-spinner loading-sm"></span>
<% end %>
Save Configuration
</button>
</div>
</div>
</div>
<% end %>
<%!-- Usage Tab --%>
<%= if @active_tab == "usage" do %>
<%= if @usage_loaded do %>
<%!-- Stats Cards --%>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<%!-- Today Stats --%>
<div class="stat bg-base-100 rounded-box shadow">
<div class="stat-title">Today</div>
<div class="stat-value text-primary">
{format_number(@usage_stats.today.total_requests)}
</div>
<div class="stat-desc">
{PhoenixKit.AI.Request.format_tokens(@usage_stats.today.total_tokens)} tokens
</div>
</div>
<%!-- Last 30 Days --%>
<div class="stat bg-base-100 rounded-box shadow">
<div class="stat-title">Last 30 Days</div>
<div class="stat-value">
{format_number(@usage_stats.last_30_days.total_requests)}
</div>
<div class="stat-desc">
{PhoenixKit.AI.Request.format_tokens(@usage_stats.last_30_days.total_tokens)} tokens
</div>
</div>
<%!-- Success Rate --%>
<div class="stat bg-base-100 rounded-box shadow">
<div class="stat-title">Success Rate</div>
<div class="stat-value text-success">
{@usage_stats.last_30_days.success_rate}%
</div>
<div class="stat-desc">
{@usage_stats.last_30_days.error_count} errors
</div>
</div>
<%!-- Avg Latency --%>
<div class="stat bg-base-100 rounded-box shadow">
<div class="stat-title">Avg Latency</div>
<div class="stat-value">
{PhoenixKit.AI.Request.format_latency(@usage_stats.last_30_days.avg_latency_ms)}
</div>
<div class="stat-desc">last 30 days</div>
</div>
</div>
<%!-- All Time Stats --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title mb-4">
<.icon name="hero-chart-bar" class="w-5 h-5" /> All Time Statistics
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div class="text-sm text-base-content/60">Total Requests</div>
<div class="text-2xl font-bold">
{format_number(@usage_stats.all_time.total_requests)}
</div>
</div>
<div>
<div class="text-sm text-base-content/60">Total Tokens</div>
<div class="text-2xl font-bold">
{PhoenixKit.AI.Request.format_tokens(@usage_stats.all_time.total_tokens)}
</div>
</div>
<div>
<div class="text-sm text-base-content/60">Successful</div>
<div class="text-2xl font-bold text-success">
{format_number(@usage_stats.all_time.success_count)}
</div>
</div>
<div>
<div class="text-sm text-base-content/60">Errors</div>
<div class="text-2xl font-bold text-error">
{format_number(@usage_stats.all_time.error_count)}
</div>
</div>
</div>
</div>
</div>
<%!-- Recent Requests --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">
<.icon name="hero-clock" class="w-5 h-5" /> Recent Requests
</h2>
<%= if length(@usage_requests) > 0 do %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Time</th>
<th>Model</th>
<th>Tokens</th>
<th>Latency</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<%= for request <- @usage_requests do %>
<tr>
<td class="text-sm">
<.time_ago datetime={request.inserted_at} />
</td>
<td>
<span class="font-mono text-sm">
{PhoenixKit.AI.Request.short_model_name(request.model)}
</span>
</td>
<td>
{PhoenixKit.AI.Request.format_tokens(request.total_tokens)}
</td>
<td>
{PhoenixKit.AI.Request.format_latency(request.latency_ms)}
</td>
<td>
<span class={"badge badge-sm #{PhoenixKit.AI.Request.status_color(request.status)}"}>
{PhoenixKit.AI.Request.status_label(request.status)}
</span>
<%= if request.error_message do %>
<span
class="tooltip tooltip-left cursor-help"
data-tip={request.error_message}
>
<.icon
name="hero-exclamation-circle"
class="w-4 h-4 text-error ml-1"
/>
</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%= if length(@usage_requests) < @usage_total_requests do %>
<div class="text-center mt-4">
<button class="btn btn-outline btn-sm" phx-click="load_more_requests">
Load More
</button>
</div>
<% end %>
<% else %>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<span>
No requests yet. Usage statistics will appear here once you start making AI requests.
</span>
</div>
<% end %>
</div>
</div>
<% else %>
<%!-- Loading state --%>
<div class="flex justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
<% end %>
<% end %>
<% else %>
<%!-- Setup State: No accounts yet --%>
<div class="text-center">
<p class="text-base-content/70 mb-6">
To get started, add your first AI provider account.
</p>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/ai/accounts/new")}
class="btn btn-primary"
>
<.icon name="hero-plus" class="w-5 h-5 mr-2" /> Add Account
</.link>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>