noahsettersten commited on
Commit
6fe951d
1 Parent(s): c6ea04f

feat: Additional UI elements

Browse files

- Style upload form
- Add header to results
- Set up different status states (:pending, :loading, :success)

assets/tailwind.config.js CHANGED
@@ -18,8 +18,10 @@ module.exports = {
18
  brand: "#0A8390",
19
  "brand-active": "#93BC6B",
20
  "type-black-primary": "#202020",
 
21
  "type-black-tertiary": "#202020B2",
22
  "button-deactivated-background": "#010101",
 
23
  },
24
  fontFamily: {
25
  sans: ["Poppins", ...defaultTheme.fontFamily.sans],
 
18
  brand: "#0A8390",
19
  "brand-active": "#93BC6B",
20
  "type-black-primary": "#202020",
21
+ "type-black-secondary": "#202020CC",
22
  "type-black-tertiary": "#202020B2",
23
  "button-deactivated-background": "#010101",
24
+ "light-divider": "#EBEBEB",
25
  },
26
  fontFamily: {
27
  sans: ["Poppins", ...defaultTheme.fontFamily.sans],
lib/medical_transcription_web/components/components.ex CHANGED
@@ -6,19 +6,29 @@ defmodule MedicalTranscriptionWeb.Components do
6
 
7
  def upload_form(assigns) do
8
  ~H"""
9
- <form id="audio-form" phx-submit="save" phx-change="validate">
10
- <div class="flex items-center pb-4 border-b border-[#444444]/20">
 
 
 
 
 
 
 
 
 
 
 
11
  <.live_file_input
12
- class="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"
13
  upload={@audio_upload}
14
  />
15
- <button
16
- name="submit-btn"
17
- class="rounded-full bg-brand hover:bg-brand-active text-white py-2 px-4 font-light text-black"
18
- >
19
- Submit <span aria-hidden="true">&rarr;</span>
20
  </button>
21
  </div>
 
 
22
  </form>
23
  """
24
  end
@@ -34,6 +44,46 @@ defmodule MedicalTranscriptionWeb.Components do
34
  """
35
  end
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def result_list(assigns) do
38
  ~H"""
39
  <div
@@ -92,9 +142,9 @@ defmodule MedicalTranscriptionWeb.Components do
92
  class="p-2 border border-button-deactivated-background rounded-lg hover:bg-slate-200"
93
  >
94
  <%= if @response == "true" do %>
95
- <img src={~p"/images/thumbs-up.svg"} width="20" height="20" />
96
  <% else %>
97
- <img src={~p"/images/thumbs-down.svg"} width="20" height="20" />
98
  <% end %>
99
  </button>
100
  """
 
6
 
7
  def upload_form(assigns) do
8
  ~H"""
9
+ <form
10
+ id="audio-form"
11
+ phx-submit="save"
12
+ phx-change="validate"
13
+ class="w-full flex flex-col items-center gap-[21px]"
14
+ >
15
+ <div class="w-full px-4 py-[19px] rounded-[10px] flex items-center bg-light-divider">
16
+ <label
17
+ for={@audio_upload.ref}
18
+ class="mr-4 px-[16.5px] py-[9.941667px] rounded-lg bg-[#444444]/10"
19
+ >
20
+ <img src={~p"/images/paperclip.svg"} width="15" height="30" />
21
+ </label>
22
  <.live_file_input
23
+ class="flex-1 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"
24
  upload={@audio_upload}
25
  />
26
+ <button name="submit-btn" title="Upload and process file">
27
+ <img src={~p"/images/upload.svg"} />
 
 
 
28
  </button>
29
  </div>
30
+
31
+ <p class="leading-normal text-type-black-secondary">Audio file can be .mp3</p>
32
  </form>
33
  """
34
  end
 
44
  """
45
  end
46
 
47
+ def result_heading(assigns) do
48
+ # TODO: Generate the summary keywords from the audio transcription
49
+
50
+ ~H"""
51
+ <div class="flex justify-between">
52
+ <div class="flex items-center">
53
+ <%= if @status == :loading do %>
54
+ <img src={~p"/images/loading.svg"} width="36" class="mr-6 animate-spin" />
55
+ <% end %>
56
+ <%= if @status == :success do %>
57
+ <img src={~p"/images/checkmark.svg"} width="46" class="mr-[17px]" />
58
+ <% end %>
59
+ <div class="px-[14px] py-3 flex items-center gap-3 bg-brand rounded-lg text-white">
60
+ <img src={~p"/images/document.svg"} width="20" />
61
+ <span class="font-secondary"><%= @filename %></span>
62
+ </div>
63
+ </div>
64
+ <button phx-click="reset_to_pending" type="button" class="p-3 rounded-lg bg-[#444444]/10">
65
+ <.icon name="hero-x-circle" class="w-6 h-6" />
66
+ </button>
67
+ </div>
68
+
69
+ <div class="border-b border-[#444444]/20">
70
+ <div class="px-4 py-2 flex items-center gap-2">
71
+ <img src={~p"/images/calendar.svg"} width="16" />
72
+
73
+ <span class="text-sm leading-normal font-bold text-type-black-tertiary uppercase">
74
+ <%= Calendar.strftime(DateTime.now!("Etc/UTC"), "%a, %b %d, %Y, %I:%M %p") %>
75
+ </span>
76
+ </div>
77
+ <div class="px-4 pt-2 pb-10 flex flex-col gap-2">
78
+ <p class="leading-normal font-bold text-type-black-primary uppercase">Summary Keywords</p>
79
+ <p class="text-sm leading-normal text-type-black-tertiary">
80
+ coronary, artery, disease, unstable, angina, admitted
81
+ </p>
82
+ </div>
83
+ </div>
84
+ """
85
+ end
86
+
87
  def result_list(assigns) do
88
  ~H"""
89
  <div
 
142
  class="p-2 border border-button-deactivated-background rounded-lg hover:bg-slate-200"
143
  >
144
  <%= if @response == "true" do %>
145
+ <img src={~p"/images/thumbs-up.svg"} width="20" height="20" class="w-5 h-5 min-w-[20px]" />
146
  <% else %>
147
+ <img src={~p"/images/thumbs-down.svg"} width="20" height="20" class="w-5 h-5 min-w-[20px]" />
148
  <% end %>
149
  </button>
150
  """
lib/medical_transcription_web/components/layouts/app.html.heex CHANGED
@@ -1,4 +1,4 @@
1
- <div class="flex gap-5">
2
  <header class="w-[335px] min-w-[335px] pt-6 border-r border-gray-200 flex flex-col gap-12">
3
  <div class="flex items-center gap-[10.5px]">
4
  <a href="/">
@@ -14,8 +14,8 @@
14
  </div>
15
  </header>
16
 
17
- <main class="pl-16 pr-16 pt-[25px]">
18
- <div class="mx-auto max-w-5xl">
19
  <.flash_group flash={@flash} />
20
  <%= @inner_content %>
21
  </div>
 
1
+ <div class="min-h-[calc(100vh-56px)] flex gap-5">
2
  <header class="w-[335px] min-w-[335px] pt-6 border-r border-gray-200 flex flex-col gap-12">
3
  <div class="flex items-center gap-[10.5px]">
4
  <a href="/">
 
14
  </div>
15
  </header>
16
 
17
+ <main class="flex-1 pl-16 pr-16 pt-[25px]">
18
+ <div class="flex flex-col h-full mx-auto max-w-5xl">
19
  <.flash_group flash={@flash} />
20
  <%= @inner_content %>
21
  </div>
lib/medical_transcription_web/live/home_live/index.ex CHANGED
@@ -4,13 +4,19 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
4
 
5
  @impl Phoenix.LiveView
6
  def mount(_params, _session, socket) do
7
- {:ok,
8
- socket
9
- |> assign(:uploaded_files, [])
10
- # |> stream(:transcription_rows, [])
11
- |> stream(:transcription_rows, SampleResults.get_sample_results())
12
- |> assign(:transcription_in_progress, false)
13
- |> allow_upload(:audio, accept: ~w(.mp3), max_entries: 1)}
 
 
 
 
 
 
14
  end
15
 
16
  @impl Phoenix.LiveView
@@ -23,10 +29,22 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
23
  # - Stream audio recording instead of uploaded audio.
24
 
25
  ~H"""
26
- <div class="flex flex-col space-y-6">
27
- <.upload_form audio_upload={@uploads.audio} />
28
- <.loading_message visible={@transcription_in_progress} />
29
- <.result_list rows={@streams.transcription_rows} />
 
 
 
 
 
 
 
 
 
 
 
 
30
  </div>
31
  """
32
  end
@@ -38,6 +56,11 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
38
 
39
  @impl true
40
  def handle_event("save", _params, socket) do
 
 
 
 
 
41
  uploaded_files =
42
  consume_uploaded_entries(socket, :audio, fn %{path: path}, _entry ->
43
  dest = Path.join("priv/static/uploads", Path.basename(path))
@@ -52,9 +75,9 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
52
 
53
  socket =
54
  socket
55
- |> assign(:transcription_in_progress, true)
56
  |> assign(:transcription_rows, [])
57
- |> update(:uploaded_files, &(&1 ++ uploaded_files))
58
 
59
  {:noreply, socket}
60
  end
@@ -77,6 +100,11 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
77
  {:noreply, socket}
78
  end
79
 
 
 
 
 
 
80
  @impl true
81
  def handle_info({:transcription_row, chunk_result}, socket) do
82
  # The processing sends a message as each chunk of text is coded. See here for some background and potential
@@ -89,7 +117,7 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
89
  # See this Fly article for the usage of Task.async to start `transcribe_and_tag_audio/2` and handle the end of the
90
  # task here: https://fly.io/phoenix-files/liveview-async-task/
91
  Process.demonitor(ref, [:flush])
92
- {:noreply, assign(socket, :transcription_in_progress, false)}
93
  end
94
 
95
  defp transcribe_and_tag_audio(live_view_pid, audio_file_path) do
 
4
 
5
  @impl Phoenix.LiveView
6
  def mount(_params, _session, socket) do
7
+ # We're storing atoms in `:status` as LiveView's AsyncResult doesn't support modeling a "pending" state, but only
8
+ # loading / ok / failed.
9
+ socket =
10
+ socket
11
+ |> assign(:uploaded_file_name, nil)
12
+ |> stream(:transcription_rows, [])
13
+ |> assign(:status, :pending)
14
+ # To test the success UI, replace the two lines above with these:
15
+ # |> stream(:transcription_rows, SampleResults.get_sample_results())
16
+ # |> assign(:status, :success)
17
+ |> allow_upload(:audio, accept: ~w(.mp3), max_entries: 1)
18
+
19
+ {:ok, socket}
20
  end
21
 
22
  @impl Phoenix.LiveView
 
29
  # - Stream audio recording instead of uploaded audio.
30
 
31
  ~H"""
32
+ <div class="flex-1 flex flex-col space-y-6">
33
+ <%= if @status == :loading || @status == :success do %>
34
+ <.result_heading status={@status} filename={@uploaded_file_name} />
35
+ <% end %>
36
+
37
+ <%= if @status != :pending do %>
38
+ <.result_list rows={@streams.transcription_rows} />
39
+ <% end %>
40
+
41
+ <%= if @status == :pending do %>
42
+ <div class="flex-1 flex flex-col justify-center items-center gap-4">
43
+ <img src={~p"/images/logo.svg"} width="106" />
44
+ <p class="text-2xl leading-normal font-semibold">Medical Code Transcriber</p>
45
+ </div>
46
+ <.upload_form audio_upload={@uploads.audio} />
47
+ <% end %>
48
  </div>
49
  """
50
  end
 
56
 
57
  @impl true
58
  def handle_event("save", _params, socket) do
59
+ filename =
60
+ socket.assigns.uploads.audio.entries
61
+ |> Enum.at(0)
62
+ |> Map.get(:client_name)
63
+
64
  uploaded_files =
65
  consume_uploaded_entries(socket, :audio, fn %{path: path}, _entry ->
66
  dest = Path.join("priv/static/uploads", Path.basename(path))
 
75
 
76
  socket =
77
  socket
78
+ |> assign(:status, :loading)
79
  |> assign(:transcription_rows, [])
80
+ |> assign(:uploaded_file_name, filename)
81
 
82
  {:noreply, socket}
83
  end
 
100
  {:noreply, socket}
101
  end
102
 
103
+ @impl true
104
+ def handle_event("reset_to_pending", _params, socket) do
105
+ {:noreply, assign(socket, :status, :pending)}
106
+ end
107
+
108
  @impl true
109
  def handle_info({:transcription_row, chunk_result}, socket) do
110
  # The processing sends a message as each chunk of text is coded. See here for some background and potential
 
117
  # See this Fly article for the usage of Task.async to start `transcribe_and_tag_audio/2` and handle the end of the
118
  # task here: https://fly.io/phoenix-files/liveview-async-task/
119
  Process.demonitor(ref, [:flush])
120
+ {:noreply, assign(socket, :status, :success)}
121
  end
122
 
123
  defp transcribe_and_tag_audio(live_view_pid, audio_file_path) do
lib/medical_transcription_web/live/home_live/sample_results.ex CHANGED
@@ -15,10 +15,26 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index.SampleResults do
15
  label: "Coronary artery anomaly",
16
  score: 0.892520010471344
17
  },
18
- %TagResult{code: "412", label: "Old myocardial infarct", score: 0.8386646509170532},
19
- %TagResult{code: "4111", label: "Intermed coronary synd", score: 0.8203237056732178},
20
- %TagResult{code: "4475", label: "Necrosis of artery", score: 0.8200777173042297},
21
- %TagResult{code: "E9424", label: "Adv eff coronary vasodil", score: 0.8076164722442627}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  ]
23
  },
24
  %{
 
15
  label: "Coronary artery anomaly",
16
  score: 0.892520010471344
17
  },
18
+ %TagResult{
19
+ code: "41412",
20
+ label: "Dissection of coronary artery",
21
+ score: 0.8386646509170532
22
+ },
23
+ %TagResult{
24
+ code: "V717",
25
+ label: "Observation for suspected cardiovascular disease",
26
+ score: 0.8203237056732178
27
+ },
28
+ %TagResult{
29
+ code: "41400",
30
+ label: "Coronary atherosclerosis of unspecified type of vessel, native or graft",
31
+ score: 0.8200777173042297
32
+ },
33
+ %TagResult{
34
+ code: "V812",
35
+ label: "Screening for other and unspecified cardiovascular conditions",
36
+ score: 0.8076164722442627
37
+ }
38
  ]
39
  },
40
  %{
priv/static/images/calendar.svg ADDED
priv/static/images/checkmark.svg ADDED
priv/static/images/document.svg ADDED
priv/static/images/loading.svg ADDED
priv/static/images/paperclip.svg ADDED
priv/static/images/upload.svg ADDED