Current section
Files
Jump to
Current section
Files
lib/superintelligence_web/live/chat_live.ex
defmodule SuperintelligenceWeb.ChatLive do
use SuperintelligenceWeb, :live_view
alias Superintelligence.Conversations
alias Superintelligence.Conversations.{Conversation, Message}
alias Superintelligence.AIAgent
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Conversations.subscribe()
end
conversations = Conversations.list_conversations()
socket =
socket
|> assign(:conversations, conversations)
|> assign(:current_conversation, nil)
|> assign(:messages, [])
|> assign(:message_input, "")
|> assign(:processing, false)
|> assign(:visualization_data, nil)
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _uri, socket) do
case Conversations.get_conversation(id) do
{:ok, conversation} ->
if connected?(socket) do
Conversations.subscribe_to_conversation(id)
end
messages = Conversations.list_messages(id)
{:noreply,
socket
|> assign(:current_conversation, conversation)
|> assign(:messages, messages)}
{:error, :not_found} ->
{:noreply,
socket
|> put_flash(:error, "Conversation not found")
|> push_navigate(to: ~p"/chat")}
end
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
@impl true
def handle_event("new_conversation", _params, socket) do
{:ok, conversation} = Conversations.create_conversation()
{:noreply,
socket
|> push_navigate(to: ~p"/chat/#{conversation.id}")}
end
@impl true
def handle_event("select_conversation", %{"id" => id}, socket) do
{:noreply, push_navigate(socket, to: ~p"/chat/#{id}")}
end
@impl true
def handle_event("delete_conversation", %{"id" => id}, socket) do
with {:ok, conversation} <- Conversations.get_conversation(id),
{:ok, _} <- Conversations.delete_conversation(conversation) do
socket =
if socket.assigns.current_conversation && socket.assigns.current_conversation.id == id do
socket
|> assign(:current_conversation, nil)
|> assign(:messages, [])
|> push_navigate(to: ~p"/chat")
else
socket
end
{:noreply, socket}
else
_ ->
{:noreply, put_flash(socket, :error, "Failed to delete conversation")}
end
end
@impl true
def handle_event("update_message_input", %{"value" => value}, socket) do
{:noreply, assign(socket, :message_input, value)}
end
@impl true
def handle_event("send_message", _params, socket) do
if socket.assigns.current_conversation && String.trim(socket.assigns.message_input) != "" do
# Create user message
{:ok, _user_message} = Conversations.create_message(
socket.assigns.current_conversation.id,
%{
role: "user",
content: socket.assigns.message_input
}
)
# Process with AI agent
socket =
socket
|> assign(:message_input, "")
|> assign(:processing, true)
Task.start(fn ->
process_ai_response(
socket.assigns.current_conversation.id,
socket.assigns.message_input
)
end)
{:noreply, socket}
else
{:noreply, socket}
end
end
@impl true
def handle_event("clear_visualization", _params, socket) do
{:noreply,
socket
|> assign(:visualization_data, nil)
|> push_event("draw", %{type: "clear", data: %{}})}
end
@impl true
def handle_event("demo_visualization", %{"type" => type}, socket) do
data = case type do
"chart" ->
%{
type: "bar",
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
values: [65, 59, 80, 81, 56]
}
"custom" ->
%{
commands: [
%{method: "beginPath", args: []},
%{method: "arc", args: [150, 150, 50, 0, 2 * :math.pi()]},
%{method: "fillStyle", args: ["#4F46E5"]},
%{method: "fill", args: []}
]
}
_ ->
%{}
end
{:noreply,
socket
|> assign(:visualization_data, data)
|> push_event("draw", %{type: type, data: data})}
end
@impl true
def handle_info({:conversation_created, conversation}, socket) do
conversations = [conversation | socket.assigns.conversations]
{:noreply, assign(socket, :conversations, conversations)}
end
@impl true
def handle_info({:conversation_updated, conversation}, socket) do
conversations =
socket.assigns.conversations
|> Enum.map(fn c ->
if c.id == conversation.id, do: conversation, else: c
end)
|> Enum.sort_by(& &1.updated_at, {:desc, DateTime})
socket = assign(socket, :conversations, conversations)
socket =
if socket.assigns.current_conversation && socket.assigns.current_conversation.id == conversation.id do
assign(socket, :current_conversation, conversation)
else
socket
end
{:noreply, socket}
end
@impl true
def handle_info({:conversation_deleted, conversation}, socket) do
conversations =
socket.assigns.conversations
|> Enum.reject(fn c -> c.id == conversation.id end)
{:noreply, assign(socket, :conversations, conversations)}
end
@impl true
def handle_info({:message_created, message}, socket) do
if socket.assigns.current_conversation && message.conversation_id == socket.assigns.current_conversation.id do
messages = socket.assigns.messages ++ [message]
socket =
socket
|> assign(:messages, messages)
|> assign(:processing, false)
# Handle visualization commands in AI responses
socket =
if message.role == "assistant" && message.metadata[:visualization] do
push_event(socket, "draw", message.metadata[:visualization])
else
socket
end
{:noreply, socket}
else
{:noreply, socket}
end
end
defp process_ai_response(conversation_id, user_message) do
# Simulate AI processing
Process.sleep(1000)
# Generate response based on message content
{response, visualization} = generate_ai_response(user_message)
metadata = if visualization, do: %{visualization: visualization}, else: %{}
Conversations.create_message(
conversation_id,
%{
role: "assistant",
content: response,
metadata: metadata
}
)
end
defp generate_ai_response(message) do
cond do
String.contains?(String.downcase(message), ["chart", "graph", "visualiz"]) ->
{
"I'll create a visualization for you. Here's a sample chart showing monthly data:",
%{
type: "chart",
data: %{
type: "bar",
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
values: Enum.map(1..5, fn _ -> :rand.uniform(100) end)
}
}
}
String.contains?(String.downcase(message), "hello") ->
{"Hello! I'm your AI assistant. I can help you with various tasks and create visualizations. How can I assist you today?", nil}
true ->
{"I understand your request. Let me help you with that. Feel free to ask me to create visualizations or analyze data!", nil}
end
end
@impl true
def render(assigns) do
~H"""
<div class="flex h-screen bg-background">
<!-- Conversations List -->
<div class="w-80 conversation-list flex flex-col">
<div class="p-4 border-b border-border">
<button
phx-click="new_conversation"
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
<div class="flex items-center justify-center gap-2">
<.icon name="hero-plus" class="w-5 h-5" />
<span>New Conversation</span>
</div>
</button>
</div>
<div class="flex-1 overflow-y-auto scrollbar-thin">
<div class="p-2">
<%= for conversation <- @conversations do %>
<div
class={[
"group relative p-3 rounded-lg cursor-pointer transition-colors mb-1",
if(@current_conversation && @current_conversation.id == conversation.id,
do: "bg-secondary",
else: "hover:bg-secondary/50"
)
]}
phx-click="select_conversation"
phx-value-id={conversation.id}
>
<div class="pr-8">
<h3 class="font-medium text-sm"><%= conversation.title %></h3>
<p class="text-xs text-muted-foreground mt-1">
<%= format_datetime(conversation.updated_at) %>
</p>
</div>
<button
class="absolute right-2 top-3 opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/10 rounded"
phx-click="delete_conversation"
phx-value-id={conversation.id}
phx-click-away
>
<.icon name="hero-trash" class="w-4 h-4 text-destructive" />
</button>
</div>
<% end %>
</div>
</div>
</div>
<!-- Main Chat Area -->
<div class="flex-1 flex flex-col">
<%= if @current_conversation do %>
<!-- Chat Header -->
<div class="p-4 border-b border-border">
<h1 class="text-xl font-semibold"><%= @current_conversation.title %></h1>
</div>
<!-- Messages Area -->
<div class="flex-1 flex gap-4 p-4 overflow-hidden">
<!-- Chat Messages -->
<div class="flex-1 flex flex-col">
<div
id="chat-messages"
class="chat-messages px-4 py-2"
phx-hook="ChatMessages"
>
<%= for message <- @messages do %>
<div class={[
"mb-4 flex",
if(message.role == "user", do: "justify-end", else: "justify-start")
]}>
<div class={[
"max-w-[70%] px-4 py-2 rounded-lg",
if(message.role == "user",
do: "bg-primary text-primary-foreground",
else: "bg-secondary text-secondary-foreground"
)
]}>
<p class="whitespace-pre-wrap"><%= message.content %></p>
</div>
</div>
<% end %>
<%= if @processing do %>
<div class="mb-4 flex justify-start">
<div class="bg-secondary text-secondary-foreground px-4 py-2 rounded-lg">
<div class="flex items-center gap-2">
<span class="animate-pulse">Thinking...</span>
</div>
</div>
</div>
<% end %>
</div>
<!-- Input Area -->
<div class="border-t border-border pt-4">
<form phx-submit="send_message" class="flex gap-2">
<input
type="text"
value={@message_input}
phx-keyup="update_message_input"
phx-value-value
class="flex-1 px-4 py-2 bg-input border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Type your message..."
disabled={@processing}
/>
<button
type="submit"
class="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
disabled={@processing || String.trim(@message_input) == ""}
>
<.icon name="hero-paper-airplane" class="w-5 h-5" />
</button>
</form>
</div>
</div>
<!-- Visualization Canvas -->
<div class="w-96 visualization-canvas">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-medium">Visualization</h3>
<div class="flex gap-1">
<button
phx-click="demo_visualization"
phx-value-type="chart"
class="px-2 py-1 text-xs bg-secondary hover:bg-secondary/80 rounded"
>
Demo Chart
</button>
<button
phx-click="clear_visualization"
class="px-2 py-1 text-xs bg-secondary hover:bg-secondary/80 rounded"
>
Clear
</button>
</div>
</div>
<div id="visualization-canvas" phx-hook="VisualizationCanvas" class="w-full h-full">
<canvas class="w-full h-full"></canvas>
</div>
</div>
</div>
<% else %>
<!-- No Conversation Selected -->
<div class="flex-1 flex items-center justify-center">
<div class="text-center">
<.icon name="hero-chat-bubble-left-right" class="w-16 h-16 mx-auto text-muted-foreground mb-4" />
<h2 class="text-xl font-semibold mb-2">No Conversation Selected</h2>
<p class="text-muted-foreground mb-4">Select a conversation or create a new one to start chatting</p>
<button
phx-click="new_conversation"
class="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
Start New Conversation
</button>
</div>
</div>
<% end %>
</div>
</div>
"""
end
defp format_datetime(datetime) do
Calendar.strftime(datetime, "%b %d, %I:%M %p")
end
end