REFACTOR: update embeddings to formkit (#1188)
This commit is contained in:
parent
ec8018333e
commit
51ca942d7d
|
@ -1,23 +1,19 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { cached, tracked } from "@glimmer/tracking";
|
||||||
import { Input, Textarea } from "@ember/component";
|
|
||||||
import { concat, fn, get } from "@ember/helper";
|
import { concat, fn, get } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { action } from "@ember/object";
|
||||||
import { action, computed } from "@ember/object";
|
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
||||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
|
||||||
import { later } from "@ember/runloop";
|
import { later } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
import { eq, not } from "truth-helpers";
|
||||||
import BackButton from "discourse/components/back-button";
|
import BackButton from "discourse/components/back-button";
|
||||||
|
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
|
import Form from "discourse/components/form";
|
||||||
import icon from "discourse/helpers/d-icon";
|
import icon from "discourse/helpers/d-icon";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import AdminSectionLandingItem from "admin/components/admin-section-landing-item";
|
import AdminSectionLandingItem from "admin/components/admin-section-landing-item";
|
||||||
import AdminSectionLandingWrapper from "admin/components/admin-section-landing-wrapper";
|
import AdminSectionLandingWrapper from "admin/components/admin-section-landing-wrapper";
|
||||||
import ComboBox from "select-kit/components/combo-box";
|
|
||||||
import DTooltip from "float-kit/components/d-tooltip";
|
|
||||||
import not from "truth-helpers/helpers/not";
|
|
||||||
|
|
||||||
export default class AiEmbeddingEditor extends Component {
|
export default class AiEmbeddingEditor extends Component {
|
||||||
@service toasts;
|
@service toasts;
|
||||||
|
@ -27,12 +23,35 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
|
|
||||||
@tracked isSaving = false;
|
@tracked isSaving = false;
|
||||||
@tracked selectedPreset = null;
|
@tracked selectedPreset = null;
|
||||||
|
|
||||||
@tracked testRunning = false;
|
@tracked testRunning = false;
|
||||||
@tracked testResult = null;
|
@tracked testResult = null;
|
||||||
@tracked testError = null;
|
@tracked testError = null;
|
||||||
@tracked apiKeySecret = true;
|
@tracked currentProvider = null;
|
||||||
@tracked editingModel = null;
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
if (this.args.model) {
|
||||||
|
this.currentProvider = this.args.model.provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get formData() {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (this.selectedPreset) {
|
||||||
|
data = this.store
|
||||||
|
.createRecord("ai-embedding", this.selectedPreset)
|
||||||
|
.workingCopy();
|
||||||
|
} else {
|
||||||
|
data = this.args.model.workingCopy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalData = JSON.parse(JSON.stringify(data));
|
||||||
|
this._originalFormData = originalData;
|
||||||
|
|
||||||
|
return originalData;
|
||||||
|
}
|
||||||
|
|
||||||
get selectedProviders() {
|
get selectedProviders() {
|
||||||
const t = (provName) => {
|
const t = (provName) => {
|
||||||
|
@ -50,18 +69,9 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.args.embeddings.resultSetMeta.distance_functions.map((df) => {
|
return this.args.embeddings.resultSetMeta.distance_functions.map((df) => {
|
||||||
let iconName;
|
|
||||||
|
|
||||||
if (df === "<=>") {
|
|
||||||
iconName = "discourse-spaceship-operator";
|
|
||||||
} else if (df === "<#>") {
|
|
||||||
iconName = "discourse-negative-inner-product";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: df,
|
id: df,
|
||||||
name: t(df),
|
name: t(df),
|
||||||
icon: iconName,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -88,13 +98,17 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
return !this.selectedPreset && this.args.model.isNew;
|
return !this.selectedPreset && this.args.model.isNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("editingModel.provider")
|
|
||||||
get metaProviderParams() {
|
get metaProviderParams() {
|
||||||
return (
|
const provider = this.currentProvider;
|
||||||
this.args.embeddings.resultSetMeta.provider_params[
|
if (!provider) {
|
||||||
this.editingModel?.provider
|
return {};
|
||||||
] || {}
|
}
|
||||||
);
|
|
||||||
|
const embeddings = this.args.embeddings || {};
|
||||||
|
const meta = embeddings.resultSetMeta || {};
|
||||||
|
const providerParams = meta.provider_params || {};
|
||||||
|
|
||||||
|
return providerParams[provider] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get testErrorMessage() {
|
get testErrorMessage() {
|
||||||
|
@ -105,6 +119,10 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
return this.testRunning || this.testResult !== null;
|
return this.testRunning || this.testResult !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get seeded() {
|
||||||
|
return this.args.model.id < 0;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
configurePreset(preset) {
|
configurePreset(preset) {
|
||||||
this.selectedPreset =
|
this.selectedPreset =
|
||||||
|
@ -113,40 +131,96 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
preset.id
|
preset.id
|
||||||
) || {};
|
) || {};
|
||||||
|
|
||||||
this.editingModel = this.store
|
if (this.selectedPreset.provider) {
|
||||||
.createRecord("ai-embedding", this.selectedPreset)
|
this.currentProvider = this.selectedPreset.provider;
|
||||||
.workingCopy();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateModel() {
|
setProvider(provider, { set }) {
|
||||||
this.editingModel = this.args.model.workingCopy();
|
set("provider", provider);
|
||||||
|
|
||||||
|
this.currentProvider = provider;
|
||||||
|
|
||||||
|
const providerParams =
|
||||||
|
this.args.embeddings?.resultSetMeta?.provider_params || {};
|
||||||
|
const params = providerParams[provider] || {};
|
||||||
|
|
||||||
|
const initialParams = {};
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
const keys = Object.keys(params);
|
||||||
|
keys.forEach((key) => {
|
||||||
|
initialParams[key] = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set("provider_params", initialParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
get providerParams() {
|
||||||
|
const normalizeParam = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return { type: "text" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return { type: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: value.type || "text",
|
||||||
|
values: (value.values || []).map((v) => ({ id: v, name: v })),
|
||||||
|
default: value.default,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.entries(this.metaProviderParams).reduce(
|
||||||
|
(acc, [field, value]) => {
|
||||||
|
acc[field] = normalizeParam(value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
makeApiKeySecret() {
|
resetForm() {
|
||||||
this.apiKeySecret = true;
|
this.selectedPreset = null;
|
||||||
|
this.currentProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleApiKeySecret() {
|
async save(formData) {
|
||||||
this.apiKeySecret = !this.apiKeySecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async save() {
|
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
const isNew = this.args.model.isNew;
|
const isNew = this.args.model.isNew;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.editingModel.save();
|
const dataToSave = { ...formData };
|
||||||
|
|
||||||
|
if (this.selectedPreset) {
|
||||||
|
// new embeddings
|
||||||
|
const newModel = this.store.createRecord("ai-embedding", {
|
||||||
|
...this.selectedPreset,
|
||||||
|
...dataToSave,
|
||||||
|
});
|
||||||
|
await newModel.save();
|
||||||
|
this.args.embeddings.addObject(newModel);
|
||||||
|
} else {
|
||||||
|
// existing embeddings
|
||||||
|
await this.args.model.save(dataToSave);
|
||||||
|
}
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
this.args.embeddings.addObject(this.editingModel);
|
|
||||||
this.router.transitionTo(
|
this.router.transitionTo(
|
||||||
"adminPlugins.show.discourse-ai-embeddings.index"
|
"adminPlugins.show.discourse-ai-embeddings.index"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const savedProvider = this.currentProvider;
|
||||||
|
|
||||||
|
this._originalFormData = JSON.parse(JSON.stringify(dataToSave));
|
||||||
|
this.currentProvider = savedProvider;
|
||||||
|
|
||||||
this.toasts.success({
|
this.toasts.success({
|
||||||
data: { message: i18n("discourse_ai.embeddings.saved") },
|
data: { message: i18n("discourse_ai.embeddings.saved") },
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
|
@ -162,11 +236,11 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async test() {
|
async test(data) {
|
||||||
this.testRunning = true;
|
this.testRunning = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configTestResult = await this.editingModel.testConfig();
|
const configTestResult = await this.args.model.testConfig(data);
|
||||||
this.testResult = configTestResult.success;
|
this.testResult = configTestResult.success;
|
||||||
|
|
||||||
if (this.testResult) {
|
if (this.testResult) {
|
||||||
|
@ -191,7 +265,7 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
return this.args.model
|
return this.args.model
|
||||||
.destroyRecord()
|
.destroyRecord()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.args.llms.removeObject(this.args.model);
|
this.args.embeddings.removeObject(this.args.model);
|
||||||
this.router.transitionTo(
|
this.router.transitionTo(
|
||||||
"adminPlugins.show.discourse-ai-embeddings.index"
|
"adminPlugins.show.discourse-ai-embeddings.index"
|
||||||
);
|
);
|
||||||
|
@ -201,52 +275,44 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
resetForm() {
|
|
||||||
this.selectedPreset = null;
|
|
||||||
this.editingModel = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form
|
{{#if this.showPresets}}
|
||||||
{{didInsert this.updateModel @model.id}}
|
<BackButton
|
||||||
{{didUpdate this.updateModel @model.id}}
|
@route="adminPlugins.show.discourse-ai-embeddings"
|
||||||
class="form-horizontal ai-embedding-editor"
|
@label="discourse_ai.embeddings.back"
|
||||||
>
|
/>
|
||||||
{{#if this.showPresets}}
|
<div class="control-group">
|
||||||
<BackButton
|
<h2>{{i18n "discourse_ai.embeddings.presets"}}</h2>
|
||||||
@route="adminPlugins.show.discourse-ai-embeddings"
|
<AdminSectionLandingWrapper>
|
||||||
@label="discourse_ai.embeddings.back"
|
{{#each this.presets as |preset|}}
|
||||||
/>
|
<AdminSectionLandingItem
|
||||||
<div class="control-group">
|
@titleLabelTranslated={{preset.name}}
|
||||||
<h2>{{i18n "discourse_ai.embeddings.presets"}}</h2>
|
@taglineLabel={{concat
|
||||||
<AdminSectionLandingWrapper>
|
"discourse_ai.embeddings.providers."
|
||||||
{{#each this.presets as |preset|}}
|
preset.provider
|
||||||
<AdminSectionLandingItem
|
}}
|
||||||
@titleLabelTranslated={{preset.name}}
|
data-preset-id={{preset.id}}
|
||||||
@taglineLabel={{concat
|
class="ai-llms-list-editor__templates-list-item"
|
||||||
"discourse_ai.embeddings.providers."
|
>
|
||||||
preset.provider
|
<:buttons as |buttons|>
|
||||||
}}
|
<buttons.Default
|
||||||
data-preset-id={{preset.id}}
|
@action={{fn this.configurePreset preset}}
|
||||||
class="ai-llms-list-editor__templates-list-item"
|
@icon="gear"
|
||||||
>
|
@label="discourse_ai.llms.preconfigured.button"
|
||||||
<:buttons as |buttons|>
|
/>
|
||||||
<buttons.Default
|
</:buttons>
|
||||||
@action={{fn this.configurePreset preset}}
|
</AdminSectionLandingItem>
|
||||||
@icon="gear"
|
{{/each}}
|
||||||
@label="discourse_ai.llms.preconfigured.button"
|
</AdminSectionLandingWrapper>
|
||||||
/>
|
</div>
|
||||||
</:buttons>
|
{{else}}
|
||||||
</AdminSectionLandingItem>
|
<Form
|
||||||
|
@onSubmit={{this.save}}
|
||||||
{{/each}}
|
@data={{this.formData}}
|
||||||
</AdminSectionLandingWrapper>
|
class="form-horizontal ai-embedding-editor {{if this.seeded 'seeded'}}"
|
||||||
|
as |form data|
|
||||||
</div>
|
>
|
||||||
|
{{#if @model.isNew}}
|
||||||
{{else}}
|
|
||||||
{{#if this.editingModel.isNew}}
|
|
||||||
<DButton
|
<DButton
|
||||||
@action={{this.resetForm}}
|
@action={{this.resetForm}}
|
||||||
@label="back_button"
|
@label="back_button"
|
||||||
|
@ -259,194 +325,231 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
@label="discourse_ai.embeddings.back"
|
@label="discourse_ai.embeddings.back"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.embeddings.display_name"}}</label>
|
|
||||||
<Input
|
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__display-name"
|
|
||||||
@type="text"
|
|
||||||
@value={{this.editingModel.display_name}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.embeddings.provider"}}</label>
|
@name="display_name"
|
||||||
<ComboBox
|
@title={{i18n "discourse_ai.embeddings.display_name"}}
|
||||||
@value={{this.editingModel.provider}}
|
@validation="required|length:1,100"
|
||||||
@content={{this.selectedProviders}}
|
@format="large"
|
||||||
@class="ai-embedding-editor__provider"
|
class="ai-embedding-editor__display-name"
|
||||||
/>
|
as |field|
|
||||||
</div>
|
>
|
||||||
|
<field.Input />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.embeddings.url"}}</label>
|
@name="provider"
|
||||||
<Input
|
@title={{i18n "discourse_ai.embeddings.provider"}}
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__url"
|
@validation="required"
|
||||||
@type="text"
|
@format="large"
|
||||||
@value={{this.editingModel.url}}
|
@onSet={{this.setProvider}}
|
||||||
required="true"
|
class="ai-embedding-editor__provider"
|
||||||
/>
|
as |field|
|
||||||
</div>
|
>
|
||||||
|
<field.Select as |select|>
|
||||||
|
{{#each this.selectedProviders as |provider|}}
|
||||||
|
<select.Option
|
||||||
|
@value={{provider.id}}
|
||||||
|
>{{provider.name}}</select.Option>
|
||||||
|
{{/each}}
|
||||||
|
</field.Select>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.embeddings.api_key"}}</label>
|
@name="url"
|
||||||
<div class="ai-embedding-editor__secret-api-key-group">
|
@title={{i18n "discourse_ai.embeddings.url"}}
|
||||||
<Input
|
@validation="required"
|
||||||
@value={{this.editingModel.api_key}}
|
@format="large"
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__api-key"
|
class="ai-embedding-editor__url"
|
||||||
@type={{if this.apiKeySecret "password" "text"}}
|
as |field|
|
||||||
required="true"
|
>
|
||||||
{{on "focusout" this.makeApiKeySecret}}
|
<field.Input />
|
||||||
/>
|
</form.Field>
|
||||||
<DButton
|
|
||||||
@action={{this.toggleApiKeySecret}}
|
|
||||||
@icon="far-eye-slash"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.embeddings.tokenizer"}}</label>
|
@name="api_key"
|
||||||
<ComboBox
|
@title={{i18n "discourse_ai.embeddings.api_key"}}
|
||||||
@value={{this.editingModel.tokenizer_class}}
|
@validation="required"
|
||||||
@content={{@embeddings.resultSetMeta.tokenizers}}
|
@format="large"
|
||||||
@class="ai-embedding-editor__tokenizer"
|
class="ai-embedding-editor__api-key"
|
||||||
/>
|
as |field|
|
||||||
</div>
|
>
|
||||||
|
<field.Password />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
<div class="control-group">
|
<form.Field
|
||||||
<label>{{i18n "discourse_ai.embeddings.dimensions"}}</label>
|
@name="tokenizer_class"
|
||||||
<Input
|
@title={{i18n "discourse_ai.embeddings.tokenizer"}}
|
||||||
|
@validation="required"
|
||||||
|
@format="large"
|
||||||
|
class="ai-embedding-editor__tokenizer"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Select as |select|>
|
||||||
|
{{#each @embeddings.resultSetMeta.tokenizers as |tokenizer|}}
|
||||||
|
<select.Option
|
||||||
|
@value={{tokenizer.id}}
|
||||||
|
>{{tokenizer.name}}</select.Option>
|
||||||
|
{{/each}}
|
||||||
|
</field.Select>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="dimensions"
|
||||||
|
@title={{i18n "discourse_ai.embeddings.dimensions"}}
|
||||||
|
@validation="required"
|
||||||
|
@format="large"
|
||||||
|
@tooltip={{if
|
||||||
|
@model.isNew
|
||||||
|
(i18n "discourse_ai.embeddings.hints.dimensions_warning")
|
||||||
|
}}
|
||||||
|
class="ai-embedding-editor__dimensions"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input
|
||||||
@type="number"
|
@type="number"
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__dimensions"
|
|
||||||
step="any"
|
step="any"
|
||||||
min="0"
|
min="0"
|
||||||
lang="en"
|
lang="en"
|
||||||
@value={{this.editingModel.dimensions}}
|
disabled={{not @model.isNew}}
|
||||||
required="true"
|
|
||||||
disabled={{not this.editingModel.isNew}}
|
|
||||||
/>
|
/>
|
||||||
{{#if this.editingModel.isNew}}
|
</form.Field>
|
||||||
<DTooltip
|
|
||||||
@icon="circle-exclamation"
|
<form.Field
|
||||||
@content={{i18n
|
@name="matryoshka_dimensions"
|
||||||
"discourse_ai.embeddings.hints.dimensions_warning"
|
@title={{i18n "discourse_ai.embeddings.matryoshka_dimensions"}}
|
||||||
}}
|
@tooltip={{i18n
|
||||||
/>
|
"discourse_ai.embeddings.hints.matryoshka_dimensions"
|
||||||
|
}}
|
||||||
|
@format="large"
|
||||||
|
class="ai-embedding-editor__matryoshka_dimensions"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Checkbox />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="embed_prompt"
|
||||||
|
@title={{i18n "discourse_ai.embeddings.embed_prompt"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.embeddings.hints.embed_prompt"}}
|
||||||
|
@format="large"
|
||||||
|
class="ai-embedding-editor__embed_prompt"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Textarea />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="search_prompt"
|
||||||
|
@title={{i18n "discourse_ai.embeddings.search_prompt"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.embeddings.hints.search_prompt"}}
|
||||||
|
@format="large"
|
||||||
|
class="ai-embedding-editor__search_prompt"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Textarea />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="max_sequence_length"
|
||||||
|
@title={{i18n "discourse_ai.embeddings.max_sequence_length"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.embeddings.hints.sequence_length"}}
|
||||||
|
@validation="required"
|
||||||
|
@format="large"
|
||||||
|
class="ai-embedding-editor__max_sequence_length"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Input @type="number" step="any" min="0" lang="en" />
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
@name="pg_function"
|
||||||
|
@title={{i18n "discourse_ai.embeddings.distance_function"}}
|
||||||
|
@tooltip={{i18n "discourse_ai.embeddings.hints.distance_function"}}
|
||||||
|
@format="large"
|
||||||
|
@validation="required"
|
||||||
|
class="ai-embedding-editor__distance_functions"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Select @includeNone={{false}} as |select|>
|
||||||
|
{{#each this.distanceFunctions as |df|}}
|
||||||
|
<select.Option @value={{df.id}}>{{df.name}}</select.Option>
|
||||||
|
{{/each}}
|
||||||
|
</field.Select>
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
{{! provider-specific content }}
|
||||||
|
{{#if this.currentProvider}}
|
||||||
|
{{#if data.provider_params}}
|
||||||
|
<form.Object @name="provider_params" as |object name|>
|
||||||
|
{{#let (get this.providerParams name) as |params|}}
|
||||||
|
{{#if params}}
|
||||||
|
<object.Field
|
||||||
|
@name={{name}}
|
||||||
|
@title={{i18n
|
||||||
|
(concat "discourse_ai.embeddings.provider_fields." name)
|
||||||
|
}}
|
||||||
|
@format="large"
|
||||||
|
@validation="required"
|
||||||
|
class="ai-embedding-editor-provider-param__{{params.type}}"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
{{#if (eq params.type "enum")}}
|
||||||
|
<field.Select @includeNone={{false}} as |select|>
|
||||||
|
{{#each params.values as |option|}}
|
||||||
|
<select.Option
|
||||||
|
@value={{option.id}}
|
||||||
|
>{{option.name}}</select.Option>
|
||||||
|
{{/each}}
|
||||||
|
</field.Select>
|
||||||
|
{{else if (eq params.type "checkbox")}}
|
||||||
|
<field.Checkbox />
|
||||||
|
{{else}}
|
||||||
|
<field.Input @type={{params.type}} />
|
||||||
|
{{/if}}
|
||||||
|
</object.Field>
|
||||||
|
{{/if}}
|
||||||
|
{{/let}}
|
||||||
|
</form.Object>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
{{/if}}
|
||||||
|
|
||||||
<div class="control-group ai-embedding-editor__matryoshka_dimensions">
|
<form.Actions class="ai-embedding-editor__action_panel">
|
||||||
<Input
|
<form.Button
|
||||||
@type="checkbox"
|
@action={{fn this.test data}}
|
||||||
@checked={{this.editingModel.matryoshka_dimensions}}
|
|
||||||
/>
|
|
||||||
<label>{{i18n "discourse_ai.embeddings.matryoshka_dimensions"}}
|
|
||||||
</label>
|
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n
|
|
||||||
"discourse_ai.embeddings.hints.matryoshka_dimensions"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.embeddings.embed_prompt"}}</label>
|
|
||||||
<Textarea
|
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__embed_prompt"
|
|
||||||
@value={{this.editingModel.embed_prompt}}
|
|
||||||
/>
|
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.embeddings.hints.embed_prompt"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.embeddings.search_prompt"}}</label>
|
|
||||||
<Textarea
|
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__search_prompt"
|
|
||||||
@value={{this.editingModel.search_prompt}}
|
|
||||||
/>
|
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.embeddings.hints.search_prompt"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.embeddings.max_sequence_length"}}</label>
|
|
||||||
<Input
|
|
||||||
@type="number"
|
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__max_sequence_length"
|
|
||||||
step="any"
|
|
||||||
min="0"
|
|
||||||
lang="en"
|
|
||||||
@value={{this.editingModel.max_sequence_length}}
|
|
||||||
required="true"
|
|
||||||
/>
|
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.embeddings.hints.sequence_length"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.embeddings.distance_function"}}</label>
|
|
||||||
<ComboBox
|
|
||||||
@value={{this.editingModel.pg_function}}
|
|
||||||
@content={{this.distanceFunctions}}
|
|
||||||
@class="ai-embedding-editor__distance_functions"
|
|
||||||
/>
|
|
||||||
<DTooltip
|
|
||||||
@icon="circle-question"
|
|
||||||
@content={{i18n "discourse_ai.embeddings.hints.distance_function"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#each-in this.metaProviderParams as |field type|}}
|
|
||||||
<div
|
|
||||||
class="control-group ai-embedding-editor-provider-param__{{type}}"
|
|
||||||
>
|
|
||||||
<label>
|
|
||||||
{{i18n (concat "discourse_ai.embeddings.provider_fields." field)}}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
@type="text"
|
|
||||||
class="ai-embedding-editor-input ai-embedding-editor__{{field}}"
|
|
||||||
@value={{mut (get this.editingModel.provider_params field)}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/each-in}}
|
|
||||||
|
|
||||||
<div class="control-group ai-embedding-editor__action_panel">
|
|
||||||
<DButton
|
|
||||||
class="ai-embedding-editor__test"
|
|
||||||
@action={{this.test}}
|
|
||||||
@disabled={{this.testRunning}}
|
@disabled={{this.testRunning}}
|
||||||
@label="discourse_ai.embeddings.tests.title"
|
@label="discourse_ai.embeddings.tests.title"
|
||||||
|
class="ai-embedding-editor__test"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DButton
|
<form.Submit
|
||||||
class="btn-primary ai-embedding-editor__save"
|
|
||||||
@action={{this.save}}
|
|
||||||
@disabled={{this.isSaving}}
|
|
||||||
@label="discourse_ai.embeddings.save"
|
@label="discourse_ai.embeddings.save"
|
||||||
|
class="btn-primary ai-embedding-editor__save"
|
||||||
/>
|
/>
|
||||||
{{#unless this.editingModel.isNew}}
|
|
||||||
<DButton
|
{{#unless data.isNew}}
|
||||||
|
<form.Button
|
||||||
@action={{this.delete}}
|
@action={{this.delete}}
|
||||||
class="btn-danger ai-embedding-editor__delete"
|
|
||||||
@label="discourse_ai.embeddings.delete"
|
@label="discourse_ai.embeddings.delete"
|
||||||
|
class="btn-danger ai-embedding-editor__delete"
|
||||||
/>
|
/>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
</form.Actions>
|
||||||
|
|
||||||
<div class="control-group ai-embedding-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.embeddings.tests.running"}}
|
@title="test_results"
|
||||||
{{else}}
|
@format="full"
|
||||||
|
class="ai-embedding-editor-tests"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
<ConditionalLoadingSpinner
|
||||||
|
@size="small"
|
||||||
|
@condition={{this.testRunning}}
|
||||||
|
>
|
||||||
{{#if this.testResult}}
|
{{#if this.testResult}}
|
||||||
<div class="ai-embedding-editor-tests__success">
|
<div class="ai-embedding-editor-tests__success">
|
||||||
{{icon "check"}}
|
{{icon "check"}}
|
||||||
|
@ -458,11 +561,11 @@ export default class AiEmbeddingEditor extends Component {
|
||||||
{{this.testErrorMessage}}
|
{{this.testErrorMessage}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
</ConditionalLoadingSpinner>
|
||||||
{{/if}}
|
</field.Custom>
|
||||||
</div>
|
</form.Field>
|
||||||
</div>
|
{{/if}}
|
||||||
{{/if}}
|
</Form>
|
||||||
</form>
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
RSpec.describe "Managing Embeddings configurations", type: :system, js: true do
|
RSpec.describe "Managing Embeddings 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 { sign_in(admin) }
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
@ -16,8 +17,8 @@ RSpec.describe "Managing Embeddings configurations", type: :system, js: true do
|
||||||
|
|
||||||
find("[data-preset-id='text-embedding-3-small'] button").click()
|
find("[data-preset-id='text-embedding-3-small'] button").click()
|
||||||
|
|
||||||
find("input.ai-embedding-editor__api-key").fill_in(with: api_key)
|
form.field("api_key").fill_in(api_key)
|
||||||
find(".ai-embedding-editor__save").click()
|
form.submit
|
||||||
|
|
||||||
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-embeddings")
|
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-embeddings")
|
||||||
|
|
||||||
|
@ -45,35 +46,27 @@ RSpec.describe "Managing Embeddings configurations", type: :system, js: true do
|
||||||
|
|
||||||
find("[data-preset-id='manual'] button").click()
|
find("[data-preset-id='manual'] button").click()
|
||||||
|
|
||||||
find("input.ai-embedding-editor__display-name").fill_in(with: "text-embedding-3-small")
|
form.field("display_name").fill_in("text-embedding-3-small")
|
||||||
|
form.field("provider").select(EmbeddingDefinition::OPEN_AI)
|
||||||
select_kit = PageObjects::Components::SelectKit.new(".ai-embedding-editor__provider")
|
form.field("url").fill_in("https://api.openai.com/v1/embeddings")
|
||||||
select_kit.expand
|
form.field("api_key").fill_in(api_key)
|
||||||
select_kit.select_row_by_value(EmbeddingDefinition::OPEN_AI)
|
form.field("tokenizer_class").select("DiscourseAi::Tokenizer::OpenAiTokenizer")
|
||||||
|
|
||||||
find("input.ai-embedding-editor__url").fill_in(with: "https://api.openai.com/v1/embeddings")
|
|
||||||
find("input.ai-embedding-editor__api-key").fill_in(with: api_key)
|
|
||||||
|
|
||||||
select_kit = PageObjects::Components::SelectKit.new(".ai-embedding-editor__tokenizer")
|
|
||||||
select_kit.expand
|
|
||||||
select_kit.select_row_by_value("DiscourseAi::Tokenizer::OpenAiTokenizer")
|
|
||||||
|
|
||||||
embed_prefix = "On creation:"
|
embed_prefix = "On creation:"
|
||||||
search_prefix = "On search:"
|
search_prefix = "On search:"
|
||||||
find(".ai-embedding-editor__embed_prompt").fill_in(with: embed_prefix)
|
form.field("embed_prompt").fill_in(embed_prefix)
|
||||||
find(".ai-embedding-editor__search_prompt").fill_in(with: search_prefix)
|
form.field("search_prompt").fill_in(search_prefix)
|
||||||
|
form.field("dimensions").fill_in(1536)
|
||||||
|
form.field("max_sequence_length").fill_in(8191)
|
||||||
|
form.field("pg_function").select("<=>")
|
||||||
|
form.field("provider_params.model_name").fill_in("text-embedding-3-small")
|
||||||
|
|
||||||
find("input.ai-embedding-editor__dimensions").fill_in(with: 1536)
|
form.submit
|
||||||
find("input.ai-embedding-editor__max_sequence_length").fill_in(with: 8191)
|
|
||||||
|
|
||||||
select_kit = PageObjects::Components::SelectKit.new(".ai-embedding-editor__distance_functions")
|
|
||||||
select_kit.expand
|
|
||||||
select_kit.select_row_by_value("<=>")
|
|
||||||
find(".ai-embedding-editor__save").click()
|
|
||||||
|
|
||||||
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-embeddings")
|
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-embeddings")
|
||||||
|
|
||||||
embedding_def = EmbeddingDefinition.order(:id).last
|
embedding_def = EmbeddingDefinition.order(:id).last
|
||||||
|
|
||||||
expect(embedding_def.api_key).to eq(api_key)
|
expect(embedding_def.api_key).to eq(api_key)
|
||||||
|
|
||||||
preset = EmbeddingDefinition.presets.find { |p| p[:preset_id] == "text-embedding-3-small" }
|
preset = EmbeddingDefinition.presets.find { |p| p[:preset_id] == "text-embedding-3-small" }
|
||||||
|
|
|
@ -14,16 +14,6 @@
|
||||||
<path d="M448 309.64V413H64V290h190c12.23 0 18.46-14.71 9.94-23.48-7.56-7.78-14.48-16.2-20.66-25.16-5.36-7.77-14.24-12.36-23.69-12.36H64V93h130c10.65 0 20.14-6.92 23.18-17.12a193.4 193.4 0 0 1 9.74-25.76c4.56-9.86-2.77-21.12-13.63-21.12H64C28.65 29 0 57.65 0 93v320c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V263.8c0-9.16-10.7-14.14-17.7-8.25a194.056 194.056 0 0 1-25.76 18.32c-12.66 7.53-20.53 21.05-20.53 35.77Z"/><path d="M247.46 122.15c-6.45 2.44-10.75 8.6-10.75 15.49s4.3 13.05 10.75 15.49l81.01 30.4 30.4 81.01c2.44 6.45 8.6 10.75 15.49 10.75s13.05-4.3 15.49-10.75l30.4-81.01 81.01-30.4c6.45-2.44 10.75-8.6 10.75-15.49s-4.3-13.05-10.75-15.49l-81.01-30.4-30.4-81.01c-2.44-6.45-8.6-10.75-15.49-10.75s-13.05 4.3-15.49 10.75l-30.4 81.01-81.01 30.4Z"/>
|
<path d="M448 309.64V413H64V290h190c12.23 0 18.46-14.71 9.94-23.48-7.56-7.78-14.48-16.2-20.66-25.16-5.36-7.77-14.24-12.36-23.69-12.36H64V93h130c10.65 0 20.14-6.92 23.18-17.12a193.4 193.4 0 0 1 9.74-25.76c4.56-9.86-2.77-21.12-13.63-21.12H64C28.65 29 0 57.65 0 93v320c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V263.8c0-9.16-10.7-14.14-17.7-8.25a194.056 194.056 0 0 1-25.76 18.32c-12.66 7.53-20.53 21.05-20.53 35.77Z"/><path d="M247.46 122.15c-6.45 2.44-10.75 8.6-10.75 15.49s4.3 13.05 10.75 15.49l81.01 30.4 30.4 81.01c2.44 6.45 8.6 10.75 15.49 10.75s13.05-4.3 15.49-10.75l30.4-81.01 81.01-30.4c6.45-2.44 10.75-8.6 10.75-15.49s-4.3-13.05-10.75-15.49l-81.01-30.4-30.4-81.01c-2.44-6.45-8.6-10.75-15.49-10.75s-13.05 4.3-15.49 10.75l-30.4 81.01-81.01 30.4Z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<!-- "Discourse Spaceship Operator" is a Discourse derivative of https://fontawesome.com/icons/equals?f=classic&s=solid and https://fontawesome.com/icons/less-than?f=classic&s=solid -->
|
|
||||||
<symbol id="discourse-spaceship-operator" viewBox="0 0 448 512">
|
|
||||||
<path d="M132.3 195.8c2.7 5.5.5 12.2-5 14.9L36 256.4l91.3 45.7c5.5 2.7 7.7 9.4 5 14.9s-9.4 7.7-14.9 5L6.2 266.4c-3.8-1.9-6.2-5.7-6.2-9.9s2.4-8.1 6.2-9.9L117.5 191c5.5-2.7 12.2-.5 14.9 5ZM315.7 317c-2.7-5.5-.5-12.2 5-14.9l91.3-45.7-91.3-45.7c-5.5-2.7-7.7-9.4-5-14.9s9.4-7.7 14.9-5l111.3 55.6c3.8 1.9 6.2 5.7 6.2 9.9s-2.4 8.1-6.2 9.9l-111.3 55.6c-5.5 2.7-12.2.5-14.9-5ZM169.2 211.5c-5.6 0-10.1 5.3-10.1 11.8s4.5 11.8 10.1 11.8h110.9c5.6 0 10.1-5.3 10.1-11.8s-4.5-11.8-10.1-11.8H169.2Zm0 65.9c-5.6 0-10.1 5.3-10.1 11.8s4.5 11.8 10.1 11.8h110.9c5.6 0 10.1-5.3 10.1-11.8s-4.5-11.8-10.1-11.8H169.2Z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<!-- "Discourse Negative Inner Product" is a Discourse derivative of https://fontawesome.com/icons/hashtag?f=classic&s=solid and https://fontawesome.com/icons/less-than?f=classic&s=solid -->
|
|
||||||
<symbol id="discourse-negative-inner-product" viewBox="0 0 448 512">
|
|
||||||
<path d="M132.3 195.8c2.7 5.5.5 12.2-5 14.9L36 256.4l91.3 45.7c5.5 2.7 7.7 9.4 5 14.9s-9.4 7.7-14.9 5L6.2 266.4c-3.8-1.9-6.2-5.7-6.2-9.9s2.4-8.1 6.2-9.9L117.5 191c5.5-2.7 12.2-.5 14.9 5ZM315.7 317c-2.7-5.5-.5-12.2 5-14.9l91.3-45.7-91.3-45.7c-5.5-2.7-7.7-9.4-5-14.9s9.4-7.7 14.9-5l111.3 55.6c3.8 1.9 6.2 5.7 6.2 9.9s-2.4 8.1-6.2 9.9l-111.3 55.6c-5.5 2.7-12.2.5-14.9-5ZM211.9 187.6c5.2.9 8.8 5.8 7.9 11.1l-3 17.7h28.6l3.5-20.9c.9-5.2 5.8-8.8 11.1-7.9 5.2.9 8.8 5.8 7.9 11.1l-2.9 17.7h17.5c5.3 0 9.6 4.3 9.6 9.6s-4.3 9.6-9.6 9.6h-20.7l-6.4 38.5h17.5c5.3 0 9.6 4.3 9.6 9.6s-4.3 9.6-9.6 9.6h-20.7l-3.5 20.9c-.9 5.2-5.8 8.8-11.1 7.9s-8.8-5.8-7.9-11.1l3-17.7h-28.6l-3.5 20.9c-.9 5.2-5.8 8.8-11.1 7.9-5.2-.9-8.8-5.8-7.9-11.1l2.9-17.7H167c-5.3 0-9.6-4.3-9.6-9.6s4.3-9.6 9.6-9.6h20.7l6.4-38.5h-17.5c-5.3 0-9.6-4.3-9.6-9.6s4.3-9.6 9.6-9.6h20.7l3.5-20.9c.9-5.2 5.8-8.8 11.1-7.9Zm1.7 48-6.4 38.5h28.6l6.4-38.5h-28.6Z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol id="discobot" viewBox="0 0 512 512">
|
<symbol id="discobot" viewBox="0 0 512 512">
|
||||||
<path d="M175.61 481.26H26.03c-6.75 0-13.36-2.67-18.42-7.59-5.06-5.06-7.87-11.81-7.59-18.84V290.76C.3 242.12 19.42 196 54.43 161.28c35.43-35.43 82.67-54.83 132.71-54.83h.56c49.77 0 96.3 19.54 131.17 55.11a187.257 187.257 0 0 1 48.5 88.57c5.06 21.79-6.19 44.14-26.71 53.14-14.62 6.47-69.17 27.7-170.67 37.96 4.5 11.25 16.17 23.34 46.68 29.66 29.8 6.19 64.53 4.22 87.59 2.95 9.84-.56 16.87-.98 21.79-.56 6.33.42 11.81 4.22 14.62 9.98 2.67 5.62 2.25 12.37-1.41 17.57-6.19 9-13.22 17.57-20.95 25.31-33.88 34.44-79.29 53.99-127.79 54.97h-14.76c0 .14-.14.14-.28.14h.14ZM35.17 446.11h154.65c34.87-.7 67.9-13.36 94.19-35.85-22.07.7-49.49.42-74.51-4.78-58.48-12.23-74.23-45.41-77.18-71.28-.7-6.47 1.12-12.79 5.2-17.85s10.12-8.15 16.59-8.72c82.81-7.31 143.26-23.62 172.22-36.27 5.06-2.25 7.87-7.73 6.61-13.07-6.33-27.13-19.82-51.88-39.36-71.84-28.12-28.68-65.8-44.57-106.14-44.57h-.56c-40.91 0-79.15 15.89-107.97 44.57-28.26 28.12-43.86 65.37-43.86 104.6v155.07c-.14 0 0 0 0 0h.14Zm-10.13 0ZM17.87 290.9Z"/><circle cx="171.11" cy="217.94" r="32.05"/><path d="M511.76 293.71c-.7-5.2-3.51-9.84-7.73-12.93s-9.42-4.5-14.62-3.66l-52.3 7.73c-5.2.7-9.84 3.51-12.93 7.73-3.09 4.22-4.5 9.42-3.66 14.62.98 6.61 5.2 12.23 11.39 15.04 2.53 1.12 5.34 1.69 8.01 1.69s1.97 0 2.95-.28l52.3-7.73c5.2-.7 9.84-3.51 12.93-7.73s4.5-9.42 3.66-14.62v.14ZM490.67 388.19l-71.98-36.41c-9.28-5.34-21.37-2.25-26.71 7.03-2.67 4.5-3.37 9.84-2.11 14.9 1.27 5.06 4.5 9.42 9.14 11.95l71.98 36.41c1.27.7 2.53 1.27 3.94 1.69 1.97.56 3.94.84 5.9.84 6.89 0 13.36-3.51 16.87-9.7 5.48-9.42 2.39-21.51-7.03-26.99v.28Z"/>
|
<path d="M175.61 481.26H26.03c-6.75 0-13.36-2.67-18.42-7.59-5.06-5.06-7.87-11.81-7.59-18.84V290.76C.3 242.12 19.42 196 54.43 161.28c35.43-35.43 82.67-54.83 132.71-54.83h.56c49.77 0 96.3 19.54 131.17 55.11a187.257 187.257 0 0 1 48.5 88.57c5.06 21.79-6.19 44.14-26.71 53.14-14.62 6.47-69.17 27.7-170.67 37.96 4.5 11.25 16.17 23.34 46.68 29.66 29.8 6.19 64.53 4.22 87.59 2.95 9.84-.56 16.87-.98 21.79-.56 6.33.42 11.81 4.22 14.62 9.98 2.67 5.62 2.25 12.37-1.41 17.57-6.19 9-13.22 17.57-20.95 25.31-33.88 34.44-79.29 53.99-127.79 54.97h-14.76c0 .14-.14.14-.28.14h.14ZM35.17 446.11h154.65c34.87-.7 67.9-13.36 94.19-35.85-22.07.7-49.49.42-74.51-4.78-58.48-12.23-74.23-45.41-77.18-71.28-.7-6.47 1.12-12.79 5.2-17.85s10.12-8.15 16.59-8.72c82.81-7.31 143.26-23.62 172.22-36.27 5.06-2.25 7.87-7.73 6.61-13.07-6.33-27.13-19.82-51.88-39.36-71.84-28.12-28.68-65.8-44.57-106.14-44.57h-.56c-40.91 0-79.15 15.89-107.97 44.57-28.26 28.12-43.86 65.37-43.86 104.6v155.07c-.14 0 0 0 0 0h.14Zm-10.13 0ZM17.87 290.9Z"/><circle cx="171.11" cy="217.94" r="32.05"/><path d="M511.76 293.71c-.7-5.2-3.51-9.84-7.73-12.93s-9.42-4.5-14.62-3.66l-52.3 7.73c-5.2.7-9.84 3.51-12.93 7.73-3.09 4.22-4.5 9.42-3.66 14.62.98 6.61 5.2 12.23 11.39 15.04 2.53 1.12 5.34 1.69 8.01 1.69s1.97 0 2.95-.28l52.3-7.73c5.2-.7 9.84-3.51 12.93-7.73s4.5-9.42 3.66-14.62v.14ZM490.67 388.19l-71.98-36.41c-9.28-5.34-21.37-2.25-26.71 7.03-2.67 4.5-3.37 9.84-2.11 14.9 1.27 5.06 4.5 9.42 9.14 11.95l71.98 36.41c1.27.7 2.53 1.27 3.94 1.69 1.97.56 3.94.84 5.9.84 6.89 0 13.36-3.51 16.87-9.7 5.48-9.42 2.39-21.51-7.03-26.99v.28Z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.1 KiB |
Loading…
Reference in New Issue