diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index f501bc956..a70760396 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -389,43 +389,15 @@ display: none; } - .card__body { - display: block; - margin: 0; - } - - .card__footer { - margin-block: -0.2em 2em; - } - .card__header { margin-block-start: calc(var(--card-padding-block) * -1.1); margin-inline: calc(-1 * var(--card-padding-inline)); max-inline-size: unset; } - .workflow-stage--current { - transition: translate 150ms ease-out; - - [open] & { - translate: -2em; - } - } - - .card__link { - z-index: 1; - } - - .card__meta-text--updated { - border: 0; - font-size: var(--text-x-small); - opacity: 0.66; - padding: 0; - text-transform: none; - - .local-time-value { - font-weight: normal; - } + .card__body { + display: block; + margin: 0; } .workflow-stage--current { @@ -438,6 +410,40 @@ flex: 0 1 auto; inline-size: fit-content; margin: 0 0 0 auto; + transition: translate 150ms ease-out; + + [open] & { + translate: -2em; + } + } + + .card__link { + z-index: 1; + } + + .card__footer { + margin-block: -0.2em 2em; + } + + .card__meta { + grid-template-areas: "text-updated"; + grid-template-columns: 1fr; + } + + .card__meta-text { + line-height: 1.5; + } + + .card__meta-text--updated { + border: 0; + font-size: var(--text-x-small); + opacity: 0.66; + padding: 0; + text-transform: none; + + .local-time-value { + font-weight: normal; + } } .card__bubble { diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index abb1e7947..b6b95418b 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -43,4 +43,17 @@ module FiltersHelper concat tag.kbd(key) end end + + def sorted_by_label(sort_value) + case sort_value + when "newest" + "Newest to oldest" + when "oldest" + "Oldest to newest" + when "latest" + "Recently updated" + else + sort_value.humanize + end + end end diff --git a/app/models/ai/usage_cost.rb b/app/models/ai/usage_cost.rb new file mode 100644 index 000000000..442be5480 --- /dev/null +++ b/app/models/ai/usage_cost.rb @@ -0,0 +1,43 @@ +class Ai::UsageCost + attr_reader :model_id, :input_tokens, :output_tokens + + class << self + def from_llm_response(response) + new \ + model_id: response.model_id, + input_tokens: response.input_tokens, + output_tokens: response.output_tokens + end + end + + def initialize(model_id:, input_tokens:, output_tokens:) + @model_id = model_id + @input_tokens = input_tokens + @output_tokens = output_tokens + end + + def in_microcents + input_cost_in_microcents + output_cost_in_microcents + end + + def input_cost_in_microcents + calculate_token_cost(input_tokens, model_info.input_price_per_million) + end + + def output_cost_in_microcents + calculate_token_cost(output_tokens, model_info.output_price_per_million) + end + + private + def calculate_token_cost(token_count, price_per_million) + return 0 unless price_per_million + + single_token_price = price_per_million.to_d / 1_000_000 + token_cost_dollars = token_count * single_token_price + Ai::Quota::Money.wrap(token_cost_dollars).in_microcents + end + + def model_info + @model_info ||= RubyLLM.models.find(model_id) + end +end diff --git a/app/models/conversation/message/respondable.rb b/app/models/conversation/message/respondable.rb index d3d8f03a2..6485a25e8 100644 --- a/app/models/conversation/message/respondable.rb +++ b/app/models/conversation/message/respondable.rb @@ -16,9 +16,9 @@ module Conversation::Message::Respondable model_id: response.model_id, input_tokens: response.input_tokens, output_tokens: response.output_tokens, - input_cost_in_microcents: response.input_cost_in_microcents, - output_cost_in_microcents: response.output_cost_in_microcents, - cost_in_microcents: response.cost_in_microcents + input_cost_in_microcents: response.cost.input_cost_in_microcents, + output_cost_in_microcents: response.cost.output_cost_in_microcents, + cost_in_microcents: response.cost.in_microcents } conversation.respond(response.answer, **message_attributes) diff --git a/app/models/conversation/message/response_generator/response.rb b/app/models/conversation/message/response_generator/response.rb index a62bf5c74..a4b1981a3 100644 --- a/app/models/conversation/message/response_generator/response.rb +++ b/app/models/conversation/message/response_generator/response.rb @@ -8,41 +8,7 @@ class Conversation::Message::ResponseGenerator::Response @model_id = model_id end - def cost_in_microcents - input_cost_in_microcents + output_cost_in_microcents + def cost + @cost ||= Ai::UsageCost.from_llm_response(self) end - - def input_cost_in_microcents - return unless token_price = input_token_price_microcents - - (input_tokens * token_price).to_i - end - - def input_token_price_microcents - return unless model_info - - price_per_million_tokens_in_microcents(model_info.input_price_per_million) - end - - def output_cost_in_microcents - return unless token_price = output_token_price_microcents - - (output_tokens * token_price).to_i - end - - def output_token_price_microcents - return unless model_info - - price_per_million_tokens_in_microcents(model_info.output_price_per_million) - end - - def model_info - @model_info ||= RubyLLM.models.find(model_id) - end - - private - def price_per_million_tokens_in_microcents(price) - single_token_price = price.to_d / 1_000_000 - Ai::Quota::Money.wrap(single_token_price).in_microcents - end end diff --git a/app/models/event/activity_summary.rb b/app/models/event/activity_summary.rb index b275bc455..4a68f2dc9 100644 --- a/app/models/event/activity_summary.rb +++ b/app/models/event/activity_summary.rb @@ -8,11 +8,11 @@ class Event::ActivitySummary < ApplicationRecord key = key_for(events) # Outside to avoid holding the transaction during the LLM request - summary, cost_in_microcents = Event::Summarizer.new(events).summarize + summarizer = Event::Summarizer.new(events) create_or_find_by!(key: key) do |record| - record.content = summary - record.cost_in_microcents = cost_in_microcents + record.content = summarizer.summarized_content + record.cost_in_microcents = summarizer.cost.in_microcents end end diff --git a/app/models/event/summarizer.rb b/app/models/event/summarizer.rb index cf10b57c6..f4d8c77d4 100644 --- a/app/models/event/summarizer.rb +++ b/app/models/event/summarizer.rb @@ -27,9 +27,12 @@ class Event::Summarizer @llm_model = llm_model end - def summarize - response = chat.ask join_prompts("Summarize the following content:", summarizable_content) - [ response.content, calculate_cost_in_microcents(response) ] + def summarized_content + llm_response.content + end + + def cost + Ai::UsageCost.from_llm_response(llm_response) end def summarizable_content @@ -39,25 +42,12 @@ class Event::Summarizer private attr_reader :prompt, :llm_model + def llm_response + @llm_response ||= chat.ask join_prompts("Summarize the following content:", summarizable_content) + end + def chat chat = RubyLLM.chat(model: llm_model) chat.with_instructions(join_prompts(prompt, domain_model_prompt, user_data_injection_prompt)) end - - def calculate_cost_in_microcents(response) - model_info = RubyLLM.models.find(response.model_id) - - input_cost = calculate_token_cost(response.input_tokens, model_info.input_price_per_million) - output_cost = calculate_token_cost(response.output_tokens, model_info.output_price_per_million) - - input_cost + output_cost - end - - def calculate_token_cost(token_count, price_per_million) - return 0 unless price_per_million - - single_token_price = price_per_million.to_d / 1_000_000 - token_cost_dollars = token_count * single_token_price - Ai::Quota::Money.wrap(token_cost_dollars).in_microcents - end end diff --git a/app/models/user.rb b/app/models/user.rb index 229518ff3..39475882a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ class User < ApplicationRecord - include Accessor, Attachable, Assignee, Configurable, Mentionable, Named, Notifiable, Role, - Searcher, SignalUser, Staff, Transferable, Conversational, AiQuota + include Accessor, AiQuota, Assignee, Attachable, Configurable, Conversational, Mentionable, Named, + Notifiable, Role, Searcher, SignalUser, Staff, Transferable include Timelined # Depends on Accessor has_one_attached :avatar diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index 448445c0b..065e3bfb0 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -56,6 +56,7 @@ class User::Filtering end def show_tags? + return unless Tag.any? expanded? || filter.tags.any? end diff --git a/app/views/accounts/settings/_entropy_configuration.html.erb b/app/views/accounts/settings/_entropy_configuration.html.erb index 12dea2bbc..53992b1c4 100644 --- a/app/views/accounts/settings/_entropy_configuration.html.erb +++ b/app/views/accounts/settings/_entropy_configuration.html.erb @@ -1,5 +1,5 @@
-

Do or Die

+

Sink or Swim

Choose default settings for this account. You can override them in individual collections.

<%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %>
diff --git a/app/views/cards/container/_engagement.html.erb b/app/views/cards/container/_engagement.html.erb index ed6fdda32..b59aff454 100644 --- a/app/views/cards/container/_engagement.html.erb +++ b/app/views/cards/container/_engagement.html.erb @@ -5,7 +5,7 @@ <%= form.hidden_field :engagement, value: "on_deck" %> <%= form.label :engagement, value: "on_deck", class: "btn", aria: { label: "Move to On Deck"} do %> <%= form.radio_button :engagement, "on_deck", checked: @card.on_deck?, class: "for-screen-reader", data: { action: "change->form#submit" } %> - Maybe? + On Deck <% end %> <% end %> @@ -20,7 +20,7 @@ <%= form.hidden_field :engagement, value: "doing" %> <%= form.label :engagement, value: "doing", class: "btn", aria: { label: "Move to Doing"} do %> <%= form.radio_button :engagement, "doing", checked: @card.doing?, class: "for-screen-reader", data: { action: "change->form#submit" } %> - Yes! + Working On <% end %> <% end %> diff --git a/app/views/cards/index/_workflow_filter.html.erb b/app/views/cards/index/_workflow_filter.html.erb index c5a610e70..d48ba88e0 100644 --- a/app/views/cards/index/_workflow_filter.html.erb +++ b/app/views/cards/index/_workflow_filter.html.erb @@ -1,9 +1,9 @@
diff --git a/app/views/cards/index/engagement/_doing.html.erb b/app/views/cards/index/engagement/_doing.html.erb index cf54fc057..8598d7963 100644 --- a/app/views/cards/index/engagement/_doing.html.erb +++ b/app/views/cards/index/engagement/_doing.html.erb @@ -3,7 +3,7 @@ <%= render "cards/index/workflow_filter", workflow: workflow, filter: filter %> <% else %>

- Yes! + Working On

<% end %> diff --git a/app/views/cards/index/engagement/_on_deck.html.erb b/app/views/cards/index/engagement/_on_deck.html.erb index 4814dd817..cd50efed4 100644 --- a/app/views/cards/index/engagement/_on_deck.html.erb +++ b/app/views/cards/index/engagement/_on_deck.html.erb @@ -1,5 +1,5 @@
-

Maybe?

+

On Deck

<% if page.used? %> <%= render partial: "cards/display/preview", collection: page.records, as: :card, locals: { draggable: true }, cached: true %> diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb index 127aab427..33c975269 100644 --- a/app/views/collections/edit/_auto_close.html.erb +++ b/app/views/collections/edit/_auto_close.html.erb @@ -1,4 +1,4 @@ <%= turbo_frame_tag @collection, :entropy_configuration do %> -

Do or Die

+

Sink or Swim

<%= render "entropy/auto_close", model: collection, url: collection_entropy_configuration_path(collection) %> <% end %> diff --git a/app/views/entropy/_auto_close.html.erb b/app/views/entropy/_auto_close.html.erb index f25b4cc5f..c7ce89d34 100644 --- a/app/views/entropy/_auto_close.html.erb +++ b/app/views/entropy/_auto_close.html.erb @@ -16,7 +16,7 @@
- Cards in Maybe?/Yes! + Cards in On Deck/Working On

Back to The Stream after…

<%= form_with model: model, url: url, data: { controller: "form" } do |form| %> <%= render "entropy/knob", diff --git a/app/views/filters/settings/_sorted_by.html.erb b/app/views/filters/settings/_sorted_by.html.erb index d2fe0b247..ccd7ef853 100644 --- a/app/views/filters/settings/_sorted_by.html.erb +++ b/app/views/filters/settings/_sorted_by.html.erb @@ -4,7 +4,7 @@
" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside"> @@ -26,7 +26,7 @@ <% end %> <%= form.button type: "submit", class: "popup__item btn" do %> - <%= sort.humanize %> + <%= sorted_by_label(sort) %> <% if filter.sorted_by == sort %> <%= icon_tag "check", class: "checked flex-item-justify-end" %> <% end %> diff --git a/app/views/public/collections/show/_doing.html.erb b/app/views/public/collections/show/_doing.html.erb index 557dbd7ff..7e19195ed 100644 --- a/app/views/public/collections/show/_doing.html.erb +++ b/app/views/public/collections/show/_doing.html.erb @@ -1,6 +1,6 @@

- Yes! + Working On

<% if page.used? %> diff --git a/app/views/public/collections/show/_on_deck.html.erb b/app/views/public/collections/show/_on_deck.html.erb index 127e0cf50..5494a2917 100644 --- a/app/views/public/collections/show/_on_deck.html.erb +++ b/app/views/public/collections/show/_on_deck.html.erb @@ -1,6 +1,6 @@

- Maybe? + On Deck

<% if page.used? %> diff --git a/test/models/ai/response_cost_test.rb b/test/models/ai/response_cost_test.rb new file mode 100644 index 000000000..a0f5109a7 --- /dev/null +++ b/test/models/ai/response_cost_test.rb @@ -0,0 +1,22 @@ +require "test_helper" + +class Ai::ResponseCostTest < ActiveSupport::TestCase + test "price calculations" do + response_cost = Ai::UsageCost.new( + model_id: "gpt-4", + input_tokens: 198, + output_tokens: 2 + ) + + # We've got 198 input tokens, so that's + # 198 * 3000 = 594000 + assert_equal 594000, response_cost.input_cost_in_microcents + + # We've got 2 output tokens, so that's + # 2 * 6000 = 12000 + assert_equal 12000, response_cost.output_cost_in_microcents + + # So the total is 594000 + 12000 micro-cents + assert_equal 606000, response_cost.in_microcents + end +end diff --git a/test/models/conversation/response_generator/response_test.rb b/test/models/conversation/response_generator/response_test.rb deleted file mode 100644 index daa752736..000000000 --- a/test/models/conversation/response_generator/response_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "test_helper" - -class Conversation::Message::ResponseGenerator::ResponseTest < ActiveSupport::TestCase - test "price calculations" do - response = Conversation::Message::ResponseGenerator::Response.new( - answer: "Hi!", - input_tokens: 198, - output_tokens: 2, - model_id: "gpt-4" - ) - - # The price of an input token is 30 USD per million tokens - # and 60 USD per million output tokens - # That's 0.00003 cents per input token and 0.00006 cents - # per output token - # Which is 3000 micro-cents per input token and 6000 micro-cents - # per output token - assert_equal "3000.0".to_d, response.input_token_price_microcents - assert_equal "6000.0".to_d, response.output_token_price_microcents - - # We've got 198 input tokens, so that's - # 198 * 3000 = 594000 - assert_equal 594000, response.input_cost_in_microcents - - # We've got 2 output tokens, so that's - # 2 * 6000 = 12 - assert_equal 12000, response.output_cost_in_microcents - - # So the total is 594000 + 12000 micro-cents - assert_equal 606000, response.cost_in_microcents - end -end