diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 038861c..ba370fc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,7 @@ name: CI
on:
pull_request:
push:
- branches: [ main ]
+ branches: [main]
jobs:
scan_ruby:
@@ -89,6 +89,13 @@ jobs:
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432
+ DB_NAME: medispeak
+ DB_NAME_TEST: medispeak_test
+ DB_HOST: localhost
+ DB_USERNAME: postgres
+ DB_PASSWORD: postgres
+ DB_PORT: 5432
+
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:test:prepare test test:system
diff --git a/Gemfile b/Gemfile
index b3d6ff5..565bf3b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -37,7 +37,7 @@ gem "devise", "~> 4.9"
# Tailwind CSS
gem "tailwindcss-rails", "~> 2.7"
# CORS for API
-gem 'rack-cors', "~> 2.0.1"
+gem "rack-cors", "~> 2.0.1"
diff --git a/app/dashboards/form_field_dashboard.rb b/app/dashboards/form_field_dashboard.rb
index bf992e8..4457d5e 100644
--- a/app/dashboards/form_field_dashboard.rb
+++ b/app/dashboards/form_field_dashboard.rb
@@ -10,7 +10,16 @@ class FormFieldDashboard < Administrate::BaseDashboard
ATTRIBUTE_TYPES = {
id: Field::Number,
description: Field::String,
+ friendly_name: Field::String,
+ field_type: Field::Select.with_options(
+ collection: FormField.field_types.keys.map { |t| [ t.humanize, t ] }
+ ),
metadata: Field::String.with_options(searchable: false),
+ minimum: Field::String,
+ maximum: Field::String,
+ enum_options: Field::StringArray.with_options(
+ hint: "Enter options for single/multi select fields, one per line"
+ ),
page: Field::BelongsTo,
title: Field::String,
created_at: Field::DateTime,
@@ -25,6 +34,8 @@ class FormFieldDashboard < Administrate::BaseDashboard
COLLECTION_ATTRIBUTES = %i[
id
title
+ friendly_name
+ field_type
description
page
].freeze
@@ -34,9 +45,14 @@ class FormFieldDashboard < Administrate::BaseDashboard
SHOW_PAGE_ATTRIBUTES = %i[
id
title
+ friendly_name
+ field_type
description
page
metadata
+ minimum
+ maximum
+ enum_options
created_at
updated_at
].freeze
@@ -45,10 +61,15 @@ class FormFieldDashboard < Administrate::BaseDashboard
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
+ page
+ friendly_name
title
description
- page
metadata
+ field_type
+ minimum
+ maximum
+ enum_options
].freeze
# COLLECTION_FILTERS
diff --git a/app/helpers/openai_helper.rb b/app/helpers/openai_helper.rb
index c3ab900..6cb6ce0 100644
--- a/app/helpers/openai_helper.rb
+++ b/app/helpers/openai_helper.rb
@@ -14,7 +14,7 @@ def ai_transcribe(file)
def system_prompt(transcription)
return transcription.page.prompt if transcription.page.prompt.present?
- "You are an AI assitant filling the form for an user. Make sure that you do not populate the form with any data that the user did not provide. Ensure all data shared by users are correctly split into function arguments."
+ "You are an AI assistant filling the form for a user. Make sure that you do not populate the form with any data that the user did not provide. Ensure all data shared by users are correctly split into function arguments."
end
def ai_generate_completion(transcription)
@@ -71,11 +71,10 @@ def ai_generate_completion(transcription)
end
def smart_description(form_field, context)
- if context[form_field.title]
- "#{form_field.description}; Context: #{context[form_field.title]}"
- else
- form_field.description
- end
+ base_description = form_field.description.presence || form_field.friendly_name
+ context_info = context[form_field.title] if context.present?
+
+ [ base_description, context_info ].compact.join("; Context: ")
end
def usage_tokens(transcription, usage)
@@ -89,10 +88,9 @@ def usage_tokens(transcription, usage)
def create_types_form_form_fields(form_fields, context)
fields = {}
form_fields.each do |form_field|
- fields[form_field.title] = {
- type: :string,
+ fields[form_field.title] = form_field.to_json_schema_for_ai.merge(
description: smart_description(form_field, context)
- }
+ )
end
fields
end
diff --git a/app/models/form_field.rb b/app/models/form_field.rb
index dff2002..7f1f7cc 100644
--- a/app/models/form_field.rb
+++ b/app/models/form_field.rb
@@ -1,3 +1,84 @@
class FormField < ApplicationRecord
belongs_to :page
+
+ # Using string enum for better readability
+ enum field_type: {
+ string: "string", # Text input
+ number: "number", # Numeric input
+ boolean: "boolean", # Yes/No values
+ single_select: "single_select", # Single choice from options
+ multi_select: "multi_select" # Multiple choices from options
+ }
+
+ validates :title, presence: true
+ validates :friendly_name, presence: true
+ validates :field_type, presence: true
+ validate :validate_select_options
+ validate :validate_number_constraints
+
+ # Virtual attribute for enum options
+ attr_writer :enum_options_raw
+
+ def enum_options_raw
+ @enum_options_raw || enum_options&.join("\n")
+ end
+
+ before_validation :process_enum_options
+
+ def to_json_schema_for_ai
+ schema = {
+ type: json_schema_type,
+ description: description
+ }
+
+ case field_type
+ when "single_select", "multi_select"
+ schema[:enum] = enum_options if enum_options.present?
+ when "number"
+ schema[:minimum] = minimum if minimum.present?
+ schema[:maximum] = maximum if maximum.present?
+ end
+
+ schema.compact
+ end
+
+ private
+
+ def json_schema_type
+ case field_type
+ when "single_select"
+ "string"
+ when "multi_select"
+ "array"
+ else
+ field_type
+ end
+ end
+
+ def validate_select_options
+ return unless [ "single_select", "multi_select" ].include?(field_type)
+
+ if enum_options.blank? || enum_options.any?(&:blank?)
+ errors.add(:enum_options, "must have at least one non-empty option for select fields")
+ end
+ end
+
+ def validate_number_constraints
+ return unless field_type == "number"
+
+ if minimum.present? && maximum.present? && maximum.to_f < minimum.to_f
+ errors.add(:maximum, "must be greater than minimum")
+ end
+ end
+
+ def process_enum_options
+ return unless @enum_options_raw.present?
+
+ # Split by newlines, remove empty lines and whitespace
+ self.enum_options = @enum_options_raw
+ .split("\n")
+ .map(&:strip)
+ .reject(&:blank?)
+ .uniq
+ end
end
diff --git a/app/views/fields/string_array/_form.html.erb b/app/views/fields/string_array/_form.html.erb
new file mode 100644
index 0000000..d68bebf
--- /dev/null
+++ b/app/views/fields/string_array/_form.html.erb
@@ -0,0 +1,15 @@
+
+ <%= f.label field.attribute %>
+
+
+ <%= f.text_area "#{field.attribute}_raw",
+ value: field.data&.join("\n"),
+ class: "string-array-input",
+ rows: 5,
+ placeholder: "Enter one option per line" %>
+ <% if field.options.key?(:hint) %>
+
+ <%= field.options[:hint] %>
+
+ <% end %>
+
diff --git a/app/views/fields/string_array/_index.html.erb b/app/views/fields/string_array/_index.html.erb
new file mode 100644
index 0000000..6d9dbc9
--- /dev/null
+++ b/app/views/fields/string_array/_index.html.erb
@@ -0,0 +1 @@
+<%= field.to_s %>
diff --git a/app/views/fields/string_array/_show.html.erb b/app/views/fields/string_array/_show.html.erb
new file mode 100644
index 0000000..3819d11
--- /dev/null
+++ b/app/views/fields/string_array/_show.html.erb
@@ -0,0 +1,5 @@
+
+ <% field.data&.each do |item| %>
+ - <%= item %>
+ <% end %>
+
diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb
index 8750ca2..aa4c4f5 100644
--- a/config/initializers/cors.rb
+++ b/config/initializers/cors.rb
@@ -1,6 +1,6 @@
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
- origins '*'
- resource '*', headers: :any, methods: [:get, :post, :patch, :put]
+ origins "*"
+ resource "*", headers: :any, methods: [ :get, :post, :patch, :put ]
end
end
diff --git a/db/migrate/20241113130440_add_validation_fields_to_form_fields.rb b/db/migrate/20241113130440_add_validation_fields_to_form_fields.rb
new file mode 100644
index 0000000..acab43e
--- /dev/null
+++ b/db/migrate/20241113130440_add_validation_fields_to_form_fields.rb
@@ -0,0 +1,21 @@
+class AddValidationFieldsToFormFields < ActiveRecord::Migration[8.0]
+ def change
+ add_column :form_fields, :friendly_name, :string
+ add_column :form_fields, :field_type, :string, null: false, default: 'string'
+
+ # For number fields
+ add_column :form_fields, :minimum, :string
+ add_column :form_fields, :maximum, :string
+
+ # For select fields
+ add_column :form_fields, :enum_options, :string, array: true, default: []
+
+ # Copy existing title values to friendly_name
+ FormField.find_each do |field|
+ field.update_column(:friendly_name, field.title)
+ end
+
+ # Make friendly_name required after copying data
+ change_column_null :form_fields, :friendly_name, false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ccffd20..42a8fa4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2024_10_02_081501) do
+ActiveRecord::Schema[8.0].define(version: 2024_11_13_130440) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -86,6 +86,11 @@
t.jsonb "metadata", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "friendly_name", null: false
+ t.string "field_type", default: "string", null: false
+ t.string "minimum"
+ t.string "maximum"
+ t.string "enum_options", default: [], array: true
t.index ["page_id"], name: "index_form_fields_on_page_id"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 8da2279..cff81ba 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -58,63 +58,205 @@
# Create form fields for the Consultation Form page (Care template)
consultation_form_fields = [
- { title: "history_of_present_illness", description: "Details about the current illness, including when" },
- { title: "examination_details", description: "Details about the examination performed and clinic" },
- { title: "weight", description: "The patient's weight, usually measured in kilogram" },
- { title: "height", description: "The patient's height, usually measured in centimet" },
- { title: "consultation_notes", description: "General Advice given to the patient" },
- { title: "special_instruction", description: "Any special instructions for the patient or other" },
- { title: "treatment_plan", description: "Treatment plan for the patient, including prescrib" },
- { title: "patient_no", description: "The inpatient or out patient number assigned to th" }
+ {
+ title: "history_of_present_illness",
+ friendly_name: "History of Present Illness",
+ description: "Details about the current illness, including when",
+ field_type: "string"
+ },
+ {
+ title: "examination_details",
+ friendly_name: "Examination Details",
+ description: "Details about the examination performed and clinic",
+ field_type: "string"
+ },
+ {
+ title: "weight",
+ friendly_name: "Weight",
+ description: "The patient's weight, usually measured in kilogram",
+ field_type: "number",
+ minimum: 0,
+ maximum: 500
+ },
+ {
+ title: "height",
+ friendly_name: "Height",
+ description: "The patient's height, usually measured in centimeters",
+ field_type: "number",
+ minimum: 0,
+ maximum: 300
+ },
+ {
+ title: "consultation_notes",
+ friendly_name: "Consultation Notes",
+ description: "General Advice given to the patient",
+ field_type: "string"
+ },
+ {
+ title: "special_instruction",
+ friendly_name: "Special Instructions",
+ description: "Any special instructions for the patient or other",
+ field_type: "string"
+ },
+ {
+ title: "treatment_plan",
+ friendly_name: "Treatment Plan",
+ description: "Treatment plan for the patient, including prescriptions",
+ field_type: "string"
+ },
+ {
+ title: "patient_no",
+ friendly_name: "Patient Number",
+ description: "The inpatient or out patient number assigned to the patient",
+ field_type: "string"
+ }
]
-consultation_form_fields.each do |field_data|
- FormField.create!(
- page: consultation_form,
- title: field_data[:title],
- description: field_data[:description]
- )
-end
-
# Create form fields for the Log Update page (Care template)
log_update_fields = [
- { title: "physical_examination_info", description: "Physical Examination Details, including any complaints" },
- { title: "other_details", description: "Other details, including Treatement Plans, Advice," },
- { title: "diastolic", description: "Blood Pressure Diastolic as integer" },
- { title: "systolic", description: "Blood Pressure Systolic as integer" },
- { title: "temperature", description: "Temperature in Fahrenheit" },
- { title: "resp", description: "Respiratory Rate as integer" },
- { title: "spo2", description: "SPO2 Value as integer" },
- { title: "pulse", description: "Pulse Rate as integer" }
+ {
+ title: "physical_examination_info",
+ friendly_name: "Physical Examination Info",
+ description: "Physical Examination Details, including any complaints",
+ field_type: "string"
+ },
+ {
+ title: "other_details",
+ friendly_name: "Other Details",
+ description: "Other details, including Treatment Plans, Advice",
+ field_type: "string"
+ },
+ {
+ title: "diastolic",
+ friendly_name: "Diastolic BP",
+ description: "Blood Pressure Diastolic as integer",
+ field_type: "number",
+ minimum: 0,
+ maximum: 200
+ },
+ {
+ title: "systolic",
+ friendly_name: "Systolic BP",
+ description: "Blood Pressure Systolic as integer",
+ field_type: "number",
+ minimum: 0,
+ maximum: 300
+ },
+ {
+ title: "temperature",
+ friendly_name: "Temperature",
+ description: "Temperature in Fahrenheit",
+ field_type: "number",
+ minimum: 90,
+ maximum: 110
+ },
+ {
+ title: "resp",
+ friendly_name: "Respiratory Rate",
+ description: "Respiratory Rate as integer",
+ field_type: "number",
+ minimum: 0,
+ maximum: 100
+ },
+ {
+ title: "spo2",
+ friendly_name: "SPO2",
+ description: "SPO2 Value as integer",
+ field_type: "number",
+ minimum: 0,
+ maximum: 100
+ },
+ {
+ title: "pulse",
+ friendly_name: "Pulse Rate",
+ description: "Pulse Rate as integer",
+ field_type: "number",
+ minimum: 0,
+ maximum: 300
+ }
]
-log_update_fields.each do |field_data|
- FormField.create!(
- page: log_update,
- title: field_data[:title],
- description: field_data[:description]
- )
-end
-
# Create form fields for the Demo Page 1 (Medispeak template)
medispeak_demo_fields = [
- { title: "gender", description: "Gender from options Male, Female, Other" },
- { title: "symptoms", description: "Applicable Symptoms from options Headache, Fever," },
- { title: "preferred_time", description: "The Preferred Time from the options Morning, After" },
- { title: "full_name", description: "Full name" },
- { title: "age", description: "Age of the user" },
- { title: "email", description: "Email of the user" },
- { title: "phone_number", description: "Phone Number of the user" },
- { title: "additional_notes", description: "Full text of the conversation" },
- { title: "date_of_birth", description: "Date of Birth as a Javascript Date String" }
+ {
+ title: "gender",
+ friendly_name: "Gender",
+ description: "Gender from options Male, Female, Other",
+ field_type: "single_select",
+ enum_options: [ "Male", "Female", "Other" ]
+ },
+ {
+ title: "symptoms",
+ friendly_name: "Symptoms",
+ description: "Applicable Symptoms from options",
+ field_type: "multi_select",
+ enum_options: [ "Headache", "Fever", "Cough", "Cold", "Body Pain" ]
+ },
+ {
+ title: "preferred_time",
+ friendly_name: "Preferred Time",
+ description: "The Preferred Time from the options",
+ field_type: "single_select",
+ enum_options: [ "Morning", "Afternoon", "Evening" ]
+ },
+ {
+ title: "full_name",
+ friendly_name: "Full Name",
+ description: "Full name",
+ field_type: "string"
+ },
+ {
+ title: "age",
+ friendly_name: "Age",
+ description: "Age of the user",
+ field_type: "number",
+ minimum: 0,
+ maximum: 150
+ },
+ {
+ title: "email",
+ friendly_name: "Email",
+ description: "Email of the user",
+ field_type: "string"
+ },
+ {
+ title: "phone_number",
+ friendly_name: "Phone Number",
+ description: "Phone Number of the user",
+ field_type: "string"
+ },
+ {
+ title: "additional_notes",
+ friendly_name: "Additional Notes",
+ description: "Full text of the conversation",
+ field_type: "string"
+ },
+ {
+ title: "date_of_birth",
+ friendly_name: "Date of Birth",
+ description: "Date of Birth as a Javascript Date String",
+ field_type: "string"
+ }
]
-medispeak_demo_fields.each do |field_data|
- FormField.create!(
- page: medispeak_demo_page,
- title: field_data[:title],
- description: field_data[:description]
- )
+# Update the creation loops to include all attributes
+[
+ [ consultation_form, consultation_form_fields ],
+ [ log_update, log_update_fields ],
+ [ medispeak_demo_page, medispeak_demo_fields ]
+].each do |page, fields|
+ fields.each do |field_data|
+ FormField.create!(
+ page: page,
+ title: field_data[:title],
+ friendly_name: field_data[:friendly_name],
+ description: field_data[:description],
+ field_type: field_data[:field_type],
+ minimum: field_data[:minimum],
+ maximum: field_data[:maximum],
+ enum_options: field_data[:enum_options]
+ )
+ end
end
puts "Seeds created successfully!"
diff --git a/lib/administrate/field/string_array.rb b/lib/administrate/field/string_array.rb
new file mode 100644
index 0000000..19f2c81
--- /dev/null
+++ b/lib/administrate/field/string_array.rb
@@ -0,0 +1,19 @@
+require "administrate/field/base"
+
+module Administrate
+ module Field
+ class StringArray < Administrate::Field::Base
+ def to_s
+ data&.join("\n")
+ end
+
+ def self.permitted_attribute(attr, _options = nil)
+ [ "#{attr}_raw", attr => [] ]
+ end
+
+ def self.html_class
+ "string_array"
+ end
+ end
+ end
+end