Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit c816517

Browse files
committed
Merge branch 'main' into dev/glimmer-post-stream
2 parents db08849 + 1074801 commit c816517

File tree

30 files changed

+847
-466
lines changed

30 files changed

+847
-466
lines changed

app/controllers/discourse_ai/ai_bot/conversations_controller.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ def index
1515
Topic
1616
.private_messages_for_user(current_user)
1717
.where(user: current_user) # Only show PMs where the current user is the author
18-
.joins(:topic_users)
19-
.where(topic_users: { user_id: bot_user_ids })
18+
.joins(
19+
"INNER JOIN topic_custom_fields tcf ON tcf.topic_id = topics.id
20+
AND tcf.name = '#{DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD}'
21+
AND tcf.value = 't'",
22+
)
2023
.distinct
2124

2225
total = base_query.count

assets/javascripts/discourse/components/ai-bot-header-icon.gjs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import Component from "@glimmer/component";
22
import { action } from "@ember/object";
33
import { service } from "@ember/service";
44
import DButton from "discourse/components/d-button";
5+
import { defaultHomepage } from "discourse/lib/utilities";
56
import { i18n } from "discourse-i18n";
67
import { composeAiBotMessage } from "../lib/ai-bot-helper";
8+
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
79

810
export default class AiBotHeaderIcon extends Component {
9-
@service currentUser;
10-
@service siteSettings;
1111
@service composer;
12+
@service currentUser;
1213
@service router;
14+
@service sidebarState;
15+
@service siteSettings;
1316

1417
get bots() {
1518
const availableBots = this.currentUser.ai_enabled_chat_bots
@@ -23,20 +26,39 @@ export default class AiBotHeaderIcon extends Component {
2326
return this.bots.length > 0 && this.siteSettings.ai_bot_add_to_header;
2427
}
2528

29+
get icon() {
30+
if (this.clickShouldRouteOutOfConversations) {
31+
return "shuffle";
32+
}
33+
return "robot";
34+
}
35+
36+
get clickShouldRouteOutOfConversations() {
37+
return (
38+
this.siteSettings.ai_enable_experimental_bot_ux &&
39+
this.sidebarState.currentPanel?.key === AI_CONVERSATIONS_PANEL
40+
);
41+
}
42+
2643
@action
27-
compose() {
44+
onClick() {
45+
if (this.clickShouldRouteOutOfConversations) {
46+
return this.router.transitionTo(`discovery.${defaultHomepage()}`);
47+
}
48+
2849
if (this.siteSettings.ai_enable_experimental_bot_ux) {
2950
return this.router.transitionTo("discourse-ai-bot-conversations");
3051
}
52+
3153
composeAiBotMessage(this.bots[0], this.composer);
3254
}
3355

3456
<template>
3557
{{#if this.showHeaderButton}}
3658
<li>
3759
<DButton
38-
@action={{this.compose}}
39-
@icon="robot"
60+
@action={{this.onClick}}
61+
@icon={{this.icon}}
4062
title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
4163
class="ai-bot-button icon btn-flat"
4264
/>

assets/javascripts/discourse/components/ai-bot-sidebar-new-conversation.gjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
23
import { service } from "@ember/service";
34
import DButton from "discourse/components/d-button";
45
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
@@ -14,12 +15,18 @@ export default class AiBotSidebarNewConversation extends Component {
1415
);
1516
}
1617

18+
@action
19+
routeTo() {
20+
this.router.transitionTo("/discourse-ai/ai-bot/conversations");
21+
this.args.outletArgs?.toggleNavigationMenu?.();
22+
}
23+
1724
<template>
1825
{{#if this.shouldRender}}
1926
<DButton
20-
@route="/discourse-ai/ai-bot/conversations"
2127
@label="discourse_ai.ai_bot.conversations.new"
2228
@icon="plus"
29+
@action={{this.routeTo}}
2330
class="ai-new-question-button btn-default"
2431
/>
2532
{{/if}}

assets/javascripts/discourse/components/ai-llms-list-editor.gjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export default class AiLlmsListEditor extends Component {
116116
return i18n("discourse_ai.llms.usage.ai_persona", {
117117
persona: usage.name,
118118
});
119+
} else if (usage.type === "automation") {
120+
return i18n("discourse_ai.llms.usage.automation", {
121+
name: usage.name,
122+
});
119123
} else {
120124
return i18n("discourse_ai.llms.usage." + usage.type);
121125
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { hash } from "@ember/helper";
4+
import { next } from "@ember/runloop";
5+
import { service } from "@ember/service";
6+
import { i18n } from "discourse-i18n";
7+
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
8+
9+
const PERSONA_SELECTOR_KEY = "ai_persona_selector_id";
10+
const LLM_SELECTOR_KEY = "ai_llm_selector_id";
11+
12+
export default class AiPersonaLlmSelector extends Component {
13+
@service currentUser;
14+
@service keyValueStore;
15+
16+
@tracked llm;
17+
@tracked allowLLMSelector = true;
18+
19+
constructor() {
20+
super(...arguments);
21+
22+
if (this.botOptions?.length) {
23+
this.#loadStoredPersona();
24+
this.#loadStoredLlm();
25+
26+
next(() => {
27+
this.resetTargetRecipients();
28+
});
29+
}
30+
}
31+
32+
get composer() {
33+
return this.args?.outletArgs?.model;
34+
}
35+
36+
get hasLlmSelector() {
37+
return this.currentUser.ai_enabled_chat_bots.any((bot) => !bot.is_persona);
38+
}
39+
40+
get botOptions() {
41+
if (!this.currentUser.ai_enabled_personas) {
42+
return;
43+
}
44+
45+
let enabledPersonas = this.currentUser.ai_enabled_personas;
46+
47+
if (!this.hasLlmSelector) {
48+
enabledPersonas = enabledPersonas.filter((persona) => persona.username);
49+
}
50+
51+
return enabledPersonas.map((persona) => {
52+
return {
53+
id: persona.id,
54+
name: persona.name,
55+
description: persona.description,
56+
};
57+
});
58+
}
59+
60+
get filterable() {
61+
return this.botOptions.length > 8;
62+
}
63+
64+
get value() {
65+
return this._value;
66+
}
67+
68+
set value(newValue) {
69+
this._value = newValue;
70+
this.keyValueStore.setItem(PERSONA_SELECTOR_KEY, newValue);
71+
this.args.setPersonaId(newValue);
72+
this.setAllowLLMSelector();
73+
this.resetTargetRecipients();
74+
}
75+
76+
setAllowLLMSelector() {
77+
if (!this.hasLlmSelector) {
78+
this.allowLLMSelector = false;
79+
return;
80+
}
81+
82+
const persona = this.currentUser.ai_enabled_personas.find(
83+
(innerPersona) => innerPersona.id === this._value
84+
);
85+
86+
this.allowLLMSelector = !persona?.force_default_llm;
87+
}
88+
89+
get currentLlm() {
90+
return this.llm;
91+
}
92+
93+
set currentLlm(newValue) {
94+
this.llm = newValue;
95+
this.keyValueStore.setItem(LLM_SELECTOR_KEY, newValue);
96+
97+
this.resetTargetRecipients();
98+
}
99+
100+
resetTargetRecipients() {
101+
if (this.allowLLMSelector) {
102+
const botUsername = this.currentUser.ai_enabled_chat_bots.find(
103+
(bot) => bot.id === this.llm
104+
).username;
105+
this.args.setTargetRecipient(botUsername);
106+
} else {
107+
const persona = this.currentUser.ai_enabled_personas.find(
108+
(innerPersona) => innerPersona.id === this._value
109+
);
110+
this.args.setTargetRecipient(persona.username || "");
111+
}
112+
}
113+
114+
get llmOptions() {
115+
const availableBots = this.currentUser.ai_enabled_chat_bots
116+
.filter((bot) => !bot.is_persona)
117+
.filter(Boolean);
118+
119+
return availableBots
120+
.map((bot) => {
121+
return {
122+
id: bot.id,
123+
name: bot.display_name,
124+
};
125+
})
126+
.sort((a, b) => a.name.localeCompare(b.name));
127+
}
128+
129+
get showLLMSelector() {
130+
return this.allowLLMSelector && this.llmOptions.length > 1;
131+
}
132+
133+
#loadStoredPersona() {
134+
let personaId = this.keyValueStore.getItem(PERSONA_SELECTOR_KEY);
135+
136+
this._value = this.botOptions[0].id;
137+
if (personaId) {
138+
personaId = parseInt(personaId, 10);
139+
if (this.botOptions.any((bot) => bot.id === personaId)) {
140+
this._value = personaId;
141+
}
142+
}
143+
144+
this.args.setPersonaId(this._value);
145+
}
146+
147+
#loadStoredLlm() {
148+
this.setAllowLLMSelector();
149+
150+
if (this.hasLlmSelector) {
151+
let llm = this.keyValueStore.getItem(LLM_SELECTOR_KEY);
152+
153+
const llmOption =
154+
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
155+
this.llmOptions[0];
156+
157+
if (llmOption) {
158+
llm = llmOption.id;
159+
} else {
160+
llm = "";
161+
}
162+
163+
if (llm) {
164+
next(() => {
165+
this.currentLlm = llm;
166+
});
167+
}
168+
}
169+
}
170+
171+
<template>
172+
<div class="persona-llm-selector">
173+
<div class="persona-llm-selector__selection-wrapper gpt-persona">
174+
{{#if @showLabels}}
175+
<label>{{i18n "discourse_ai.ai_bot.persona"}}</label>
176+
{{/if}}
177+
<DropdownSelectBox
178+
class="persona-llm-selector__persona-dropdown"
179+
@value={{this.value}}
180+
@content={{this.botOptions}}
181+
@options={{hash
182+
icon=(if @showLabels "angle-down" "robot")
183+
filterable=this.filterable
184+
}}
185+
/>
186+
</div>
187+
{{#if this.showLLMSelector}}
188+
<div class="persona-llm-selector__selection-wrapper llm-selector">
189+
{{#if @showLabels}}
190+
<label>{{i18n "discourse_ai.ai_bot.llm"}}</label>
191+
{{/if}}
192+
<DropdownSelectBox
193+
class="persona-llm-selector__llm-dropdown"
194+
@value={{this.currentLlm}}
195+
@content={{this.llmOptions}}
196+
@options={{hash icon=(if @showLabels "angle-down" "globe")}}
197+
/>
198+
</div>
199+
{{/if}}
200+
</div>
201+
</template>
202+
}

0 commit comments

Comments
 (0)