Packages
AI module for PhoenixKit — endpoints, prompts, completions, and usage tracking
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_ai/web/playground.html.heex
<div class="flex flex-col w-full px-4 py-6">
<.admin_page_header
title={gettext("AI Playground")}
subtitle={gettext("Test endpoints and prompts interactively")}
/>
<%!-- Inner form capped at 7xl: wider response/messages area on large
monitors, but single-line inputs don't stretch absurdly on
ultrawide displays. --%>
<div class="max-w-7xl mx-auto space-y-6 w-full">
<form phx-change="change" phx-submit="send" class="space-y-6">
<.form_section title={gettext("Configuration")} icon="hero-cog-6-tooth" class="bg-base-200">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<%!-- Endpoint Selector --%>
<div>
<.select
name="endpoint_uuid"
label={gettext("Endpoint") <> " *"}
prompt={gettext("Select an endpoint...")}
value={@selected_endpoint_uuid}
options={
Enum.map(@endpoints, fn endpoint ->
{"#{endpoint.name} (#{PhoenixKitAI.Endpoint.short_model_name(endpoint.model)})",
endpoint.uuid}
end)
}
/>
<%= if Enum.empty?(@endpoints) do %>
<p class="text-sm text-warning mt-1">
{gettext("No enabled endpoints.")}
<.pk_link navigate="/admin/ai/endpoints/new" class="link link-primary">
{gettext("Create one")}
</.pk_link>
</p>
<% end %>
</div>
<%!-- Prompt Selector --%>
<.select
name="prompt_uuid"
label={gettext("Prompt (Optional)")}
prompt={gettext("Freeform (no prompt template)")}
value={@selected_prompt_uuid}
options={Enum.map(@prompts, fn prompt -> {prompt.name, prompt.uuid} end)}
/>
</div>
</.form_section>
<.form_section
title={if @selected_prompt, do: @selected_prompt.name, else: gettext("Message")}
icon={if @selected_prompt, do: "hero-document-text", else: "hero-chat-bubble-left-right"}
class="bg-base-200"
>
<%= if @selected_prompt do %>
<%!-- Prompt Mode --%>
<%= if @selected_prompt.description do %>
<p class="text-sm text-base-content/60">{@selected_prompt.description}</p>
<% end %>
<%!-- System Prompt Preview --%>
<%= if @selected_prompt.system_prompt && @selected_prompt.system_prompt != "" do %>
<div class="mt-2">
<label class="block text-xs font-medium text-base-content/60 mb-1">
{gettext("System Prompt")}
</label>
<div class="bg-base-300 rounded-lg p-3 text-sm font-mono whitespace-pre-wrap text-base-content/80">
{@selected_prompt.system_prompt}
</div>
</div>
<% end %>
<%!-- Editable Content --%>
<div class="mt-2" id={"edited-content-wrap-#{@selected_prompt.uuid}"} phx-update="ignore">
<.textarea
name="edited_content"
label={gettext("Prompt Template (editable)")}
value={@edited_content || @selected_prompt.content}
class="font-mono text-sm"
rows={
max(
5,
length(String.split(@edited_content || @selected_prompt.content, "\n")) + 2
)
}
phx-debounce="300"
/>
</div>
<%!-- Variable Inputs --%>
<%= if length(@edited_variables) > 0 do %>
<div class="divider text-sm">{gettext("Variables")}</div>
<div class="space-y-3">
<%= for var <- @edited_variables do %>
<div>
<label class="block text-sm font-medium mb-1">
<span class="font-mono badge badge-primary badge-outline badge-sm mr-1">
{"{{#{var}}}"}
</span>
</label>
<.textarea
name={"variables[#{var}]"}
value={Map.get(@variable_values, var, "")}
class="text-sm"
rows="2"
placeholder={gettext("Enter value for %{name}...", name: var)}
phx-debounce="300"
/>
</div>
<% end %>
</div>
<% end %>
<% else %>
<%!-- Freeform Mode --%>
<.textarea
name="system"
label={gettext("System Prompt (Optional)")}
value={@freeform_system}
class="text-sm"
rows="2"
placeholder={gettext("Optional system instructions...")}
phx-debounce="300"
/>
<.textarea
name="message"
label={gettext("Message") <> " *"}
value={@freeform_message}
class="text-sm"
rows="4"
placeholder={gettext("Type your message to the AI...")}
phx-debounce="300"
/>
<% end %>
<%!-- Actions --%>
<div class="flex justify-end gap-2 mt-4">
<button
type="button"
class="btn btn-ghost btn-sm"
phx-click="clear"
disabled={@sending}
>
{gettext("Clear")}
</button>
<button
type="submit"
class="btn btn-primary"
phx-disable-with={gettext("Sending…")}
disabled={@sending || is_nil(@selected_endpoint_uuid)}
>
<%= if @sending do %>
<span class="loading loading-spinner loading-sm"></span> {gettext("Sending…")}
<% else %>
<.icon name="hero-paper-airplane" class="w-4 h-4 mr-1" /> {gettext("Send")}
<% end %>
</button>
</div>
</.form_section>
</form>
<%!-- Response Area --%>
<div id="playground-response" class="space-y-6">
<%!-- Loading Skeleton --%>
<%= if @sending do %>
<div
id="playground-skeleton"
class="card bg-base-200 animate-pulse"
phx-mounted={JS.dispatch("phx:scroll-into-view", to: "#playground-skeleton")}
>
<div class="card-body">
<div class="flex items-center gap-2 mb-3">
<span class="loading loading-spinner loading-sm text-primary"></span>
<span class="text-sm text-base-content/60">{gettext("Waiting for response...")}</span>
</div>
<div class="space-y-3">
<div class="bg-base-300 rounded h-4 w-3/4"></div>
<div class="bg-base-300 rounded h-4 w-full"></div>
<div class="bg-base-300 rounded h-4 w-5/6"></div>
<div class="bg-base-300 rounded h-4 w-2/3"></div>
</div>
</div>
</div>
<% end %>
<%!-- Error Display --%>
<%= if @response_error do %>
<div
id="playground-error"
class="alert alert-error"
phx-mounted={JS.dispatch("phx:scroll-into-view", to: "#playground-error")}
>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<div class="font-medium">{gettext("Error")}</div>
<div class="text-sm">{@response_error}</div>
</div>
</div>
<% end %>
<%!-- Response Card --%>
<%= if @response_text do %>
<.form_section
title={gettext("Response")}
icon="hero-sparkles"
class="bg-base-200"
id="playground-result"
phx-mounted={JS.dispatch("phx:scroll-into-view", to: "#playground-result")}
>
<%!-- Usage Stats --%>
<%= if @response_usage do %>
<div class="flex flex-wrap gap-3 text-xs text-base-content/60">
<span>
<.icon name="hero-arrow-up" class="w-3 h-3 inline" />
{PhoenixKitAI.Request.format_tokens(@response_usage.prompt_tokens)} {gettext("in")}
</span>
<span>
<.icon name="hero-arrow-down" class="w-3 h-3 inline" />
{PhoenixKitAI.Request.format_tokens(
@response_usage.completion_tokens
)} {gettext("out")}
</span>
<span>
<.icon name="hero-calculator" class="w-3 h-3 inline" />
{PhoenixKitAI.Request.format_tokens(@response_usage.total_tokens)} {gettext("total")}
</span>
<%= if @response_usage.cost_cents do %>
<span>
<.icon name="hero-currency-dollar" class="w-3 h-3 inline" />
{PhoenixKitAI.Request.format_cost(@response_usage.cost_cents)}
</span>
<% end %>
</div>
<% end %>
<%!-- Response Content --%>
<div class="bg-base-300 rounded-lg p-4 mt-2 text-sm">
<pre class="whitespace-pre-wrap font-[inherit] m-0">{@response_text}</pre>
</div>
</.form_section>
<% end %>
</div>
<script>
document.addEventListener("phx:scroll-into-view", function(e) {
e.target.scrollIntoView({behavior: "smooth", block: "start"});
});
</script>
</div>
</div>