1
0
mirror of https://github.com/algora-io/tv.git synced 2025-03-17 20:17:45 +02:00
algora-tv/lib/algora_web/live/cossgpt_og_live.ex
2024-05-22 17:53:10 +03:00

276 lines
9.7 KiB
Elixir

defmodule AlgoraWeb.COSSGPTOGLive do
use AlgoraWeb, :live_view
alias Algora.{Library, ML, Cache, Util}
@impl true
def render(assigns) do
~H"""
<div class="px-4 py-8 text-white mx-auto overflow-hidden flex gap-[20rem]">
<div class="ml-[18rem] h-[calc(100vh-88px)] flex flex-col items-center justify-center scale-150">
<h1 class="flex items-center gap-4 text-8xl font-bold font-mono text-green-300 [text-shadow:#000_10px_5px_10px]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-20 w-20 text-green-300"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" />
</svg>
<span class="text-green-300">COSS</span><span class="text-green-400">gpt</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-20 w-20 text-green-400"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" />
</svg>
</h1>
<form class="mt-10 w-full max-w-lg mx-auto scale-[1.3]" phx-submit="search">
<label for="query" class="mb-2 text-sm font-medium sr-only text-white">
Search
</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none">
<svg
class="w-8 h-8 text-green-200"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<input
type="search"
id="query"
name="query"
value={@query}
autocomplete="off"
class="block w-full p-4 ps-14 border rounded-lg border-green-400 bg-white/[5%] placeholder-gray-400 text-green-100 ring-4 ring-green-400 focus:outline-none text-2xl font-medium"
placeholder="Anything about commercial open-source software..."
required
/>
<button
type="submit"
class="text-green-950 text-2xl absolute end-2.5 bottom-2.5 focus:ring-4 focus:outline-none font-bold rounded-lg px-4 py-2 bg-green-200 hover:bg-green-700 focus:ring-green-800"
>
Learn
</button>
</div>
</form>
<div class="mt-20 scale-[1.37]">
<div class="uppercase text-center text-gray-300 tracking-tight text-xs font-semibold">
Suggestions
</div>
<div class="mt-2 flex flex-wrap gap-4 justify-center max-w-2xl mx-auto">
<button
:for={
suggestion <- [
"Business models and pricing",
"Choosing a license",
"How to hire engineers"
]
}
phx-click="search"
phx-value-query={suggestion}
class="bg-white/10 text-gray-200 font-medium text-sm px-3 py-2 ring-1 ring-white/20 shadow-inner inline-flex rounded-lg hover:ring-white/25 hover:bg-white/5 hover:text-white transition-colors"
>
<%= suggestion %>
</button>
</div>
</div>
</div>
<div class="flex">
<div :if={@results} class="flex-1 space-y-8">
<div
:for={%{video: video, segments: segments} <- @results}
class="flex flex-col lg:flex-row gap-8"
>
<.link
navigate={video_url(video, Enum.at(segments, 0))}
class="w-full shrink-0 max-w-[40rem]"
>
<.video_thumbnail video={video} class="w-full rounded-2xl" />
</.link>
<div>
<div>
<.link
navigate={video_url(video, Enum.at(segments, 0))}
class="text-lg font-bold line-clamp-2"
>
<%= video.title %>
</.link>
<p class="text-sm text-gray-300"><%= Timex.from_now(video.inserted_at) %></p>
<.link navigate={"/#{video.channel_handle}"} class="mt-2 flex items-center gap-2">
<span class="relative flex items-center h-8 w-8 shrink-0 overflow-hidden rounded-full">
<img
class="aspect-square h-full w-full"
alt={video.channel_name}
src={video.channel_avatar_url}
/>
</span>
<span class="text-sm text-gray-300"><%= video.channel_name %></span>
</.link>
</div>
<div class="mt-4 relative">
<div class="w-full h-full pointer-events-none absolute bg-gradient-to-r from-transparent from-[75%] to-gray-900 rounded-xl">
</div>
<div class="bg-white/[7.5%] border border-white/[20%] p-4 rounded-xl flex gap-8 w-[calc(100vw-2rem)] lg:w-[22rem] xl:w-[40rem] overflow-x-auto pb-4 -mb-4 scrollbar-thin">
<.link
:for={segment <- segments}
class="space-x-2"
navigate={video_url(video, segment)}
>
<div class="w-[66vw] lg:w-[20rem] xl:w-[28rem]">
<p class="text-base font-semibold text-green-400">
<%= Library.to_hhmmss(segment.start) %>
</p>
<p class="mt-2 text-sm">
<span
:for={word <- segment.body |> String.split(~r/\s/)}
class={[matches_query?(@query_words, word) && "text-green-300 font-medium"]}
>
<%= word %>
</span>
</p>
</div>
</.link>
</div>
</div>
</div>
</div>
</div>
<div :if={@task} class="flex-1 space-y-8">
<div :for={_ <- 1..2} class="flex gap-8">
<div class="w-1/2 rounded-2xl aspect-video bg-white/20 animate-pulse"></div>
<div class="w-1/2 rounded-2xl aspect-video bg-white/20 animate-pulse"></div>
</div>
</div>
</div>
</div>
"""
end
defp video_url(video, segment) do
params =
case segment do
nil -> ""
s -> "?t=#{trunc(s.start)}"
end
"/#{video.channel_handle}/#{video.id}#{params}"
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, socket |> apply_action(socket.assigns.live_action, params)}
end
@impl true
def handle_event("search", %{"query" => query}, socket) do
{:noreply, socket |> push_patch(to: ~p"/og/cossgpt?#{%{query: query}}")}
end
@impl true
def handle_info({ref, result}, socket) when socket.assigns.task.ref == ref do
{:noreply, assign(socket, task: nil, results: result)}
end
def handle_info(_, socket) do
{:noreply, socket}
end
defp apply_action(socket, :index, params) do
socket =
case params["query"] || "" do
"" ->
socket
|> assign(
query: nil,
query_words: nil,
task: nil,
results: nil
)
query ->
socket
|> assign(
query: query,
query_words: query |> String.split(~r/\s/) |> Enum.map(&normalize_word/1),
task: Task.async(fn -> fetch_results(query) end),
results: nil
)
end
socket |> assign(:page_title, "COSSgpt")
end
defp fetch_results(query) do
[%{"embedding" => embedding}] =
Cache.fetch("embeddings/#{Slug.slugify(query)}", fn ->
ML.create_embedding(query)
end)
index = ML.load_index!()
segments = ML.get_relevant_chunks(index, embedding)
to_result = fn video ->
%{
video: video,
segments: segments |> Enum.filter(fn s -> s.video_id == video.id end)
}
end
segments
|> Enum.map(fn %Library.Segment{video_id: video_id} -> video_id end)
|> Enum.uniq()
|> Library.list_videos_by_ids()
|> Enum.map(to_result)
end
defp normalize_word(s) do
s
|> String.replace(~r/[^A-Za-z0-9]/, "")
|> String.downcase()
end
defp matches_query?(query_words, s) do
query_words
|> Enum.any?(fn s2 ->
s1 = normalize_word(s)
String.length(s1) >= 3 and
String.length(s2) >= 3 and
(String.contains?(s1, s2) or String.contains?(s2, s1)) and
!Util.common_word?(s1) and
!Util.common_word?(s2)
end)
end
end