Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9369d9d0d7 | ||
|
|
5bf61ef9e1 | ||
|
|
107f14456b | ||
|
|
269f169abf | ||
|
|
7d7c169afa | ||
|
|
9211b211f5 | ||
|
|
24e6aa52bb | ||
|
|
48fff48aa2 | ||
|
|
bd40076dc1 | ||
|
|
3533cd1acc | ||
|
|
51ca942d7d | ||
|
|
ec8018333e | ||
|
|
6aaf8a0619 | ||
|
|
ac29d3080f | ||
|
|
65503a5038 | ||
|
|
b17c688162 | ||
|
|
e59184b65c | ||
|
|
8f4cd2fcbd | ||
|
|
f4708d4178 | ||
|
|
833f914e34 | ||
|
|
168d9d8eb9 | ||
|
|
3da4f5eac3 | ||
|
|
bb32d0d737 | ||
|
|
339251a371 | ||
|
|
511b10285d | ||
|
|
94e0834b88 | ||
|
|
1ce25c5a8b | ||
|
|
1b570fcd01 | ||
|
|
5b63e597d0 | ||
|
|
01893bb6ed | ||
|
|
453bb4821f | ||
|
|
e255c7a8f0 | ||
|
|
8863cf0c86 | ||
|
|
b49d454ad9 | ||
|
|
584f5f2b6e | ||
|
|
fff0bc0f8c | ||
|
|
28af4434c5 | ||
|
|
f6eedf3e0b | ||
|
|
3f20b24aa3 | ||
|
|
5368b0c7d0 | ||
|
|
eccfbad046 | ||
|
|
a3fb5cdc21 | ||
|
|
e15952031d | ||
|
|
aa13d16022 | ||
|
|
9aafca03f5 | ||
|
|
0018475987 | ||
|
|
fe19133dd4 | ||
|
|
84e791a941 | ||
|
|
2486e0e2dd | ||
|
|
08377bab35 | ||
|
|
43cbb7f45f | ||
|
|
4f94675a72 | ||
|
|
55dde0a9e6 | ||
|
|
09f4895302 | ||
|
|
04678cab04 | ||
|
|
f922012499 | ||
|
|
6765a13a40 | ||
|
|
24f0e1262d | ||
|
|
1f9f330ce2 | ||
|
|
70248ccfca | ||
|
|
af47873f28 | ||
|
|
37bf160d26 | ||
|
|
3a755ca883 | ||
|
|
12f00a62d2 | ||
|
|
0c9466059c | ||
|
|
02f0908963 |
@@ -4,3 +4,4 @@ node_modules
|
||||
.env
|
||||
evals/log
|
||||
evals/cases
|
||||
config/eval-llms.local.yml
|
||||
|
||||
+59
-45
@@ -1,79 +1,93 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (7.1.3.4)
|
||||
activesupport (8.0.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
ast (2.4.2)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.1.8)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.4.1)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.0)
|
||||
drb (2.2.1)
|
||||
i18n (1.14.5)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.7.2)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.24.1)
|
||||
mutex_m (0.2.0)
|
||||
parallel (1.25.1)
|
||||
parser (3.3.3.0)
|
||||
json (2.10.2)
|
||||
language_server-protocol (3.17.0.4)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.6.6)
|
||||
minitest (5.25.5)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
prettier_print (1.2.1)
|
||||
racc (1.8.0)
|
||||
rack (3.1.10)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.9.2)
|
||||
rexml (3.3.9)
|
||||
rubocop (1.64.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.74.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.3)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.21.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-discourse (3.8.1)
|
||||
rubocop-capybara (2.22.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-discourse (3.12.1)
|
||||
activesupport (>= 6.1)
|
||||
rubocop (>= 1.59.0)
|
||||
rubocop-capybara (>= 2.0.0)
|
||||
rubocop-factory_bot (>= 2.0.0)
|
||||
rubocop-rails (>= 2.25.0)
|
||||
lint_roller (>= 1.1.0)
|
||||
rubocop (>= 1.73.2)
|
||||
rubocop-capybara (>= 2.22.0)
|
||||
rubocop-factory_bot (>= 2.27.0)
|
||||
rubocop-rails (>= 2.30.3)
|
||||
rubocop-rspec (>= 3.0.1)
|
||||
rubocop-rspec_rails (>= 2.30.0)
|
||||
rubocop-factory_bot (2.26.1)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rails (2.25.1)
|
||||
rubocop-rspec_rails (>= 2.31.0)
|
||||
rubocop-factory_bot (2.27.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rails (2.30.3)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.0.2)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec_rails (2.30.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec (~> 3, >= 3.0.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rspec (3.5.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec_rails (2.31.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec (~> 3.5)
|
||||
ruby-progressbar (1.13.0)
|
||||
securerandom (0.4.1)
|
||||
syntax_tree (6.2.0)
|
||||
prettier_print (>= 1.2.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uri (1.0.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -83,4 +97,4 @@ DEPENDENCIES
|
||||
syntax_tree
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.10
|
||||
2.6.6
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Civilized Discourse Construction Kit
|
||||
Copyright (c) Civilized Discourse Construction Kit, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -3,3 +3,27 @@
|
||||
**Plugin Summary**
|
||||
|
||||
For more information, please see: https://meta.discourse.org/t/discourse-ai/259214?u=falco
|
||||
|
||||
### Evals
|
||||
|
||||
The directory `evals` contains AI evals for the Discourse AI plugin.
|
||||
You may create a local config by copying `config/eval-llms.yml` to `config/eval-llms.local.yml` and modifying the values.
|
||||
|
||||
To run them use:
|
||||
|
||||
cd evals
|
||||
./run --help
|
||||
|
||||
```
|
||||
Usage: evals/run [options]
|
||||
-e, --eval NAME Name of the evaluation to run
|
||||
--list-models List models
|
||||
-m, --model NAME Model to evaluate (will eval all models if not specified)
|
||||
-l, --list List evals
|
||||
```
|
||||
|
||||
To run evals you will need to configure API keys in your environment:
|
||||
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key
|
||||
GEMINI_API_KEY=your_gemini_api_key
|
||||
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class DiscourseAiToolsNewRoute extends DiscourseRoute {
|
||||
beforeModel(transition) {
|
||||
this.preset = transition.to.queryParams.presetId || "empty_tool";
|
||||
}
|
||||
|
||||
async model() {
|
||||
return this.store.createRecord("ai-tool");
|
||||
}
|
||||
@@ -8,10 +12,10 @@ export default class DiscourseAiToolsNewRoute extends DiscourseRoute {
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools");
|
||||
|
||||
controller.set("allTools", toolsModel);
|
||||
controller.set("presets", toolsModel.resultSetMeta.presets);
|
||||
controller.set("llms", toolsModel.resultSetMeta.llms);
|
||||
controller.set("settings", toolsModel.resultSetMeta.settings);
|
||||
controller.set("selectedPreset", this.preset);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -5,5 +5,6 @@
|
||||
@presets={{this.presets}}
|
||||
@llms={{this.llms}}
|
||||
@settings={{this.settings}}
|
||||
@selectedPreset={{this.selectedPreset}}
|
||||
/>
|
||||
</section>
|
||||
@@ -44,6 +44,31 @@ module DiscourseAi
|
||||
|
||||
render json: { bot_username: bot_user.username_lower }, status: 200
|
||||
end
|
||||
|
||||
def discover
|
||||
ai_persona =
|
||||
AiPersona.all_personas.find do |persona|
|
||||
persona.id == SiteSetting.ai_bot_discover_persona.to_i
|
||||
end
|
||||
|
||||
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
|
||||
raise Discourse::InvalidAccess.new
|
||||
end
|
||||
|
||||
if ai_persona.default_llm_id.blank?
|
||||
render_json_error "Discover persona is missing a default LLM model.", status: 503
|
||||
return
|
||||
end
|
||||
|
||||
query = params[:query]
|
||||
raise Discourse::InvalidParameters.new("Missing query to discover") if query.blank?
|
||||
|
||||
RateLimiter.new(current_user, "ai_bot_discover_#{current_user.id}", 3, 1.minute).performed!
|
||||
|
||||
Jobs.enqueue(:stream_discover_reply, user_id: current_user.id, query: query)
|
||||
|
||||
render json: {}, status: 200
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,7 +53,12 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def suggest_title
|
||||
input = get_text_param!
|
||||
if params[:topic_id]
|
||||
topic = Topic.find_by(id: params[:topic_id])
|
||||
input = DiscourseAi::Summarization::Strategies::TopicSummary.new(topic).targets_data
|
||||
else
|
||||
input = get_text_param!
|
||||
end
|
||||
|
||||
prompt = CompletionPrompt.enabled_by_name("generate_titles")
|
||||
raise Discourse::InvalidParameters.new(:mode) if !prompt
|
||||
@@ -73,22 +78,26 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def suggest_category
|
||||
input = get_text_param!
|
||||
input_hash = { text: input }
|
||||
if params[:topic_id]
|
||||
opts = { topic_id: params[:topic_id] }
|
||||
else
|
||||
input = get_text_param!
|
||||
opts = { text: input }
|
||||
end
|
||||
|
||||
render json:
|
||||
DiscourseAi::AiHelper::SemanticCategorizer.new(
|
||||
input_hash,
|
||||
current_user,
|
||||
).categories,
|
||||
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(current_user, opts).categories,
|
||||
status: 200
|
||||
end
|
||||
|
||||
def suggest_tags
|
||||
input = get_text_param!
|
||||
input_hash = { text: input }
|
||||
if params[:topic_id]
|
||||
opts = { topic_id: params[:topic_id] }
|
||||
else
|
||||
input = get_text_param!
|
||||
opts = { text: input }
|
||||
end
|
||||
|
||||
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(input_hash, current_user).tags,
|
||||
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(current_user, opts).tags,
|
||||
status: 200
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module Sentiment
|
||||
class SentimentController < ::Admin::StaffController
|
||||
include Constants
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
DEFAULT_POSTS_LIMIT = 50
|
||||
MAX_POSTS_LIMIT = 100
|
||||
|
||||
def posts
|
||||
group_by = params.required(:group_by)&.to_sym
|
||||
group_value = params.required(:group_value).presence
|
||||
start_date = params[:start_date].presence
|
||||
end_date = params[:end_date]
|
||||
threshold = SENTIMENT_THRESHOLD
|
||||
|
||||
raise Discourse::InvalidParameters if %i[category tag].exclude?(group_by)
|
||||
|
||||
limit = fetch_limit_from_params(default: DEFAULT_POSTS_LIMIT, max: MAX_POSTS_LIMIT)
|
||||
offset = params[:offset].to_i || 0
|
||||
|
||||
case group_by
|
||||
when :category
|
||||
grouping_clause = "c.name"
|
||||
grouping_join = "" # categories already joined
|
||||
when :tag
|
||||
grouping_clause = "tags.name"
|
||||
grouping_join =
|
||||
"INNER JOIN topic_tags tt ON tt.topic_id = p.topic_id INNER JOIN tags ON tags.id = tt.tag_id"
|
||||
end
|
||||
|
||||
posts =
|
||||
DB.query(
|
||||
<<~SQL,
|
||||
SELECT
|
||||
p.id AS post_id,
|
||||
p.topic_id,
|
||||
t.fancy_title AS topic_title,
|
||||
p.cooked as post_cooked,
|
||||
p.user_id,
|
||||
p.post_number,
|
||||
p.created_at AS created_at,
|
||||
u.username,
|
||||
u.name,
|
||||
u.uploaded_avatar_id,
|
||||
c.id AS category_id,
|
||||
c.name AS category_name,
|
||||
c.color AS category_color,
|
||||
c.slug AS category_slug,
|
||||
c.description AS category_description,
|
||||
(CASE
|
||||
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
|
||||
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
|
||||
ELSE 'neutral'
|
||||
END) AS sentiment
|
||||
FROM posts p
|
||||
INNER JOIN topics t ON t.id = p.topic_id
|
||||
INNER JOIN classification_results cr ON cr.target_id = p.id AND cr.target_type = 'Post'
|
||||
LEFT JOIN users u ON u.id = p.user_id
|
||||
LEFT JOIN categories c ON c.id = t.category_id
|
||||
#{grouping_join}
|
||||
WHERE
|
||||
#{grouping_clause} = :group_value AND
|
||||
t.archetype = 'regular' AND
|
||||
p.user_id > 0 AND
|
||||
cr.model_used = 'cardiffnlp/twitter-roberta-base-sentiment-latest' AND
|
||||
((:start_date IS NULL OR p.created_at > :start_date) AND (:end_date IS NULL OR p.created_at < :end_date))
|
||||
AND p.deleted_at IS NULL
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
SQL
|
||||
group_value: group_value,
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
threshold: threshold,
|
||||
limit: limit + 1,
|
||||
offset: offset,
|
||||
)
|
||||
|
||||
has_more = posts.length > limit
|
||||
posts.pop if has_more
|
||||
|
||||
render_json_dump(
|
||||
posts:
|
||||
serialize_data(
|
||||
posts,
|
||||
AiSentimentPostSerializer,
|
||||
scope: guardian,
|
||||
add_raw: true,
|
||||
add_excerpt: true,
|
||||
add_title: true,
|
||||
),
|
||||
has_more: has_more,
|
||||
next_offset: has_more ? offset + limit : nil,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
module ::Jobs
|
||||
class PostSentimentAnalysis < ::Jobs::Base
|
||||
sidekiq_options queue: "low"
|
||||
|
||||
def execute(args)
|
||||
return unless SiteSetting.ai_sentiment_enabled
|
||||
return if (post_id = args[:post_id]).blank?
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class StreamDiscoverReply < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
return if (user = User.find_by(id: args[:user_id])).nil?
|
||||
return if (query = args[:query]).blank?
|
||||
|
||||
ai_persona_klass =
|
||||
AiPersona.all_personas.find do |persona|
|
||||
persona.id == SiteSetting.ai_bot_discover_persona.to_i
|
||||
end
|
||||
|
||||
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
|
||||
return
|
||||
end
|
||||
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
|
||||
|
||||
bot =
|
||||
DiscourseAi::AiBot::Bot.as(
|
||||
Discourse.system_user,
|
||||
persona: ai_persona_klass.new,
|
||||
model: llm_model,
|
||||
)
|
||||
|
||||
streamed_reply = +""
|
||||
start = Time.now
|
||||
|
||||
base = { query: query, model_used: llm_model.display_name }
|
||||
|
||||
bot.reply(
|
||||
{ conversation_context: [{ type: :user, content: query }], skip_tool_details: true },
|
||||
) do |partial|
|
||||
streamed_reply << partial
|
||||
|
||||
# Throttle updates.
|
||||
if (Time.now - start > 0.3) || Rails.env.test?
|
||||
payload = base.merge(done: false, ai_discover_reply: streamed_reply)
|
||||
publish_update(user, payload)
|
||||
start = Time.now
|
||||
end
|
||||
end
|
||||
|
||||
publish_update(user, base.merge(done: true, ai_discover_reply: streamed_reply))
|
||||
end
|
||||
|
||||
def publish_update(user, payload)
|
||||
MessageBus.publish("/discourse-ai/ai-bot/discover", payload, user_ids: [user.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,19 @@ class AiArtifactVersion < ActiveRecord::Base
|
||||
validates :html, length: { maximum: 65_535 }
|
||||
validates :css, length: { maximum: 65_535 }
|
||||
validates :js, length: { maximum: 65_535 }
|
||||
|
||||
# used when generating test cases
|
||||
def write_to(path)
|
||||
css_path = "#{path}/main.css"
|
||||
html_path = "#{path}/main.html"
|
||||
js_path = "#{path}/main.js"
|
||||
instructions_path = "#{path}/instructions.txt"
|
||||
|
||||
File.write(css_path, css)
|
||||
File.write(html_path, html)
|
||||
File.write(js_path, js)
|
||||
File.write(instructions_path, change_description)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
class ClassificationResult < ActiveRecord::Base
|
||||
belongs_to :target, polymorphic: true
|
||||
|
||||
def self.has_sentiment_classification?
|
||||
where(classification_type: "sentiment").exists?
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
@@ -26,13 +26,23 @@ class LlmModel < ActiveRecord::Base
|
||||
access_key_id: :text,
|
||||
region: :text,
|
||||
disable_native_tools: :checkbox,
|
||||
disable_temperature: :checkbox,
|
||||
disable_top_p: :checkbox,
|
||||
enable_reasoning: :checkbox,
|
||||
reasoning_tokens: :number,
|
||||
},
|
||||
anthropic: {
|
||||
disable_native_tools: :checkbox,
|
||||
disable_temperature: :checkbox,
|
||||
disable_top_p: :checkbox,
|
||||
enable_reasoning: :checkbox,
|
||||
reasoning_tokens: :number,
|
||||
},
|
||||
open_ai: {
|
||||
organization: :text,
|
||||
disable_native_tools: :checkbox,
|
||||
disable_temperature: :checkbox,
|
||||
disable_top_p: :checkbox,
|
||||
disable_streaming: :checkbox,
|
||||
reasoning_effort: {
|
||||
type: :enum,
|
||||
@@ -65,6 +75,8 @@ class LlmModel < ActiveRecord::Base
|
||||
provider_order: :text,
|
||||
provider_quantizations: :text,
|
||||
disable_streaming: :checkbox,
|
||||
disable_temperature: :checkbox,
|
||||
disable_top_p: :checkbox,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
@@ -173,6 +173,7 @@ end
|
||||
# force_review :boolean default(FALSE), not null
|
||||
# reject_reason :text
|
||||
# potentially_illegal :boolean default(FALSE)
|
||||
# type_source :string default("unknown"), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -230,6 +230,7 @@ end
|
||||
# force_review :boolean default(FALSE), not null
|
||||
# reject_reason :text
|
||||
# potentially_illegal :boolean default(FALSE)
|
||||
# type_source :string default("unknown"), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AiSentimentPostSerializer < ApplicationSerializer
|
||||
attributes :post_id,
|
||||
:topic_id,
|
||||
:topic_title,
|
||||
:post_number,
|
||||
:username,
|
||||
:name,
|
||||
:avatar_template,
|
||||
:excerpt,
|
||||
:sentiment,
|
||||
:truncated,
|
||||
:category,
|
||||
:created_at
|
||||
|
||||
def avatar_template
|
||||
User.avatar_template(object.username, object.uploaded_avatar_id)
|
||||
end
|
||||
|
||||
def excerpt
|
||||
Post.excerpt(object.post_cooked)
|
||||
end
|
||||
|
||||
def truncated
|
||||
object.post_cooked.length > SiteSetting.post_excerpt_maxlength
|
||||
end
|
||||
|
||||
def category
|
||||
{
|
||||
id: object.category_id,
|
||||
name: object.category_name,
|
||||
color: object.category_color,
|
||||
slug: object.category_slug,
|
||||
description: object.category_description,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -8,10 +8,6 @@ class ProblemCheck::AiLlmStatus < ProblemCheck
|
||||
llm_errors
|
||||
end
|
||||
|
||||
def base_path
|
||||
Discourse.base_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def llm_errors
|
||||
@@ -26,20 +22,21 @@ class ProblemCheck::AiLlmStatus < ProblemCheck
|
||||
blk.call
|
||||
nil
|
||||
rescue => e
|
||||
error_message = parse_error_message(e.message)
|
||||
message =
|
||||
"#{I18n.t("dashboard.problem.ai_llm_status", { base_path: base_path, model_name: model.display_name, model_id: model.id })}"
|
||||
details = {
|
||||
model_id: model.id,
|
||||
model_name: model.display_name,
|
||||
error: parse_error_message(e.message),
|
||||
url: "#{Discourse.base_path}/admin/plugins/discourse-ai/ai-llms/#{model.id}/edit",
|
||||
}
|
||||
|
||||
message = I18n.t("dashboard.problem.ai_llm_status", details)
|
||||
|
||||
Problem.new(
|
||||
message,
|
||||
priority: "high",
|
||||
identifier: "ai_llm_status",
|
||||
target: model.id,
|
||||
details: {
|
||||
model_id: model.id,
|
||||
model_name: model.display_name,
|
||||
error: error_message,
|
||||
},
|
||||
details:,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,8 +32,10 @@ export default class AiTool extends RestModel {
|
||||
|
||||
if (parameter.enum && parameter.enum.length) {
|
||||
parameter.enum = new TrackedArray(parameter.enum);
|
||||
parameter.isEnum = true;
|
||||
} else {
|
||||
parameter.enum = null;
|
||||
parameter.isEnum = false;
|
||||
}
|
||||
|
||||
return parameter;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import getURL from "discourse/lib/get-url";
|
||||
|
||||
export default class AiArtifactComponent extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
@tracked expanded = false;
|
||||
@tracked showingArtifact = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
|
||||
class Block {
|
||||
@tracked show = false;
|
||||
@tracked shown = false;
|
||||
@tracked blinking = false;
|
||||
|
||||
constructor(args = {}) {
|
||||
this.show = args.show ?? false;
|
||||
this.shown = args.shown ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCKS_SIZE = 20; // changing this requires to change css accordingly
|
||||
|
||||
export default class AiBlinkingAnimation extends Component {
|
||||
blocks = [...Array.from({ length: BLOCKS_SIZE }, () => new Block())];
|
||||
|
||||
#nextBlockBlinkingTimer;
|
||||
#blockBlinkingTimer;
|
||||
#blockShownTimer;
|
||||
|
||||
@action
|
||||
setupAnimation() {
|
||||
this.blocks.firstObject.show = true;
|
||||
this.blocks.firstObject.shown = true;
|
||||
}
|
||||
|
||||
@action
|
||||
onBlinking(block) {
|
||||
if (!block.blinking) {
|
||||
return;
|
||||
}
|
||||
|
||||
block.show = false;
|
||||
|
||||
this.#nextBlockBlinkingTimer = discourseLater(
|
||||
this,
|
||||
() => {
|
||||
this.#nextBlock(block).blinking = true;
|
||||
},
|
||||
250
|
||||
);
|
||||
|
||||
this.#blockBlinkingTimer = discourseLater(
|
||||
this,
|
||||
() => {
|
||||
block.blinking = false;
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
onShowing(block) {
|
||||
if (!block.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#blockShownTimer = discourseLater(
|
||||
this,
|
||||
() => {
|
||||
this.#nextBlock(block).show = true;
|
||||
this.#nextBlock(block).shown = true;
|
||||
|
||||
if (this.blocks.lastObject === block) {
|
||||
this.blocks.firstObject.blinking = true;
|
||||
}
|
||||
},
|
||||
250
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
teardownAnimation() {
|
||||
cancel(this.#blockShownTimer);
|
||||
cancel(this.#nextBlockBlinkingTimer);
|
||||
cancel(this.#blockBlinkingTimer);
|
||||
}
|
||||
|
||||
#nextBlock(currentBlock) {
|
||||
if (currentBlock === this.blocks.lastObject) {
|
||||
return this.blocks.firstObject;
|
||||
} else {
|
||||
return this.blocks.objectAt(this.blocks.indexOf(currentBlock) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<ul class="ai-blinking-animation" {{didInsert this.setupAnimation}}>
|
||||
{{#each this.blocks as |block|}}
|
||||
<li
|
||||
class={{concatClass
|
||||
"ai-blinking-animation__list-item"
|
||||
(if block.show "show")
|
||||
(if block.shown "is-shown")
|
||||
(if block.blinking "blink")
|
||||
}}
|
||||
{{didUpdate (fn this.onBlinking block) block.blinking}}
|
||||
{{didUpdate (fn this.onShowing block) block.show}}
|
||||
{{willDestroy this.teardownAnimation}}
|
||||
></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default class AiComposerHelperMenu extends Component {
|
||||
@service siteSettings;
|
||||
@service currentUser;
|
||||
@service site;
|
||||
|
||||
@tracked newSelectedText;
|
||||
@tracked diff;
|
||||
@tracked customPromptValue = "";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,10 @@ export default class AiLlmEditorForm extends Component {
|
||||
provider: model.provider,
|
||||
enabled_chat_bot: model.enabled_chat_bot,
|
||||
vision_enabled: model.vision_enabled,
|
||||
provider_params: model.provider_params,
|
||||
provider_params: this.computeProviderParams(
|
||||
model.provider,
|
||||
model.provider_params
|
||||
),
|
||||
llm_quotas: model.llm_quotas,
|
||||
};
|
||||
}
|
||||
@@ -128,12 +131,12 @@ export default class AiLlmEditorForm extends Component {
|
||||
return !this.args.model.isNew;
|
||||
}
|
||||
|
||||
computeProviderParams(provider) {
|
||||
computeProviderParams(provider, currentParams = {}) {
|
||||
const params = this.args.llms.resultSetMeta.provider_params[provider] ?? {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(params).map(([k, v]) => [
|
||||
k,
|
||||
v?.type === "enum" ? v.default : null,
|
||||
currentParams[k] ?? (v?.type === "enum" ? v.default : null),
|
||||
])
|
||||
);
|
||||
}
|
||||
@@ -272,7 +275,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
@validation="required|length:1,100"
|
||||
@disabled={{this.seeded}}
|
||||
@format="large"
|
||||
@tooltip={{i18n "discourse_ai.llms.hints.max_prompt_tokens"}}
|
||||
@tooltip={{i18n "discourse_ai.llms.hints.display_name"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Input />
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
|
||||
export default class AiSearchDiscoveriesTooltip extends Component {
|
||||
@service discobotDiscoveries;
|
||||
|
||||
<template>
|
||||
<span class="ai-search-discoveries-tooltip">
|
||||
<DTooltip @placement="top-end" @interactive={{true}}>
|
||||
<:trigger>
|
||||
{{icon "circle-info"}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
<div class="ai-search-discoveries-tooltip__content">
|
||||
<div class="ai-search-discoveries-tooltip__header">
|
||||
{{i18n "discourse_ai.discobot_discoveries.tooltip.header"}}
|
||||
</div>
|
||||
|
||||
<div class="ai-search-discoveries-tooltip__description">
|
||||
{{#if this.discobotDiscoveries.modelUsed}}
|
||||
{{i18n
|
||||
"discourse_ai.discobot_discoveries.tooltip.content"
|
||||
model=this.discobotDiscoveries.modelUsed
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="ai-search-discoveries-tooltip__actions">
|
||||
<DButton
|
||||
class="btn-transparent btn-primary"
|
||||
@label="discourse_ai.discobot_discoveries.tooltip.actions.info"
|
||||
@href="https://meta.discourse.org/t/conversational-ai-search-coming-to-discourse-ai/355939"
|
||||
/>
|
||||
<DButton
|
||||
class="btn-transparent btn-danger"
|
||||
@label="discourse_ai.discobot_discoveries.tooltip.actions.disable"
|
||||
@action={{this.discobotDiscoveries.disableDiscoveries}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</DTooltip>
|
||||
</span>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { cancel, later } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import CookText from "discourse/components/cook-text";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AiBlinkingAnimation from "./ai-blinking-animation";
|
||||
|
||||
const DISCOVERY_TIMEOUT_MS = 10000;
|
||||
const STREAMED_TEXT_SPEED = 23;
|
||||
|
||||
export default class AiSearchDiscoveries extends Component {
|
||||
@service search;
|
||||
@service messageBus;
|
||||
@service discobotDiscoveries;
|
||||
@service appEvents;
|
||||
|
||||
@tracked loadingDiscoveries = false;
|
||||
@tracked hideDiscoveries = false;
|
||||
@tracked fullDiscoveryToggled = false;
|
||||
@tracked discoveryPreviewLength = this.args.discoveryPreviewLength || 150;
|
||||
|
||||
@tracked isStreaming = false;
|
||||
@tracked streamedText = "";
|
||||
|
||||
discoveryTimeout = null;
|
||||
typingTimer = null;
|
||||
streamedTextLength = 0;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.appEvents.on(
|
||||
"full-page-search:trigger-search",
|
||||
this,
|
||||
this.triggerDiscovery
|
||||
);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.appEvents.off(
|
||||
"full-page-search:trigger-search",
|
||||
this,
|
||||
this.triggerDiscovery
|
||||
);
|
||||
}
|
||||
|
||||
typeCharacter() {
|
||||
if (this.streamedTextLength < this.discobotDiscoveries.discovery.length) {
|
||||
this.streamedText += this.discobotDiscoveries.discovery.charAt(
|
||||
this.streamedTextLength
|
||||
);
|
||||
this.streamedTextLength++;
|
||||
|
||||
this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED);
|
||||
} else {
|
||||
this.typingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onTextUpdate() {
|
||||
this.cancelTypingTimer();
|
||||
this.typeCharacter();
|
||||
}
|
||||
|
||||
cancelTypingTimer() {
|
||||
if (this.typingTimer) {
|
||||
cancel(this.typingTimer);
|
||||
}
|
||||
}
|
||||
|
||||
resetStreaming() {
|
||||
this.cancelTypingTimer();
|
||||
this.streamedText = "";
|
||||
this.streamedTextLength = 0;
|
||||
}
|
||||
|
||||
@bind
|
||||
async _updateDiscovery(update) {
|
||||
if (this.query === update.query) {
|
||||
if (this.discoveryTimeout) {
|
||||
cancel(this.discoveryTimeout);
|
||||
}
|
||||
|
||||
if (!this.discobotDiscoveries.discovery) {
|
||||
this.discobotDiscoveries.discovery = "";
|
||||
}
|
||||
|
||||
const newText = update.ai_discover_reply;
|
||||
this.discobotDiscoveries.modelUsed = update.model_used;
|
||||
this.loadingDiscoveries = false;
|
||||
|
||||
// Handling short replies.
|
||||
if (update.done) {
|
||||
this.discobotDiscoveries.discovery = newText;
|
||||
this.streamedText = newText;
|
||||
this.isStreaming = false;
|
||||
|
||||
// Clear pending animations
|
||||
this.cancelTypingTimer();
|
||||
} else if (newText.length > this.discobotDiscoveries.discovery.length) {
|
||||
this.discobotDiscoveries.discovery = newText;
|
||||
this.isStreaming = true;
|
||||
await this.onTextUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
unsubscribe() {
|
||||
this.messageBus.unsubscribe(
|
||||
"/discourse-ai/ai-bot/discover",
|
||||
this._updateDiscovery
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
subscribe() {
|
||||
this.messageBus.subscribe(
|
||||
"/discourse-ai/ai-bot/discover",
|
||||
this._updateDiscovery
|
||||
);
|
||||
}
|
||||
|
||||
get query() {
|
||||
return this.args?.searchTerm || this.search.activeGlobalSearchTerm;
|
||||
}
|
||||
|
||||
get toggleLabel() {
|
||||
if (this.fullDiscoveryToggled) {
|
||||
return "discourse_ai.discobot_discoveries.collapse";
|
||||
} else {
|
||||
return "discourse_ai.discobot_discoveries.tell_me_more";
|
||||
}
|
||||
}
|
||||
|
||||
get toggleIcon() {
|
||||
if (this.fullDiscoveryToggled) {
|
||||
return "chevron-up";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get canShowExpandtoggle() {
|
||||
return (
|
||||
!this.loadingDiscoveries &&
|
||||
this.renderedDiscovery.length > this.discoveryPreviewLength
|
||||
);
|
||||
}
|
||||
|
||||
get renderedDiscovery() {
|
||||
return this.isStreaming
|
||||
? this.streamedText
|
||||
: this.discobotDiscoveries.discovery;
|
||||
}
|
||||
|
||||
get renderPreviewOnly() {
|
||||
return !this.fullDiscoveryToggled && this.canShowExpandtoggle;
|
||||
}
|
||||
|
||||
@action
|
||||
async triggerDiscovery() {
|
||||
if (this.discobotDiscoveries.lastQuery === this.query) {
|
||||
this.hideDiscoveries = false;
|
||||
return;
|
||||
} else {
|
||||
this.resetStreaming();
|
||||
this.discobotDiscoveries.resetDiscovery();
|
||||
}
|
||||
|
||||
this.hideDiscoveries = false;
|
||||
this.loadingDiscoveries = true;
|
||||
this.discoveryTimeout = later(
|
||||
this,
|
||||
this.timeoutDiscovery,
|
||||
DISCOVERY_TIMEOUT_MS
|
||||
);
|
||||
|
||||
try {
|
||||
this.discobotDiscoveries.lastQuery = this.query;
|
||||
await ajax("/discourse-ai/ai-bot/discover", {
|
||||
data: { query: this.query },
|
||||
});
|
||||
} catch {
|
||||
this.hideDiscoveries = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleDiscovery() {
|
||||
this.fullDiscoveryToggled = !this.fullDiscoveryToggled;
|
||||
}
|
||||
|
||||
timeoutDiscovery() {
|
||||
this.loadingDiscoveries = false;
|
||||
this.discobotDiscoveries.discovery = "";
|
||||
|
||||
this.discobotDiscoveries.discoveryTimedOut = true;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ai-search-discoveries"
|
||||
{{didInsert this.subscribe @searchTerm}}
|
||||
{{didUpdate this.subscribe @searchTerm}}
|
||||
{{didInsert this.triggerDiscovery this.query}}
|
||||
{{willDestroy this.unsubscribe}}
|
||||
>
|
||||
<div class="ai-search-discoveries__completion">
|
||||
{{#if this.loadingDiscoveries}}
|
||||
<AiBlinkingAnimation />
|
||||
{{else if this.discobotDiscoveries.discoveryTimedOut}}
|
||||
{{i18n "discourse_ai.discobot_discoveries.timed_out"}}
|
||||
{{else}}
|
||||
<article
|
||||
class={{concatClass
|
||||
"ai-search-discoveries__discovery"
|
||||
(if this.renderPreviewOnly "preview")
|
||||
(if this.isStreaming "streaming")
|
||||
"streamable-content"
|
||||
}}
|
||||
>
|
||||
<div class="cooked">
|
||||
<CookText @rawText={{this.renderedDiscovery}} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{#if this.canShowExpandtoggle}}
|
||||
<DButton
|
||||
class="btn-flat btn-text ai-search-discoveries__toggle"
|
||||
@label={{this.toggleLabel}}
|
||||
@icon={{this.toggleIcon}}
|
||||
@action={{this.toggleDiscovery}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -1,126 +1,18 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AiBlinkingAnimation from "./ai-blinking-animation";
|
||||
import AiIndicatorWave from "./ai-indicator-wave";
|
||||
|
||||
class Block {
|
||||
@tracked show = false;
|
||||
@tracked shown = false;
|
||||
@tracked blinking = false;
|
||||
const AiSummarySkeleton = <template>
|
||||
<div class="ai-summary__container">
|
||||
<AiBlinkingAnimation />
|
||||
|
||||
constructor(args = {}) {
|
||||
this.show = args.show ?? false;
|
||||
this.shown = args.shown ?? false;
|
||||
}
|
||||
}
|
||||
<span>
|
||||
<div class="ai-summary__generating-text">
|
||||
{{i18n "summary.in_progress"}}
|
||||
</div>
|
||||
<AiIndicatorWave @loading={{true}} />
|
||||
</span>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
const BLOCKS_SIZE = 20; // changing this requires to change css accordingly
|
||||
|
||||
export default class AiSummarySkeleton extends Component {
|
||||
blocks = [...Array.from({ length: BLOCKS_SIZE }, () => new Block())];
|
||||
|
||||
#nextBlockBlinkingTimer;
|
||||
#blockBlinkingTimer;
|
||||
#blockShownTimer;
|
||||
|
||||
@action
|
||||
setupAnimation() {
|
||||
this.blocks.firstObject.show = true;
|
||||
this.blocks.firstObject.shown = true;
|
||||
}
|
||||
|
||||
@action
|
||||
onBlinking(block) {
|
||||
if (!block.blinking) {
|
||||
return;
|
||||
}
|
||||
|
||||
block.show = false;
|
||||
|
||||
this.#nextBlockBlinkingTimer = discourseLater(
|
||||
this,
|
||||
() => {
|
||||
this.#nextBlock(block).blinking = true;
|
||||
},
|
||||
250
|
||||
);
|
||||
|
||||
this.#blockBlinkingTimer = discourseLater(
|
||||
this,
|
||||
() => {
|
||||
block.blinking = false;
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
onShowing(block) {
|
||||
if (!block.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#blockShownTimer = discourseLater(
|
||||
this,
|
||||
() => {
|
||||
this.#nextBlock(block).show = true;
|
||||
this.#nextBlock(block).shown = true;
|
||||
|
||||
if (this.blocks.lastObject === block) {
|
||||
this.blocks.firstObject.blinking = true;
|
||||
}
|
||||
},
|
||||
250
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
teardownAnimation() {
|
||||
cancel(this.#blockShownTimer);
|
||||
cancel(this.#nextBlockBlinkingTimer);
|
||||
cancel(this.#blockBlinkingTimer);
|
||||
}
|
||||
|
||||
#nextBlock(currentBlock) {
|
||||
if (currentBlock === this.blocks.lastObject) {
|
||||
return this.blocks.firstObject;
|
||||
} else {
|
||||
return this.blocks.objectAt(this.blocks.indexOf(currentBlock) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="ai-summary__container">
|
||||
<ul class="ai-summary__list" {{didInsert this.setupAnimation}}>
|
||||
{{#each this.blocks as |block|}}
|
||||
<li
|
||||
class={{concatClass
|
||||
"ai-summary__list-item"
|
||||
(if block.show "show")
|
||||
(if block.shown "is-shown")
|
||||
(if block.blinking "blink")
|
||||
}}
|
||||
{{didUpdate (fn this.onBlinking block) block.blinking}}
|
||||
{{didUpdate (fn this.onShowing block) block.show}}
|
||||
{{willDestroy this.teardownAnimation}}
|
||||
></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
<span>
|
||||
<div class="ai-summary__generating-text">
|
||||
{{i18n "summary.in_progress"}}
|
||||
</div>
|
||||
<AiIndicatorWave @loading={{true}} />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
export default AiSummarySkeleton;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user