medicode / lib /medicode_web /components /transcription_chunk_codings_component.ex
timgremore's picture
feat: Move vector coding state to codings component
5a90f65
defmodule MedicodeWeb.Components.TranscriptionTextCodingsComponent do
@moduledoc """
Represents a portion of transcribed text.
"""
use Phoenix.Component
use MedicodeWeb, :live_component
use MedicodeWeb, :verified_routes
import Ecto.Query
alias Medicode.Transcriptions.TranscriptionChunkCodeVector
alias Medicode.Utilities
alias Medicode.Repo
@impl Phoenix.LiveComponent
def update_many(assigns_sockets) do
list_of_ids = Enum.map(assigns_sockets, fn {assigns, _sockets} -> assigns.chunk_id end)
current_user =
assigns_sockets
|> Enum.at(0)
|> Tuple.to_list()
|> Enum.at(0)
|> Map.get(:current_user)
code_feedbacks =
from(
f in Medicode.Feedback.CodeFeedback,
right_join: c in assoc(f, :transcription_chunk_code_vectors),
where: f.user_id == ^current_user.id and f.transcription_chunk_id in ^list_of_ids,
order_by: [desc: c.cosine_similarity, asc: c.id],
select: [:code_vector_id, :response, :transcription_chunk_id]
)
|> Repo.all()
transcription_chunk_code_vectors =
TranscriptionChunkCodeVector
|> join(:inner, [c], v in assoc(c, :code_vector))
|> where([c], c.transcription_chunk_id in ^list_of_ids)
|> order_by([c], desc: c.cosine_similarity, asc: c.id)
|> preload([:code_vector, :assigned_by_user])
|> Repo.all()
Enum.map(assigns_sockets, fn {assigns, socket} ->
chunk_code_vectors_with_feedback =
transcription_chunk_code_vectors
|> Enum.filter(&(&1.transcription_chunk_id == assigns.chunk_id))
|> Enum.reduce([], fn item, acc ->
transcription_chunk_code_feedback =
Enum.find(code_feedbacks, fn code_feedback ->
code_feedback.transcription_chunk_id == item.transcription_chunk_id &&
code_feedback.code_vector_id == item.code_vector_id
end)
acc ++
[
%{
transcription_chunk_code_vector: item,
code_feedback: transcription_chunk_code_feedback
}
]
end)
socket
|> assign(:id, "#{assigns.id}-codings")
|> assign(:current_user, assigns.current_user)
|> assign(:chunk_code_vectors_with_feedback, chunk_code_vectors_with_feedback)
|> assign(:chunk_id, assigns.chunk_id)
|> assign(:text, assigns.text)
|> assign(:on_feedback, assigns.on_feedback)
|> assign(:on_remove_code, assigns.on_remove_code)
|> assign(:on_finalize_code, assigns.on_finalize_code)
end)
end
@impl Phoenix.LiveComponent
def render(assigns) do
~H"""
<div id={@id}>
<p :if={is_nil(@chunk_code_vectors_with_feedback)}>No code vectors</p>
<.tag_result
:for={
%{
transcription_chunk_code_vector: transcription_chunk_code_vector,
code_feedback: code_feedback
} <- @chunk_code_vectors_with_feedback
}
transcription_chunk_code_vector={transcription_chunk_code_vector}
chunk_id={@chunk_id}
code_vector={transcription_chunk_code_vector.code_vector}
score={1.0}
text={@text}
weighting={[1.0]}
current_user={@current_user}
myself={@myself}
code_feedback={code_feedback}
assigned_by_user={transcription_chunk_code_vector.assigned_by_user}
/>
</div>
"""
end
def tag_result(assigns) do
~H"""
<div class="group ease-out duration-300 transition-all flex items-center gap-4 px-[14px] py-3 text-sm">
<div class="flex gap-3">
<.feedback_button
chunk_id={@chunk_id}
code_feedback={@code_feedback}
code_vector={@code_vector}
text={@text}
response="true"
myself={@myself}
/>
<.feedback_button
chunk_id={@chunk_id}
code_feedback={@code_feedback}
code_vector={@code_vector}
text={@text}
response="false"
myself={@myself}
/>
</div>
<div class="w-1 h-full border-l border-[#444444]/20"></div>
<div
id={"chunk-#{@chunk_id}-coding-#{@code_vector.id}"}
class={"w-full flex flex-col gap-1 font-secondary text-type-black-primary ml-4 px-2 py-1 rounded #{code_color(@weighting)}"}
title={code_title(@score, @weighting)}
>
<div class="flex flex-row justify-between">
<p class="text-lg font-bold leading-[22.97px]"><%= @code_vector.code %></p>
<button
:if={is_nil(@transcription_chunk_code_vector.finalized_at)}
type="button"
class="transition-all duration-300 opacity-0 group-hover:opacity-100 hover:bg-slate-200 px-2 border border-1 border-iron-mountain rounded-md"
phx-click="finalize_code"
phx-value-chunk_id={@chunk_id}
phx-value-code_vector_id={@code_vector.id}
phx-target={@myself}
>
Finalize
</button>
<button
:if={!is_nil(@transcription_chunk_code_vector.finalized_at)}
type="button"
class="transition-all duration-300 opacity-0 group-hover:opacity-100 hover:bg-slate-200 px-2 border border-1 border-iron-mountain rounded-md"
phx-click="finalize_code"
phx-value-chunk_id={@chunk_id}
phx-value-code_vector_id={@code_vector.id}
phx-target={@myself}
title="Clear"
>
<.icon name="hero-check-badge" class="opacity-40 group-hover:opacity-70" />
</button>
</div>
<p class="text-base leading-[20.42px]"><%= @code_vector.description %></p>
<div class="flex flex-row items-center w-full gap-4">
<p :if={!is_nil(@assigned_by_user)} class="text-xs text-base leading-[20.42px]">
Assigned by: <%= @assigned_by_user.email %>
</p>
<button
:if={
!is_nil(@assigned_by_user) && String.equivalent?(@assigned_by_user.id, @current_user.id)
}
phx-target={@myself}
phx-click="remove_code"
phx-value-code_vector_id={@code_vector.id}
phx-value-chunk_id={@chunk_id}
type="button"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="opacity-40 group-hover:opacity-70" />
</button>
</div>
</div>
</div>
"""
end
# Supporting function for displaying a button to provide feedback in `tag_result/1`
defp feedback_button(assigns) do
~H"""
<button
data-feedback={data_feedback_for_code_vector(@code_feedback, @response)}
class="border-1 border-iron-mountain data-[feedback=positive]:bg-accepted-primary data-[feedback=positive]:border-accepted-secondary data-[feedback=negative]:bg-rejected-primary data-[feedback=negative]:border-rejected-secondary p-2 border border-button-deactivated-background rounded-lg hover:bg-slate-200"
phx-click="toggle_feedback"
phx-value-chunk_id={@chunk_id}
phx-value-code_vector_id={@code_vector.id}
phx-value-text={@text}
phx-value-response={@response}
phx-target={@myself}
type="button"
>
<img
:if={@response == "true"}
src={~p"/images/thumbs-up.svg"}
width="20"
height="20"
class="w-5 h-5 min-w-[20px]"
/>
<img
:if={@response == "false"}
src={~p"/images/thumbs-down.svg"}
width="20"
height="20"
class="w-5 h-5 min-w-[20px]"
/>
</button>
"""
end
@impl Phoenix.LiveComponent
def handle_event("toggle_feedback", params, socket) do
socket.assigns.on_feedback.(params)
{:noreply, socket}
end
def handle_event("remove_code", params, socket) do
socket.assigns.on_remove_code.(params)
{:noreply, socket}
end
def handle_event("finalize_code", params, socket) do
socket.assigns.on_finalize_code.(params)
{:noreply, socket}
end
# Supporting function for determining background color for code in `tag_result/1`
defp code_color(weighting) do
cond do
:positive in weighting and :negative in weighting -> "bg-orange-200/40"
:positive in weighting -> "bg-emerald-200/40"
:negative in weighting -> "bg-red-200/40"
true -> ""
end
end
# Supporting function for building title attribute for code in `tag_result/1`
defp code_title(score, weighting) do
weighting_description =
if length(weighting) > 0 do
weighting
|> Utilities.tally()
|> Utilities.tally_to_string()
|> String.replace_prefix("", "Weighting: ")
else
""
end
"Similarity score: #{trunc(score * 100) / 100}. #{weighting_description}"
end
defp data_feedback_for_code_vector(
%Medicode.Feedback.CodeFeedback{response: true},
"true" = _response
),
do: :positive
defp data_feedback_for_code_vector(
%Medicode.Feedback.CodeFeedback{response: false},
"false" = _response
),
do: :negative
defp data_feedback_for_code_vector(_, _), do: nil
end