This commit is contained in:
@@ -38,6 +38,10 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
GenServer.call(via(name), {:tool_config, {:set_tool_choice, tool_choice}})
|
GenServer.call(via(name), {:tool_config, {:set_tool_choice, tool_choice}})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_provider(name, provider_id) when is_binary(provider_id) do
|
||||||
|
GenServer.call(via(name), {:tool_config, {:set_provider, provider_id}})
|
||||||
|
end
|
||||||
|
|
||||||
def register_liveview_pid(name, liveview_pid) when is_pid(liveview_pid) do
|
def register_liveview_pid(name, liveview_pid) when is_pid(liveview_pid) do
|
||||||
GenServer.call(via(name), {:session, {:register_liveview_pid, liveview_pid}})
|
GenServer.call(via(name), {:session, {:register_liveview_pid, liveview_pid}})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
defmodule ElixirAi.ChatRunner.ToolConfig do
|
defmodule ElixirAi.ChatRunner.ToolConfig do
|
||||||
alias ElixirAi.{AiTools, Conversation}
|
alias ElixirAi.{AiProvider, AiTools, Conversation}
|
||||||
|
|
||||||
def handle_call({:set_tool_choice, tool_choice}, _from, state) do
|
def handle_call({:set_tool_choice, tool_choice}, _from, state) do
|
||||||
Conversation.update_tool_choice(state.name, tool_choice)
|
Conversation.update_tool_choice(state.name, tool_choice)
|
||||||
@@ -19,4 +19,13 @@ defmodule ElixirAi.ChatRunner.ToolConfig do
|
|||||||
liveview_tools: liveview_tools
|
liveview_tools: liveview_tools
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_call({:set_provider, provider_id}, _from, state) do
|
||||||
|
with :ok <- Conversation.update_provider(state.name, provider_id),
|
||||||
|
{:ok, provider} <- AiProvider.find_by_id(provider_id) do
|
||||||
|
{:reply, {:ok, provider}, %{state | provider: provider}}
|
||||||
|
else
|
||||||
|
error -> {:reply, error, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -114,6 +114,34 @@ defmodule ElixirAi.AiProvider do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_by_id(id) do
|
||||||
|
case Ecto.UUID.dump(id) do
|
||||||
|
{:ok, binary_id} ->
|
||||||
|
sql = """
|
||||||
|
SELECT id, name, model_name, api_token, completions_url
|
||||||
|
FROM ai_providers
|
||||||
|
WHERE id = $(id)
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = %{"id" => binary_id}
|
||||||
|
|
||||||
|
case DbHelpers.run_sql(sql, params, providers_topic(), AiProviderSchema.schema()) do
|
||||||
|
{:error, _} ->
|
||||||
|
{:error, :db_error}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
[row | _] ->
|
||||||
|
{:ok, row |> convert_uuid_to_string() |> then(&struct(AiProviderSchema, &1))}
|
||||||
|
end
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:error, :invalid_uuid}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def delete(id) do
|
def delete(id) do
|
||||||
sql = "DELETE FROM ai_providers WHERE id = $(id)::uuid"
|
sql = "DELETE FROM ai_providers WHERE id = $(id)::uuid"
|
||||||
params = %{"id" => id}
|
params = %{"id" => id}
|
||||||
|
|||||||
@@ -173,6 +173,31 @@ defmodule ElixirAi.Conversation do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_provider(name, provider_id) when is_binary(provider_id) do
|
||||||
|
case Ecto.UUID.dump(provider_id) do
|
||||||
|
{:ok, binary_id} ->
|
||||||
|
sql = """
|
||||||
|
UPDATE conversations
|
||||||
|
SET ai_provider_id = $(ai_provider_id), updated_at = $(updated_at)
|
||||||
|
WHERE name = $(name)
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
"name" => name,
|
||||||
|
"ai_provider_id" => binary_id,
|
||||||
|
"updated_at" => now()
|
||||||
|
}
|
||||||
|
|
||||||
|
case DbHelpers.run_sql(sql, params, "conversations") do
|
||||||
|
{:error, :db_error} -> {:error, :db_error}
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:error, :invalid_uuid}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def find_id(name) do
|
def find_id(name) do
|
||||||
sql = "SELECT id FROM conversations WHERE name = $(name) LIMIT 1"
|
sql = "SELECT id FROM conversations WHERE name = $(name) LIMIT 1"
|
||||||
params = %{"name" => name}
|
params = %{"name" => name}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
import ElixirAiWeb.Spinner
|
import ElixirAiWeb.Spinner
|
||||||
import ElixirAiWeb.ChatMessage
|
import ElixirAiWeb.ChatMessage
|
||||||
import ElixirAiWeb.ChatProviderDisplay
|
import ElixirAiWeb.ChatProviderDisplay
|
||||||
alias ElixirAi.ChatRunner
|
alias ElixirAi.{AiProvider, ChatRunner, ConversationManager}
|
||||||
alias ElixirAi.ConversationManager
|
|
||||||
import ElixirAi.PubsubTopics
|
import ElixirAi.PubsubTopics
|
||||||
|
|
||||||
def mount(%{"name" => name}, _session, socket) do
|
def mount(%{"name" => name}, _session, socket) do
|
||||||
@@ -27,6 +26,7 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
|> assign(streaming_response: conversation.streaming_response)
|
|> assign(streaming_response: conversation.streaming_response)
|
||||||
|> assign(background_color: "bg-cyan-950/30")
|
|> assign(background_color: "bg-cyan-950/30")
|
||||||
|> assign(provider: conversation.provider)
|
|> assign(provider: conversation.provider)
|
||||||
|
|> assign(providers: AiProvider.all())
|
||||||
|> assign(db_error: nil)
|
|> assign(db_error: nil)
|
||||||
|> assign(ai_error: nil)}
|
|> assign(ai_error: nil)}
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
|> assign(streaming_response: nil)
|
|> assign(streaming_response: nil)
|
||||||
|> assign(background_color: "bg-cyan-950/30")
|
|> assign(background_color: "bg-cyan-950/30")
|
||||||
|> assign(provider: nil)
|
|> assign(provider: nil)
|
||||||
|
|> assign(providers: AiProvider.all())
|
||||||
|> assign(db_error: Exception.format(:error, reason))
|
|> assign(db_error: Exception.format(:error, reason))
|
||||||
|> assign(ai_error: nil)}
|
|> assign(ai_error: nil)}
|
||||||
end
|
end
|
||||||
@@ -57,7 +58,7 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
←
|
←
|
||||||
</.link>
|
</.link>
|
||||||
<span class="flex-1">{@conversation_name}</span>
|
<span class="flex-1">{@conversation_name}</span>
|
||||||
<.chat_provider_display provider={@provider} />
|
<.chat_provider_display provider={@provider} providers={@providers} />
|
||||||
</div>
|
</div>
|
||||||
<%= if @db_error do %>
|
<%= if @db_error do %>
|
||||||
<div class="mx-4 mt-2 px-3 py-2 rounded text-sm text-red-400 bg-red-950/40" role="alert">
|
<div class="mx-4 mt-2 px-3 py-2 rounded text-sm text-red-400 bg-red-950/40" role="alert">
|
||||||
@@ -122,6 +123,13 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
{:noreply, assign(socket, user_input: user_input)}
|
{:noreply, assign(socket, user_input: user_input)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("change_provider", %{"id" => provider_id}, socket) do
|
||||||
|
case ChatRunner.set_provider(socket.assigns.conversation_name, provider_id) do
|
||||||
|
{:ok, provider} -> {:noreply, assign(socket, provider: provider)}
|
||||||
|
_error -> {:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("submit", %{"user_input" => user_input}, socket) when user_input != "" do
|
def handle_event("submit", %{"user_input" => user_input}, socket) when user_input != "" do
|
||||||
ChatRunner.new_user_message(socket.assigns.conversation_name, user_input)
|
ChatRunner.new_user_message(socket.assigns.conversation_name, user_input)
|
||||||
{:noreply, assign(socket, user_input: "")}
|
{:noreply, assign(socket, user_input: "")}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
defmodule ElixirAiWeb.ChatProviderDisplay do
|
defmodule ElixirAiWeb.ChatProviderDisplay do
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
alias Phoenix.LiveView.JS
|
||||||
|
|
||||||
attr :provider, :any, default: nil
|
attr :provider, :any, default: nil
|
||||||
|
attr :providers, :list, default: []
|
||||||
def chat_provider_display(%{provider: nil} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="flex items-center gap-1.5 text-xs text-cyan-900 italic">
|
|
||||||
No provider set
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def chat_provider_display(assigns) do
|
def chat_provider_display(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex items-center gap-2 text-xs min-w-0">
|
<div class="relative" id="provider-display">
|
||||||
<div class="flex items-center gap-1.5 px-2 py-1 rounded bg-cyan-950/50 border border-cyan-900/40 min-w-0">
|
<button
|
||||||
|
type="button"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5 px-2 py-1 rounded bg-cyan-950/50 border border-cyan-900/40 min-w-0 select-none">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -23,9 +22,61 @@ defmodule ElixirAiWeb.ChatProviderDisplay do
|
|||||||
>
|
>
|
||||||
<path d="M10 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM6 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM1.49 15.326a.78.78 0 0 1-.358-.442 3 3 0 0 1 4.308-3.516 6.484 6.484 0 0 0-1.905 3.959c-.023.222-.014.442.025.654a4.97 4.97 0 0 1-2.07-.655ZM16.44 15.98a4.97 4.97 0 0 0 2.07-.654.78.78 0 0 0 .357-.442 3 3 0 0 0-4.308-3.516 6.484 6.484 0 0 1 1.907 3.96 2.32 2.32 0 0 1-.026.654ZM18 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.304 16.19a.844.844 0 0 1-.277-.71 5 5 0 0 1 9.947 0 .843.843 0 0 1-.277.71A6.975 6.975 0 0 1 10 18a6.974 6.974 0 0 1-4.696-1.81Z" />
|
<path d="M10 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM6 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM1.49 15.326a.78.78 0 0 1-.358-.442 3 3 0 0 1 4.308-3.516 6.484 6.484 0 0 0-1.905 3.959c-.023.222-.014.442.025.654a4.97 4.97 0 0 1-2.07-.655ZM16.44 15.98a4.97 4.97 0 0 0 2.07-.654.78.78 0 0 0 .357-.442 3 3 0 0 0-4.308-3.516 6.484 6.484 0 0 1 1.907 3.96 2.32 2.32 0 0 1-.026.654ZM18 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.304 16.19a.844.844 0 0 1-.277-.71 5 5 0 0 1 9.947 0 .843.843 0 0 1-.277.71A6.975 6.975 0 0 1 10 18a6.974 6.974 0 0 1-4.696-1.81Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<%= if @provider do %>
|
||||||
<span class="text-cyan-400 font-medium truncate">{@provider.name}</span>
|
<span class="text-cyan-400 font-medium truncate">{@provider.name}</span>
|
||||||
<span class="text-cyan-800">·</span>
|
<span class="text-cyan-800">·</span>
|
||||||
<span class="text-cyan-600 truncate">{@provider.model_name}</span>
|
<span class="text-cyan-600 truncate">{@provider.model_name}</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-cyan-800 italic">No provider</span>
|
||||||
|
<% end %>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-2.5 h-2.5 text-cyan-700 ml-0.5 shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
id="provider-dropdown"
|
||||||
|
class="hidden absolute right-0 top-full mt-1 z-50 min-w-max bg-gray-950 border border-cyan-900/40 rounded shadow-xl overflow-hidden"
|
||||||
|
phx-click-away={JS.hide(to: "#provider-dropdown")}
|
||||||
|
>
|
||||||
|
<%= if @providers == [] do %>
|
||||||
|
<div class="px-3 py-2 text-xs text-gray-500 italic">No providers configured</div>
|
||||||
|
<% else %>
|
||||||
|
<%= for p <- @providers do %>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click={
|
||||||
|
JS.hide(to: "#provider-dropdown")
|
||||||
|
|> JS.push("change_provider", value: %{id: p.id})
|
||||||
|
}
|
||||||
|
class={[
|
||||||
|
"flex flex-col px-3 py-2 text-left w-full text-xs hover:bg-cyan-950/60 transition-colors",
|
||||||
|
if(@provider && @provider.name == p.name,
|
||||||
|
do: "text-cyan-400",
|
||||||
|
else: "text-gray-300"
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<span class="font-medium">{p.name}</span>
|
||||||
|
<span class={
|
||||||
|
if @provider && @provider.name == p.name,
|
||||||
|
do: "text-cyan-700",
|
||||||
|
else: "text-gray-500"
|
||||||
|
}>
|
||||||
|
{p.model_name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user