From c0b1e408bf52c80a1321036c1b4e71ab9ceadabe Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 23 Mar 2026 16:38:51 -0600 Subject: [PATCH] seafoam color migration started --- assets/css/app.css | 37 +-- assets/css/markdown.css | 46 ++-- assets/tailwind.config.js | 104 -------- config/config.exs | 2 +- lib/elixir_ai/ai_tools.ex | 2 +- lib/elixir_ai_web/admin/admin_live.ex | 34 +-- lib/elixir_ai_web/chat/chat_live.ex | 6 +- lib/elixir_ai_web/chat/chat_message.ex | 233 +++++++++++++++--- .../chat/chat_provider_display.ex | 22 +- .../components/form_components.ex | 6 +- .../components/layouts/root.html.heex | 2 +- lib/elixir_ai_web/home/ai_providers_live.ex | 22 +- lib/elixir_ai_web/home/home_live.ex | 10 +- lib/elixir_ai_web/voice/voice_live.ex | 36 +-- 14 files changed, 309 insertions(+), 253 deletions(-) delete mode 100644 assets/tailwind.config.js diff --git a/assets/css/app.css b/assets/css/app.css index 98fdfe7..53d9dae 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -3,10 +3,24 @@ @source "../js/**/*.js"; @source "../../lib/elixir_ai_web.ex"; @source "../../lib/elixir_ai_web/**/*.*ex"; +@source inline("{bg-seafoam-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}"); @plugin "@tailwindcss/forms"; @plugin "../heroicons_plugin.js"; @theme { + --color-brand: #fd4f00; + + --color-seafoam-50: #ecfeff; + --color-seafoam-100: #cffafe; + --color-seafoam-200: #a5f3fc; + --color-seafoam-300: #67e8f9; + --color-seafoam-400: #22d3ee; + --color-seafoam-500: #06b6d4; + --color-seafoam-600: #0891b2; + --color-seafoam-700: #0e7490; + --color-seafoam-800: #155e75; + --color-seafoam-900: #164e63; + --color-seafoam-950: #083344; } @variant phx-click-loading (&.phx-click-loading, .phx-click-loading &); @@ -17,7 +31,7 @@ /* Form Elements */ label { - @apply block text-sm font-medium text-cyan-300 mb-1; + @apply block text-sm font-medium text-seafoam-300 mb-1; } input[type="text"], @@ -29,11 +43,11 @@ input[type="url"], textarea, select { @apply w-full rounded-md px-3 py-2 text-sm - bg-cyan-950 text-cyan-50 placeholder-cyan-600 - border border-cyan-800 + bg-seafoam-950 text-seafoam-50 placeholder-seafoam-600 + border border-seafoam-800 outline-none transition-colors duration-150 - focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500; + focus:border-seafoam-500 focus:ring-1 focus:ring-seafoam-500; } textarea { @@ -43,19 +57,19 @@ textarea { button[type="submit"], input[type="submit"] { @apply px-4 py-2 rounded-md text-sm font-medium - bg-cyan-600 text-white - border border-cyan-500 + bg-seafoam-600 text-white + border border-seafoam-500 transition-colors duration-150 - hover:bg-cyan-500 + hover:bg-seafoam-500 disabled:opacity-40 disabled:cursor-not-allowed; } fieldset { - @apply border border-cyan-800 rounded-md px-4 py-3; + @apply border border-seafoam-800 rounded-md px-4 py-3; } legend { - @apply text-sm font-semibold text-cyan-400 px-1; + @apply text-sm font-semibold text-seafoam-400 px-1; } button { @@ -72,8 +86,3 @@ button { .reasoning-content.collapsed { @apply opacity-0 max-h-0 pt-0 pb-0 mb-0; } - - - - - diff --git a/assets/css/markdown.css b/assets/css/markdown.css index 024f539..9857315 100644 --- a/assets/css/markdown.css +++ b/assets/css/markdown.css @@ -1,7 +1,7 @@ /* Rendered Markdown */ .markdown { - @apply text-cyan-50 leading-7 text-base; + @apply text-seafoam-50 leading-7 text-base; } /* Headings */ @@ -11,26 +11,26 @@ .markdown h4, .markdown h5, .markdown h6 { - @apply font-semibold text-cyan-300 mt-6 mb-2 leading-tight; + @apply font-semibold text-seafoam-300 mt-6 mb-2 leading-tight; } .markdown h1 { - @apply text-3xl border-b border-cyan-900 pb-1; + @apply text-3xl border-b border-seafoam-900 pb-1; } .markdown h2 { - @apply text-2xl border-b border-cyan-900 pb-1; + @apply text-2xl border-b border-seafoam-900 pb-1; } .markdown h3 { - @apply text-xl text-cyan-200; + @apply text-xl text-seafoam-200; } .markdown h4 { - @apply text-lg text-cyan-200; + @apply text-lg text-seafoam-200; } .markdown h5 { - @apply text-base text-cyan-100; + @apply text-base text-seafoam-100; } .markdown h6 { - @apply text-sm text-cyan-100; + @apply text-sm text-seafoam-100; } .markdown p { @@ -38,33 +38,33 @@ } .markdown a { - @apply text-cyan-400 underline underline-offset-2 transition-colors duration-150 hover:text-cyan-300; + @apply text-seafoam-400 underline underline-offset-2 transition-colors duration-150 hover:text-seafoam-300; } .markdown strong { - @apply font-bold text-cyan-100; + @apply font-bold text-seafoam-100; } .markdown em { - @apply italic text-cyan-200; + @apply italic text-seafoam-200; } .markdown code { - @apply font-mono text-sm bg-cyan-950 text-cyan-300 px-1 py-0.5 rounded border border-cyan-900; + @apply font-mono text-sm bg-seafoam-950 text-seafoam-300 px-1 py-0.5 rounded border border-seafoam-900; } .markdown pre { - @apply bg-cyan-950 border border-cyan-900 rounded-lg px-5 py-4 overflow-x-auto my-4; + @apply bg-seafoam-950 border border-seafoam-900 rounded-lg px-5 py-4 overflow-x-auto my-4; } .markdown pre code { - @apply bg-transparent border-0 p-0 text-sm text-cyan-100; + @apply bg-transparent border-0 p-0 text-sm text-seafoam-100; } .markdown blockquote { - @apply border-l-2 border-cyan-700 my-4 px-4 py-2 bg-cyan-950 text-cyan-200 rounded-r italic; + @apply border-l-2 border-seafoam-700 my-4 px-4 py-2 bg-seafoam-950 text-seafoam-200 rounded-r italic; } .markdown hr { - @apply border-0 border-t border-cyan-900 my-6; + @apply border-0 border-t border-seafoam-900 my-6; } .markdown ul, .markdown ol { @@ -80,7 +80,7 @@ @apply my-1; } .markdown li::marker { - @apply text-cyan-700; + @apply text-seafoam-700; } .markdown ul ul, @@ -95,22 +95,22 @@ @apply block w-full border-collapse my-4 text-sm overflow-x-auto; } .markdown thead { - @apply bg-cyan-950; + @apply bg-seafoam-950; } .markdown th { - @apply text-left px-3 py-2 text-cyan-300 font-semibold border-b-2 border-cyan-700; + @apply text-left px-3 py-2 text-seafoam-300 font-semibold border-b-2 border-seafoam-700; } .markdown td { - @apply px-3 py-2 border-b border-cyan-900 text-cyan-100; + @apply px-3 py-2 border-b border-seafoam-900 text-seafoam-100; } .markdown tbody tr:hover { - @apply bg-cyan-950; + @apply bg-seafoam-950; } .markdown img { - @apply max-w-full rounded-md border border-cyan-900 my-2; + @apply max-w-full rounded-md border border-seafoam-900 my-2; } .markdown input[type="checkbox"] { - @apply accent-cyan-700 mr-1; + @apply accent-seafoam-700 mr-1; } diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js deleted file mode 100644 index ce8719a..0000000 --- a/assets/tailwind.config.js +++ /dev/null @@ -1,104 +0,0 @@ -// 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"); - -module.exports = { - content: [ - "./js/**/*.js", - "../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: [ - require("@tailwindcss/forms"), - // Allows prefixing tailwind classes with LiveView classes to add rules - // only when LiveView classes are applied, for example: - // - //
- // - 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 = {}; - let icons = [ - ["", "/24/outline"], - ["-solid", "/24/solid"], - ["-mini", "/20/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 }, - ); - }), - ], -}; diff --git a/config/config.exs b/config/config.exs index b145435..8729216 100644 --- a/config/config.exs +++ b/config/config.exs @@ -25,7 +25,7 @@ config :esbuild, ] config :tailwind, - version: "4.0.9", + version: "4.2.2", elixir_ai: [ args: ~w( --input=css/app.css diff --git a/lib/elixir_ai/ai_tools.ex b/lib/elixir_ai/ai_tools.ex index 4e0bbc5..e477aea 100644 --- a/lib/elixir_ai/ai_tools.ex +++ b/lib/elixir_ai/ai_tools.ex @@ -83,7 +83,7 @@ defmodule ElixirAi.AiTools do "color" => %{ "type" => "string", "enum" => [ - "bg-cyan-950/30", + "bg-seafoam-950/30", "bg-red-950/30", "bg-green-950/30", "bg-blue-950/30", diff --git a/lib/elixir_ai_web/admin/admin_live.ex b/lib/elixir_ai_web/admin/admin_live.ex index 285d614..c8df99a 100644 --- a/lib/elixir_ai_web/admin/admin_live.ex +++ b/lib/elixir_ai_web/admin/admin_live.ex @@ -108,7 +108,7 @@ defmodule ElixirAiWeb.AdminLive do def render(assigns) do ~H"""
-

Cluster Admin

+

Cluster Admin

<%= for {node, status} <- @cluster_info.nodes do %> @@ -121,12 +121,12 @@ defmodule ElixirAiWeb.AdminLive do |> Enum.filter(fn {_, n} -> n == node end) |> Enum.group_by(fn {view, _} -> view end) %> -
-
+
+
- {node} + {node} <%= if node == Node.self() do %> - self + self <% end %>
<.status_badge status={status} /> @@ -135,12 +135,12 @@ defmodule ElixirAiWeb.AdminLive do
<%= if node_singletons != [] do %>
-

+

Singletons

<%= for {module, _} <- node_singletons do %> -
+
{inspect(module)}
<% end %> @@ -150,15 +150,15 @@ defmodule ElixirAiWeb.AdminLive do <%= if node_runners != [] do %>
-

+

Chat Runners - + {length(node_runners)}

<%= for {name, _, _} <- node_runners do %> -
+
{name}
<% end %> @@ -168,14 +168,14 @@ defmodule ElixirAiWeb.AdminLive do <%= if node_liveviews != %{} do %>
-

+

LiveViews

<%= for {view, instances} <- node_liveviews do %> -
- {short_module(view)} - ×{length(instances)} +
+ {short_module(view)} + ×{length(instances)}
<% end %>
@@ -183,7 +183,7 @@ defmodule ElixirAiWeb.AdminLive do <% end %> <%= if node_singletons == [] and node_runners == [] and node_liveviews == %{} do %> -

No active processes

+

No active processes

<% end %>
@@ -207,7 +207,7 @@ defmodule ElixirAiWeb.AdminLive do <% end %> -

Refreshes every 1s or on node events.

+

Refreshes every 1s or on node events.

""" end @@ -236,7 +236,7 @@ defmodule ElixirAiWeb.AdminLive do unreachable <% other -> %> - + {inspect(other)} <% end %> diff --git a/lib/elixir_ai_web/chat/chat_live.ex b/lib/elixir_ai_web/chat/chat_live.ex index b82d3bd..94ec723 100644 --- a/lib/elixir_ai_web/chat/chat_live.ex +++ b/lib/elixir_ai_web/chat/chat_live.ex @@ -24,7 +24,7 @@ defmodule ElixirAiWeb.ChatLive do |> assign(user_input: "") |> assign(messages: conversation.messages) |> assign(streaming_response: conversation.streaming_response) - |> assign(background_color: "bg-cyan-950/30") + |> assign(background_color: "bg-seafoam-950/30") |> assign(provider: conversation.provider) |> assign(providers: AiProvider.all()) |> assign(db_error: nil) @@ -42,7 +42,7 @@ defmodule ElixirAiWeb.ChatLive do |> assign(user_input: "") |> assign(messages: []) |> assign(streaming_response: nil) - |> assign(background_color: "bg-cyan-950/30") + |> assign(background_color: "bg-seafoam-950/30") |> assign(provider: nil) |> assign(providers: AiProvider.all()) |> assign(db_error: Exception.format(:error, reason)) @@ -54,7 +54,7 @@ defmodule ElixirAiWeb.ChatLive do ~H"""
- <.link navigate={~p"/"} class="text-cyan-700 hover:text-cyan-400 transition-colors"> + <.link navigate={~p"/"} class="text-seafoam-700 hover:text-seafoam-400 transition-colors"> ← {@conversation_name} diff --git a/lib/elixir_ai_web/chat/chat_message.ex b/lib/elixir_ai_web/chat/chat_message.ex index a8a6f39..33cf701 100644 --- a/lib/elixir_ai_web/chat/chat_message.ex +++ b/lib/elixir_ai_web/chat/chat_message.ex @@ -9,8 +9,8 @@ defmodule ElixirAiWeb.ChatMessage do def tool_result_message(assigns) do ~H""" -
-
+
+
- tool result - {@tool_call_id} + tool result + {@tool_call_id}
-
{@content}
+
{@content}
""" @@ -38,7 +38,7 @@ defmodule ElixirAiWeb.ChatMessage do def user_message(assigns) do ~H"""
-
+
{@content}
@@ -85,14 +85,14 @@ defmodule ElixirAiWeb.ChatMessage do <%= if @reasoning_content && @reasoning_content != "" do %>
@@ -146,14 +146,14 @@ defmodule ElixirAiWeb.ChatMessage do <%= if @reasoning_content && @reasoning_content != "" do %> + called
- <.tool_call_args arguments={@arguments} /> +
""" end @@ -279,17 +313,51 @@ defmodule ElixirAiWeb.ChatMessage do attr :arguments, :any, default: nil defp pending_tool_call(assigns) do + assigns = + assigns + |> assign(:_id, "tc-#{:erlang.phash2({assigns.name, assigns.arguments})}") + |> assign(:_truncated, truncate_args(assigns.arguments)) + ~H""" -
-
+
+
<.tool_call_icon /> - {@name} - - + {@name} + + {@_truncated} + + + + + running
- <.tool_call_args arguments={@arguments} /> +
""" end @@ -300,21 +368,50 @@ defmodule ElixirAiWeb.ChatMessage do defp success_tool_call(assigns) do assigns = - assign( - assigns, + assigns + |> assign( :result_str, case assigns.result do s when is_binary(s) -> s other -> inspect(other, pretty: true, limit: :infinity) end ) + |> assign(:_id, "tc-#{:erlang.phash2({assigns.name, assigns.arguments})}") + |> assign(:_truncated, truncate_args(assigns.arguments)) ~H""" -
-
+
+
<.tool_call_icon /> - {@name} - + {@name} + + {@_truncated} + + + + done
- <.tool_call_args arguments={@arguments} /> +
-
result
+
result
{@result_str}
@@ -344,12 +443,44 @@ defmodule ElixirAiWeb.ChatMessage do attr :error, :string, required: true defp error_tool_call(assigns) do + assigns = + assigns + |> assign(:_id, "tc-#{:erlang.phash2({assigns.name, assigns.arguments})}") + |> assign(:_truncated, truncate_args(assigns.arguments)) + ~H""" -
-
+
+
<.tool_call_icon /> - {@name} - + {@name} + + {@_truncated} + + + + error
- <.tool_call_args arguments={@arguments} /> +
error
{@error}
@@ -370,6 +503,24 @@ defmodule ElixirAiWeb.ChatMessage do """ end + defp truncate_args(nil), do: nil + defp truncate_args(""), do: nil + + defp truncate_args(args) when is_binary(args) do + compact = + case Jason.decode(args) do + {:ok, decoded} -> Jason.encode!(decoded) + _ -> args + end + + if String.length(compact) > 72, do: String.slice(compact, 0, 69) <> "\u2026", else: compact + end + + defp truncate_args(args) do + compact = Jason.encode!(args) + if String.length(compact) > 72, do: String.slice(compact, 0, 69) <> "\u2026", else: compact + end + attr :arguments, :any, default: nil defp tool_call_args(%{arguments: args} = assigns) when not is_nil(args) and args != "" do @@ -390,9 +541,9 @@ defmodule ElixirAiWeb.ChatMessage do ) ~H""" -
-
arguments
-
{@pretty_args}
+
+
arguments
+
{@pretty_args}
""" end diff --git a/lib/elixir_ai_web/chat/chat_provider_display.ex b/lib/elixir_ai_web/chat/chat_provider_display.ex index 00745c1..50f701e 100644 --- a/lib/elixir_ai_web/chat/chat_provider_display.ex +++ b/lib/elixir_ai_web/chat/chat_provider_display.ex @@ -13,27 +13,27 @@ defmodule ElixirAiWeb.ChatProviderDisplay do phx-click={JS.toggle(to: "#provider-dropdown")} class="flex items-center gap-2 text-xs min-w-0 cursor-pointer hover:opacity-80 transition-opacity" > -
+
<%= if @provider do %> - {@provider.name} - · - {@provider.model_name} + {@provider.name} + · + {@provider.model_name} <% else %> - No provider + No provider <% end %>