Skip to content
Merged
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
51 changes: 45 additions & 6 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

:root {
--background: #0a0a0f;
--foreground: #d4d0c8;
--foreground: #b7b3ac;
--border: #1a1a2e;
--border-light: #2a2a3e;
--accent: #00d9ff;
Expand Down Expand Up @@ -41,22 +41,26 @@ body {
display: flex;
flex-direction: column;
min-height: 100vh;
}

main {
/* Responsive viewport padding and gap */
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}

@media (min-width: 768px) {
body {
main {
padding: 2rem;
gap: 1.5rem;
}
}

@media (min-width: 1024px) {
body {
padding: 3rem;
main {
padding: 1rem 3rem 3rem 3rem;
gap: 2rem;
}
}
Expand All @@ -67,6 +71,7 @@ h1, h2, h3, h4, h5, h6 {
letter-spacing: 0.02em;
text-transform: uppercase;
line-height: 1.2;
margin: 0;
}

a {
Expand Down Expand Up @@ -142,15 +147,14 @@ textarea {
background: var(--user-message-bg);
color: var(--foreground);
border: none;
border-left: 3px solid var(--accent);
border-left: 3px solid var(--accent-active);
border-radius: 8px;
padding: 1rem;
resize: vertical;
}

textarea:focus {
outline: none;
border-left-color: var(--accent-active);
}

input {
Expand Down Expand Up @@ -189,3 +193,38 @@ dt {
dd {
margin: 0;
}

header {
position: sticky;
top: 0;
background: linear-gradient(to bottom, rgba(10, 10, 15, 0.8) 25%, rgba(10, 10, 15, 0));
z-index: 10;
padding: 1rem;
}

header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
mask-image: linear-gradient(to bottom, black 65%, transparent);
-webkit-mask-image: linear-gradient(to bottom, black 65%, transparent);
pointer-events: none;
z-index: -1;
}

@media (min-width: 768px) {
header {
padding: 1.5rem 2rem;
}
}

@media (min-width: 1024px) {
header {
padding: 2rem 3rem;
}
}
5 changes: 3 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def logout
# GET /account
def account
return redirect_to root_path, alert: "Please sign in" unless current_resonance
return redirect_to root_path unless current_resonance.active_subscription?

@subscription = current_resonance.subscription_details
render "application/account"
Expand Down Expand Up @@ -236,9 +237,9 @@ def destroy_subscription

if current_resonance.cancel_subscription(immediately: immediately)
if immediately
redirect_to account_path, notice: "Subscription canceled immediately."
redirect_to root_path, notice: "Subscription canceled immediately."
else
redirect_to root_path, notice: "Subscription canceled. You'll have access until the end of your billing period."
redirect_to account_path, notice: "Subscription canceled. You'll have access until the end of your billing period."
end
else
redirect_to account_path, alert: "Unable to cancel subscription. Please try again."
Expand Down
29 changes: 17 additions & 12 deletions app/javascript/controllers/chat_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ export default class extends Controller {
this.loadExistingMessages()
this.loadSavedInput()
this.saveDebounceTimeout = null

// Scroll to bottom after loading messages
if (this.narrativeValue && this.narrativeValue.length > 0) {
// Use requestAnimationFrame to ensure DOM is fully rendered
requestAnimationFrame(() => {
window.scrollTo({ top: document.body.scrollHeight, behavior: "instant" })
})
}
}

loadExistingMessages() {
if (this.narrativeValue && this.narrativeValue.length > 0) {
this.narrativeValue.forEach(message => {
const text = message.content[0].text
this.addMessage(message.role, text)
this.addMessage(message.role, text, { skipScroll: true })
})
}
}
Expand Down Expand Up @@ -225,7 +233,6 @@ export default class extends Controller {
console.error("Stream error:", error)
assistantElement.textContent = `⚠️ Error: ${error.message}`
assistantElement.classList.remove("pulsing", "loading")
assistantElement.style.borderLeftColor = getComputedStyle(document.documentElement).getPropertyValue('--message-border').trim()
} finally {
// Re-enable input when done
this.inputTarget.disabled = false
Expand All @@ -234,8 +241,6 @@ export default class extends Controller {
}

handleSSEEvent(event, data, element) {
const messageBorder = getComputedStyle(document.documentElement).getPropertyValue('--message-border').trim()

switch (event) {
case "message_start":
element.classList.remove("pulsing")
Expand All @@ -253,7 +258,6 @@ export default class extends Controller {
case "message_stop":
element.classList.remove("pulsing", "loading")
element.style.animation = ""
element.style.borderLeftColor = messageBorder
break

case "universe_time":
Expand All @@ -264,19 +268,17 @@ export default class extends Controller {
case "end":
element.classList.remove("pulsing", "loading")
element.style.animation = ""
element.style.borderLeftColor = messageBorder
break

case "error":
element.textContent = `⚠️ ${data.error.message}`
element.classList.remove("pulsing", "loading")
element.style.animation = ""
element.style.borderLeftColor = messageBorder
break
}
}

addMessage(role, text) {
addMessage(role, text, options = {}) {
const messageElement = document.createElement("div")
messageElement.classList.add("chat-message", role)
messageElement.textContent = text
Expand All @@ -290,23 +292,26 @@ export default class extends Controller {
padding: 1rem;
border-radius: 8px;
background: ${role === "user" ? userBg : assistantBg};
border-left: 3px solid ${role === "user" ? accent : messageBorder};
${role === "user" ? `border-left: 3px solid ${accent};` : ''}
white-space: pre-wrap;
font-family: 'Lightward Favorit Mono', 'Courier New', monospace;
`
this.logTarget.appendChild(messageElement)
messageElement.scrollIntoView({ behavior: "smooth", block: "end" })

// Only scroll if not explicitly skipped (for initial load)
if (!options.skipScroll) {
messageElement.scrollIntoView({ behavior: "smooth", block: "end" })
}

return messageElement
}

addPulsingMessage(role) {
const messageElement = this.addMessage(role, "")
messageElement.classList.add("pulsing")
const accentActive = getComputedStyle(document.documentElement).getPropertyValue('--accent-active').trim()
messageElement.style.cssText += `
min-height: 3rem;
animation: pulse 1.5s ease-in-out infinite;
border-left-color: ${accentActive};
`
return messageElement
}
Expand Down
27 changes: 0 additions & 27 deletions app/views/application/_subscription_tiers.html.erb

This file was deleted.

10 changes: 3 additions & 7 deletions app/views/application/account.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1>Account</h1>

<div style="margin: 2rem 0;">
<div>
<h2>Subscription</h2>

<% if @subscription %>
Expand Down Expand Up @@ -51,17 +51,13 @@
<dd>
Not subscribed
</dd>
<dt><strong>Manage:</strong></dt>
<dd>
<%= render "subscription_tiers" %>
</dd>
</dl>
<% end %>
</div>

<div style="margin: 3rem 0; padding-top: 2rem; border-top: 1px solid var(--border);">
<div style="border-top: 1px solid var(--border); padding-top: 2rem;">
<h2>Begin again</h2>
<p>Want to clear in-universe memory completely? (This will not reset the day counter, and will not cancel your subscription.)</p>
<p>Want to clear in-universe memory completely? (This will advance the day counter by 1. This will not affect your subscription.)</p>
<p style="margin-top: 1rem; color: var(--warning);">This action cannot be undone.</p>

<%= button_to "Begin Again",
Expand Down
4 changes: 2 additions & 2 deletions app/views/application/chat.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div data-controller="chat" data-chat-narrative-value="<%= @narrative.to_json %>" data-chat-universe-time-value="<%= current_resonance.universe_time %>" data-chat-saved-textarea-value="<%= h(current_resonance.textarea || "") %>" style="display: flex; flex-direction: column; gap: 1rem;">
<div data-controller="chat" data-chat-narrative-value="<%= @narrative.to_json %>" data-chat-universe-time-value="<%= current_resonance.universe_time %>" data-chat-saved-textarea-value="<%= h(current_resonance.textarea || "") %>" style="display: flex; flex-direction: column; gap: 2rem;">
<div id="chat-log" data-chat-target="log" style="display: flex; flex-direction: column; gap: 1rem;">
<!-- Messages will appear here -->
</div>
Expand All @@ -11,7 +11,7 @@
style="width: 100%; resize: none; overflow: hidden;"
></textarea>

<div style="display: flex; gap: 1rem; align-items: center;">
<div style="display: flex; gap: 2rem; align-items: center;">
<button
data-action="click->chat#send"
>
Expand Down
28 changes: 27 additions & 1 deletion app/views/application/subscribe.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
<h1>Subscribe to <%= current_resonance.universe_day == 1 ? "Begin" : "Continue" %></h1>

<%= render "subscription_tiers" %>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<div>All tiers provide identical access – this number is you signaling to yourself what this means to you.</div>

<div style="display: flex; flex-wrap: wrap; gap: 1rem;">
<%= form_with url: subscription_path, method: :post, data: { turbo: false } do |f| %>
<%= f.hidden_field :tier, value: "tier_1" %>
<%= f.submit "$1/month", name: nil %>
<% end %>

<%= form_with url: subscription_path, method: :post, data: { turbo: false } do |f| %>
<%= f.hidden_field :tier, value: "tier_10" %>
<%= f.submit "$10/month", name: nil %>
<% end %>

<%= form_with url: subscription_path, method: :post, data: { turbo: false } do |f| %>
<%= f.hidden_field :tier, value: "tier_100" %>
<%= f.submit "$100/month", name: nil %>
<% end %>

<%= form_with url: subscription_path, method: :post, data: { turbo: false } do |f| %>
<%= f.hidden_field :tier, value: "tier_1000" %>
<%= f.submit "$1000/month", name: nil %>
<% end %>
</div>

<div style="opacity: 0.6;">Learn more about Pay What Feels Good: <a href="https://lightward.inc/pricing" target="_blank">lightward.inc/pricing</a></div>
</div>
Loading