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