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 @@
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 %>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 @@