Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions assets/css/cart.lit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,14 @@
&:hover,
&:active,
&:focus {
background-color: #2c1760;
background-color: #2c1760;
}
}

&:hover,
&:active,
&:focus {
background-color: #6042BC;
background-color: #6042BC;
}

&:focus-visible {
Expand All @@ -205,15 +205,29 @@
z-index: 2;
width: .75rem;
height: .75rem;

[part='spinner-path'] {
stroke: hsl(260, 72%, 91%);
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}


}

[part='message']{
[part='messaage-tag']{
font-weight: bold;
color: #007BFF;
font-size: 0.9em;
}

[part='message-content'] {
margin-left: 20px;
font-size: 0.8em;
}
}


@keyframes rotate {
100% {
transform: rotate(360deg);
Expand Down
1 change: 1 addition & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ window.liveSocket = liveSocket
import './launch-cart-additem';
import './launch-cart';
import './launch-form';
import './launch-bot';
import './web-hooks';
import './form-emails';
import './launch-recaptcha';
64 changes: 64 additions & 0 deletions assets/js/launch-bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { html, LitElement, css } from 'lit'
import { customElement, property, query, state } from 'lit/decorators.js'
import { liveState, liveStateConfig } from 'phx-live-state';
import cartStyles from '../css/cart.lit.scss';


interface Message {
role: string;
content: string;
}

@customElement('launch-bot')
@liveState({
properties: ['conversation'],
events: {
send: ['add_message']
}
})


export class LaunchFormElement extends LitElement {
static styles = cartStyles;

@property()
@liveStateConfig('url')
url: string = '';

@state()
conversation: Array<Message> = [];

@property({attribute: 'bot-id'})
botId: string = '';

@query('#message-text')
messageText: HTMLInputElement | undefined;

@liveStateConfig('topic')
get topic() {return `launch_bot:${this.botId}`;}

sendMessage(ev: Event) {
ev.preventDefault();
console.log(this.conversation)
this.dispatchEvent(new CustomEvent('add_message', { detail: {text: this.messageText?.value}}));
this.messageText!.value = '';
}

render() {
return html `
<ul>
${this.conversation && this.conversation.map((message: Message) => html`
<li part="message">
<span part="message-tag">${message.role}</span>
<div part="message-content">${message.content}</div>
</li>
`)}
</ul>
<form>
<input type="text" id="message-text" name="message" />
<button @click=${this.sendMessage}>Send</button>
</form>
`;
}

}
4 changes: 3 additions & 1 deletion lib/launch_cart/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ defmodule LaunchCart.Application do
# Start the PubSub system
{Phoenix.PubSub, name: LaunchCart.PubSub},
# Start the Endpoint (http/https)
LaunchCartWeb.Endpoint
LaunchCartWeb.Endpoint,
# Start a worker by calling: LaunchCart.Worker.start_link(arg)
# {LaunchCart.Worker, arg}

{Task.Supervisor, name: LaunchCart.TaskSupervisor} #no idea what this does tbh Thanks ChatGPT :D
] ++
Application.get_env(:launch_cart, :supervised_processes, [
{Cachex,
Expand Down
112 changes: 112 additions & 0 deletions lib/launch_cart/bot.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
example_message = {:ok,
%{
choices: [
%{
"finish_reason" => "stop",
"index" => 0,
"message" => %{
"content" => "The maximum runway length at Butler County Airport (KHAO) is 5,000 feet.",
"role" => "assistant"
}
}
],
created: 1684089842,
id: "chatcmpl-7GAzKmrOqljU1X96BRUPVcDceBeVW",
model: "gpt-3.5-turbo-0301",
object: "chat.completion",
usage: %{
"completion_tokens" => 20,
"prompt_tokens" => 24,
"total_tokens" => 44
}
}}


defmodule LaunchCart.Bot do
require HTTPoison
require Logger

def stream(messages) do
url = "https://api.openai.com/v1/chat/completions"
body = Jason.encode!(body(messages, true))
headers = headers()

Stream.resource(
fn -> HTTPoison.post!(url, body, headers, stream_to: self(), async: :once) end,
&handle_async_response/1,
&close_async_response/1
)
end

defp close_async_response(resp) do
:hackney.stop_async(resp)
end

defp handle_async_response({:done, resp}) do
{:halt, resp}
end

defp handle_async_response(%HTTPoison.AsyncResponse{id: id} = resp) do
receive do
%HTTPoison.AsyncStatus{id: ^id, code: code} ->
Logger.info("openai,request,status,#{inspect(code)}")
HTTPoison.stream_next(resp)
{[], resp}

%HTTPoison.AsyncHeaders{id: ^id, headers: headers} ->
Logger.info("openai,request,headers,#{inspect(headers)}")
HTTPoison.stream_next(resp)
{[], resp}

%HTTPoison.AsyncChunk{id: ^id, chunk: chunk} ->
HTTPoison.stream_next(resp)
parse_chunk(chunk, resp)

%HTTPoison.AsyncEnd{id: ^id} ->
{:halt, resp}
end
end

defp parse_chunk(chunk, resp) do
{chunk, done?} =
chunk
|> String.split("data:")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> Enum.reduce({"", false}, fn trimmed, {chunk, is_done?} ->
case Jason.decode(trimmed) do
{:ok, res} ->
IO.inspect(res, label: "openai,response")
delta = List.first(res["choices"])["delta"]
content = Map.get(delta, "content") || ""
{chunk <> content, is_done? or false}

{:error, %{data: "[DONE]"}} ->
{chunk, is_done? or true}
end
end)

if done? do
{[chunk], {:done, resp}}
else
{[chunk], resp}
end
end

defp headers() do
[
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer #{System.get_env("OPENAI_API_KEY")}"
]
end

defp body(messages, streaming?) do
%{
model: "gpt-3.5-turbo",
messages: messages,
stream: streaming?,
max_tokens: 1024
}
end
end
17 changes: 17 additions & 0 deletions lib/launch_cart/stream_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule LaunchCart.StreamHandler do
use GenServer

def start_link({channel_pid, stream}) do
GenServer.start_link(__MODULE__, {channel_pid, stream})
end

@spec init({any, any}) :: {:ok, %{channel_pid: any}}
def init({channel_pid, stream}) do
Task.start_link(fn ->
for chunk <- stream do
send(channel_pid, {:render_response_chunk, chunk})
end
end)
{:ok, %{channel_pid: channel_pid}}
end
end
62 changes: 62 additions & 0 deletions lib/launch_cart_web/channels/launch_bot_channel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
defmodule LaunchCartWeb.LaunchBotChannel do
use LiveState.Channel, web_module: LaunchCartWeb

alias LaunchCart.Bot

@impl true
def init("launch_bot:" <> bot_id, _params, _socket) do
{:ok, %{bot_id: bot_id, conversation: []}}
end

# @impl true
# def handle_event("add_message", %{"text" => text}, %{conversation: messages} = state) do
# messages = messages ++ [%{"content" => text, "role" => "user"}]
# case OpenAI.chat_completion(model: "gpt-3.5-turbo", messages: messages) do
# {:ok, %{choices: [%{"message" => message} | _]}} ->
# IO.inspect(messages ++ message)
# {:noreply, %{conversation: messages ++ [message]} }
# {:error, error} ->
# IO.inspect(error)
# {:noreply, state}
# end
# end


# @impl true
# def handle_event("add_message", %{"text" => text}, %{conversation: messages} = state) do
# messages = messages ++ [%{"content" => text, "role" => "user"}]
# Bot.stream(messages)
# |> Enum.take(1)
# |> Enum.each(fn message ->
# IO.inspect( messages ++ [%{"content" => message, "role" => "user"}])
# # push(socket, "new_message", %{"content" => message})
# {:noreply, %{conversation: messages ++ [%{"content" => message, "role" => "user"}]} }
# end)
# # {:noreply, %{conversation: messages}}
# end

# @impl true
# def handle_event("add_message", %{"text" => text}, %{conversation: messages} = state) do
# messages = messages ++ [%{"content" => text, "role" => "user"}]
# stream = Bot.stream(messages)
# {:noreply, %{conversation: messages ++ [%{"content" => stream_response(stream), "role" => "user"}]} }
# end

@impl true
def handle_event("add_message", %{"text" => text}, %{conversation: messages} = state) do
messages = [%{"content" => text, "role" => "user"} | messages]
IO.inspect(Enum.reverse(messages))
stream = Bot.stream(Enum.reverse(messages))
messages = [%{"content" => "", "role" => "assistant"} | messages]
{:ok, _pid} = LaunchCart.StreamHandler.start_link({self(), stream})

{:noreply, %{conversation: messages}}
end

@impl true
def handle_message({:render_response_chunk, chunk}, %{conversation: [%{"content" => current_message} | messages ]} = state) do
current_message = "#{current_message}#{chunk}"
IO.inspect(current_message)
{:noreply, %{conversation: [%{"content" => current_message, "role" => "assistant"} | messages]}}
end
end
2 changes: 2 additions & 0 deletions lib/launch_cart_web/channels/user_socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule LaunchCartWeb.UserSocket do

channel "launch_form:*", LaunchCartWeb.LaunchFormChannel

channel "launch_bot:*", LaunchCartWeb.LaunchBotChannel

# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
Expand Down
5 changes: 5 additions & 0 deletions lib/launch_cart_web/controllers/page_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ defmodule LaunchCartWeb.PageController do
render(conn, "usage_docs.html")
end

def launch_bot(conn, _params) do
url = "#{String.replace(Endpoint.url(), "http:", "ws:")}/socket"
render(conn, "launch_bot.html", url: url)
end

def fake_store(conn, %{"store_id" => store_id}) do
store = Stores.get_store!(store_id)
url = "#{String.replace(Endpoint.url(), "http:", "ws:")}/socket"
Expand Down
1 change: 1 addition & 0 deletions lib/launch_cart_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule LaunchCartWeb.Router do
get "/", PageController, :index
get "/api_docs", PageController, :api_docs
get "/usage_docs", PageController, :usage_docs
get "/launch_bot", PageController, :launch_bot
end

# Other scopes may use custom stacks.
Expand Down
3 changes: 3 additions & 0 deletions lib/launch_cart_web/templates/page/launch_bot.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1>Launch bot</h1>

<launch-bot url={@url} bot-id="1234"></launch-bot>
6 changes: 4 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ defmodule LaunchCart.MixProject do
{:swoosh, "~> 1.3"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:httpoison, ">= 0.0.0"},
{:httpoison, "~> 2.0"},
{:poison, "~> 5.0"},
{:live_elements, ">= 0.0.0"},
{:wallaby, "~> 0.30.2",
git: "https://github.com/launchscout/wallaby.git",
branch: "shadow-dom",
runtime: false,
only: :test}
only: :test},
{:openai, "~> 0.5.2"}
]
end

Expand Down
Loading