lib\moldb\search.ex
defmodule Moldb.Search do
  import Ecto.Query, warn: false
  alias Moldb.Repo
  alias Moldb.Compounds.{Compound, Substance}

  @type search_result :: %{
    type: String.t(),
    id: Ecto.UUID.t(),
    name: String.t(),
    description: String.t()
  }

  @spec search(String.t(), non_neg_integer(), pos_integer(), map()) :: [search_result()]
  def search(query, page, per_page, filters) when is_binary(query) and is_integer(page) and is_integer(per_page) and is_map(filters) do
    offset = (page - 1) * per_page

    compound_query = from(c in Compound,
      select: %{type: fragment("'compound'"), id: c.id, name: c.name, description: c.description}
    )

    substance_query = from(s in Substance,
      select: %{type: fragment("'substance'"), id: s.id, name: s.name, description: s.description}
    )

    union_query = compound_query
    |> union_all(^substance_query)

    Repo.all(
      from q in subquery(union_query),
        where: ilike(q.name, ^"%#{String.replace(query, "%", "\\%")}%"),
        where: ^build_filters(filters),
        order_by: [desc: fragment("similarity(?, ?)", q.name, ^query)],
        offset: ^offset,
        limit: ^per_page,
        select: %{
          type: q.type,
          id: q.id,
          name: q.name,
          description: fragment("LEFT(?, 100)", q.description)
        }
    )
  end

  @spec suggest(String.t(), pos_integer()) :: [String.t()]
  def suggest(query, limit) when is_binary(query) and is_integer(limit) do
    escaped_query = String.replace(query, "%", "\\%")

    compound_query = from(c in Compound, select: c.name)
    substance_query = from(s in Substance, select: s.name)

    union_query = compound_query
    |> union_all(^substance_query)

    Repo.all(
      from q in subquery(
        from u in subquery(union_query),
          where: ilike(u.name, ^"#{escaped_query}%"),
          select: %{
            name: u.name,
            similarity: fragment("similarity(?, ?)", u.name, ^query)
          }
      ),
      order_by: [desc: q.similarity],
      distinct: q.name,
      limit: ^limit,
      select: q.name
    )
  end

  defp build_filters(filters) do
    Enum.reduce(filters, dynamic(true), fn
      {"status", status}, acc ->
        dynamic([q], ^acc and q.status == ^status)
      {"category", category}, acc ->
        dynamic([q], ^acc and q.category == ^category)
      _, acc ->
        acc
    end)
  end
end

lib\moldb\compounds.ex
defmodule Moldb.Compounds do
  @moduledoc """
  Documentation for Moldb.Compounds.
  """
  import Ecto.Query, warn: false
  alias Moldb.Repo
  alias Moldb.Compounds.{Compound, Substance, CompoundProperty}

  def list_compounds(opts \\ []) do
    Compound
    |> maybe_preload(opts[:preload])
    |> Repo.all()
  end

  def get_compound!(id, opts \\ []) do
    Compound
    |> maybe_preload(opts[:preload])
    |> Repo.get!(id)
  end

  def get_compound_with_assocs!(id) do
    Repo.get!(Compound, id)
    |> Repo.preload([:substances, :properties, :synonyms])
  end

  def create_compound(attrs \\ %{}) do
    %Compound{}
    |> Compound.changeset(attrs)
    |> Repo.insert()
  end

  def update_compound(%Compound{} = compound, attrs) do
    compound
    |> Compound.changeset(attrs)
    |> Repo.update()
  end

  def delete_compound(%Compound{} = compound), do: Repo.delete(compound)

  def change_compound(%Compound{} = compound, attrs \\ %{}), do: Compound.changeset(compound, attrs)

  def list_substances(opts \\ []) do
    Substance
    |> maybe_preload(opts[:preload])
    |> Repo.all()
  end

  def get_substance!(id, opts \\ []) do
    Substance
    |> maybe_preload(opts[:preload])
    |> Repo.get!(id)
  end

  def create_substance(attrs \\ %{}) do
    %Substance{}
    |> Substance.changeset(attrs)
    |> Repo.insert()
  end

  def update_substance(%Substance{} = substance, attrs) do
    substance
    |> Substance.changeset(attrs)
    |> Repo.update()
  end

  def delete_substance(%Substance{} = substance), do: Repo.delete(substance)

  def change_substance(%Substance{} = substance, attrs \\ %{}), do: Substance.changeset(substance, attrs)

  def compound_name(%Compound{} = compound), do: compound.name || "Unknown"
  def name(%Substance{} = substance), do: substance.name || "Unknown"

  def list_categories do
    CompoundProperty
    |> Repo.all()
    |> Enum.map(& &1.property_category)
    |> Enum.uniq()
  end

  defp maybe_preload(query, nil), do: query
  defp maybe_preload(query, preloads), do: from(q in query, preload: ^preloads)
end

lib\moldb_web\live\search_live.ex
defmodule MoldbWeb.SearchLive do
  use MoldbWeb, :live_view
  alias Moldb.Search
  alias MoldbWeb.Components.SearchForm
  alias MoldbWeb.Components.SearchResults
  alias MoldbWeb.Components.Pagination

  @per_page 20

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(query: "", page: 1, has_more: false, loading: false, results_count: 0)
     |> stream(:results, [])}
  end

  @impl true
  def handle_params(%{"query" => query, "page" => page}, _url, socket) do
    {page, _} = Integer.parse(page)
    {:noreply, socket |> assign(query: query, page: page, loading: true) |> fetch_results()}
  end

  @impl true
  def handle_params(_params, _url, socket), do: {:noreply, socket}

  @impl true
  def render(assigns) do
    ~H"""
    <div class="max-w-2xl mx-auto p-6">
      <h1 class="text-3xl font-bold mb-6 text-center text-gray-800">Molecular Database Search</h1>
      <.live_component module={SearchForm} id="search_form" query={@query} />
      <.live_component module={SearchResults} id="search_results" results={@streams.results} results_count={@results_count} />
      <.live_component module={Pagination} id="pagination" has_more={@has_more} page={@page} loading={@loading} />
    </div>
    """
  end

  @impl true
  def handle_event("search", %{"query" => query}, socket) do
    {:noreply, push_navigate(socket, to: ~p"/search?query=#{query}&page=1")}
  end

  @impl true
  def handle_event("suggest", %{"query" => query}, socket) when byte_size(query) >= 2 do
    suggestions = Search.suggest(query, 5)
    {:noreply, push_event(socket, "update_suggestions", %{suggestions: suggestions})}
  end

  @impl true
  def handle_event("suggest", _params, socket) do
    {:noreply, push_event(socket, "update_suggestions", %{suggestions: []})}
  end

  @impl true
  def handle_event("load_more", _, socket) do
    %{query: query, page: page} = socket.assigns
    {:noreply, push_navigate(socket, to: ~p"/search?query=#{query}&page=#{page + 1}")}
  end

  defp fetch_results(%{assigns: %{query: query, page: page}} = socket) do
    filters = %{}  # Define your filters here as needed
    results = Search.search(query, page, @per_page, filters)
    has_more = length(results) == @per_page

    socket
    |> stream(:results, results, reset: page == 1)
    |> assign(has_more: has_more, loading: false, results_count: length(results))
  end
end

lib\moldb_web\live\components\pagination_component.ex
defmodule MoldbWeb.Components.Pagination do
  use MoldbWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <div class="mt-4 text-center">
      <%= if @has_more do %>
        <button
          phx-click="load_more"
          class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
          disabled={@loading}
        >
          <%= if @loading, do: "Loading...", else: "Load More" %>
        </button>
      <% end %>
    </div>
    """
  end
end

lib\moldb_web\live\components\search_form.ex
defmodule MoldbWeb.Components.SearchForm do
  use MoldbWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <form phx-submit="search" phx-change="suggest" id="search_form" class="relative">
      <input type="text"
             name="query"
             id="search-input"
             value={@query}
             placeholder="Search for compounds or substances..."
             autocomplete="off"
             phx-debounce="300"
             class="w-full p-4 pr-12 text-sm border-gray-200 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
             phx-hook="Autocomplete"
      />
      <button type="submit" class="absolute inset-y-0 right-0 px-4 text-gray-600 hover:text-blue-600">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
          <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
        </svg>
      </button>
      <div id="suggestions" class="absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-lg hidden">
      </div>
    </form>
    """
  end

  @impl true
  def handle_event("search", %{"query" => query}, socket) do
    # Push the event to the parent LiveView to handle the search
    {:noreply, push_navigate(socket, to: ~p"/search?query=#{query}&page=1")}
  end

  @impl true
  def handle_event("suggest", %{"query" => query}, socket) when byte_size(query) >= 2 do
    suggestions = Moldb.Search.suggest(query, 5)
    {:noreply, push_event(socket, "update_suggestions", %{suggestions: suggestions})}
  end

  @impl true
  def handle_event("suggest", _params, socket) do
    {:noreply, push_event(socket, "update_suggestions", %{suggestions: []})}
  end
end

lib\moldb_web\live\components\search_results_component.ex
defmodule MoldbWeb.Components.SearchResults do
  use MoldbWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <div id="search-results" class="space-y-4 mt-8">
      <%= if @results_count == 0 do %>
        <p class="text-center text-gray-600">No results found. Try a different search term.</p>
      <% else %>
        <div id="results" phx-update="stream">
          <%= for {id, result} <- @results do %>
            <div
              id={id}
              class="result bg-white p-4 rounded-lg shadow hover:shadow-md transition-shadow duration-200 ease-in-out cursor-pointer"
              phx-click={JS.navigate(result_path(result))}
            >
              <h3 class="text-lg font-semibold text-gray-800 mb-2"><%= result.name %></h3>
              <p class="text-sm text-gray-600"><%= result.description %></p>
              <span class="inline-block mt-2 px-2 py-1 text-xs font-semibold text-gray-700 bg-gray-200 rounded-full">
                <%= String.capitalize(result.type) %>
              </span>
            </div>
          <% end %>
        </div>
      <% end %>
    </div>
    """
  end

  defp result_path(result) do
    case result.type do
      "compound" -> ~p"/compound/#{result.id}"
      "substance" -> ~p"/substance/#{result.id}"
      _ -> "#"
    end
  end
end

assets\js\app.js
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import Autocomplete from "./autocomplete"


let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: {Autocomplete}  // Register the Autocomplete hook here
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

assets\js\autocomplete.js
const Autocomplete = {
  mounted() {
    this.el.addEventListener("input", () => {
      this.pushEventTo("#search_form", "suggest", {query: this.el.value});
    });

    this.handleEvent("update_suggestions", ({suggestions}) => {
      this.showSuggestions(suggestions);
    });

    document.addEventListener("click", (e) => {
      if (!this.el.contains(e.target)) {
        this.hideSuggestions();
      }
    });
  },
  showSuggestions(suggestions) {
    const suggestionsEl = document.getElementById("suggestions");
    suggestionsEl.innerHTML = "";

    if (suggestions.length > 0) {
      suggestionsEl.classList.remove("hidden");
      suggestions.forEach((suggestion) => {
        const li = document.createElement("li");
        li.textContent = suggestion;
        li.className = "px-4 py-2 cursor-pointer hover:bg-gray-100";
        li.addEventListener("click", () => {
          this.el.value = suggestion;
          this.hideSuggestions();
          this.pushEventTo("#search_form", "search", { query: suggestion });
        });
        suggestionsEl.appendChild(li);
      });
    } else {
      this.hideSuggestions();
    }
  },
  hideSuggestions() {
    const suggestionsEl = document.getElementById("suggestions");
    suggestionsEl.classList.add("hidden");
  }
};

export default Autocomplete; Analyze and review the code, Refactor to remove redundant code, improve performance, minimalize payloads, simplify and improve modularity.