|
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: []) |
|
|
|
@doc """ |
|
Shows the status and keywords for the current session. |
|
""" |
|
def result_heading(assigns) do |
|
~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]" |
|
/> |
|
|
|
<.record_button_in_heading status={@transcription.status} /> |
|
|
|
<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"> |
|
<%= Calendar.strftime(DateTime.now!("Etc/UTC"), "%a, %b %d, %Y, %I:%M %p") %> |
|
</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> |
|
""" |
|
end |
|
|
|
# Renders the record button within the result_heading component |
|
defp record_button_in_heading(assigns) when assigns.status == :streaming_audio do |
|
~H""" |
|
<button |
|
disabled |
|
phx-click="toggle_recording" |
|
title="Not available" |
|
class="cursor-not-allowed mr-6 px-4 py-3 bg-red-200 rounded-lg" |
|
> |
|
<.icon name="hero-microphone" class="animate-pulse" /> |
|
</button> |
|
""" |
|
end |
|
|
|
defp record_button_in_heading(assigns) do |
|
~H""" |
|
<button |
|
disabled |
|
phx-click="toggle_recording" |
|
title="Not available" |
|
class="cursor-not-allowed mr-6 px-4 py-3 bg-emerald-200 rounded-lg" |
|
> |
|
<.icon name="hero-microphone" /> |
|
</button> |
|
""" |
|
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 |
|
|