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 +2 -0
- lib/medical_transcription_web/components/components.ex +60 -10
- lib/medical_transcription_web/components/layouts/app.html.heex +3 -3
- lib/medical_transcription_web/live/home_live/index.ex +42 -14
- lib/medical_transcription_web/live/home_live/sample_results.ex +20 -4
- priv/static/images/calendar.svg +5 -0
- priv/static/images/checkmark.svg +3 -0
- priv/static/images/document.svg +3 -0
- priv/static/images/loading.svg +3 -0
- priv/static/images/paperclip.svg +3 -0
- priv/static/images/upload.svg +4 -0
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
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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">→</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 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(:
|
56 |
|> assign(:transcription_rows, [])
|
57 |
-
|>
|
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, :
|
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{
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|