|
defmodule MedicodeWeb.Components do |
|
@moduledoc """ |
|
Functional UI components for the main transcription and coding view. |
|
""" |
|
|
|
use Phoenix.Component |
|
use MedicodeWeb, :verified_routes |
|
|
|
import MedicodeWeb.CoreComponents |
|
|
|
attr(:audio_upload, Phoenix.LiveView.UploadConfig, required: true) |
|
|
|
@doc """ |
|
Displays a form containing a button for listening to live audio and a file upload for recorded audio files. |
|
""" |
|
def upload_form(assigns) do |
|
~H""" |
|
<form |
|
id="audio-form" |
|
phx-submit="save" |
|
phx-change="validate" |
|
class="w-full flex flex-col items-center gap-[21px]" |
|
> |
|
<div class="w-full px-4 py-[19px] rounded-[10px] flex items-center bg-light-divider"> |
|
<button |
|
type="button" |
|
disabled |
|
phx-click="toggle_recording" |
|
title="Not available" |
|
class="cursor-not-allowed mr-2 h-full px-3 py-2.5 rounded-lg border border-emerald-300 bg-emerald-200" |
|
> |
|
<.icon name="hero-microphone" class="w-6 h-6" /> |
|
</button> |
|
|
|
<label |
|
for={@audio_upload.ref} |
|
class="cursor-pointer mr-4 px-[16.5px] py-[9.941667px] rounded-lg bg-[ |
|
> |
|
<img src={~p"/images/paperclip.svg"} width="15" height="30" /> |
|
</label> |
|
<.live_file_input |
|
class="flex-1 cursor-pointer file:hidden file:font-secondary file:text-sm file:rounded-full file:px-4 file:py-2 file:border-0 file:bg-brand file:hover:bg-brand-active file:text-white" |
|
upload={@audio_upload} |
|
/> |
|
<button name="submit-btn" title="Upload and process file"> |
|
<img src={~p"/images/upload.svg"} /> |
|
</button> |
|
</div> |
|
|
|
<p class="leading-normal text-type-black-secondary">Audio file can be .mp3</p> |
|
</form> |
|
""" |
|
end |
|
|
|
attr(:visible, :boolean, required: true) |
|
|
|
@doc """ |
|
A loading icon and message displayed while the audio is being processed. |
|
""" |
|
def loading_message(assigns) do |
|
~H""" |
|
<%= if @visible do %> |
|
<div class="flex gap-2 items-center p-2 rounded-md text-slate-800 text-sm bg-slate-200 border border-slate-300"> |
|
<.icon name="hero-arrow-path" class="w-4 h-4 animate-spin" /> |
|
<p>Transcribing and tagging audio file...</p> |
|
</div> |
|
<% end %> |
|
""" |
|
end |
|
|
|
attr(:id, :string, required: true) |
|
attr(:target, :any, required: true) |
|
attr(:transcription, Medicode.Transcriptions.Transcription, required: true) |
|
attr(:summary_keywords, :list, default: []) |
|
attr(:finalized_codes, :list, default: []) |
|
attr(:timezone, :string, default: "Etc/UTC") |
|
|
|
@doc """ |
|
Shows the status and keywords for the current session. |
|
""" |
|
def result_heading(assigns) do |
|
assigns = |
|
assign_new(assigns, :transcript_inserted_at, fn _ -> |
|
assigns.transcription.inserted_at |
|
|> DateTime.shift_zone!(assigns.timezone) |
|
|> Calendar.strftime("%a, %b %d, %Y, %I:%M %p") |
|
end) |
|
|
|
~H""" |
|
<div class="flex justify-between"> |
|
<div class="flex items-center"> |
|
<img |
|
:if={@transcription.status == :waiting} |
|
src={~p"/images/loading.svg"} |
|
width="36" |
|
class="mr-6 animate-spin" |
|
/> |
|
<img |
|
:if={@transcription.status == :finished} |
|
src={~p"/images/checkmark.svg"} |
|
width="46" |
|
class="mr-[17px]" |
|
/> |
|
|
|
<div class="px-[14px] py-3 flex items-center gap-3 bg-brand rounded-lg text-white mr-4 overflow-hidden"> |
|
<img src={~p"/images/document.svg"} width="20" /> |
|
<p |
|
id={@id} |
|
contenteditable |
|
phx-hook="ContentEditor" |
|
phx-event-name="rename_transcription" |
|
role="textbox" |
|
class="font-secondary" |
|
> |
|
<%= @transcription.filename %> |
|
</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="border-b border-[#444444]/20"> |
|
<div class="px-4 py-2 flex items-center gap-2"> |
|
<img src={~p"/images/calendar.svg"} width="16" /> |
|
|
|
<span class="text-sm leading-normal font-bold text-type-black-tertiary uppercase"> |
|
<%= @transcript_inserted_at %> |
|
</span> |
|
</div> |
|
<div class="px-4 pt-2 pb-10 flex flex-col gap-2"> |
|
<p class="leading-normal font-bold text-type-black-primary uppercase">Summary Keywords</p> |
|
|
|
<div |
|
class="flex flex-row items-center divide-x divide-black/15 text-sm leading-normal text-type-black-tertiary" |
|
id="keyword_list" |
|
> |
|
<!-- coronary, artery, disease, unstable, angina, admitted --> |
|
<%= for keyword <- format_keywords(@summary_keywords) do %> |
|
<span class="px-2" title={keyword.score}><%= keyword.keyword %></span> |
|
<% end %> |
|
</div> |
|
</div> |
|
<div :if={!Enum.empty?(@finalized_codes)} class="px-4 pt-2 pb-10 flex flex-col gap-2"> |
|
<p class="leading-normal font-bold text-type-black-primary uppercase">Finalized Codes</p> |
|
|
|
<div |
|
class="flex flex-row items-center divide-x divide-black/15 text-sm leading-normal text-type-black-tertiary" |
|
id="finalized_vector_code_list" |
|
> |
|
<span :for={code_vector <- @finalized_codes} class="px-2" title={code_vector.code}> |
|
<%= code_vector.code %> |
|
</span> |
|
</div> |
|
|
|
<.link |
|
href={~p"/transcription/reports/#{@transcription.id}"} |
|
class="font-semibold text-brand hover:underline" |
|
download={"transcription-report-#{@transcription.id}.pdf"} |
|
> |
|
Download Report |
|
</.link> |
|
</div> |
|
</div> |
|
""" |
|
end |
|
|
|
# Formats keywords for display in the result_heading component |
|
defp format_keywords(keyword_predictions) do |
|
keyword_predictions |
|
|> List.flatten() |
|
|> Enum.sort_by(& &1.score, :desc) |
|
|> Enum.take(7) |
|
end |
|
|
|
attr(:code, :string, required: true) |
|
attr(:label, :string, required: true) |
|
|
|
@doc """ |
|
Displays a single code and its description. |
|
""" |
|
def code_display(assigns) do |
|
~H""" |
|
<div class="py-4 text-sm flex flex-col gap-1 font-secondary text-type-black-primary rounded"> |
|
<p class="text-lg font-bold leading-[22.97px]"><%= @code %></p> |
|
<p class="text-base leading-[20.42px]"><%= @label %></p> |
|
</div> |
|
""" |
|
end |
|
end |
|
|