mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-28 10:32:15 +00:00
DEV: converts llm admin page to use form kit (#1099)
This also converts the quota editor, and the quota modal.
This commit is contained in:
parent
43c56d7c92
commit
40e996b174
@ -25,9 +25,9 @@ export default class AiLlm extends RestModel {
|
|||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async testConfig() {
|
async testConfig(llmConfig) {
|
||||||
return await ajax(`/admin/plugins/discourse-ai/ai-llms/test.json`, {
|
return await ajax("/admin/plugins/discourse-ai/ai-llms/test.json", {
|
||||||
data: { ai_llm: this.createProperties() },
|
data: { ai_llm: llmConfig ?? this.createProperties() },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,76 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { cached, tracked } from "@glimmer/tracking";
|
||||||
import { Input } from "@ember/component";
|
import { concat, fn, get } from "@ember/helper";
|
||||||
import { concat, get, hash } from "@ember/helper";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action, computed } from "@ember/object";
|
import { action, computed } from "@ember/object";
|
||||||
import { LinkTo } from "@ember/routing";
|
import { LinkTo } from "@ember/routing";
|
||||||
import { later } from "@ember/runloop";
|
import { later } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { eq } from "truth-helpers";
|
import { eq, gt } from "truth-helpers";
|
||||||
import DButton from "discourse/components/d-button";
|
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||||
|
import Form from "discourse/components/form";
|
||||||
import Avatar from "discourse/helpers/bound-avatar-template";
|
import Avatar from "discourse/helpers/bound-avatar-template";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import icon from "discourse-common/helpers/d-icon";
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import AdminUser from "admin/models/admin-user";
|
import AdminUser from "admin/models/admin-user";
|
||||||
import ComboBox from "select-kit/components/combo-box";
|
import DurationSelector from "./ai-quota-duration-selector";
|
||||||
import DTooltip from "float-kit/components/d-tooltip";
|
|
||||||
import AiLlmQuotaEditor from "./ai-llm-quota-editor";
|
|
||||||
import AiLlmQuotaModal from "./modal/ai-llm-quota-modal";
|
import AiLlmQuotaModal from "./modal/ai-llm-quota-modal";
|
||||||
|
|
||||||
export default class AiLlmEditorForm extends Component {
|
export default class AiLlmEditorForm extends Component {
|
||||||
@service toasts;
|
@service toasts;
|
||||||
@service router;
|
@service router;
|
||||||
@service dialog;
|
@service dialog;
|
||||||
|
@service modal;
|
||||||
|
|
||||||
@tracked isSaving = false;
|
@tracked isSaving = false;
|
||||||
|
|
||||||
@tracked testRunning = false;
|
@tracked testRunning = false;
|
||||||
@tracked testResult = null;
|
@tracked testResult = null;
|
||||||
@tracked testError = null;
|
@tracked testError = null;
|
||||||
@tracked apiKeySecret = true;
|
|
||||||
@tracked quotaCount = 0;
|
|
||||||
|
|
||||||
@tracked modalIsVisible = false;
|
@cached
|
||||||
|
get formData() {
|
||||||
|
if (this.args.llmTemplate) {
|
||||||
|
let [id, modelName] = this.args.llmTemplate.split(/-(.*)/);
|
||||||
|
if (id === "none") {
|
||||||
|
return { provider_params: {} };
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
const info = this.args.llms.resultSetMeta.presets.findBy("id", id);
|
||||||
super(...arguments);
|
const modelInfo = info.models.findBy("name", modelName);
|
||||||
this.updateQuotaCount();
|
const params =
|
||||||
|
this.args.llms.resultSetMeta.provider_params[info.provider] ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
max_prompt_tokens: modelInfo.tokens,
|
||||||
|
tokenizer: info.tokenizer,
|
||||||
|
url: modelInfo.endpoint || info.endpoint,
|
||||||
|
display_name: modelInfo.display_name,
|
||||||
|
name: modelInfo.name,
|
||||||
|
provider: info.provider,
|
||||||
|
provider_params: Object.fromEntries(
|
||||||
|
Object.entries(params).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
v?.type === "enum" ? v.default : null,
|
||||||
|
])
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { model } = this.args;
|
||||||
|
|
||||||
|
return {
|
||||||
|
max_prompt_tokens: model.max_prompt_tokens,
|
||||||
|
api_key: model.api_key,
|
||||||
|
tokenizer: model.tokenizer,
|
||||||
|
url: model.url,
|
||||||
|
display_name: model.display_name,
|
||||||
|
name: model.name,
|
||||||
|
provider: model.provider,
|
||||||
|
enabled_chat_bot: model.enabled_chat_bot,
|
||||||
|
vision_enabled: model.vision_enabled,
|
||||||
|
provider_params: model.provider_params,
|
||||||
|
llm_quotas: model.llm_quotas,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedProviders() {
|
get selectedProviders() {
|
||||||
@ -44,9 +78,17 @@ export default class AiLlmEditorForm extends Component {
|
|||||||
return i18n(`discourse_ai.llms.providers.${provName}`);
|
return i18n(`discourse_ai.llms.providers.${provName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.args.llms.resultSetMeta.providers.map((prov) => {
|
return this.args.llms.resultSetMeta.providers
|
||||||
return { id: prov, name: t(prov) };
|
.map((prov) => {
|
||||||
});
|
return { id: prov, name: t(prov) };
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
get tokenizers() {
|
||||||
|
return this.args.llms.resultSetMeta.tokenizers.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get adminUser() {
|
get adminUser() {
|
||||||
@ -94,55 +136,49 @@ export default class AiLlmEditorForm extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get showQuotas() {
|
|
||||||
return this.quotaCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showAddQuotaButton() {
|
get showAddQuotaButton() {
|
||||||
return !this.showQuotas && !this.args.model.isNew;
|
return !this.args.model.isNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateQuotaCount() {
|
openAddQuotaModal(addItemToCollection) {
|
||||||
this.quotaCount = this.args.model?.llm_quotas?.length || 0;
|
this.modal.show(AiLlmQuotaModal, {
|
||||||
}
|
model: { llm: this.args.model, addItemToCollection },
|
||||||
|
|
||||||
@action
|
|
||||||
openAddQuotaModal() {
|
|
||||||
this.modalIsVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed("args.model.provider")
|
|
||||||
get metaProviderParams() {
|
|
||||||
const params =
|
|
||||||
this.args.llms.resultSetMeta.provider_params[this.args.model.provider] ||
|
|
||||||
{};
|
|
||||||
|
|
||||||
return Object.entries(params).map(([field, value]) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return { field, type: value };
|
|
||||||
} else if (typeof value === "object") {
|
|
||||||
if (value.values) {
|
|
||||||
value = { ...value };
|
|
||||||
value.values = value.values.map((v) => {
|
|
||||||
return { id: v, name: v };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.args.model.provider_params[field] =
|
|
||||||
this.args.model.provider_params[field] || value.default;
|
|
||||||
return { field, ...value };
|
|
||||||
}
|
|
||||||
return { field, type: "text" }; // fallback
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async save() {
|
metaProviderParams(provider) {
|
||||||
|
const params = this.args.llms.resultSetMeta.provider_params[provider] || {};
|
||||||
|
|
||||||
|
return Object.entries(params).reduce((acc, [field, value]) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
acc[field] = { type: value };
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
if (value.values) {
|
||||||
|
value = { ...value };
|
||||||
|
value.values = value.values.map((v) => ({ id: v, name: v }));
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[field] = {
|
||||||
|
type: value.type || "text",
|
||||||
|
values: value.values || [],
|
||||||
|
default: value.default ?? undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
acc[field] = { type: "text" }; // fallback
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async save(data) {
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
const isNew = this.args.model.isNew;
|
const isNew = this.args.model.isNew;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.args.model.save();
|
await this.args.model.save(data);
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
this.args.llms.addObject(this.args.model);
|
this.args.llms.addObject(this.args.model);
|
||||||
@ -163,11 +199,11 @@ export default class AiLlmEditorForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async test() {
|
async test(data) {
|
||||||
this.testRunning = true;
|
this.testRunning = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configTestResult = await this.args.model.testConfig();
|
const configTestResult = await this.args.model.testConfig(data);
|
||||||
this.testResult = configTestResult.success;
|
this.testResult = configTestResult.success;
|
||||||
|
|
||||||
if (this.testResult) {
|
if (this.testResult) {
|
||||||
@ -184,16 +220,6 @@ export default class AiLlmEditorForm extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
makeApiKeySecret() {
|
|
||||||
this.apiKeySecret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
toggleApiKeySecret() {
|
|
||||||
this.apiKeySecret = !this.apiKeySecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
delete() {
|
delete() {
|
||||||
return this.dialog.confirm({
|
return this.dialog.confirm({
|
||||||
@ -212,154 +238,167 @@ export default class AiLlmEditorForm extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
closeAddQuotaModal() {
|
|
||||||
this.modalIsVisible = false;
|
|
||||||
this.updateQuotaCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.seeded}}
|
<Form
|
||||||
<div class="alert alert-info">
|
@onSubmit={{this.save}}
|
||||||
{{icon "circle-exclamation"}}
|
@data={{this.formData}}
|
||||||
{{i18n "discourse_ai.llms.seeded_warning"}}
|
class="ai-llm-editor {{if this.seeded 'seeded'}}"
|
||||||
</div>
|
as |form data|
|
||||||
{{/if}}
|
>
|
||||||
{{#if this.modulesUsingModel}}
|
{{#if this.seeded}}
|
||||||
<div class="alert alert-info">
|
<form.Alert @icon="circle-info">
|
||||||
{{icon "circle-exclamation"}}
|
{{i18n "discourse_ai.llms.seeded_warning"}}
|
||||||
{{this.inUseWarning}}
|
</form.Alert>
|
||||||
</div>
|
{{/if}}
|
||||||
{{/if}}
|
|
||||||
<form class="form-horizontal ai-llm-editor {{if this.seeded 'seeded'}}">
|
{{#if this.modulesUsingModel}}
|
||||||
<div class="control-group">
|
<form.Alert @icon="circle-info">
|
||||||
<label>{{i18n "discourse_ai.llms.display_name"}}</label>
|
{{this.inUseWarning}}
|
||||||
<Input
|
</form.Alert>
|
||||||
class="ai-llm-editor-input ai-llm-editor__display-name"
|
{{/if}}
|
||||||
@type="text"
|
|
||||||
@value={{@model.display_name}}
|
<form.Field
|
||||||
disabled={{this.seeded}}
|
@name="display_name"
|
||||||
/>
|
@title={{i18n "discourse_ai.llms.display_name"}}
|
||||||
</div>
|
@validation="required|length:1,100"
|
||||||
<div class="control-group">
|
@disabled={{this.seeded}}
|
||||||
<label>{{i18n "discourse_ai.llms.name"}}</label>
|
@format="large"
|
||||||
<Input
|
@tooltip={{i18n "discourse_ai.llms.hints.max_prompt_tokens"}}
|
||||||
class="ai-llm-editor-input ai-llm-editor__name"
|
as |field|
|
||||||
@type="text"
|
>
|
||||||
@value={{@model.name}}
|
<field.Input />
|
||||||
disabled={{this.seeded}}
|
</form.Field>
|
||||||
/>
|
|
||||||
<DTooltip
|
<form.Field
|
||||||
@icon="circle-question"
|
@name="name"
|
||||||
@content={{i18n "discourse_ai.llms.hints.name"}}
|
@title={{i18n "discourse_ai.llms.name"}}
|
||||||
/>
|
@tooltip={{i18n "discourse_ai.llms.hints.name"}}
|
||||||
</div>
|
@validation="required"
|
||||||
<div class="control-group">
|
@disabled={{this.seeded}}
|
||||||
<label>{{i18n "discourse_ai.llms.provider"}}</label>
|
@format="large"
|
||||||
<ComboBox
|
as |field|
|
||||||
@value={{@model.provider}}
|
>
|
||||||
@content={{this.selectedProviders}}
|
<field.Input />
|
||||||
@class="ai-llm-editor__provider"
|
</form.Field>
|
||||||
@options={{hash disabled=this.seeded}}
|
|
||||||
/>
|
<form.Field
|
||||||
</div>
|
@name="provider"
|
||||||
|
@title={{i18n "discourse_ai.llms.provider"}}
|
||||||
|
@disabled={{this.seeded}}
|
||||||
|
@format="large"
|
||||||
|
@validation="required"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Select as |select|>
|
||||||
|
{{#each this.selectedProviders as |provider|}}
|
||||||
|
<select.Option
|
||||||
|
@value={{provider.id}}
|
||||||
|
>{{provider.name}}</select.Option>
|
||||||
|
{{/each}}
|
||||||
|
</field.Select>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
{{#unless this.seeded}}
|
{{#unless this.seeded}}
|
||||||
{{#if this.canEditURL}}
|
{{#if this.canEditURL}}
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.llms.url"}}</label>
|
@name="url"
|
||||||
<Input
|
@title={{i18n "discourse_ai.llms.url"}}
|
||||||
class="ai-llm-editor-input ai-llm-editor__url"
|
@validation="required"
|
||||||
@type="text"
|
@format="large"
|
||||||
@value={{@model.url}}
|
as |field|
|
||||||
required="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.llms.api_key"}}</label>
|
|
||||||
<div class="ai-llm-editor__secret-api-key-group">
|
|
||||||
<Input
|
|
||||||
@value={{@model.api_key}}
|
|
||||||
class="ai-llm-editor-input ai-llm-editor__api-key"
|
|
||||||
@type={{if this.apiKeySecret "password" "text"}}
|
|
||||||
required="true"
|
|
||||||
{{on "focusout" this.makeApiKeySecret}}
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@action={{this.toggleApiKeySecret}}
|
|
||||||
@icon="far-eye-slash"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{#each this.metaProviderParams as |param|}}
|
|
||||||
<div
|
|
||||||
class="control-group ai-llm-editor-provider-param__{{param.type}}"
|
|
||||||
>
|
>
|
||||||
<label>{{i18n
|
<field.Input />
|
||||||
(concat "discourse_ai.llms.provider_fields." param.field)
|
</form.Field>
|
||||||
}}</label>
|
{{/if}}
|
||||||
{{#if (eq param.type "enum")}}
|
|
||||||
<ComboBox
|
<form.Field
|
||||||
@value={{mut (get @model.provider_params param.field)}}
|
@name="api_key"
|
||||||
@content={{param.values}}
|
@title={{i18n "discourse_ai.llms.api_key"}}
|
||||||
/>
|
@validation="required"
|
||||||
{{else if (eq param.type "checkbox")}}
|
@format="large"
|
||||||
<Input
|
as |field|
|
||||||
@type={{param.type}}
|
>
|
||||||
@checked={{mut (get @model.provider_params param.field)}}
|
<field.Password />
|
||||||
/>
|
</form.Field>
|
||||||
{{else}}
|
|
||||||
<Input
|
<form.Object @name="provider_params" as |object name|>
|
||||||
@type={{param.type}}
|
{{#let
|
||||||
@value={{mut (get @model.provider_params param.field)}}
|
(get (this.metaProviderParams data.provider) name)
|
||||||
/>
|
as |params|
|
||||||
{{/if}}
|
}}
|
||||||
</div>
|
<object.Field
|
||||||
{{/each}}
|
@name={{name}}
|
||||||
<div class="control-group">
|
@title={{i18n (concat "discourse_ai.llms.provider_fields." name)}}
|
||||||
<label>{{i18n "discourse_ai.llms.tokenizer"}}</label>
|
@format="large"
|
||||||
<ComboBox
|
as |field|
|
||||||
@value={{@model.tokenizer}}
|
>
|
||||||
@content={{@llms.resultSetMeta.tokenizers}}
|
{{#if (eq params.type "enum")}}
|
||||||
@class="ai-llm-editor__tokenizer"
|
<field.Select as |select|>
|
||||||
/>
|
{{#each params.values as |option|}}
|
||||||
</div>
|
<select.Option
|
||||||
<div class="control-group">
|
@value={{option.id}}
|
||||||
<label>{{i18n "discourse_ai.llms.max_prompt_tokens"}}</label>
|
>{{option.name}}</select.Option>
|
||||||
<Input
|
{{/each}}
|
||||||
@type="number"
|
</field.Select>
|
||||||
class="ai-llm-editor-input ai-llm-editor__max-prompt-tokens"
|
{{else if (eq params.type "checkbox")}}
|
||||||
step="any"
|
<field.Checkbox />
|
||||||
min="0"
|
{{else}}
|
||||||
lang="en"
|
<field.Input @type={{params.type}} />
|
||||||
@value={{@model.max_prompt_tokens}}
|
{{/if}}
|
||||||
required="true"
|
</object.Field>
|
||||||
/>
|
{{/let}}
|
||||||
<DTooltip
|
</form.Object>
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.llms.hints.max_prompt_tokens"}}
|
<form.Field
|
||||||
/>
|
@name="tokenizer"
|
||||||
</div>
|
@title={{i18n "discourse_ai.llms.tokenizer"}}
|
||||||
<div class="control-group ai-llm-editor__vision-enabled">
|
@disabled={{this.seeded}}
|
||||||
<Input @type="checkbox" @checked={{@model.vision_enabled}} />
|
@format="large"
|
||||||
<label>{{i18n "discourse_ai.llms.vision_enabled"}}</label>
|
@validation="required"
|
||||||
<DTooltip
|
as |field|
|
||||||
@icon="circle-question"
|
>
|
||||||
@content={{i18n "discourse_ai.llms.hints.vision_enabled"}}
|
<field.Select as |select|>
|
||||||
/>
|
{{#each this.tokenizers as |tokenizer|}}
|
||||||
</div>
|
<select.Option
|
||||||
<div class="control-group ai-llm-editor__enabled-chat-bot">
|
@value={{tokenizer.id}}
|
||||||
<Input @type="checkbox" @checked={{@model.enabled_chat_bot}} />
|
>{{tokenizer.name}}</select.Option>
|
||||||
<label>{{i18n "discourse_ai.llms.enabled_chat_bot"}}</label>
|
{{/each}}
|
||||||
<DTooltip
|
</field.Select>
|
||||||
@icon="circle-question"
|
</form.Field>
|
||||||
@content={{i18n "discourse_ai.llms.hints.enabled_chat_bot"}}
|
|
||||||
/>
|
<form.Field
|
||||||
</div>
|
@name="max_prompt_tokens"
|
||||||
|
@title={{i18n "discourse_ai.llms.max_prompt_tokens"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.llms.hints.max_prompt_tokens"}}
|
||||||
|
@validation="required"
|
||||||
|
@format="large"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input @type="number" step="any" min="0" lang="en" />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="vision_enabled"
|
||||||
|
@title={{i18n "discourse_ai.llms.vision_enabled"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.llms.hints.vision_enabled"}}
|
||||||
|
@format="large"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Checkbox />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="enabled_chat_bot"
|
||||||
|
@title={{i18n "discourse_ai.llms.enabled_chat_bot"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.llms.hints.enabled_chat_bot"}}
|
||||||
|
@format="large"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Checkbox />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
{{#if @model.user}}
|
{{#if @model.user}}
|
||||||
<div class="control-group">
|
<form.Container @title={{i18n "discourse_ai.llms.ai_bot_user"}}>
|
||||||
<label>{{i18n "discourse_ai.llms.ai_bot_user"}}</label>
|
|
||||||
<a
|
<a
|
||||||
class="avatar"
|
class="avatar"
|
||||||
href={{@model.user.path}}
|
href={{@model.user.path}}
|
||||||
@ -370,76 +409,166 @@ export default class AiLlmEditorForm extends Component {
|
|||||||
<LinkTo @route="adminUser" @model={{this.adminUser}}>
|
<LinkTo @route="adminUser" @model={{this.adminUser}}>
|
||||||
{{@model.user.username}}
|
{{@model.user.username}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</div>
|
</form.Container>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.showQuotas}}
|
{{#if (gt data.llm_quotas.length 0)}}
|
||||||
<div class="control-group">
|
<form.Container @title={{i18n "discourse_ai.llms.quotas.title"}}>
|
||||||
<label>{{i18n "discourse_ai.llms.quotas.title"}}</label>
|
<table class="ai-llm-quotas__table">
|
||||||
<AiLlmQuotaEditor
|
<thead class="ai-llm-quotas__table-head">
|
||||||
@model={{@model}}
|
<tr class="ai-llm-quotas__header-row">
|
||||||
@groups={{@groups}}
|
<th class="ai-llm-quotas__header">{{i18n
|
||||||
@didUpdate={{this.updateQuotaCount}}
|
"discourse_ai.llms.quotas.group"
|
||||||
/>
|
}}</th>
|
||||||
</div>
|
<th class="ai-llm-quotas__header">{{i18n
|
||||||
|
"discourse_ai.llms.quotas.max_tokens"
|
||||||
|
}}</th>
|
||||||
|
<th class="ai-llm-quotas__header">{{i18n
|
||||||
|
"discourse_ai.llms.quotas.max_usages"
|
||||||
|
}}</th>
|
||||||
|
<th class="ai-llm-quotas__header">{{i18n
|
||||||
|
"discourse_ai.llms.quotas.duration"
|
||||||
|
}}</th>
|
||||||
|
<th
|
||||||
|
class="ai-llm-quotas__header ai-llm-quotas__header--actions"
|
||||||
|
></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="ai-llm-quotas__table-body">
|
||||||
|
<form.Collection
|
||||||
|
@name="llm_quotas"
|
||||||
|
@tagName="tr"
|
||||||
|
class="ai-llm-quotas__row"
|
||||||
|
as |collection index collectionData|
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ai-llm-quotas__cell"
|
||||||
|
>{{collectionData.group_name}}</td>
|
||||||
|
<td class="ai-llm-quotas__cell">
|
||||||
|
<collection.Field
|
||||||
|
@name="max_tokens"
|
||||||
|
@title="max_tokens"
|
||||||
|
@showTitle={{false}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input
|
||||||
|
@type="number"
|
||||||
|
class="ai-llm-quotas__input"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</collection.Field>
|
||||||
|
</td>
|
||||||
|
<td class="ai-llm-quotas__cell">
|
||||||
|
<collection.Field
|
||||||
|
@name="max_usages"
|
||||||
|
@title="max_usages"
|
||||||
|
@showTitle={{false}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input
|
||||||
|
@type="number"
|
||||||
|
class="ai-llm-quotas__input"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</collection.Field>
|
||||||
|
</td>
|
||||||
|
<td class="ai-llm-quotas__cell">
|
||||||
|
<collection.Field
|
||||||
|
@name="duration_seconds"
|
||||||
|
@title="duration_seconds"
|
||||||
|
@showTitle={{false}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
<DurationSelector
|
||||||
|
@value={{collectionData.duration_seconds}}
|
||||||
|
@onChange={{field.set}}
|
||||||
|
/>
|
||||||
|
</field.Custom>
|
||||||
|
</collection.Field>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form.Button
|
||||||
|
@icon="trash-can"
|
||||||
|
@action={{fn collection.remove index}}
|
||||||
|
class="btn-danger ai-llm-quotas__delete-btn"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</form.Collection>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form.Container>
|
||||||
|
|
||||||
|
<form.Button
|
||||||
|
@action={{fn
|
||||||
|
this.openAddQuotaModal
|
||||||
|
(fn form.addItemToCollection "llm_quotas")
|
||||||
|
}}
|
||||||
|
@icon="plus"
|
||||||
|
@label="discourse_ai.llms.quotas.add"
|
||||||
|
class="ai-llm-editor__add-quota-btn"
|
||||||
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="control-group ai-llm-editor__action_panel">
|
<form.Actions>
|
||||||
<DButton
|
<form.Button
|
||||||
class="ai-llm-editor__test"
|
@action={{fn this.test data}}
|
||||||
@action={{this.test}}
|
|
||||||
@disabled={{this.testRunning}}
|
@disabled={{this.testRunning}}
|
||||||
@label="discourse_ai.llms.tests.title"
|
@label="discourse_ai.llms.tests.title"
|
||||||
/>
|
/>
|
||||||
{{#if this.showAddQuotaButton}}
|
|
||||||
<DButton
|
<form.Submit />
|
||||||
@action={{this.openAddQuotaModal}}
|
|
||||||
|
{{#if (eq data.llm_quotas.length 0)}}
|
||||||
|
<form.Button
|
||||||
|
@action={{fn
|
||||||
|
this.openAddQuotaModal
|
||||||
|
(fn form.addItemToCollection "llm_quotas")
|
||||||
|
}}
|
||||||
@label="discourse_ai.llms.quotas.add"
|
@label="discourse_ai.llms.quotas.add"
|
||||||
class="btn"
|
class="ai-llm-editor__add-quota-btn"
|
||||||
/>
|
/>
|
||||||
{{#if this.modalIsVisible}}
|
|
||||||
<AiLlmQuotaModal
|
|
||||||
@model={{hash llm=@model}}
|
|
||||||
@closeModal={{this.closeAddQuotaModal}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<DButton
|
|
||||||
class="btn-primary ai-llm-editor__save"
|
|
||||||
@action={{this.save}}
|
|
||||||
@disabled={{this.isSaving}}
|
|
||||||
@label="discourse_ai.llms.save"
|
|
||||||
/>
|
|
||||||
{{#unless @model.isNew}}
|
{{#unless @model.isNew}}
|
||||||
<DButton
|
<form.Button
|
||||||
@action={{this.delete}}
|
@action={{this.delete}}
|
||||||
class="btn-danger ai-llm-editor__delete"
|
|
||||||
@label="discourse_ai.llms.delete"
|
@label="discourse_ai.llms.delete"
|
||||||
|
class="btn-danger"
|
||||||
/>
|
/>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</div>
|
</form.Actions>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
<div class="control-group ai-llm-editor-tests">
|
{{#if this.displayTestResult}}
|
||||||
{{#if this.displayTestResult}}
|
<form.Field
|
||||||
{{#if this.testRunning}}
|
@showTitle={{false}}
|
||||||
<div class="spinner small"></div>
|
@name="test_results"
|
||||||
{{i18n "discourse_ai.llms.tests.running"}}
|
@title="test_results"
|
||||||
{{else}}
|
@format="full"
|
||||||
{{#if this.testResult}}
|
as |field|
|
||||||
<div class="ai-llm-editor-tests__success">
|
>
|
||||||
{{icon "check"}}
|
<field.Custom>
|
||||||
{{i18n "discourse_ai.llms.tests.success"}}
|
<ConditionalLoadingSpinner
|
||||||
</div>
|
@size="small"
|
||||||
{{else}}
|
@condition={{this.testRunning}}
|
||||||
<div class="ai-llm-editor-tests__failure">
|
>
|
||||||
{{icon "xmark"}}
|
{{#if this.testResult}}
|
||||||
{{this.testErrorMessage}}
|
<div class="ai-llm-editor-tests__success">
|
||||||
</div>
|
{{icon "check"}}
|
||||||
{{/if}}
|
{{i18n "discourse_ai.llms.tests.success"}}
|
||||||
{{/if}}
|
</div>
|
||||||
{{/if}}
|
{{else}}
|
||||||
</div>
|
<div class="ai-llm-editor-tests__failure">
|
||||||
</form>
|
{{icon "xmark"}}
|
||||||
|
{{this.testErrorMessage}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</ConditionalLoadingSpinner>
|
||||||
|
</field.Custom>
|
||||||
|
</form.Field>
|
||||||
|
{{/if}}
|
||||||
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,16 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import BackButton from "discourse/components/back-button";
|
import BackButton from "discourse/components/back-button";
|
||||||
import AiLlmEditorForm from "./ai-llm-editor-form";
|
import AiLlmEditorForm from "./ai-llm-editor-form";
|
||||||
|
|
||||||
export default class AiLlmEditor extends Component {
|
const AiLlmEditor = <template>
|
||||||
constructor() {
|
<BackButton
|
||||||
super(...arguments);
|
@route="adminPlugins.show.discourse-ai-llms"
|
||||||
if (this.args.llmTemplate) {
|
@label="discourse_ai.llms.back"
|
||||||
this.configurePreset();
|
/>
|
||||||
}
|
<AiLlmEditorForm
|
||||||
}
|
@model={{@model}}
|
||||||
|
@llmTemplate={{@llmTemplate}}
|
||||||
|
@llms={{@llms}}
|
||||||
|
/>
|
||||||
|
</template>;
|
||||||
|
|
||||||
@action
|
export default AiLlmEditor;
|
||||||
configurePreset() {
|
|
||||||
let [id, model] = this.args.llmTemplate.split(/-(.*)/);
|
|
||||||
if (id === "none") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = this.args.llms.resultSetMeta.presets.findBy("id", id);
|
|
||||||
const modelInfo = info.models.findBy("name", model);
|
|
||||||
|
|
||||||
this.args.model.setProperties({
|
|
||||||
max_prompt_tokens: modelInfo.tokens,
|
|
||||||
tokenizer: info.tokenizer,
|
|
||||||
url: modelInfo.endpoint || info.endpoint,
|
|
||||||
display_name: modelInfo.display_name,
|
|
||||||
name: modelInfo.name,
|
|
||||||
provider: info.provider,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BackButton
|
|
||||||
@route="adminPlugins.show.discourse-ai-llms"
|
|
||||||
@label="discourse_ai.llms.back"
|
|
||||||
/>
|
|
||||||
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
|
|
||||||
</template>
|
|
||||||
}
|
|
||||||
|
@ -1,178 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { fn, hash } from "@ember/helper";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
import DurationSelector from "./ai-quota-duration-selector";
|
|
||||||
import AiLlmQuotaModal from "./modal/ai-llm-quota-modal";
|
|
||||||
|
|
||||||
export default class AiLlmQuotaEditor extends Component {
|
|
||||||
@service store;
|
|
||||||
@service dialog;
|
|
||||||
@service site;
|
|
||||||
|
|
||||||
@tracked newQuotaGroupIds = null;
|
|
||||||
@tracked newQuotaTokens = null;
|
|
||||||
@tracked newQuotaUsages = null;
|
|
||||||
@tracked newQuotaDuration = 86400; // 1 day default
|
|
||||||
@tracked modalIsVisible = false;
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateExistingQuotaTokens(quota, event) {
|
|
||||||
quota.max_tokens = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateExistingQuotaUsages(quota, event) {
|
|
||||||
quota.max_usages = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateExistingQuotaDuration(quota, value) {
|
|
||||||
quota.duration_seconds = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
openAddQuotaModal() {
|
|
||||||
this.modalIsVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get canAddQuota() {
|
|
||||||
return (
|
|
||||||
this.newQuotaGroupId &&
|
|
||||||
(this.newQuotaTokens || this.newQuotaUsages) &&
|
|
||||||
this.newQuotaDuration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateQuotaTokens(event) {
|
|
||||||
this.newQuotaTokens = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateQuotaUsages(event) {
|
|
||||||
this.newQuotaUsages = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateQuotaDuration(event) {
|
|
||||||
this.newQuotaDuration = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateGroups(groups) {
|
|
||||||
this.newQuotaGroupIds = groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async addQuota() {
|
|
||||||
const quota = {
|
|
||||||
group_id: this.newQuotaGroupIds[0],
|
|
||||||
group_name: this.site.groups.findBy("id", this.newQuotaGroupIds[0])?.name,
|
|
||||||
llm_model_id: this.args.model.id,
|
|
||||||
max_tokens: this.newQuotaTokens,
|
|
||||||
max_usages: this.newQuotaUsages,
|
|
||||||
duration_seconds: this.newQuotaDuration,
|
|
||||||
};
|
|
||||||
this.args.model.llm_quotas.pushObject(quota);
|
|
||||||
if (this.args.didUpdate) {
|
|
||||||
this.args.didUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async deleteQuota(quota) {
|
|
||||||
this.args.model.llm_quotas.removeObject(quota);
|
|
||||||
if (this.args.didUpdate) {
|
|
||||||
this.args.didUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
closeAddQuotaModal() {
|
|
||||||
this.modalIsVisible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="ai-llm-quotas">
|
|
||||||
<table class="ai-llm-quotas__table">
|
|
||||||
<thead class="ai-llm-quotas__table-head">
|
|
||||||
<tr class="ai-llm-quotas__header-row">
|
|
||||||
<th class="ai-llm-quotas__header">{{i18n
|
|
||||||
"discourse_ai.llms.quotas.group"
|
|
||||||
}}</th>
|
|
||||||
<th class="ai-llm-quotas__header">{{i18n
|
|
||||||
"discourse_ai.llms.quotas.max_tokens"
|
|
||||||
}}</th>
|
|
||||||
<th class="ai-llm-quotas__header">{{i18n
|
|
||||||
"discourse_ai.llms.quotas.max_usages"
|
|
||||||
}}</th>
|
|
||||||
<th class="ai-llm-quotas__header">{{i18n
|
|
||||||
"discourse_ai.llms.quotas.duration"
|
|
||||||
}}</th>
|
|
||||||
<th
|
|
||||||
class="ai-llm-quotas__header ai-llm-quotas__header--actions"
|
|
||||||
></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="ai-llm-quotas__table-body">
|
|
||||||
{{#each @model.llm_quotas as |quota|}}
|
|
||||||
<tr class="ai-llm-quotas__row">
|
|
||||||
<td class="ai-llm-quotas__cell">{{quota.group_name}}</td>
|
|
||||||
<td class="ai-llm-quotas__cell">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={{quota.max_tokens}}
|
|
||||||
class="ai-llm-quotas__input"
|
|
||||||
min="1"
|
|
||||||
{{on "input" (fn this.updateExistingQuotaTokens quota)}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="ai-llm-quotas__cell">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={{quota.max_usages}}
|
|
||||||
class="ai-llm-quotas__input"
|
|
||||||
min="1"
|
|
||||||
{{on "input" (fn this.updateExistingQuotaUsages quota)}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="ai-llm-quotas__cell">
|
|
||||||
<DurationSelector
|
|
||||||
@value={{quota.duration_seconds}}
|
|
||||||
@onChange={{fn this.updateExistingQuotaDuration quota}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="ai-llm-quotas__cell ai-llm-quotas__cell--actions">
|
|
||||||
<DButton
|
|
||||||
@icon="trash-can"
|
|
||||||
class="btn-danger ai-llm-quotas__delete-btn"
|
|
||||||
@action={{fn this.deleteQuota quota}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="ai-llm-quotas__actions">
|
|
||||||
<DButton
|
|
||||||
@action={{this.openAddQuotaModal}}
|
|
||||||
@icon="plus"
|
|
||||||
@label="discourse_ai.llms.quotas.add"
|
|
||||||
class="btn"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if this.modalIsVisible}}
|
|
||||||
<AiLlmQuotaModal
|
|
||||||
@model={{hash llm=@model}}
|
|
||||||
@closeModal={{this.closeAddQuotaModal}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
}
|
|
@ -1,66 +1,26 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { cached } from "@glimmer/tracking";
|
||||||
import { hash } from "@ember/helper";
|
import { fn, hash } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { not } from "truth-helpers";
|
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
import DModal from "discourse/components/d-modal";
|
import DModal from "discourse/components/d-modal";
|
||||||
|
import Form from "discourse/components/form";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import GroupChooser from "select-kit/components/group-chooser";
|
import GroupChooser from "select-kit/components/group-chooser";
|
||||||
import DTooltip from "float-kit/components/d-tooltip";
|
|
||||||
import DurationSelector from "../ai-quota-duration-selector";
|
import DurationSelector from "../ai-quota-duration-selector";
|
||||||
|
|
||||||
export default class AiLlmQuotaModal extends Component {
|
export default class AiLlmQuotaModal extends Component {
|
||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
@tracked groupIds = null;
|
|
||||||
@tracked maxTokens = null;
|
|
||||||
@tracked maxUsages = null;
|
|
||||||
@tracked duration = 86400; // Default 1 day
|
|
||||||
|
|
||||||
get canSave() {
|
|
||||||
return (
|
|
||||||
this.groupIds?.length > 0 &&
|
|
||||||
(this.maxTokens || this.maxUsages) &&
|
|
||||||
this.duration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateGroups(groups) {
|
save(data) {
|
||||||
this.groupIds = groups;
|
const quota = { ...data };
|
||||||
}
|
quota.group_name = this.site.groups.findBy("id", data.group_id).name;
|
||||||
|
quota.llm_model_id = this.args.model.id;
|
||||||
|
|
||||||
@action
|
this.args.model.addItemToCollection(quota);
|
||||||
updateDuration(value) {
|
|
||||||
this.duration = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateMaxTokens(event) {
|
|
||||||
this.maxTokens = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateMaxUsages(event) {
|
|
||||||
this.maxUsages = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
save() {
|
|
||||||
const quota = {
|
|
||||||
group_id: this.groupIds[0],
|
|
||||||
group_name: this.site.groups.findBy("id", this.groupIds[0]).name,
|
|
||||||
llm_model_id: this.args.model.id,
|
|
||||||
max_tokens: this.maxTokens,
|
|
||||||
max_usages: this.maxUsages,
|
|
||||||
duration_seconds: this.duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.args.model.llm.llm_quotas.pushObject(quota);
|
|
||||||
this.args.closeModal();
|
this.args.closeModal();
|
||||||
|
|
||||||
if (this.args.model.onSave) {
|
if (this.args.model.onSave) {
|
||||||
this.args.model.onSave();
|
this.args.model.onSave();
|
||||||
}
|
}
|
||||||
@ -75,6 +35,39 @@ export default class AiLlmQuotaModal extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get quota() {
|
||||||
|
return {
|
||||||
|
group_id: null,
|
||||||
|
llm_model_id: null,
|
||||||
|
max_tokens: null,
|
||||||
|
max_usages: null,
|
||||||
|
duration_seconds: moment.duration(1, "day").asSeconds(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setGroupId(field, groups) {
|
||||||
|
field.set(groups[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
validateForm(data, { addError, removeError }) {
|
||||||
|
if (!data.max_tokens && !data.max_usages) {
|
||||||
|
addError("max_tokens", {
|
||||||
|
title: i18n("discourse_ai.llms.quotas.max_tokens"),
|
||||||
|
message: i18n("discourse_ai.llms.quotas.max_tokens_required"),
|
||||||
|
});
|
||||||
|
addError("max_usages", {
|
||||||
|
title: i18n("discourse_ai.llms.quotas.max_usages"),
|
||||||
|
message: i18n("discourse_ai.llms.quotas.max_usages_required"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
removeError("max_tokens");
|
||||||
|
removeError("max_usages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DModal
|
<DModal
|
||||||
@title={{i18n "discourse_ai.llms.quotas.add_title"}}
|
@title={{i18n "discourse_ai.llms.quotas.add_title"}}
|
||||||
@ -82,63 +75,70 @@ export default class AiLlmQuotaModal extends Component {
|
|||||||
class="ai-llm-quota-modal"
|
class="ai-llm-quota-modal"
|
||||||
>
|
>
|
||||||
<:body>
|
<:body>
|
||||||
<div class="control-group">
|
<Form
|
||||||
<label>{{i18n "discourse_ai.llms.quotas.group"}}</label>
|
@validate={{this.validateForm}}
|
||||||
<GroupChooser
|
@onSubmit={{this.save}}
|
||||||
@value={{this.groupIds}}
|
@data={{this.quota}}
|
||||||
@content={{this.availableGroups}}
|
as |form data|
|
||||||
@onChange={{this.updateGroups}}
|
>
|
||||||
@options={{hash maximum=1}}
|
<form.Field
|
||||||
/>
|
@name="group_id"
|
||||||
</div>
|
@title={{i18n "discourse_ai.llms.quotas.group"}}
|
||||||
|
@validation="required"
|
||||||
|
@format="large"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
<GroupChooser
|
||||||
|
@value={{data.group_id}}
|
||||||
|
@content={{this.availableGroups}}
|
||||||
|
@onChange={{fn this.setGroupId field}}
|
||||||
|
@options={{hash maximum=1}}
|
||||||
|
/>
|
||||||
|
</field.Custom>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.llms.quotas.max_tokens"}}</label>
|
@name="max_tokens"
|
||||||
<input
|
@title={{i18n "discourse_ai.llms.quotas.max_tokens"}}
|
||||||
type="number"
|
@tooltip={{i18n "discourse_ai.llms.quotas.max_tokens_help"}}
|
||||||
value={{this.maxTokens}}
|
@format="large"
|
||||||
class="input-large"
|
as |field|
|
||||||
min="1"
|
>
|
||||||
{{on "input" this.updateMaxTokens}}
|
<field.Input @type="number" min="1" />
|
||||||
/>
|
</form.Field>
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.llms.quotas.max_tokens_help"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.llms.quotas.max_usages"}}</label>
|
@name="max_usages"
|
||||||
<input
|
@title={{i18n "discourse_ai.llms.quotas.max_usages"}}
|
||||||
type="number"
|
@tooltip={{i18n "discourse_ai.llms.quotas.max_usages_help"}}
|
||||||
value={{this.maxUsages}}
|
@format="large"
|
||||||
class="input-large"
|
as |field|
|
||||||
min="1"
|
>
|
||||||
{{on "input" this.updateMaxUsages}}
|
<field.Input @type="number" min="1" />
|
||||||
/>
|
</form.Field>
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.llms.quotas.max_usages_help"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.llms.quotas.duration"}}</label>
|
@name="duration_seconds"
|
||||||
<DurationSelector
|
@title={{i18n "discourse_ai.llms.quotas.duration"}}
|
||||||
@value={{this.duration}}
|
@validation="required"
|
||||||
@onChange={{this.updateDuration}}
|
@format="large"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
<DurationSelector
|
||||||
|
@value={{data.duration_seconds}}
|
||||||
|
@onChange={{field.set}}
|
||||||
|
/>
|
||||||
|
</field.Custom>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Submit
|
||||||
|
@label="discourse_ai.llms.quotas.add"
|
||||||
|
class="btn-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Form>
|
||||||
</:body>
|
</:body>
|
||||||
|
|
||||||
<:footer>
|
|
||||||
<DButton
|
|
||||||
@action={{this.save}}
|
|
||||||
@label="discourse_ai.llms.quotas.add"
|
|
||||||
@disabled={{not this.canSave}}
|
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
</:footer>
|
|
||||||
</DModal>
|
</DModal>
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -15,24 +15,6 @@
|
|||||||
.ai-llm-editor {
|
.ai-llm-editor {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
|
|
||||||
.ai-llm-editor-input {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-llm-editor-provider-param {
|
|
||||||
&__checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fk-d-tooltip__icon {
|
|
||||||
padding-left: 0.25em;
|
|
||||||
color: var(--primary-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-llm-editor-tests {
|
.ai-llm-editor-tests {
|
||||||
&__failure {
|
&__failure {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
@ -42,21 +24,6 @@
|
|||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__api-key {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__secret-api-key-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__vision-enabled,
|
|
||||||
&__enabled-chat-bot {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="ai-llms-list-editor"] {
|
[class*="ai-llms-list-editor"] {
|
||||||
@ -110,8 +77,8 @@
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
|
||||||
gap: 1em 2em;
|
gap: 1em 2em;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
border-top: 3px solid var(--primary-low); // matches tbody border
|
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
|
border-top: 3px solid var(--primary-low);
|
||||||
}
|
}
|
||||||
&-list-item {
|
&-list-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -360,7 +360,9 @@ en:
|
|||||||
custom: "Custom..."
|
custom: "Custom..."
|
||||||
hours: "hours"
|
hours: "hours"
|
||||||
max_tokens_help: "Maximum number of tokens (words and characters) that each user in this group can use within the specified duration. Tokens are the units used by AI models to process text - roughly 1 token = 4 characters or 3/4 of a word."
|
max_tokens_help: "Maximum number of tokens (words and characters) that each user in this group can use within the specified duration. Tokens are the units used by AI models to process text - roughly 1 token = 4 characters or 3/4 of a word."
|
||||||
|
max_tokens_required: "Must be set if max usages is not set"
|
||||||
max_usages_help: "Maximum number of times each user in this group can use the AI model within the specified duration. This quota is tracked per individual user, not shared across the group."
|
max_usages_help: "Maximum number of times each user in this group can use the AI model within the specified duration. This quota is tracked per individual user, not shared across the group."
|
||||||
|
max_usages_required: "Must be set if max tokens is not set"
|
||||||
usage:
|
usage:
|
||||||
ai_bot: "AI bot"
|
ai_bot: "AI bot"
|
||||||
ai_helper: "Helper"
|
ai_helper: "Helper"
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
RSpec.describe "Managing LLM configurations", type: :system, js: true do
|
RSpec.describe "Managing LLM configurations", type: :system, js: true do
|
||||||
fab!(:admin)
|
fab!(:admin)
|
||||||
|
|
||||||
let(:page_header) { PageObjects::Components::DPageHeader.new }
|
let(:page_header) { PageObjects::Components::DPageHeader.new }
|
||||||
|
let(:form) { PageObjects::Components::FormKit.new("form") }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.ai_bot_enabled = true
|
SiteSetting.ai_bot_enabled = true
|
||||||
@ -13,17 +15,17 @@ RSpec.describe "Managing LLM configurations", type: :system, js: true do
|
|||||||
visit "/admin/plugins/discourse-ai/ai-llms"
|
visit "/admin/plugins/discourse-ai/ai-llms"
|
||||||
|
|
||||||
find("[data-llm-id='anthropic-claude-3-5-haiku'] button").click()
|
find("[data-llm-id='anthropic-claude-3-5-haiku'] button").click()
|
||||||
find("input.ai-llm-editor__api-key").fill_in(with: "abcd")
|
form.field("api_key").fill_in("abcd")
|
||||||
find(".ai-llm-editor__enabled-chat-bot input").click
|
form.field("enabled_chat_bot").toggle
|
||||||
find(".ai-llm-editor__save").click()
|
form.submit
|
||||||
|
|
||||||
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-llms")
|
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-llms")
|
||||||
|
|
||||||
llm = LlmModel.order(:id).last
|
llm = LlmModel.order(:id).last
|
||||||
|
|
||||||
expect(llm.api_key).to eq("abcd")
|
expect(llm.api_key).to eq("abcd")
|
||||||
|
|
||||||
preset = DiscourseAi::Completions::Llm.presets.find { |p| p[:id] == "anthropic" }
|
preset = DiscourseAi::Completions::Llm.presets.find { |p| p[:id] == "anthropic" }
|
||||||
|
|
||||||
model_preset = preset[:models].find { |m| m[:name] == "claude-3-5-haiku" }
|
model_preset = preset[:models].find { |m| m[:name] == "claude-3-5-haiku" }
|
||||||
|
|
||||||
expect(llm.name).to eq("claude-3-5-haiku")
|
expect(llm.name).to eq("claude-3-5-haiku")
|
||||||
@ -37,27 +39,23 @@ RSpec.describe "Managing LLM configurations", type: :system, js: true do
|
|||||||
|
|
||||||
it "manually configures an LLM" do
|
it "manually configures an LLM" do
|
||||||
visit "/admin/plugins/discourse-ai/ai-llms"
|
visit "/admin/plugins/discourse-ai/ai-llms"
|
||||||
|
|
||||||
expect(page_header).to be_visible
|
expect(page_header).to be_visible
|
||||||
|
|
||||||
find("[data-llm-id='none'] button").click()
|
find("[data-llm-id='none'] button").click()
|
||||||
|
|
||||||
expect(page_header).to be_hidden
|
expect(page_header).to be_hidden
|
||||||
|
|
||||||
find("input.ai-llm-editor__display-name").fill_in(with: "Self-hosted LLM")
|
form.field("display_name").fill_in("Self-hosted LLM")
|
||||||
find("input.ai-llm-editor__name").fill_in(with: "llava-hf/llava-v1.6-mistral-7b-hf")
|
form.field("name").fill_in("llava-hf/llava-v1.6-mistral-7b-hf")
|
||||||
find("input.ai-llm-editor__url").fill_in(with: "srv://self-hostest.test")
|
form.field("url").fill_in("srv://self-hostest.test")
|
||||||
find("input.ai-llm-editor__api-key").fill_in(with: "1234")
|
form.field("api_key").fill_in("1234")
|
||||||
find("input.ai-llm-editor__max-prompt-tokens").fill_in(with: 8000)
|
form.field("max_prompt_tokens").fill_in(8000)
|
||||||
|
form.field("provider").select("vllm")
|
||||||
find(".ai-llm-editor__provider").click
|
form.field("tokenizer").select("DiscourseAi::Tokenizer::Llama3Tokenizer")
|
||||||
find(".select-kit-row[data-value=\"vllm\"]").click
|
form.field("vision_enabled").toggle
|
||||||
|
form.field("enabled_chat_bot").toggle
|
||||||
find(".ai-llm-editor__tokenizer").click
|
form.submit
|
||||||
find(".select-kit-row[data-name=\"Llama3Tokenizer\"]").click
|
|
||||||
|
|
||||||
find(".ai-llm-editor__vision-enabled input").click
|
|
||||||
find(".ai-llm-editor__enabled-chat-bot input").click
|
|
||||||
|
|
||||||
find(".ai-llm-editor__save").click()
|
|
||||||
|
|
||||||
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-llms")
|
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-llms")
|
||||||
|
|
||||||
@ -73,6 +71,49 @@ RSpec.describe "Managing LLM configurations", type: :system, js: true do
|
|||||||
expect(llm.user_id).not_to be_nil
|
expect(llm.user_id).not_to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with quotas" do
|
||||||
|
fab!(:llm_model_1) { Fabricate(:llm_model, name: "claude-2") }
|
||||||
|
fab!(:group_1) { Fabricate(:group) }
|
||||||
|
|
||||||
|
before { Fabricate(:llm_quota, group: group_1, llm_model: llm_model_1, max_tokens: 1000) }
|
||||||
|
|
||||||
|
it "prefills the quotas form" do
|
||||||
|
visit "/admin/plugins/discourse-ai/ai-llms/#{llm_model_1.id}/edit"
|
||||||
|
|
||||||
|
expect(page).to have_selector(
|
||||||
|
".ai-llm-quotas__table .ai-llm-quotas__cell",
|
||||||
|
text: group_1.name,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can remove a quota" do
|
||||||
|
visit "/admin/plugins/discourse-ai/ai-llms/#{llm_model_1.id}/edit"
|
||||||
|
|
||||||
|
find(".ai-llm-quotas__delete-btn:nth-child(1)").click
|
||||||
|
|
||||||
|
expect(page).to have_no_selector(
|
||||||
|
".ai-llm-quotas__table .ai-llm-quotas__cell",
|
||||||
|
text: group_1.name,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can add a quota" do
|
||||||
|
visit "/admin/plugins/discourse-ai/ai-llms/#{llm_model_1.id}/edit"
|
||||||
|
find(".ai-llm-editor__add-quota-btn").click
|
||||||
|
select_kit = PageObjects::Components::SelectKit.new(".group-chooser")
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value(1)
|
||||||
|
form = PageObjects::Components::FormKit.new(".ai-llm-quota-modal form")
|
||||||
|
form.field("max_tokens").fill_in(2000)
|
||||||
|
form.submit
|
||||||
|
|
||||||
|
expect(page).to have_selector(
|
||||||
|
".ai-llm-quotas__table .ai-llm-quotas__cell",
|
||||||
|
text: Group.find(1).name,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when seeded LLM is present" do
|
context "when seeded LLM is present" do
|
||||||
fab!(:llm_model) { Fabricate(:seeded_model) }
|
fab!(:llm_model) { Fabricate(:seeded_model) }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user