diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
index c3f479e..ce8719a 100644
--- a/assets/tailwind.config.js
+++ b/assets/tailwind.config.js
@@ -1,21 +1,30 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
-const plugin = require("tailwindcss/plugin")
-const fs = require("fs")
-const path = require("path")
+const plugin = require("tailwindcss/plugin");
+const fs = require("fs");
+const path = require("path");
module.exports = {
content: [
"./js/**/*.js",
"../lib/elixir_ai_web.ex",
- "../lib/elixir_ai_web/**/*.*ex"
+ "../lib/elixir_ai_web/**/*.*ex",
+ ],
+ safelist: [
+ "bg-cyan-950/30",
+ "bg-red-950/30",
+ "bg-green-950/30",
+ "bg-blue-950/30",
+ "bg-yellow-950/30",
+ "bg-purple-950/30",
+ "bg-pink-950/30",
],
theme: {
extend: {
colors: {
brand: "#FD4F00",
- }
+ },
},
},
plugins: [
@@ -25,50 +34,71 @@ module.exports = {
//
//
//
- plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
- plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
- plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
+ plugin(({ addVariant }) =>
+ addVariant("phx-click-loading", [
+ ".phx-click-loading&",
+ ".phx-click-loading &",
+ ]),
+ ),
+ plugin(({ addVariant }) =>
+ addVariant("phx-submit-loading", [
+ ".phx-submit-loading&",
+ ".phx-submit-loading &",
+ ]),
+ ),
+ plugin(({ addVariant }) =>
+ addVariant("phx-change-loading", [
+ ".phx-change-loading&",
+ ".phx-change-loading &",
+ ]),
+ ),
// Embeds Heroicons (https://heroicons.com) into your app.css bundle
// See your `CoreComponents.icon/1` for more information.
//
- plugin(function({matchComponents, theme}) {
- let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
- let values = {}
+ plugin(function ({ matchComponents, theme }) {
+ let iconsDir = path.join(__dirname, "../deps/heroicons/optimized");
+ let values = {};
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"],
- ["-micro", "/16/solid"]
- ]
+ ["-micro", "/16/solid"],
+ ];
icons.forEach(([suffix, dir]) => {
- fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
- let name = path.basename(file, ".svg") + suffix
- values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
- })
- })
- matchComponents({
- "hero": ({name, fullPath}) => {
- let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
- let size = theme("spacing.6")
- if (name.endsWith("-mini")) {
- size = theme("spacing.5")
- } else if (name.endsWith("-micro")) {
- size = theme("spacing.4")
- }
- return {
- [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
- "-webkit-mask": `var(--hero-${name})`,
- "mask": `var(--hero-${name})`,
- "mask-repeat": "no-repeat",
- "background-color": "currentColor",
- "vertical-align": "middle",
- "display": "inline-block",
- "width": size,
- "height": size
- }
- }
- }, {values})
- })
- ]
-}
+ fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => {
+ let name = path.basename(file, ".svg") + suffix;
+ values[name] = { name, fullPath: path.join(iconsDir, dir, file) };
+ });
+ });
+ matchComponents(
+ {
+ hero: ({ name, fullPath }) => {
+ let content = fs
+ .readFileSync(fullPath)
+ .toString()
+ .replace(/\r?\n|\r/g, "");
+ let size = theme("spacing.6");
+ if (name.endsWith("-mini")) {
+ size = theme("spacing.5");
+ } else if (name.endsWith("-micro")) {
+ size = theme("spacing.4");
+ }
+ return {
+ [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
+ "-webkit-mask": `var(--hero-${name})`,
+ mask: `var(--hero-${name})`,
+ "mask-repeat": "no-repeat",
+ "background-color": "currentColor",
+ "vertical-align": "middle",
+ display: "inline-block",
+ width: size,
+ height: size,
+ };
+ },
+ },
+ { values },
+ );
+ }),
+ ],
+};
diff --git a/lib/elixir_ai/conversation_manager.ex b/lib/elixir_ai/conversation_manager.ex
index 471149f..995eb11 100644
--- a/lib/elixir_ai/conversation_manager.ex
+++ b/lib/elixir_ai/conversation_manager.ex
@@ -109,6 +109,11 @@ defmodule ElixirAi.ConversationManager do
{:reply, Map.get(conversations, name, []), state}
end
+ def handle_info({:db_error, reason}, state) do
+ Logger.error("ConversationManager received db_error: #{inspect(reason)}")
+ {:noreply, state}
+ end
+
def handle_info({:store_message, name, message}, %{conversations: conversations} = state) do
case Conversation.find_id(name) do
{:ok, conv_id} ->
diff --git a/lib/elixir_ai/data/db_helpers.ex b/lib/elixir_ai/data/db_helpers.ex
index 3f0958b..7c9b0b9 100644
--- a/lib/elixir_ai/data/db_helpers.ex
+++ b/lib/elixir_ai/data/db_helpers.ex
@@ -7,19 +7,23 @@ defmodule ElixirAi.Data.DbHelpers do
end
def run_sql(sql, params, topic) do
+ original_sql = sql
+ original_params = params
{sql, params} = named_params_to_positional_params(sql, params)
try do
result = Ecto.Adapters.SQL.query!(ElixirAi.Repo, sql, params)
# Transform rows to maps with column names as keys
- Enum.map(result.rows, fn row ->
+ Enum.map(result.rows || [], fn row ->
Enum.zip(result.columns, row)
|> Enum.into(%{})
end)
rescue
exception ->
Logger.error("Database error: #{Exception.message(exception)}")
+ Logger.error("Failed SQL: #{original_sql}")
+ Logger.error("SQL params: #{inspect(original_params, pretty: true)}")
Phoenix.PubSub.broadcast(
ElixirAi.PubSub,
diff --git a/lib/elixir_ai/data/message.ex b/lib/elixir_ai/data/message.ex
index 56860d3..245c030 100644
--- a/lib/elixir_ai/data/message.ex
+++ b/lib/elixir_ai/data/message.ex
@@ -73,7 +73,7 @@ defmodule ElixirAi.Message do
$(role),
$(content),
$(reasoning_content),
- $(tool_calls),
+ $(tool_calls)::jsonb,
$(tool_call_id),
$(inserted_at)
)
diff --git a/lib/elixir_ai_web/live/home_live.ex b/lib/elixir_ai_web/live/home_live.ex
index ba75a72..237fb20 100644
--- a/lib/elixir_ai_web/live/home_live.ex
+++ b/lib/elixir_ai_web/live/home_live.ex
@@ -24,53 +24,62 @@ defmodule ElixirAiWeb.HomeLive do
Conversations
-
- <%= if @conversations == [] do %>
- - No conversations yet.
- <% end %>
- <%= for name <- @conversations do %>
- -
- <.link
- navigate={~p"/chat/#{name}"}
- class="block px-4 py-2 rounded-lg border border-cyan-900/40 bg-cyan-950/20 text-cyan-300 hover:border-cyan-700 hover:bg-cyan-950/40 transition-colors text-sm"
- >
- {name}
-
-
- <% end %>
-
+ <.conversation_list conversations={@conversations} />
-
+ <.create_conversation_form new_name={@new_name} ai_providers={@ai_providers} />
<%= if @error do %>
{@error}
<% end %>
-
- <%!--
- <.live_component module={ElixirAiWeb.AiProvidersLive} id="ai-providers" />
-
--%>
"""
end
+ defp conversation_list(assigns) do
+ ~H"""
+