styling, require a code for prompts, admin privs, ordering, filtering, jump-to-page

This commit is contained in:
2022-09-07 16:25:42 -04:00
parent 7990246675
commit eefd11d85a
52 changed files with 2827 additions and 300 deletions

205
lib/diffuser/accounts.ex Normal file
View File

@@ -0,0 +1,205 @@
defmodule Diffuser.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias Diffuser.Repo
alias Diffuser.Accounts.User
alias Diffuser.Generator.PromptRequest
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%User{}, ...]
"""
def list_users do
Repo.all(User)
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.get!(User, id) |> Repo.preload(votes: [:prompt_request])
@doc """
Creates a user.
## Examples
iex> create_user(%{field: value})
{:ok, %User{}}
iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a user.
## Examples
iex> update_user(user, %{field: new_value})
{:ok, %User{}}
iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a user.
## Examples
iex> delete_user(user)
{:ok, %User{}}
iex> delete_user(user)
{:error, %Ecto.Changeset{}}
"""
def delete_user(%User{} = user) do
Repo.delete(user)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user(%User{} = user, attrs \\ %{}) do
User.changeset(user, attrs)
end
alias Diffuser.Accounts.Vote
@doc """
Returns the list of votes.
## Examples
iex> list_votes()
[%Vote{}, ...]
"""
def list_votes do
Repo.all(Vote)
end
@doc """
Gets a single vote.
Raises `Ecto.NoResultsError` if the Vote does not exist.
## Examples
iex> get_vote!(123)
%Vote{}
iex> get_vote!(456)
** (Ecto.NoResultsError)
"""
def get_vote!(id), do: Repo.get!(Vote, id)
@doc """
Creates a vote.
## Examples
iex> create_vote(%{field: value})
{:ok, %Vote{}}
iex> create_vote(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_vote(%{user: user} = attrs) do
%Vote{}
|> Vote.changeset(attrs)
|> Ecto.Changeset.put_assoc(:user, user)
|> Repo.insert()
end
@doc """
Updates a vote.
## Examples
iex> update_vote(vote, %{field: new_value})
{:ok, %Vote{}}
iex> update_vote(vote, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_vote(%Vote{} = vote, attrs) do
vote
|> Vote.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a vote.
## Examples
iex> delete_vote(vote)
{:ok, %Vote{}}
iex> delete_vote(vote)
{:error, %Ecto.Changeset{}}
"""
def delete_vote(%Vote{} = vote) do
Repo.delete(vote)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking vote changes.
## Examples
iex> change_vote(vote)
%Ecto.Changeset{data: %Vote{}}
"""
def change_vote(%Vote{} = vote, attrs \\ %{}) do
Vote.changeset(vote, attrs)
end
def upvote(user, %PromptRequest{id: pr}),
do: create_vote(%{prompt_request_id: pr, user: user})
end

View File

@@ -0,0 +1,25 @@
defmodule Diffuser.Accounts.User do
use Diffuser.Schema
import Ecto.Changeset
alias Diffuser.Accounts.Vote
alias Diffuser.Generator.PromptRequest
schema "users" do
field :ip_address, :string
field :username, :string
field :code, :string
has_many :votes, Vote, on_delete: :delete_all
has_many :prompt_requests, PromptRequest, on_delete: :delete_all
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:ip_address, :username, :code])
|> cast_assoc(:votes)
|> cast_assoc(:prompt_requests)
end
end

View File

@@ -0,0 +1,20 @@
defmodule Diffuser.Accounts.Vote do
use Diffuser.Schema
import Ecto.Changeset
alias Diffuser.Generator.PromptRequest
alias Diffuser.Accounts.User
schema "votes" do
belongs_to :prompt_request, PromptRequest
belongs_to :user, User
timestamps()
end
@doc false
def changeset(vote, attrs) do
vote
|> cast(attrs, [:prompt_request_id, :user_id])
|> validate_required([])
end
end

View File

@@ -36,7 +36,9 @@ defmodule Diffuser.Generator do
"""
def get_prompt_request!(id),
do: Repo.one!(from pr in PromptRequest, where: pr.id == ^id) |> Repo.preload(:images)
do:
Repo.one!(from pr in PromptRequest, where: pr.id == ^id)
|> Repo.preload([:votes, :images, :user])
@doc """
Creates a prompt_request.
@@ -50,9 +52,18 @@ defmodule Diffuser.Generator do
{:error, %Ecto.Changeset{}}
"""
def create_prompt_request(attrs \\ %{}) do
def create_prompt_request(%{"user" => user} = attrs) do
user = Diffuser.Accounts.get_user!(user)
code = Map.get(user, :code, nil)
attrs =
if is_nil(code),
do: attrs |> Map.put("code", "garbage"),
else: attrs |> Map.put("code", code)
%PromptRequest{}
|> PromptRequest.changeset(attrs)
|> Ecto.Changeset.put_assoc(:user, user)
|> Repo.insert()
end
@@ -74,6 +85,9 @@ defmodule Diffuser.Generator do
|> Repo.update()
end
def update_prompt_request(id, attrs),
do: get_prompt_request!(id) |> update_prompt_request(attrs)
@doc """
Deletes a prompt_request.
@@ -123,9 +137,14 @@ defmodule Diffuser.Generator do
end)
end
def paginate_prompt_requests(params) do
PromptRequest
|> preload(:images)
def paginate_prompt_requests(query \\ PromptRequest, params) do
search_query = Map.get(params, :query, nil)
order_by_query = Map.get(params, :order_by, nil)
query
|> PromptRequest.search_by_prompt_or_user(search_query)
|> PromptRequest.order_by(order_by_query)
|> preload([:votes, :user, :images])
|> Repo.paginate(params)
end
end

View File

@@ -1,17 +1,26 @@
defmodule Diffuser.Generator.PromptRequest do
use Ecto.Schema
use Diffuser.Schema
use Waffle.Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Diffuser.Generator.PromptRequestResult
alias Diffuser.Accounts.{User, Vote}
@valid_codes Application.get_env(:diffuser, :valid_codes, [])
@primary_key {:id, Ecto.UUID, autogenerate: true}
schema "prompt_requests" do
field :prompt, :string
field :status, :string, default: "queued"
field :steps, :integer
field :guidance_scale, :float
field :steps, :integer, default: 32
field :completed_steps, :integer, default: 0
field :guidance_scale, :float, default: 7.5
field :began_at, :utc_datetime
field :ended_at, :utc_datetime
field :code, :string
has_many :images, PromptRequestResult, on_delete: :delete_all
has_many :votes, Vote, on_delete: :delete_all
belongs_to :user, User
timestamps()
end
@@ -19,7 +28,63 @@ defmodule Diffuser.Generator.PromptRequest do
@doc false
def changeset(prompt_request, attrs) do
prompt_request
|> cast(attrs, [:prompt, :status, :steps, :guidance_scale])
|> cast(attrs, [
:prompt,
:status,
:steps,
:guidance_scale,
:completed_steps,
:began_at,
:ended_at,
:code
])
|> validate_number(:steps, less_than_or_equal_to: 60)
|> validate_code()
|> validate_required([:prompt])
end
defp validate_code(changeset) do
validate_change(changeset, :code, fn :code, code ->
cond do
@valid_codes
|> Enum.any?(fn {_name, valid_code} ->
valid_code == code
end) ->
[]
true ->
[code: "Code is invalid."]
end
end)
end
def search_by_prompt_or_user(query, nil), do: query
def search_by_prompt_or_user(query, search_query) do
from p in query,
left_join: u in User,
on: u.id == p.user_id,
where: ilike(p.prompt, ^"%#{search_query}%")
end
def order_by(query, nil) do
from p in query,
order_by: [desc: p.inserted_at]
end
def order_by(query, "desc") do
from p in query,
join: v in Vote,
on: v.prompt_request_id == p.id,
group_by: p.id,
order_by: [desc: count(v.id)]
end
def order_by(query, "asc") do
from p in query,
left_join: v in Vote,
on: v.prompt_request_id == p.id,
group_by: p.id,
order_by: [asc: count(v.id)]
end
end

View File

@@ -1,13 +1,12 @@
defmodule Diffuser.Generator.PromptRequestResult do
use Ecto.Schema
use Diffuser.Schema
use Waffle.Ecto.Schema
import Ecto.Changeset
alias Diffuser.Generator.PromptRequest
@primary_key {:id, Ecto.UUID, autogenerate: true}
schema "prompt_request_results" do
field :image, Diffuser.Uploaders.Image.Type
belongs_to :prompt_request, PromptRequest, type: :binary_id
belongs_to :prompt_request, PromptRequest
timestamps()
end

View File

@@ -5,8 +5,6 @@ defmodule Diffuser.Generator.PromptRequestWorker do
alias Diffuser.Repo
@path [:code.priv_dir(:diffuser), "python"] |> Path.join()
@steps 10
@guidance_scale 7.5
def start(%PromptRequest{} = prompt_request) do
with {:ok, active_prompt} <-
@@ -26,24 +24,38 @@ defmodule Diffuser.Generator.PromptRequestWorker do
end
end
defp update_and_broadcast_progress(%PromptRequest{id: id} = prompt_request, new_status) do
{:ok, new_prompt} =
Generator.update_prompt_request(prompt_request, %{
status: new_status,
steps: @steps,
guidance_scale: @guidance_scale
defp update_and_broadcast_progress(prompt_request, "in_progress"),
do:
update_and_broadcast_progress(prompt_request, %{
status: "in_progress",
began_at: NaiveDateTime.utc_now()
})
defp update_and_broadcast_progress(prompt_request, "finished"),
do:
update_and_broadcast_progress(prompt_request, %{
status: "finished",
ended_at: NaiveDateTime.utc_now()
})
defp update_and_broadcast_progress(%PromptRequest{id: id} = prompt_request, attrs) do
{:ok, new_prompt} = Generator.update_prompt_request(prompt_request, attrs)
:ok = Endpoint.broadcast("request:#{id}", "request", %{prompt_request: new_prompt})
{:ok, new_prompt}
end
defp call_python(_module, _func, %{id: prompt_id, prompt: prompt}) do
defp call_python(_module, _func, %PromptRequest{
id: prompt_id,
prompt: prompt,
steps: steps,
guidance_scale: guidance_scale
}) do
port =
Port.open(
{:spawn,
~s(python #{@path}/stable_diffusion.py --prompt "#{prompt}" --output "#{@path}/#{prompt_id}.png" --num-inference-steps #{@steps})},
~s(python #{@path}/stable_diffusion.py --prompt "#{prompt}" --output "#{@path}/#{prompt_id}.png" --num-inference-steps #{steps} --guidance-scale #{guidance_scale})},
[:binary]
)
@@ -56,11 +68,18 @@ defmodule Diffuser.Generator.PromptRequestWorker do
{:ok, msg}
{^port, {:data, ":step" <> step}} ->
Endpoint.broadcast("request:#{prompt_id}", "progress", step)
{:ok, prompt_request} =
Generator.update_prompt_request(prompt_id, %{completed_steps: step})
Endpoint.broadcast(
"request:#{prompt_id}",
"progress",
%{prompt_request: prompt_request |> Repo.preload(:images)}
)
python_loop(port, prompt_id)
{^port, result} ->
IO.inspect(result, label: "RESULT")
{^port, _result} ->
python_loop(port, prompt_id)
end
end

View File

@@ -3,5 +3,5 @@ defmodule Diffuser.Repo do
otp_app: :diffuser,
adapter: Ecto.Adapters.Postgres
use Scrivener, page_size: 1
use Scrivener, page_size: 12
end

9
lib/diffuser/schema.ex Normal file
View File

@@ -0,0 +1,9 @@
defmodule Diffuser.Schema do
defmacro __using__(_) do
quote do
use Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
end
end
end

View File

@@ -1,8 +1,7 @@
defmodule DiffuserWeb.RequestChannel do
use Phoenix.Channel
def join("request:" <> room_id, _message, socket) do
IO.inspect(room_id)
def join("request:" <> _room_id, _message, socket) do
{:ok, socket}
end
end

View File

@@ -2,7 +2,6 @@ defmodule DiffuserWeb.PromptRequestLive.FormComponent do
use DiffuserWeb, :live_component
alias Diffuser.Generator
alias Diffuser.Generator.PromptRequest
@impl true
def update(%{prompt_request: prompt_request} = assigns, socket) do
@@ -28,30 +27,23 @@ defmodule DiffuserWeb.PromptRequestLive.FormComponent do
save_prompt_request(socket, socket.assigns.action, prompt_request_params)
end
defp save_prompt_request(socket, :edit, prompt_request_params) do
case Generator.update_prompt_request(socket.assigns.prompt_request, prompt_request_params) do
{:ok, %PromptRequest{} = prompt_request} ->
{:noreply,
socket
|> put_flash(:info, "Prompt request updated successfully")
|> push_redirect(to: Routes.prompt_request_show_path(socket, :show, prompt_request))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
defp save_prompt_request(socket, :new, prompt_request_params) do
defp save_prompt_request(%{assigns: %{user: _user}} = socket, :new, prompt_request_params) do
with {:ok, prompt_request} <- Generator.create_prompt_request(prompt_request_params) do
Diffuser.Generator.PromptRequestQueue.enqueue(prompt_request)
{:noreply,
socket
|> put_flash(:info, "Prompt request created successfully")
|> push_redirect(to: Routes.prompt_request_show_path(socket, :show, prompt_request))}
|> push_redirect(to: Routes.prompt_request_index_path(socket, :index))}
else
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
defp save_prompt_request(socket, :new, _) do
{:noreply,
socket
|> push_redirect(to: Routes.prompt_request_index_path(socket, :index))}
end
end

View File

@@ -1,20 +1,41 @@
<div>
<h2><%= @title %></h2>
<div class="prose grid grid-flow-col">
<!-- Put this part before </body> tag -->
<.form
let={f}
for={@changeset}
id="prompt_request-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
<.form
let={f}
for={@changeset}
id="prompt_request-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
<%= label f, :prompt %>
<%= text_input f, :prompt %>
<%= error_tag f, :prompt %>
<div>
<%= submit "Save", phx_disable_with: "Saving..." %>
</div>
</.form>
<div class="form-control w-full">
<label class="label">
<span class="label-text">What shall we dream of?</span>
</label>
<%= text_input f, :prompt, class: "input" %>
<%= error_tag f, :prompt %>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">How long should it dream?</span>
</label>
<%= number_input f, :steps, class: "input", min: 1, max: 60, step: 1 %>
<%= error_tag f, :prompt %>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">How much do your words control the dream?</span>
</label>
<%= number_input f, :guidance_scale, class: "input", step: 0.1 %>
<%= error_tag f, :prompt %>
</div>
<%= hidden_input f, :user, value: @user.id %>
<div style="text-align: end;" class="pt-6">
<%= submit "Save", phx_disable_with: "Saving...", class: "btn" %>
</div>
</.form>
</div>

View File

@@ -3,46 +3,204 @@ defmodule DiffuserWeb.PromptRequestLive.Index do
alias Diffuser.Generator
alias Diffuser.Generator.PromptRequest
alias Diffuser.Accounts
alias Diffuser.Accounts.User
alias DiffuserWeb.Endpoint
alias Phoenix.Socket.Broadcast
alias Diffuser.Repo
alias Ecto.Association.NotLoaded
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
def mount(params, %{"user" => user, "is_admin" => is_admin}, socket) do
changeset = Accounts.change_user(user)
{:ok,
socket
|> assign(:user, user)
|> assign(:params, params)
|> assign(:is_admin, is_admin)
|> assign(:user_changeset, changeset)}
end
@impl true
def handle_params(params, _url, socket) do
page = list_prompt_requests(params)
socket = socket |> apply_action(socket.assigns.live_action, params) |> assign(:page, page)
page = list_prompt_requests(params) |> subscribe_to_queued_prompts()
socket =
socket
|> apply_action(socket.assigns.live_action, params)
|> assign(:page, page)
|> assign(:params, params)
{:noreply, socket}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Prompt request")
|> assign(:page_title, "Edit Prompt")
|> assign(:prompt_request, Generator.get_prompt_request!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Prompt request")
|> assign(:page_title, "New Prompt")
|> assign(:prompt_request, %PromptRequest{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Prompt requests")
|> assign(:page_title, "Listing Prompts")
|> assign(:prompt_request, nil)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
def handle_event("delete", %{"id" => id}, %{assigns: %{params: params}} = socket) do
prompt_request = Generator.get_prompt_request!(id)
{:ok, _} = Generator.delete_prompt_request(prompt_request)
{:noreply, assign(socket, :page, list_prompt_requests(%{"page" => "2"}))}
{:noreply, assign(socket, :page, list_prompt_requests(params))}
end
def handle_event("upvote", %{"id" => id}, %{assigns: %{user: user, params: params}} = socket) do
prompt_request = Generator.get_prompt_request!(id)
Diffuser.Accounts.upvote(user, prompt_request)
{:noreply, assign(socket, :page, list_prompt_requests(params))}
end
def handle_event(
"update_user",
%{"user" => params},
%{assigns: %{user: user}} = socket
) do
case Accounts.update_user(user, params) do
{:ok, user} ->
{:noreply, assign(socket, :user, user)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :user_changeset, changeset)}
end
end
def handle_event(
"search",
%{"search" => %{"query" => query}},
%{assigns: %{params: params}} = socket
) do
params = Map.put(params, :query, query)
page = Generator.paginate_prompt_requests(PromptRequest, params)
{:noreply, socket |> assign(:params, params) |> assign(:page, page)}
end
def handle_event("clear", _, socket) do
page = Generator.paginate_prompt_requests(PromptRequest, %{page: 1})
{:noreply, socket |> assign(:params, %{page: 1}) |> assign(:page, page)}
end
def handle_event("order_by", %{"votes" => direction}, %{assigns: %{params: params}} = socket) do
direction = if direction in ["asc", "desc"], do: direction, else: nil
params = Map.put(params, :order_by, direction)
page = Generator.paginate_prompt_requests(PromptRequest, params)
{:noreply, socket |> assign(:params, params) |> assign(:page, page)}
end
def handle_event("go_to" = event, %{"jump" => %{"page" => _page} = result}, socket),
do: handle_event(event, result, socket)
def handle_event("go_to", %{"page" => page}, %{assigns: %{params: params}} = socket) do
params = Map.put(params, :page, page)
page = Generator.paginate_prompt_requests(PromptRequest, params)
{:noreply, socket |> assign(:params, params) |> assign(:page, page)}
end
defp has_voted(nil, _), do: false
defp has_voted(%{id: user_id}, %{votes: prompt_request_votes}) do
prompt_request_votes
|> Enum.any?(fn %{user_id: id} ->
user_id == id
end)
end
defp display_current_filters(params) do
query = Map.get(params, :query, nil)
order_by = Map.get(params, :order_by, nil)
page = Map.get(params, :page, 1)
query_str = if query, do: "Searching for \"#{query}\"", else: "Everything"
order_by_str = if order_by == "desc", do: " with most votes", else: ""
query_str <> order_by_str <> " on page #{page}"
end
defp display_name(%User{username: username}) when not is_nil(username), do: username
defp display_name(%User{id: id}), do: id |> String.slice(0, 8)
defp fake_name(), do: for(_ <- 1..8, into: "", do: <<Enum.random('0123456789abcdef')>>)
defp list_prompt_requests(params) do
Generator.paginate_prompt_requests(params)
Generator.paginate_prompt_requests(PromptRequest, params)
end
defp total_time(%PromptRequest{began_at: began_at, ended_at: ended_at})
when not is_nil(began_at) and not is_nil(ended_at) do
"Processing Time: #{NaiveDateTime.diff(ended_at, began_at, :second)} seconds"
end
defp total_time(_), do: ""
defp owns_prompt_request(user, %{user: %NotLoaded{} = prompt_request}),
do: user |> owns_prompt_request(prompt_request |> Repo.preload(:user))
defp owns_prompt_request(nil, _), do: false
defp owns_prompt_request(_, %{user: nil}), do: false
defp owns_prompt_request(%{ip_address: their_ip_address, id: their_id}, %{
user: %{ip_address: ip_address, id: id}
})
when their_ip_address == ip_address or their_id == id,
do: true
defp owns_prompt_request(_, _), do: false
defp subscribe_to_queued_prompts(%Scrivener.Page{entries: entries} = page) do
entries
|> Enum.each(fn %PromptRequest{id: id, status: status} ->
if status in ["queued", "in_progress"] do
Endpoint.subscribe("request:#{id}")
end
end)
page
end
@impl true
def handle_info(
%Broadcast{
topic: _,
event: event,
payload: %{prompt_request: %PromptRequest{} = prompt_request}
},
socket
)
when event in ["request", "progress"],
do: {:noreply, socket |> update_subscribed_prompt(prompt_request)}
defp update_subscribed_prompt(
%{assigns: %{page: %Scrivener.Page{entries: entries} = page}} = socket,
%PromptRequest{id: id} = prompt_request
) do
entries =
entries
|> Enum.map(
&if &1.id === id, do: prompt_request |> Repo.preload([:votes, :user, :images]), else: &1
)
page = Map.put(page, :entries, entries)
socket
|> assign(:page, page)
end
end

View File

@@ -1,5 +1,3 @@
<h1>Listing Prompt requests</h1>
<%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.prompt_request_index_path(@socket, :index)}>
<.live_component
@@ -7,52 +5,134 @@
id={@prompt_request.id || :new}
title={@page_title}
action={@live_action}
prompt_request={@prompt_request}}
prompt_request={@prompt_request}
user={@user}}
/>
</.modal>
<% end %>
<table>
<thead>
<tr>
<th>Image</th>
<th>Prompt</th>
<%= if @user do %>
<div class="p-4 max-w-7xl mx-auto bg-gray">
<div class="collapse">
<input type="checkbox" />
<div class="collapse-title text-xl btn btn-outline">
Info
</div>
<div class="collapse-content prose">
<h3 class="pt-4">IP Address: <%= @user.ip_address %></h3>
<p>Yes. Your server, Silas, will keep a record of which IP sent what.</p>
<th></th>
</tr>
</thead>
<tbody id="prompt_requests">
<%= for prompt_request <- @page.entries do %>
<tr id={"prompt_request-#{prompt_request.id}"}>
<td>
<%= for result <- prompt_request.images do %>
<img src={"#{Diffuser.Uploaders.Image.url({result.image, result})}"} />
<% end %>
</td>
<td><%= prompt_request.prompt %></td>
<h3>Username: <%= display_name(@user) %></h3>
<.form let={f} for={@user_changeset} phx-submit="update_user">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Optionally set your own username. Might I recommend <%= fake_name() %>?</span>
</label>
<%= text_input f, :username, class: "input input-bordered input-secondary" %>
<%= error_tag f, :username %>
<td>
<span><%= live_redirect "Show", to: Routes.prompt_request_show_path(@socket, :show, prompt_request) %></span>
<span><%= live_patch "Edit", to: Routes.prompt_request_index_path(@socket, :edit, prompt_request) %></span>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: prompt_request.id, data: [confirm: "Are you sure?"] %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= submit "Save Username", class: "btn" %>
</div>
</.form>
<div class="pagination">
<%= if @page.page_number > 1 do %>
<%= live_patch "<< Prev Page",
to: Routes.prompt_request_index_path(@socket, :index, page: @page.page_number - 1),
class: "pagination-link" %>
<% end %>
<%= if @page.page_number < @page.total_pages do %>
<%= live_patch "Next Page >>",
to: Routes.prompt_request_index_path(@socket, :index, page: @page.page_number + 1),
class: "pagination-link" %>
<% end %>
<h3>Code: <%= if Map.get(@user, :code, nil), do: @user.code, else: "Not set" %></h3>
<.form let={f} for={@user_changeset} phx-submit="update_user">
<div class="form-control w-full">
<label class="label">
<span class="label-text">You'll need a code from Silas to use the generator.</span>
</label>
<%= text_input f, :code, class: "input input-bordered input-secondary" %>
<%= error_tag f, :code %>
<%= submit "Save Code", class: "btn" %>
</div>
</.form>
</div>
</div>
</div>
<% end %>
<div class="p-4 max-w-7xl mx-auto grid place-items-center">
<form phx-change="search" style="width: 100%;">
<%= text_input :search, :query, placeholder: "Search for image by prompt", "phx-debounce": "1000", class: "input w-full center-placeholder" %>
</form>
<div class="pt-4" style="width: 100%; text-align: center;"><%= display_current_filters(@params) %></div>
</div>
<span><%= live_patch "New Prompt request", to: Routes.prompt_request_index_path(@socket, :new) %></span>
<div class="max-w-7xl mx-auto">
<div class="p-4 flex space-x-2 justify-center">
<div>
<button class="btn btn-outline btn-xs sm:btn-sm md:btn-md lg:btn-lg" style="min-height: 45px"><%= live_patch "New Prompt", to: Routes.prompt_request_index_path(@socket, :new), class: "pagination-link" %></button>
</div>
<div>
<button phx-click="order_by" phx-value-votes="desc" class="btn btn-xs sm:btn-sm md:btn-md lg:btn-lg btn-outline" style="min-height: 45px;">Most Votes</button>
</div>
<div>
<button phx-click="clear" class="btn btn-xs sm:btn-sm md:btn-md lg:btn-lg btn-outline" style="min-height: 45px;">Reset Filter</button>
</div>
</div>
<div class="grid gap-1 md:gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<%= for prompt_request <- @page.entries do %>
<div class="card w-90 bg-gray shadow-xl text-primary-content">
<% result = if prompt_request.images |> length > 0, do: prompt_request.images |> List.first() %>
<%= if result do %>
<figure><img src={"#{Diffuser.Uploaders.Image.url({result.image, result})}"} /></figure>
<% else %>
<figure><%= prompt_request.status %>, <%= prompt_request.completed_steps %>/<%= prompt_request.steps %></figure>
<% end %>
<div class="card-body prose">
<h2 class="card-title"><%= prompt_request.prompt %></h2>
<p>Steps: <%= prompt_request.steps %>, Guidance Scale: <%= prompt_request.guidance_scale %><br />
<%= if prompt_request.status == "finished" do %>
<%= total_time(prompt_request) %><br />
<% end %>
<%= case prompt_request.user do %>
<%= %NotLoaded{} -> %>
<% user -> %>
<%= if user do %>
Created by: <%= display_name(user) %> <%= if @is_admin, do: "(#{user.ip_address}, #{prompt_request.code})" %><br />
<% end %>
<% end %>
Votes: <%= prompt_request.votes |> Enum.count() %> <br />
<%= if has_voted(@user, prompt_request) do %>
<% else %>
<%= link "Upvote", to: "#", phx_click: "upvote", phx_value_id: prompt_request.id %>
<% end %>
</p>
<%= if prompt_request.status == "finished" and (@is_admin or owns_prompt_request(@user, prompt_request)) do %>
<div class="card-actions justify-end">
<%= link "Delete", to: "#", phx_click: "delete", phx_value_id: prompt_request.id, data: [confirm: "Are you sure?"] %>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<div class="p-4 mx-auto grid place-items-center">
<span>Total results: <%= @page.total_entries %></span>
</div>
<div class="btn-group p-4 grid grid-cols-3 mx-auto pagination-btns">
<%= if @page.total_pages > 1 do %>
<%= if @page.page_number > 1 do %>
<%= link "<< Prev", to: "#", class: "btn btn-outline", phx_click: "go_to", phx_value_page: @page.page_number - 1 %>
<% else %>
<div class="btn btn-outline btn-disabled"></div>
<% end %>
<form phx-change="go_to">
<%= select :jump, :page, 1..@page.total_pages, class: "select w-full max-w-xs", selected: @page.page_number %>
</form>
<%= if @page.page_number < @page.total_pages do %>
<%= link "Next >>", to: "#", class: "btn btn-outline", phx_click: "go_to", phx_value_page: @page.page_number + 1 %>
<% end %>
<% end %>
</div>

View File

@@ -1,13 +1,11 @@
defmodule DiffuserWeb.PromptRequestLive.Show do
use DiffuserWeb, :live_view
import Ecto.Query
alias Diffuser.Generator
alias Diffuser.Generator.{Image, PromptRequest}
alias Diffuser.Generator.PromptRequest
alias Diffuser.Repo
alias Phoenix.Socket.Broadcast
alias DiffuserWeb.Endpoint
alias Ecto.Association.NotLoaded
@impl true
def mount(%{"id" => id}, _session, socket) do

View File

@@ -1,18 +1,5 @@
<h1>Show Prompt request</h1>
<%= if @live_action in [:edit] do %>
<.modal return_to={Routes.prompt_request_show_path(@socket, :show, @prompt_request)}>
<.live_component
module={DiffuserWeb.PromptRequestLive.FormComponent}
id={@prompt_request.id}
title={@page_title}
action={@live_action}
prompt_request={@prompt_request}
return_to={Routes.prompt_request_show_path(@socket, :show, @prompt_request)}
/>
</.modal>
<% end %>
<ul>
<li>
@@ -42,5 +29,4 @@
</li>
</ul>
<span><%= live_patch "Edit", to: Routes.prompt_request_show_path(@socket, :edit, @prompt_request), class: "button" %></span> |
<span><%= live_redirect "Back", to: Routes.prompt_request_index_path(@socket, :index) %></span>

View File

@@ -0,0 +1,67 @@
defmodule DiffuserWeb.Plugs.PublicIp do
@moduledoc "Get public IP address of request from x-forwarded-for header"
def init(opts), do: opts
def call(%{assigns: %{ip: _}} = conn, _opts), do: conn
def call(conn, _opts) do
process(conn, Plug.Conn.get_req_header(conn, "x-forwarded-for"))
end
def process(%{remote_ip: remote_ip} = conn, []) do
Plug.Conn.assign(conn, :ip, to_string(:inet_parse.ntoa(remote_ip)))
end
def process(conn, vals) do
# Rewrite standard remote_ip field with value from header
ip_address = get_ip_address(conn, vals)
# See https://hexdocs.pm/plug/Plug.Conn.html
conn = %{conn | remote_ip: ip_address}
Plug.Conn.assign(conn, :ip, to_string(:inet_parse.ntoa(ip_address)))
end
defp get_ip_address(conn, vals)
defp get_ip_address(%{remote_ip: remote_ip}, []), do: remote_ip
defp get_ip_address(%{remote_ip: remote_ip} = _conn, [val | _]) do
# Split into multiple values
comps =
val
|> String.split(~r{\s*,\s*}, trim: true)
# Get rid of "unknown" values
|> Enum.filter(&(&1 != "unknown"))
# Split IP from port, if any
|> Enum.map(&hd(String.split(&1, ":")))
# Filter out blanks
|> Enum.filter(&(&1 != ""))
# Parse address into :inet.ip_address tuple
|> Enum.map(&parse_address(&1))
# Elminate internal IP addreses, e.g. 192.168.1.1
|> Enum.filter(&is_public_ip(&1))
case comps do
[] -> remote_ip
[comp | _] -> comp
end
end
defp parse_address(ip) do
case :inet.parse_ipv4strict_address(to_charlist(ip)) do
{:ok, ip_address} -> ip_address
{:error, :einval} -> :einval
end
end
defp is_public_ip(ip_address) do
case ip_address do
{10, _, _, _} -> false
{192, 168, _, _} -> false
{172, second, _, _} when second >= 16 and second <= 31 -> false
{127, 0, 0, _} -> false
{_, _, _, _} -> true
:einval -> false
end
end
end

View File

@@ -0,0 +1,37 @@
defmodule DiffuserWeb.Plugs.UserAuth do
use DiffuserWeb, :controller
import Plug.Conn
alias Diffuser.Accounts
alias Diffuser.Repo
def init(default), do: default
def call(%{remote_ip: remote_ip} = conn, _) do
remote_ip = remote_ip |> :inet_parse.ntoa() |> to_string()
case get_session(conn, :user) do
nil ->
with {:ok, user} <- Accounts.create_user(%{ip_address: remote_ip}) do
put_session(conn, :user, user |> Repo.preload(votes: [:prompt_request]))
else
{:error, _changeset} -> put_session(conn, :user, nil)
end
%Accounts.User{ip_address: user_ip} = user ->
with false <- user_ip == remote_ip,
{:ok, user} <- Accounts.update_user(user, %{ip_address: remote_ip}) do
conn |> put_session(:user, user |> Repo.preload(votes: [:prompt_request]))
else
true ->
put_session(
conn,
:user,
Accounts.get_user!(user.id) |> Repo.preload(votes: [:prompt_request])
)
_ ->
put_session(conn, :user, nil)
end
end
end
end

View File

@@ -0,0 +1,16 @@
defmodule DiffuserWeb.Plugs.ValidateAdmin do
use DiffuserWeb, :controller
import Plug.Conn
def init(default), do: default
def call(%{remote_ip: remote_ip} = conn, _) do
remote_ip = remote_ip |> :inet_parse.ntoa() |> to_string()
if remote_ip == "127.0.0.1" do
conn |> put_session(:is_admin, true)
else
conn |> put_session(:is_admin, false)
end
end
end

View File

@@ -1,5 +1,6 @@
defmodule DiffuserWeb.Router do
use DiffuserWeb, :router
alias DiffuserWeb.Plugs.{PublicIp, UserAuth, ValidateAdmin}
pipeline :browser do
plug :accepts, ["html"]
@@ -14,17 +15,18 @@ defmodule DiffuserWeb.Router do
plug :accepts, ["json"]
end
pipeline :authenticate do
plug PublicIp
plug UserAuth
plug ValidateAdmin
end
scope "/", DiffuserWeb do
pipe_through :browser
pipe_through :authenticate
get "/", PageController, :index
live "/prompt_requests", PromptRequestLive.Index, :index
live "/prompt_requests/new", PromptRequestLive.Index, :new
live "/prompt_requests/:id/edit", PromptRequestLive.Index, :edit
live "/prompt_requests/:id", PromptRequestLive.Show, :show
live "/prompt_requests/:id/show/edit", PromptRequestLive.Show, :edit
live "/", PromptRequestLive.Index, :index
live "/new", PromptRequestLive.Index, :new
end
# Other scopes may use custom stacks.
@@ -44,6 +46,7 @@ defmodule DiffuserWeb.Router do
scope "/" do
pipe_through :browser
pipe_through :authenticate
live_dashboard "/dashboard", metrics: DiffuserWeb.Telemetry
end

View File

@@ -1,8 +1,4 @@
<main class="container">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>

View File

@@ -1,30 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="black">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<meta name="csrf-token" content={csrf_token_value()}>
<%= live_title_tag assigns[:page_title] || "Diffuser", suffix: " · Phoenix Framework" %>
<%= live_title_tag assigns[:page_title] || "Diffuser" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
</head>
<body>
<header>
<section class="container">
<nav>
<ul>
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
<% end %>
</ul>
</nav>
<a href="https://phoenixframework.org/" class="phx-logo">
<img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
</a>
</section>
</header>
<%= @inner_content %>
<footer class="footer footer-center p-10 bg-gray text-primary-content">
<div>
<p>
<span class="font-bold">Silent Silas</span><br>
<span class="italic">Disappointing my parents since 1993</span>
</p>
<p>Copyright Sucks © 2022 - No rights reserved</p>
</div>
<div>
<div class="grid grid-flow-col gap-4">
<a target="_blank" rel="noopener noreferrer" href="https://git.silentsilas.com/silentsilas/Diffuser">
<img src="/images/GitHub-Mark-Light-120px-plus.png" style="max-height: 64px;" />
</a>
</div>
</div>
</footer>
</body>
</html>