|
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-[ |
|
|
|
<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 |
|
|