Current section

Files

Jump to
phoenix_kit_ai lib phoenix_kit_ai web playground.html.heex
Raw

lib/phoenix_kit_ai/web/playground.html.heex

<div class="container flex-col mx-auto px-4 py-6">
<.admin_page_header back={PhoenixKit.Utils.Routes.path("/admin")}>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold text-base-content">AI Playground</h1>
<p class="text-sm text-base-content/60 mt-0.5">
Test endpoints and prompts interactively
</p>
</.admin_page_header>
<%!-- Controls --%>
<div class="bg-base-200 rounded-lg p-6 mb-6">
<div class="flex flex-col lg:flex-row gap-4 items-center justify-between">
<%!-- Tabs --%>
<div class="tabs tabs-boxed">
<.link
navigate={PhoenixKit.Utils.Routes.ai_path() <> "/endpoints"}
class="tab"
>
<.icon name="hero-server-stack" class="w-4 h-4 mr-2" /> Endpoints
</.link>
<.link
navigate={PhoenixKit.Utils.Routes.ai_path() <> "/prompts"}
class="tab"
>
<.icon name="hero-document-text" class="w-4 h-4 mr-2" /> Prompts
</.link>
<.link
navigate={PhoenixKit.Utils.Routes.ai_path() <> "/playground"}
class="tab tab-active"
>
<.icon name="hero-beaker" class="w-4 h-4 mr-2" /> Playground
</.link>
<.link
navigate={PhoenixKit.Utils.Routes.ai_path() <> "/usage"}
class="tab"
>
<.icon name="hero-chart-bar" class="w-4 h-4 mr-2" /> Usage
</.link>
</div>
</div>
</div>
<div class="max-w-4xl mx-auto space-y-6">
<form phx-change="change" phx-submit="send" class="space-y-6">
<%!-- Configuration Card --%>
<div class="card bg-base-200">
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-cog-6-tooth" class="w-5 h-5" /> Configuration
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<%!-- Endpoint Selector --%>
<div>
<label class="block text-sm font-medium mb-2">
Endpoint <span class="text-error">*</span>
</label>
<select
class="select select-bordered w-full"
name="endpoint_uuid"
>
<option value="">Select an endpoint...</option>
<%= for endpoint <- @endpoints do %>
<option
value={endpoint.uuid}
selected={endpoint.uuid == @selected_endpoint_uuid}
>
{endpoint.name} ({PhoenixKitAI.Endpoint.short_model_name(
endpoint.model
)})
</option>
<% end %>
</select>
<%= if Enum.empty?(@endpoints) do %>
<p class="text-sm text-warning mt-1">
No enabled endpoints.
<.pk_link navigate="/admin/ai/endpoints/new" class="link link-primary">
Create one
</.pk_link>
</p>
<% end %>
</div>
<%!-- Prompt Selector --%>
<div>
<label class="block text-sm font-medium mb-2">
Prompt <span class="text-base-content/60 font-normal ml-2">Optional</span>
</label>
<select
class="select select-bordered w-full"
name="prompt_uuid"
>
<option value="">Freeform (no prompt template)</option>
<%= for prompt <- @prompts do %>
<option value={prompt.uuid} selected={prompt.uuid == @selected_prompt_uuid}>
{prompt.name}
</option>
<% end %>
</select>
</div>
</div>
</div>
</div>
<%!-- Input Card --%>
<div class="card bg-base-200">
<div class="card-body">
<%= if @selected_prompt do %>
<%!-- Prompt Mode --%>
<h2 class="card-title text-lg">
<.icon name="hero-document-text" class="w-5 h-5" /> {@selected_prompt.name}
</h2>
<%= 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">
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">
<label class="block text-xs font-medium text-base-content/60 mb-1">
Prompt Template
<span class="text-base-content/40 font-normal ml-1">(editable)</span>
</label>
<div id={"edited-content-wrap-#{@selected_prompt.uuid}"} phx-update="ignore">
<textarea
class="textarea textarea-bordered w-full font-mono text-sm"
rows={
max(
5,
length(String.split(@edited_content || @selected_prompt.content, "\n")) +
2
)
}
name="edited_content"
phx-debounce="300"
>{@edited_content || @selected_prompt.content}</textarea>
</div>
</div>
<%!-- Variable Inputs --%>
<%= if length(@edited_variables) > 0 do %>
<div class="divider text-sm">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
class="textarea textarea-bordered w-full text-sm"
rows="2"
name={"variables[#{var}]"}
placeholder={"Enter value for #{var}..."}
phx-debounce="300"
>{Map.get(@variable_values, var, "")}</textarea>
</div>
<% end %>
</div>
<% end %>
<% else %>
<%!-- Freeform Mode --%>
<h2 class="card-title text-lg">
<.icon name="hero-chat-bubble-left-right" class="w-5 h-5" /> Message
</h2>
<%!-- System Prompt --%>
<div class="mt-2">
<label class="block text-sm font-medium mb-2">
System Prompt
<span class="text-base-content/60 font-normal ml-2">Optional</span>
</label>
<textarea
class="textarea textarea-bordered w-full text-sm"
rows="2"
name="system"
placeholder="Optional system instructions..."
phx-debounce="300"
>{@freeform_system}</textarea>
</div>
<%!-- User Message --%>
<div class="mt-3">
<label class="block text-sm font-medium mb-2">
Message <span class="text-error">*</span>
</label>
<textarea
class="textarea textarea-bordered w-full text-sm"
rows="4"
name="message"
placeholder="Type your message to the AI..."
phx-debounce="300"
>{@freeform_message}</textarea>
</div>
<% 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}
>
Clear
</button>
<button
type="submit"
class="btn btn-primary"
disabled={@sending || is_nil(@selected_endpoint_uuid)}
>
<%= if @sending do %>
<span class="loading loading-spinner loading-sm"></span> Sending...
<% else %>
<.icon name="hero-paper-airplane" class="w-4 h-4 mr-1" /> Send
<% end %>
</button>
</div>
</div>
</div>
</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">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">Error</div>
<div class="text-sm">{@response_error}</div>
</div>
</div>
<% end %>
<%!-- Response Card --%>
<%= if @response_text do %>
<div
id="playground-result"
class="card bg-base-200"
phx-mounted={JS.dispatch("phx:scroll-into-view", to: "#playground-result")}
>
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-sparkles" class="w-5 h-5" /> Response
</h2>
<%!-- 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)} in
</span>
<span>
<.icon name="hero-arrow-down" class="w-3 h-3 inline" />
{PhoenixKitAI.Request.format_tokens(
@response_usage.completion_tokens
)} out
</span>
<span>
<.icon name="hero-calculator" class="w-3 h-3 inline" />
{PhoenixKitAI.Request.format_tokens(@response_usage.total_tokens)} 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>
</div>
</div>
<% end %>
</div>
<script>
document.addEventListener("phx:scroll-into-view", function(e) {
e.target.scrollIntoView({behavior: "smooth", block: "start"});
});
</script>
</div>
</div>