No messages yet.
<% end %> <%= for msg <- @messages do %> -diff --git a/assets/css/app.css b/assets/css/app.css index d69f0c0..ca5187e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -58,55 +58,17 @@ legend { @apply text-sm font-semibold text-cyan-400 px-1; } -/* Spinner */ -.loader { - width: 48px; - height: 48px; - display: inline-block; - position: relative; - transform: rotate(45deg); -} -.loader::before { - content: ''; - box-sizing: border-box; - width: 24px; - height: 24px; - position: absolute; - left: 0; - top: -24px; - animation: animloader 4s ease infinite; -} -.loader::after { - content: ''; - box-sizing: border-box; - position: absolute; - left: 0; - top: 0; - width: 24px; - height: 24px; - background: rgba(255, 255, 255, 0.85); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); - animation: animloader2 2s ease infinite; -} +@import "./spinner.css"; +@import "./markdown.css"; -@keyframes animloader { - 0% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } - 12% { box-shadow: 0 24px white, 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } - 25% { box-shadow: 0 24px white, 24px 24px white, 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } - 37% { box-shadow: 0 24px white, 24px 24px white, 24px 48px white, 0px 48px rgba(255,255,255,0); } - 50% { box-shadow: 0 24px white, 24px 24px white, 24px 48px white, 0px 48px white; } - 62% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px white, 24px 48px white, 0px 48px white; } - 75% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px white, 0px 48px white; } - 87% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px white; } - 100% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } +/* Reasoning content collapse animation */ +.reasoning-content { + @apply overflow-hidden opacity-100 max-h-[1000px] [transition:max-height_0.35s_ease,opacity_0.25s_ease,padding_0.25s_ease,margin_0.25s_ease]; } - -@keyframes animloader2 { - 0% { transform: translate(0, 0) rotateX(0) rotateY(0); } - 25% { transform: translate(100%, 0) rotateX(0) rotateY(180deg); } - 50% { transform: translate(100%, 100%) rotateX(-180deg) rotateY(180deg); } - 75% { transform: translate(0, 100%) rotateX(-180deg) rotateY(360deg); } - 100% { transform: translate(0, 0) rotateX(0) rotateY(360deg); } +.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 new file mode 100644 index 0000000..abcfa2d --- /dev/null +++ b/assets/css/markdown.css @@ -0,0 +1,129 @@ +/* Rendered Markdown */ + +.markdown { + @apply text-cyan-50 leading-7 text-base; +} + +/* Headings */ +.markdown h1, +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + @apply font-semibold text-cyan-300 mt-6 mb-2 leading-tight; +} + +.markdown h1 { + @apply text-3xl border-b border-cyan-900 pb-1; +} +.markdown h2 { + @apply text-2xl border-b border-cyan-900 pb-1; +} +.markdown h3 { + @apply text-xl text-cyan-200; +} +.markdown h4 { + @apply text-lg text-cyan-200; +} +.markdown h5 { + @apply text-base text-cyan-100; +} +.markdown h6 { + @apply text-sm text-cyan-100; +} + +/* Paragraphs */ +.markdown p { + @apply my-3; +} + +/* Links */ +.markdown a { + @apply text-cyan-400 underline underline-offset-2 transition-colors duration-150 hover:text-cyan-300; +} + +/* Strong / Em */ +.markdown strong { + @apply font-bold text-cyan-100; +} +.markdown em { + @apply italic text-cyan-200; +} + +/* Inline code */ +.markdown code { + @apply font-mono text-sm bg-cyan-950 text-cyan-300 px-1 py-0.5 rounded border border-cyan-900; +} + +/* Code blocks */ +.markdown pre { + @apply bg-cyan-950 border border-cyan-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; +} + +/* Blockquote */ +.markdown blockquote { + @apply border-l-2 border-cyan-700 my-4 px-4 py-2 bg-cyan-950 text-cyan-200 rounded-r italic; +} + +/* Horizontal rule */ +.markdown hr { + @apply border-0 border-t border-cyan-900 my-6; +} + +/* Lists */ +.markdown ul, +.markdown ol { + @apply my-3 pl-6; +} +.markdown ul { + @apply list-disc; +} +.markdown ol { + @apply list-decimal; +} +.markdown li { + @apply my-1; +} +.markdown li::marker { + @apply text-cyan-700; +} + +/* Nested lists */ +.markdown ul ul, +.markdown ol ul { + list-style-type: circle; +} +.markdown ul ul ul { + list-style-type: square; +} + +/* Tables */ +.markdown table { + @apply w-full border-collapse my-4 text-sm; +} +.markdown thead { + @apply bg-cyan-950; +} +.markdown th { + @apply text-left px-3 py-2 text-cyan-300 font-semibold border-b-2 border-cyan-700; +} +.markdown td { + @apply px-3 py-2 border-b border-cyan-900 text-cyan-100; +} +.markdown tbody tr:hover { + @apply bg-cyan-950; +} + +/* Images */ +.markdown img { + @apply max-w-full rounded-md border border-cyan-900 my-2; +} + +/* Task list checkboxes (GitHub-flavored) */ +.markdown input[type="checkbox"] { + @apply accent-cyan-700 mr-1; +} diff --git a/assets/css/spinner.css b/assets/css/spinner.css new file mode 100644 index 0000000..3bd5bc8 --- /dev/null +++ b/assets/css/spinner.css @@ -0,0 +1,50 @@ +/* Spinner */ +.loader { + width: 48px; + height: 48px; + display: inline-block; + position: relative; + transform: rotate(45deg); +} +.loader::before { + content: ''; + box-sizing: border-box; + width: 24px; + height: 24px; + position: absolute; + left: 0; + top: -24px; + animation: animloader 4s ease infinite; +} +.loader::after { + content: ''; + box-sizing: border-box; + position: absolute; + left: 0; + top: 0; + width: 24px; + height: 24px; + background: rgba(255, 255, 255, 0.85); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); + animation: animloader2 2s ease infinite; +} + +@keyframes animloader { + 0% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } + 12% { box-shadow: 0 24px white, 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } + 25% { box-shadow: 0 24px white, 24px 24px white, 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } + 37% { box-shadow: 0 24px white, 24px 24px white, 24px 48px white, 0px 48px rgba(255,255,255,0); } + 50% { box-shadow: 0 24px white, 24px 24px white, 24px 48px white, 0px 48px white; } + 62% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px white, 24px 48px white, 0px 48px white; } + 75% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px white, 0px 48px white; } + 87% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px white; } + 100% { box-shadow: 0 24px rgba(255,255,255,0), 24px 24px rgba(255,255,255,0), 24px 48px rgba(255,255,255,0), 0px 48px rgba(255,255,255,0); } +} + +@keyframes animloader2 { + 0% { transform: translate(0, 0) rotateX(0) rotateY(0); } + 25% { transform: translate(100%, 0) rotateX(0) rotateY(180deg); } + 50% { transform: translate(100%, 100%) rotateX(-180deg) rotateY(180deg); } + 75% { transform: translate(0, 100%) rotateX(-180deg) rotateY(360deg); } + 100% { transform: translate(0, 0) rotateX(0) rotateY(360deg); } +} diff --git a/assets/js/app.js b/assets/js/app.js index d5e278a..6ecae74 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -24,7 +24,6 @@ import topbar from "../vendor/topbar" let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { - longPollFallbackMs: 2500, params: {_csrf_token: csrfToken} }) diff --git a/lib/elixir_ai_web/components/chat_message.ex b/lib/elixir_ai_web/components/chat_message.ex new file mode 100644 index 0000000..320a0ee --- /dev/null +++ b/lib/elixir_ai_web/components/chat_message.ex @@ -0,0 +1,66 @@ +defmodule ElixirAiWeb.ChatMessage do + use Phoenix.Component + alias ElixirAiWeb.Markdown + alias Phoenix.LiveView.JS + + attr :content, :string, required: true + + def user_message(assigns) do + ~H""" +
No messages yet.
<% end %> <%= for msg <- @messages do %> -