diff --git a/public/lib/svg-inject.js b/public/lib/svg-inject.js
new file mode 100644
index 000000000..4e74af07c
--- /dev/null
+++ b/public/lib/svg-inject.js
@@ -0,0 +1,697 @@
+/**
+ * SVGInject - Version 1.2.3
+ * A tiny, intuitive, robust, caching solution for injecting SVG files inline into the DOM.
+ *
+ * https://github.com/iconfu/svg-inject
+ *
+ * Copyright (c) 2018 INCORS, the creators of iconfu.com
+ * @license MIT License - https://github.com/iconfu/svg-inject/blob/master/LICENSE
+ */
+
+(function(window, document) {
+ // constants for better minification
+ var _CREATE_ELEMENT_ = 'createElement';
+ var _GET_ELEMENTS_BY_TAG_NAME_ = 'getElementsByTagName';
+ var _LENGTH_ = 'length';
+ var _STYLE_ = 'style';
+ var _TITLE_ = 'title';
+ var _UNDEFINED_ = 'undefined';
+ var _SET_ATTRIBUTE_ = 'setAttribute';
+ var _GET_ATTRIBUTE_ = 'getAttribute';
+
+ var NULL = null;
+
+ // constants
+ var __SVGINJECT = '__svgInject';
+ var ID_SUFFIX = '--inject-';
+ var ID_SUFFIX_REGEX = new RegExp(ID_SUFFIX + '\\d+', "g");
+ var LOAD_FAIL = 'LOAD_FAIL';
+ var SVG_NOT_SUPPORTED = 'SVG_NOT_SUPPORTED';
+ var SVG_INVALID = 'SVG_INVALID';
+ var ATTRIBUTE_EXCLUSION_NAMES = ['src', 'alt', 'onload', 'onerror'];
+ var A_ELEMENT = document[_CREATE_ELEMENT_]('a');
+ var IS_SVG_SUPPORTED = typeof SVGRect != _UNDEFINED_;
+ var DEFAULT_OPTIONS = {
+ useCache: true,
+ copyAttributes: true,
+ makeIdsUnique: true
+ };
+ // Map of IRI referenceable tag names to properties that can reference them. This is defined in
+ // https://www.w3.org/TR/SVG11/linking.html#processingIRI
+ var IRI_TAG_PROPERTIES_MAP = {
+ clipPath: ['clip-path'],
+ 'color-profile': NULL,
+ cursor: NULL,
+ filter: NULL,
+ linearGradient: ['fill', 'stroke'],
+ marker: ['marker', 'marker-end', 'marker-mid', 'marker-start'],
+ mask: NULL,
+ pattern: ['fill', 'stroke'],
+ radialGradient: ['fill', 'stroke']
+ };
+ var INJECTED = 1;
+ var FAIL = 2;
+
+ var uniqueIdCounter = 1;
+ var xmlSerializer;
+ var domParser;
+
+
+ // creates an SVG document from an SVG string
+ function svgStringToSvgDoc(svgStr) {
+ domParser = domParser || new DOMParser();
+ return domParser.parseFromString(svgStr, 'text/xml');
+ }
+
+
+ // searializes an SVG element to an SVG string
+ function svgElemToSvgString(svgElement) {
+ xmlSerializer = xmlSerializer || new XMLSerializer();
+ return xmlSerializer.serializeToString(svgElement);
+ }
+
+
+ // Returns the absolute url for the specified url
+ function getAbsoluteUrl(url) {
+ A_ELEMENT.href = url;
+ return A_ELEMENT.href;
+ }
+
+
+ // Load svg with an XHR request
+ function loadSvg(url, callback, errorCallback) {
+ if (url) {
+ var req = new XMLHttpRequest();
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ // readyState is DONE
+ var status = req.status;
+ if (status == 200) {
+ // request status is OK
+ callback(req.responseXML, req.responseText.trim());
+ } else if (status >= 400) {
+ // request status is error (4xx or 5xx)
+ errorCallback();
+ } else if (status == 0) {
+ // request status 0 can indicate a failed cross-domain call
+ errorCallback();
+ }
+ }
+ };
+ req.open('GET', url, true);
+ req.send();
+ }
+ }
+
+
+ // Copy attributes from img element to svg element
+ function copyAttributes(imgElem, svgElem) {
+ var attribute;
+ var attributeName;
+ var attributeValue;
+ var attributes = imgElem.attributes;
+ for (var i = 0; i < attributes[_LENGTH_]; i++) {
+ attribute = attributes[i];
+ attributeName = attribute.name;
+ // Only copy attributes not explicitly excluded from copying
+ if (ATTRIBUTE_EXCLUSION_NAMES.indexOf(attributeName) == -1) {
+ attributeValue = attribute.value;
+ // If img attribute is "title", insert a title element into SVG element
+ if (attributeName == _TITLE_) {
+ var titleElem;
+ var firstElementChild = svgElem.firstElementChild;
+ if (firstElementChild && firstElementChild.localName.toLowerCase() == _TITLE_) {
+ // If the SVG element's first child is a title element, keep it as the title element
+ titleElem = firstElementChild;
+ } else {
+ // If the SVG element's first child element is not a title element, create a new title
+ // ele,emt and set it as the first child
+ titleElem = document[_CREATE_ELEMENT_ + 'NS']('http://www.w3.org/2000/svg', _TITLE_);
+ svgElem.insertBefore(titleElem, firstElementChild);
+ }
+ // Set new title content
+ titleElem.textContent = attributeValue;
+ } else {
+ // Set img attribute to svg element
+ svgElem[_SET_ATTRIBUTE_](attributeName, attributeValue);
+ }
+ }
+ }
+ }
+
+
+ // This function appends a suffix to IDs of referenced elements in the
in order to to avoid ID collision
+ // between multiple injected SVGs. The suffix has the form "--inject-X", where X is a running number which is
+ // incremented with each injection. References to the IDs are adjusted accordingly.
+ // We assume tha all IDs within the injected SVG are unique, therefore the same suffix can be used for all IDs of one
+ // injected SVG.
+ // If the onlyReferenced argument is set to true, only those IDs will be made unique that are referenced from within the SVG
+ function makeIdsUnique(svgElem, onlyReferenced) {
+ var idSuffix = ID_SUFFIX + uniqueIdCounter++;
+ // Regular expression for functional notations of an IRI references. This will find occurences in the form
+ // url(#anyId) or url("#anyId") (for Internet Explorer) and capture the referenced ID
+ var funcIriRegex = /url\("?#([a-zA-Z][\w:.-]*)"?\)/g;
+ // Get all elements with an ID. The SVG spec recommends to put referenced elements inside elements, but
+ // this is not a requirement, therefore we have to search for IDs in the whole SVG.
+ var idElements = svgElem.querySelectorAll('[id]');
+ var idElem;
+ // An object containing referenced IDs as keys is used if only referenced IDs should be uniquified.
+ // If this object does not exist, all IDs will be uniquified.
+ var referencedIds = onlyReferenced ? [] : NULL;
+ var tagName;
+ var iriTagNames = {};
+ var iriProperties = [];
+ var changed = false;
+ var i, j;
+
+ if (idElements[_LENGTH_]) {
+ // Make all IDs unique by adding the ID suffix and collect all encountered tag names
+ // that are IRI referenceable from properities.
+ for (i = 0; i < idElements[_LENGTH_]; i++) {
+ tagName = idElements[i].localName; // Use non-namespaced tag name
+ // Make ID unique if tag name is IRI referenceable
+ if (tagName in IRI_TAG_PROPERTIES_MAP) {
+ iriTagNames[tagName] = 1;
+ }
+ }
+ // Get all properties that are mapped to the found IRI referenceable tags
+ for (tagName in iriTagNames) {
+ (IRI_TAG_PROPERTIES_MAP[tagName] || [tagName]).forEach(function (mappedProperty) {
+ // Add mapped properties to array of iri referencing properties.
+ // Use linear search here because the number of possible entries is very small (maximum 11)
+ if (iriProperties.indexOf(mappedProperty) < 0) {
+ iriProperties.push(mappedProperty);
+ }
+ });
+ }
+ if (iriProperties[_LENGTH_]) {
+ // Add "style" to properties, because it may contain references in the form 'style="fill:url(#myFill)"'
+ iriProperties.push(_STYLE_);
+ }
+ // Run through all elements of the SVG and replace IDs in references.
+ // To get all descending elements, getElementsByTagName('*') seems to perform faster than querySelectorAll('*').
+ // Since svgElem.getElementsByTagName('*') does not return the svg element itself, we have to handle it separately.
+ var descElements = svgElem[_GET_ELEMENTS_BY_TAG_NAME_]('*');
+ var element = svgElem;
+ var propertyName;
+ var value;
+ var newValue;
+ for (i = -1; element != NULL;) {
+ if (element.localName == _STYLE_) {
+ // If element is a style element, replace IDs in all occurences of "url(#anyId)" in text content
+ value = element.textContent;
+ newValue = value && value.replace(funcIriRegex, function(match, id) {
+ if (referencedIds) {
+ referencedIds[id] = 1;
+ }
+ return 'url(#' + id + idSuffix + ')';
+ });
+ if (newValue !== value) {
+ element.textContent = newValue;
+ }
+ } else if (element.hasAttributes()) {
+ // Run through all property names for which IDs were found
+ for (j = 0; j < iriProperties[_LENGTH_]; j++) {
+ propertyName = iriProperties[j];
+ value = element[_GET_ATTRIBUTE_](propertyName);
+ newValue = value && value.replace(funcIriRegex, function(match, id) {
+ if (referencedIds) {
+ referencedIds[id] = 1;
+ }
+ return 'url(#' + id + idSuffix + ')';
+ });
+ if (newValue !== value) {
+ element[_SET_ATTRIBUTE_](propertyName, newValue);
+ }
+ }
+ // Replace IDs in xlink:ref and href attributes
+ ['xlink:href', 'href'].forEach(function(refAttrName) {
+ var iri = element[_GET_ATTRIBUTE_](refAttrName);
+ if (/^\s*#/.test(iri)) { // Check if iri is non-null and internal reference
+ iri = iri.trim();
+ element[_SET_ATTRIBUTE_](refAttrName, iri + idSuffix);
+ if (referencedIds) {
+ // Add ID to referenced IDs
+ referencedIds[iri.substring(1)] = 1;
+ }
+ }
+ });
+ }
+ element = descElements[++i];
+ }
+ for (i = 0; i < idElements[_LENGTH_]; i++) {
+ idElem = idElements[i];
+ // If set of referenced IDs exists, make only referenced IDs unique,
+ // otherwise make all IDs unique.
+ if (!referencedIds || referencedIds[idElem.id]) {
+ // Add suffix to element's ID
+ idElem.id += idSuffix;
+ changed = true;
+ }
+ }
+ }
+ // return true if SVG element has changed
+ return changed;
+ }
+
+
+ // For cached SVGs the IDs are made unique by simply replacing the already inserted unique IDs with a
+ // higher ID counter. This is much more performant than a call to makeIdsUnique().
+ function makeIdsUniqueCached(svgString) {
+ return svgString.replace(ID_SUFFIX_REGEX, ID_SUFFIX + uniqueIdCounter++);
+ }
+
+
+ // Inject SVG by replacing the img element with the SVG element in the DOM
+ function inject(imgElem, svgElem, absUrl, options) {
+ if (svgElem) {
+ svgElem[_SET_ATTRIBUTE_]('data-inject-url', absUrl);
+ var parentNode = imgElem.parentNode;
+ if (parentNode) {
+ if (options.copyAttributes) {
+ copyAttributes(imgElem, svgElem);
+ }
+ // Invoke beforeInject hook if set
+ var beforeInject = options.beforeInject;
+ var injectElem = (beforeInject && beforeInject(imgElem, svgElem)) || svgElem;
+ // Replace img element with new element. This is the actual injection.
+ parentNode.replaceChild(injectElem, imgElem);
+ // Mark img element as injected
+ imgElem[__SVGINJECT] = INJECTED;
+ removeOnLoadAttribute(imgElem);
+ // Invoke afterInject hook if set
+ var afterInject = options.afterInject;
+ if (afterInject) {
+ afterInject(imgElem, injectElem);
+ }
+ }
+ } else {
+ svgInvalid(imgElem, options);
+ }
+ }
+
+
+ // Merges any number of options objects into a new object
+ function mergeOptions() {
+ var mergedOptions = {};
+ var args = arguments;
+ // Iterate over all specified options objects and add all properties to the new options object
+ for (var i = 0; i < args[_LENGTH_]; i++) {
+ var argument = args[i];
+ for (var key in argument) {
+ if (argument.hasOwnProperty(key)) {
+ mergedOptions[key] = argument[key];
+ }
+ }
+ }
+ return mergedOptions;
+ }
+
+
+ // Adds the specified CSS to the document's element
+ function addStyleToHead(css) {
+ var head = document[_GET_ELEMENTS_BY_TAG_NAME_]('head')[0];
+ if (head) {
+ var style = document[_CREATE_ELEMENT_](_STYLE_);
+ style.type = 'text/css';
+ style.appendChild(document.createTextNode(css));
+ head.appendChild(style);
+ }
+ }
+
+
+ // Builds an SVG element from the specified SVG string
+ function buildSvgElement(svgStr, verify) {
+ if (verify) {
+ var svgDoc;
+ try {
+ // Parse the SVG string with DOMParser
+ svgDoc = svgStringToSvgDoc(svgStr);
+ } catch(e) {
+ return NULL;
+ }
+ if (svgDoc[_GET_ELEMENTS_BY_TAG_NAME_]('parsererror')[_LENGTH_]) {
+ // DOMParser does not throw an exception, but instead puts parsererror tags in the document
+ return NULL;
+ }
+ return svgDoc.documentElement;
+ } else {
+ var div = document.createElement('div');
+ div.innerHTML = svgStr;
+ return div.firstElementChild;
+ }
+ }
+
+
+ function removeOnLoadAttribute(imgElem) {
+ // Remove the onload attribute. Should only be used to remove the unstyled image flash protection and
+ // make the element visible, not for removing the event listener.
+ imgElem.removeAttribute('onload');
+ }
+
+
+ function errorMessage(msg) {
+ console.error('SVGInject: ' + msg);
+ }
+
+
+ function fail(imgElem, status, options) {
+ imgElem[__SVGINJECT] = FAIL;
+ if (options.onFail) {
+ options.onFail(imgElem, status);
+ } else {
+ errorMessage(status);
+ }
+ }
+
+
+ function svgInvalid(imgElem, options) {
+ removeOnLoadAttribute(imgElem);
+ fail(imgElem, SVG_INVALID, options);
+ }
+
+
+ function svgNotSupported(imgElem, options) {
+ removeOnLoadAttribute(imgElem);
+ fail(imgElem, SVG_NOT_SUPPORTED, options);
+ }
+
+
+ function loadFail(imgElem, options) {
+ fail(imgElem, LOAD_FAIL, options);
+ }
+
+
+ function removeEventListeners(imgElem) {
+ imgElem.onload = NULL;
+ imgElem.onerror = NULL;
+ }
+
+
+ function imgNotSet(msg) {
+ errorMessage('no img element');
+ }
+
+
+ function createSVGInject(globalName, options) {
+ var defaultOptions = mergeOptions(DEFAULT_OPTIONS, options);
+ var svgLoadCache = {};
+
+ if (IS_SVG_SUPPORTED) {
+ // If the browser supports SVG, add a small stylesheet that hides the
elements until
+ // injection is finished. This avoids showing the unstyled SVGs before style is applied.
+ addStyleToHead('img[onload^="' + globalName + '("]{visibility:hidden;}');
+ }
+
+
+ /**
+ * SVGInject
+ *
+ * Injects the SVG specified in the `src` attribute of the specified `img` element or array of `img`
+ * elements. Returns a Promise object which resolves if all passed in `img` elements have either been
+ * injected or failed to inject (Only if a global Promise object is available like in all modern browsers
+ * or through a polyfill).
+ *
+ * Options:
+ * useCache: If set to `true` the SVG will be cached using the absolute URL. Default value is `true`.
+ * copyAttributes: If set to `true` the attributes will be copied from `img` to `svg`. Dfault value
+ * is `true`.
+ * makeIdsUnique: If set to `true` the ID of elements in the `` element that can be references by
+ * property values (for example 'clipPath') are made unique by appending "--inject-X", where X is a
+ * running number which increases with each injection. This is done to avoid duplicate IDs in the DOM.
+ * beforeLoad: Hook before SVG is loaded. The `img` element is passed as a parameter. If the hook returns
+ * a string it is used as the URL instead of the `img` element's `src` attribute.
+ * afterLoad: Hook after SVG is loaded. The loaded `svg` element and `svg` string are passed as a
+ * parameters. If caching is active this hook will only get called once for injected SVGs with the
+ * same absolute path. Changes to the `svg` element in this hook will be applied to all injected SVGs
+ * with the same absolute path. It's also possible to return an `svg` string or `svg` element which
+ * will then be used for the injection.
+ * beforeInject: Hook before SVG is injected. The `img` and `svg` elements are passed as parameters. If
+ * any html element is returned it gets injected instead of applying the default SVG injection.
+ * afterInject: Hook after SVG is injected. The `img` and `svg` elements are passed as parameters.
+ * onAllFinish: Hook after all `img` elements passed to an SVGInject() call have either been injected or
+ * failed to inject.
+ * onFail: Hook after injection fails. The `img` element and a `status` string are passed as an parameter.
+ * The `status` can be either `'SVG_NOT_SUPPORTED'` (the browser does not support SVG),
+ * `'SVG_INVALID'` (the SVG is not in a valid format) or `'LOAD_FAILED'` (loading of the SVG failed).
+ *
+ * @param {HTMLImageElement} img - an img element or an array of img elements
+ * @param {Object} [options] - optional parameter with [options](#options) for this injection.
+ */
+ function SVGInject(img, options) {
+ options = mergeOptions(defaultOptions, options);
+
+ var run = function(resolve) {
+ var allFinish = function() {
+ var onAllFinish = options.onAllFinish;
+ if (onAllFinish) {
+ onAllFinish();
+ }
+ resolve && resolve();
+ };
+
+ if (img && typeof img[_LENGTH_] != _UNDEFINED_) {
+ // an array like structure of img elements
+ var injectIndex = 0;
+ var injectCount = img[_LENGTH_];
+
+ if (injectCount == 0) {
+ allFinish();
+ } else {
+ var finish = function() {
+ if (++injectIndex == injectCount) {
+ allFinish();
+ }
+ };
+
+ for (var i = 0; i < injectCount; i++) {
+ SVGInjectElement(img[i], options, finish);
+ }
+ }
+ } else {
+ // only one img element
+ SVGInjectElement(img, options, allFinish);
+ }
+ };
+
+ // return a Promise object if globally available
+ return typeof Promise == _UNDEFINED_ ? run() : new Promise(run);
+ }
+
+
+ // Injects a single svg element. Options must be already merged with the default options.
+ function SVGInjectElement(imgElem, options, callback) {
+ if (imgElem) {
+ var svgInjectAttributeValue = imgElem[__SVGINJECT];
+ if (!svgInjectAttributeValue) {
+ removeEventListeners(imgElem);
+
+ if (!IS_SVG_SUPPORTED) {
+ svgNotSupported(imgElem, options);
+ callback();
+ return;
+ }
+ // Invoke beforeLoad hook if set. If the beforeLoad returns a value use it as the src for the load
+ // URL path. Else use the imgElem's src attribute value.
+ var beforeLoad = options.beforeLoad;
+ var src = (beforeLoad && beforeLoad(imgElem)) || imgElem[_GET_ATTRIBUTE_]('src');
+
+ if (!src) {
+ // If no image src attribute is set do no injection. This can only be reached by using javascript
+ // because if no src attribute is set the onload and onerror events do not get called
+ if (src === '') {
+ loadFail(imgElem, options);
+ }
+ callback();
+ return;
+ }
+
+ // set array so later calls can register callbacks
+ var onFinishCallbacks = [];
+ imgElem[__SVGINJECT] = onFinishCallbacks;
+
+ var onFinish = function() {
+ callback();
+ onFinishCallbacks.forEach(function(onFinishCallback) {
+ onFinishCallback();
+ });
+ };
+
+ var absUrl = getAbsoluteUrl(src);
+ var useCacheOption = options.useCache;
+ var makeIdsUniqueOption = options.makeIdsUnique;
+
+ var setSvgLoadCacheValue = function(val) {
+ if (useCacheOption) {
+ svgLoadCache[absUrl].forEach(function(svgLoad) {
+ svgLoad(val);
+ });
+ svgLoadCache[absUrl] = val;
+ }
+ };
+
+ if (useCacheOption) {
+ var svgLoad = svgLoadCache[absUrl];
+
+ var handleLoadValue = function(loadValue) {
+ if (loadValue === LOAD_FAIL) {
+ loadFail(imgElem, options);
+ } else if (loadValue === SVG_INVALID) {
+ svgInvalid(imgElem, options);
+ } else {
+ var hasUniqueIds = loadValue[0];
+ var svgString = loadValue[1];
+ var uniqueIdsSvgString = loadValue[2];
+ var svgElem;
+
+ if (makeIdsUniqueOption) {
+ if (hasUniqueIds === NULL) {
+ // IDs for the SVG string have not been made unique before. This may happen if previous
+ // injection of a cached SVG have been run with the option makedIdsUnique set to false
+ svgElem = buildSvgElement(svgString, false);
+ hasUniqueIds = makeIdsUnique(svgElem, false);
+
+ loadValue[0] = hasUniqueIds;
+ loadValue[2] = hasUniqueIds && svgElemToSvgString(svgElem);
+ } else if (hasUniqueIds) {
+ // Make IDs unique for already cached SVGs with better performance
+ svgString = makeIdsUniqueCached(uniqueIdsSvgString);
+ }
+ }
+
+ svgElem = svgElem || buildSvgElement(svgString, false);
+
+ inject(imgElem, svgElem, absUrl, options);
+ }
+ onFinish();
+ };
+
+ if (typeof svgLoad != _UNDEFINED_) {
+ // Value for url exists in cache
+ if (svgLoad.isCallbackQueue) {
+ // Same url has been cached, but value has not been loaded yet, so add to callbacks
+ svgLoad.push(handleLoadValue);
+ } else {
+ handleLoadValue(svgLoad);
+ }
+ return;
+ } else {
+ var svgLoad = [];
+ // set property isCallbackQueue to Array to differentiate from array with cached loaded values
+ svgLoad.isCallbackQueue = true;
+ svgLoadCache[absUrl] = svgLoad;
+ }
+ }
+
+ // Load the SVG because it is not cached or caching is disabled
+ loadSvg(absUrl, function(svgXml, svgString) {
+ // Use the XML from the XHR request if it is an instance of Document. Otherwise
+ // (for example of IE9), create the svg document from the svg string.
+ var svgElem = svgXml instanceof Document ? svgXml.documentElement : buildSvgElement(svgString, true);
+
+ var afterLoad = options.afterLoad;
+ if (afterLoad) {
+ // Invoke afterLoad hook which may modify the SVG element. After load may also return a new
+ // svg element or svg string
+ var svgElemOrSvgString = afterLoad(svgElem, svgString) || svgElem;
+ if (svgElemOrSvgString) {
+ // Update svgElem and svgString because of modifications to the SVG element or SVG string in
+ // the afterLoad hook, so the modified SVG is also used for all later cached injections
+ var isString = typeof svgElemOrSvgString == 'string';
+ svgString = isString ? svgElemOrSvgString : svgElemToSvgString(svgElem);
+ svgElem = isString ? buildSvgElement(svgElemOrSvgString, true) : svgElemOrSvgString;
+ }
+ }
+
+ if (svgElem instanceof SVGElement) {
+ var hasUniqueIds = NULL;
+ if (makeIdsUniqueOption) {
+ hasUniqueIds = makeIdsUnique(svgElem, false);
+ }
+
+ if (useCacheOption) {
+ var uniqueIdsSvgString = hasUniqueIds && svgElemToSvgString(svgElem);
+ // set an array with three entries to the load cache
+ setSvgLoadCacheValue([hasUniqueIds, svgString, uniqueIdsSvgString]);
+ }
+
+ inject(imgElem, svgElem, absUrl, options);
+ } else {
+ svgInvalid(imgElem, options);
+ setSvgLoadCacheValue(SVG_INVALID);
+ }
+ onFinish();
+ }, function() {
+ loadFail(imgElem, options);
+ setSvgLoadCacheValue(LOAD_FAIL);
+ onFinish();
+ });
+ } else {
+ if (Array.isArray(svgInjectAttributeValue)) {
+ // svgInjectAttributeValue is an array. Injection is not complete so register callback
+ svgInjectAttributeValue.push(callback);
+ } else {
+ callback();
+ }
+ }
+ } else {
+ imgNotSet();
+ }
+ }
+
+
+ /**
+ * Sets the default [options](#options) for SVGInject.
+ *
+ * @param {Object} [options] - default [options](#options) for an injection.
+ */
+ SVGInject.setOptions = function(options) {
+ defaultOptions = mergeOptions(defaultOptions, options);
+ };
+
+
+ // Create a new instance of SVGInject
+ SVGInject.create = createSVGInject;
+
+
+ /**
+ * Used in onerror Event of an `
` element to handle cases when the loading the original src fails
+ * (for example if file is not found or if the browser does not support SVG). This triggers a call to the
+ * options onFail hook if available. The optional second parameter will be set as the new src attribute
+ * for the img element.
+ *
+ * @param {HTMLImageElement} img - an img element
+ * @param {String} [fallbackSrc] - optional parameter fallback src
+ */
+ SVGInject.err = function(img, fallbackSrc) {
+ if (img) {
+ if (img[__SVGINJECT] != FAIL) {
+ removeEventListeners(img);
+
+ if (!IS_SVG_SUPPORTED) {
+ svgNotSupported(img, defaultOptions);
+ } else {
+ removeOnLoadAttribute(img);
+ loadFail(img, defaultOptions);
+ }
+ if (fallbackSrc) {
+ removeOnLoadAttribute(img);
+ img.src = fallbackSrc;
+ }
+ }
+ } else {
+ imgNotSet();
+ }
+ };
+
+ window[globalName] = SVGInject;
+
+ return SVGInject;
+ }
+
+ var SVGInjectInstance = createSVGInject('SVGInject');
+
+ if (typeof module == 'object' && typeof module.exports == 'object') {
+ module.exports = SVGInjectInstance;
+ }
+})(window, document);
\ No newline at end of file
diff --git a/public/script.js b/public/script.js
index 3bf214d3a..443c09696 100644
--- a/public/script.js
+++ b/public/script.js
@@ -751,7 +751,7 @@ let create_save = {
};
//animation right menu
-let animation_duration = 250;
+let animation_duration = 125;
let animation_easing = "ease-in-out";
let popup_type = "";
let bg_file_for_del = "";
@@ -1446,25 +1446,25 @@ function insertSVGIcon(mes, extra) {
modelName = extra.api;
}
- // Fetch the SVG based on the modelName
- $.get(`/img/${modelName}.svg`, function (data) {
- // Extract the SVG content from the XML data
- let svg = $(data).find('svg');
-
- // Add classes for styling and identification
- svg.addClass('icon-svg timestamp-icon');
+ const image = new Image();
+ // Add classes for styling and identification
+ image.classList.add('icon-svg', 'timestamp-icon');
+ image.src = `/img/${modelName}.svg`;
+ image.onload = async function () {
// Check if an SVG already exists adjacent to the timestamp
let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
if (existingSVG.length) {
// Replace existing SVG
- existingSVG.replaceWith(svg);
+ existingSVG.replaceWith(image);
} else {
// Append the new SVG if none exists
- mes.find('.timestamp').after(svg);
+ mes.find('.timestamp').after(image);
}
- });
+
+ await SVGInject(this);
+ };
}
@@ -1769,7 +1769,6 @@ function scrollChatToBottom() {
function substituteParams(content, _name1, _name2, _original, _group) {
_name1 = _name1 ?? name1;
_name2 = _name2 ?? name2;
- _original = _original || '';
_group = _group ?? name2;
if (!content) {
@@ -2126,12 +2125,14 @@ function baseChatReplace(value, name1, name2) {
if (power_user.collapse_newlines) {
value = collapseNewlines(value);
}
+
+ value = value.replace(/\r/g, '');
}
return value;
}
function isStreamingEnabled() {
- return ((main_api == 'openai' && oai_settings.stream_openai && oai_settings.chat_completion_source !== chat_completion_sources.SCALE)
+ return ((main_api == 'openai' && oai_settings.stream_openai && oai_settings.chat_completion_source !== chat_completion_sources.SCALE && oai_settings.chat_completion_source !== chat_completion_sources.AI21)
|| (main_api == 'kobold' && kai_settings.streaming_kobold && kai_settings.can_use_streaming)
|| (main_api == 'novel' && nai_settings.streaming_novel)
|| (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming))
@@ -2528,8 +2529,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
let charPersonality = baseChatReplace(characters[this_chid].personality.trim(), name1, name2);
let Scenario = baseChatReplace(scenarioText.trim(), name1, name2);
let mesExamples = baseChatReplace(characters[this_chid].mes_example.trim(), name1, name2);
- let systemPrompt = baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2);
- let jailbreakPrompt = baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2);
+ let systemPrompt = power_user.prefer_character_prompt ? baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2) : '';
+ let jailbreakPrompt = power_user.prefer_character_jailbreak ? baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2) : '';
// Parse example messages
if (!mesExamples.startsWith('')) {
@@ -2982,8 +2983,9 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
bias: promptBias,
type: type,
quietPrompt: quiet_prompt,
- jailbreakPrompt: jailbreakPrompt,
cyclePrompt: cyclePrompt,
+ systemPromptOverride: systemPrompt,
+ jailbreakPromptOverride: jailbreakPrompt,
}, dryRun);
generate_data = { prompt: prompt };
@@ -4738,6 +4740,7 @@ function changeMainAPI() {
case chat_completion_sources.WINDOWAI:
case chat_completion_sources.CLAUDE:
case chat_completion_sources.OPENAI:
+ case chat_completion_sources.AI21:
default:
setupChatCompletionPromptManager(oai_settings);
break;
@@ -7178,6 +7181,11 @@ function connectAPISlash(_, text) {
source: 'openrouter',
button: '#api_button_openai',
},
+ 'ai21': {
+ selected: 'openai',
+ source: 'ai21',
+ button: '#api_button_openai',
+ }
};
const apiConfig = apiMap[text];
@@ -7396,7 +7404,7 @@ $(document).ready(function () {
}
registerSlashCommand('dupe', DupeChar, [], "– duplicates the currently selected character", true, true);
- registerSlashCommand('api', connectAPISlash, [], "(kobold, horde, novel, ooba, oai, claude, windowai) – connect to an API", true, true);
+ registerSlashCommand('api', connectAPISlash, [], "(kobold, horde, novel, ooba, oai, claude, windowai, ai21) – connect to an API", true, true);
registerSlashCommand('impersonate', doImpersonate, ['imp'], "- calls an impersonation response", true, true);
registerSlashCommand('delchat', doDeleteChat, [], "- deletes the current chat", true, true);
registerSlashCommand('closechat', doCloseChat, [], "- closes the current chat", true, true);
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index adc17cec9..6d7ac3d75 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -478,6 +478,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.SCALE] && oai_settings.chat_completion_source == chat_completion_sources.SCALE)
|| (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
|| (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
+ || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
) {
$("#api_button_openai").click();
}
diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js
index 2ce64a5fc..b5f51678b 100644
--- a/public/scripts/group-chats.js
+++ b/public/scripts/group-chats.js
@@ -63,8 +63,8 @@ import {
getCropPopup,
system_avatar,
} from "../script.js";
-import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
-import { FilterHelper } from './filters.js';
+import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map, printTagFilters } from './tags.js';
+import { FILTER_TYPES, FilterHelper } from './filters.js';
export {
selected_group,
@@ -1276,16 +1276,8 @@ function openCharacterDefinition(characterSelect) {
}
function filterGroupMembers() {
- const searchValue = $(this).val().trim().toLowerCase();
-
- if (!searchValue) {
- $("#rm_group_add_members .group_member").removeClass('hiddenBySearch');
- } else {
- $("#rm_group_add_members .group_member").each(function () {
- const isValidSearch = $(this).find(".ch_name").text().toLowerCase().includes(searchValue);
- $(this).toggleClass('hiddenBySearch', !isValidSearch);
- });
- }
+ const searchValue = $(this).val().toLowerCase();
+ groupCandidatesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue);
}
async function createGroup() {
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index ea710abcc..e83af5d91 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -45,6 +45,7 @@ import {
} from "./secrets.js";
import {
+ deepClone,
delay,
download,
getFileText, getSortableDelay,
@@ -113,9 +114,13 @@ const scale_max = 7900; // Probably more. Save some for the system prompt define
const claude_max = 8000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
const palm2_max = 7500; // The real context window is 8192, spare some for padding due to using turbo tokenizer
const claude_100k_max = 99000;
+let ai21_max = 9200; //can easily fit 9k gpt tokens because j2's tokenizer is efficient af
const unlocked_max = 100 * 1024;
const oai_max_temp = 2.0;
-const claude_max_temp = 1.0;
+const claude_max_temp = 1.0; //same as j2
+const j2_max_topk = 10.0;
+const j2_max_freq = 5.0;
+const j2_max_pres = 5.0;
const openrouter_website_model = 'OR_Website';
let biasCache = undefined;
@@ -161,13 +166,26 @@ export const chat_completion_sources = {
CLAUDE: 'claude',
SCALE: 'scale',
OPENROUTER: 'openrouter',
+ AI21: 'ai21',
};
+const prefixMap = selected_group ? {
+ assistant: "",
+ user: "",
+ system: "OOC: "
+}
+ : {
+ assistant: "{{char}}:",
+ user: "{{user}}:",
+ system: ""
+ };
+
const default_settings = {
preset_settings_openai: 'Default',
temp_openai: 0.9,
freq_pen_openai: 0.7,
pres_pen_openai: 0.7,
+ count_pen: 0.0,
top_p_openai: 1.0,
top_k_openai: 0,
stream_openai: false,
@@ -188,6 +206,7 @@ const default_settings = {
wi_format: default_wi_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-instant-v1',
+ ai21_model: 'j2-ultra',
windowai_model: '',
openrouter_model: openrouter_website_model,
jailbreak_system: false,
@@ -199,6 +218,7 @@ const default_settings = {
show_external_models: false,
proxy_password: '',
assistant_prefill: '',
+ use_ai21_tokenizer: false,
};
const oai_settings = {
@@ -206,6 +226,7 @@ const oai_settings = {
temp_openai: 1.0,
freq_pen_openai: 0,
pres_pen_openai: 0,
+ count_pen: 0.0,
top_p_openai: 1.0,
top_k_openai: 0,
stream_openai: false,
@@ -226,6 +247,7 @@ const oai_settings = {
wi_format: default_wi_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-instant-v1',
+ ai21_model: 'j2-ultra',
windowai_model: '',
openrouter_model: openrouter_website_model,
jailbreak_system: false,
@@ -237,6 +259,7 @@ const oai_settings = {
show_external_models: false,
proxy_password: '',
assistant_prefill: '',
+ use_ai21_tokenizer: false,
};
let openai_setting_names;
@@ -676,7 +699,7 @@ function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, ty
*
* @returns {Object} prompts - The prepared and merged system and user-defined prompts.
*/
-function preparePromptsForChatCompletion(Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts) {
+function preparePromptsForChatCompletion(Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride) {
const scenarioText = Scenario ? `[Circumstances and context of the dialogue: ${Scenario}]` : '';
const charPersonalityText = charPersonality ? `[${name2}'s personality: ${charPersonality}]` : '';
@@ -729,7 +752,6 @@ function preparePromptsForChatCompletion(Scenario, charPersonality, name2, world
});
// Apply character-specific main prompt
- const systemPromptOverride = promptManager.activeCharacter.data?.system_prompt ?? null;
const systemPrompt = prompts.get('main') ?? null;
if (systemPromptOverride && systemPrompt) {
const mainOriginalContent = systemPrompt.content;
@@ -739,7 +761,6 @@ function preparePromptsForChatCompletion(Scenario, charPersonality, name2, world
}
// Apply character-specific jailbreak
- const jailbreakPromptOverride = promptManager.activeCharacter.data?.post_history_instructions ?? null;
const jailbreakPrompt = prompts.get('jailbreak') ?? null;
if (jailbreakPromptOverride && jailbreakPrompt) {
const jbOriginalContent = jailbreakPrompt.content;
@@ -783,7 +804,9 @@ function prepareOpenAIMessages({
type,
quietPrompt,
extensionPrompts,
- cyclePrompt
+ cyclePrompt,
+ systemPromptOverride,
+ jailbreakPromptOverride,
} = {}, dryRun) {
// Without a character selected, there is no way to accurately calculate tokens
if (!promptManager.activeCharacter && dryRun) return [null, false];
@@ -796,7 +819,7 @@ function prepareOpenAIMessages({
try {
// Merge markers and ordered user prompts with system prompts
- const prompts = preparePromptsForChatCompletion(Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts);
+ const prompts = preparePromptsForChatCompletion(Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride);
// Fill the chat completion with as much context as the budget allows
populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt });
@@ -973,6 +996,8 @@ function getChatCompletionModel() {
return '';
case chat_completion_sources.OPENROUTER:
return oai_settings.openrouter_model !== openrouter_website_model ? oai_settings.openrouter_model : null;
+ case chat_completion_sources.AI21:
+ return oai_settings.ai21_model;
default:
throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
}
@@ -1054,9 +1079,18 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE;
const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER;
const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE;
+ const isAI21 = oai_settings.chat_completion_source == chat_completion_sources.AI21;
const isTextCompletion = oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('text-') || oai_settings.openai_model.startsWith('code-'));
- const stream = type !== 'quiet' && oai_settings.stream_openai && !isScale;
const isQuiet = type === 'quiet';
+ const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21;
+
+ if (isAI21) {
+ const joinedMsgs = openai_msgs_tosend.reduce((acc, obj) => {
+ const prefix = prefixMap[obj.role];
+ return acc + (prefix ? (selected_group ? "\n" : prefix + " ") : "") + obj.content + "\n";
+ }, "");
+ openai_msgs_tosend = substituteParams(joinedMsgs);
+ }
// If we're using the window.ai extension, use that instead
// Doesn't support logit bias yet
@@ -1113,6 +1147,13 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
generate_data['api_url_scale'] = oai_settings.api_url_scale;
}
+ if (isAI21) {
+ generate_data['use_ai21'] = true;
+ generate_data['top_k'] = parseFloat(oai_settings.top_k_openai);
+ generate_data['count_pen'] = parseFloat(oai_settings.count_pen);
+ generate_data['stop_tokens'] = [name1 + ':', 'prompt: [Start a new chat]'];
+ }
+
const generate_url = '/generate_openai';
const response = await fetch(generate_url, {
method: 'POST',
@@ -1303,6 +1344,7 @@ class TokenHandler {
}
function countTokens(messages, full = false) {
+ let shouldTokenizeAI21 = oai_settings.chat_completion_source === chat_completion_sources.AI21 && oai_settings.use_ai21_tokenizer;
let chatId = 'undefined';
try {
@@ -1335,12 +1377,12 @@ function countTokens(messages, full = false) {
if (typeof cachedCount === 'number') {
token_count += cachedCount;
}
- else {
+ else {
jQuery.ajax({
async: false,
type: 'POST', //
- url: `/tokenize_openai?model=${model}`,
+ url: shouldTokenizeAI21 ? '/tokenize_ai21' : `/tokenize_openai?model=${model}`,
data: JSON.stringify([message]),
dataType: "json",
contentType: "application/json",
@@ -1856,6 +1898,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.temp_openai = settings.temp_openai ?? default_settings.temp_openai;
oai_settings.freq_pen_openai = settings.freq_pen_openai ?? default_settings.freq_pen_openai;
oai_settings.pres_pen_openai = settings.pres_pen_openai ?? default_settings.pres_pen_openai;
+ oai_settings.count_pen = settings.count_pen ?? default_settings.count_pen;
oai_settings.top_p_openai = settings.top_p_openai ?? default_settings.top_p_openai;
oai_settings.top_k_openai = settings.top_k_openai ?? default_settings.top_k_openai;
oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai;
@@ -1871,6 +1914,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.claude_model = settings.claude_model ?? default_settings.claude_model;
oai_settings.windowai_model = settings.windowai_model ?? default_settings.windowai_model;
oai_settings.openrouter_model = settings.openrouter_model ?? default_settings.openrouter_model;
+ oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model;
oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;
oai_settings.api_url_scale = settings.api_url_scale ?? default_settings.api_url_scale;
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
@@ -1889,7 +1933,7 @@ function loadOpenAISettings(data, settings) {
if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes;
if (settings.names_in_completion !== undefined) oai_settings.names_in_completion = !!settings.names_in_completion;
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
-
+ if (settings.use_ai21_tokenizer !== undefined) oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer;
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password);
@@ -1901,6 +1945,8 @@ function loadOpenAISettings(data, settings) {
$(`#model_claude_select option[value="${oai_settings.claude_model}"`).attr('selected', true);
$('#model_windowai_select').val(oai_settings.windowai_model);
$(`#model_windowai_select option[value="${oai_settings.windowai_model}"`).attr('selected', true);
+ $('#model_ai21_select').val(oai_settings.ai21_model);
+ $(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true);
$('#openai_max_context').val(oai_settings.openai_max_context);
$('#openai_max_context_counter').text(`${oai_settings.openai_max_context}`);
$('#model_openrouter_select').val(oai_settings.openrouter_model);
@@ -1916,7 +1962,7 @@ function loadOpenAISettings(data, settings) {
$('#legacy_streaming').prop('checked', oai_settings.legacy_streaming);
$('#openai_show_external_models').prop('checked', oai_settings.show_external_models);
$('#openai_external_category').toggle(oai_settings.show_external_models);
-
+ $('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer);
if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt;
$('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
@@ -1939,6 +1985,9 @@ function loadOpenAISettings(data, settings) {
$('#pres_pen_openai').val(oai_settings.pres_pen_openai);
$('#pres_pen_counter_openai').text(Number(oai_settings.pres_pen_openai).toFixed(2));
+ $('#count_pen').val(oai_settings.count_pen);
+ $('#count_pen_counter').text(Number(oai_settings.count_pen).toFixed(2));
+
$('#top_p_openai').val(oai_settings.top_p_openai);
$('#top_p_counter_openai').text(Number(oai_settings.top_p_openai).toFixed(2));
@@ -1981,7 +2030,7 @@ async function getStatusOpen() {
return resultCheckStatusOpen();
}
- if (oai_settings.chat_completion_source == chat_completion_sources.SCALE || oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
+ if (oai_settings.chat_completion_source == chat_completion_sources.SCALE || oai_settings.chat_completion_source == chat_completion_sources.CLAUDE || oai_settings.chat_completion_source == chat_completion_sources.AI21) {
let status = 'Unable to verify key; press "Test Message" to validate.';
setOnlineStatus(status);
return resultCheckStatusOpen();
@@ -2078,9 +2127,11 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
claude_model: settings.claude_model,
windowai_model: settings.windowai_model,
openrouter_model: settings.openrouter_model,
+ ai21_model: settings.ai21_model,
temperature: settings.temp_openai,
frequency_penalty: settings.freq_pen_openai,
presence_penalty: settings.pres_pen_openai,
+ count_penalty: settings.count_pen,
top_p: settings.top_p_openai,
top_k: settings.top_k_openai,
openai_max_context: settings.openai_max_context,
@@ -2108,6 +2159,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
api_url_scale: settings.api_url_scale,
show_external_models: settings.show_external_models,
assistant_prefill: settings.assistant_prefill,
+ use_ai21_tokenizer: settings.use_ai21_tokenizer,
};
const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, {
@@ -2293,7 +2345,11 @@ async function onExportPresetClick() {
return;
}
- const preset = openai_settings[openai_setting_names[oai_settings.preset_settings_openai]];
+ const preset = deepClone(openai_settings[openai_setting_names[oai_settings.preset_settings_openai]]);
+
+ delete preset.reverse_proxy;
+ delete preset.proxy_password;
+
const presetJsonString = JSON.stringify(preset, null, 4);
download(presetJsonString, oai_settings.preset_settings_openai, 'application/json');
}
@@ -2407,6 +2463,7 @@ function onSettingsPresetChange() {
temperature: ['#temp_openai', 'temp_openai', false],
frequency_penalty: ['#freq_pen_openai', 'freq_pen_openai', false],
presence_penalty: ['#pres_pen_openai', 'pres_pen_openai', false],
+ count_penalty: ['#count_pen', 'count_pen', false],
top_p: ['#top_p_openai', 'top_p_openai', false],
top_k: ['#top_k_openai', 'top_k_openai', false],
max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true],
@@ -2414,6 +2471,7 @@ function onSettingsPresetChange() {
claude_model: ['#model_claude_select', 'claude_model', false],
windowai_model: ['#model_windowai_select', 'windowai_model', false],
openrouter_model: ['#model_openrouter_select', 'openrouter_model', false],
+ ai21_model: ['#model_ai21_select', 'ai21_model', false],
openai_max_context: ['#openai_max_context', 'openai_max_context', false],
openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false],
wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true],
@@ -2437,6 +2495,7 @@ function onSettingsPresetChange() {
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
+ use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', false],
};
const presetName = $('#settings_perset_openai').find(":selected").text();
@@ -2561,6 +2620,11 @@ async function onModelChange() {
oai_settings.openrouter_model = value;
}
+ if ($(this).is('#model_ai21_select')) {
+ console.log('AI21 model changed to', value);
+ oai_settings.ai21_model = value;
+ }
+
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
@@ -2647,6 +2711,38 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
+ if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
+ if (oai_settings.max_context_unlocked) {
+ $('#openai_max_context').attr('max', unlocked_max);
+ } else {
+ $('#openai_max_context').attr('max', ai21_max);
+ }
+
+ oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
+
+ oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
+ $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
+
+ oai_settings.freq_pen_openai = Math.min(j2_max_freq, oai_settings.freq_pen_openai < 0 ? 0 : oai_settings.freq_pen_openai);
+ $('#freq_pen_openai').attr('min', 0).attr('max', j2_max_freq).val(oai_settings.freq_pen_openai).trigger('input');
+
+ oai_settings.pres_pen_openai = Math.min(j2_max_pres, oai_settings.pres_pen_openai < 0 ? 0 : oai_settings.pres_pen_openai);
+ $('#pres_pen_openai').attr('min', 0).attr('max', j2_max_pres).val(oai_settings.pres_pen_openai).trigger('input');
+
+ oai_settings.top_k_openai = Math.min(j2_max_topk, oai_settings.top_k_openai);
+ $('#top_k_openai').attr('max', j2_max_topk).val(oai_settings.top_k_openai).trigger('input');
+ } else if (oai_settings.chat_completion_source != chat_completion_sources.AI21) {
+ oai_settings.freq_pen_openai = Math.min(2.0, oai_settings.freq_pen_openai);
+ $('#freq_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.freq_pen_openai).trigger('input');
+
+ oai_settings.freq_pen_openai = Math.min(2.0, oai_settings.pres_pen_openai);
+ $('#pres_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.freq_pen_openai).trigger('input');
+
+ oai_settings.top_k_openai = Math.min(200, oai_settings.top_k_openai);
+ $('#top_k_openai').attr('max', 200).val(oai_settings.top_k_openai).trigger('input');
+ }
+
saveSettingsDebounced();
eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value);
}
@@ -2737,6 +2833,19 @@ async function onConnectButtonClick(e) {
}
}
+ if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
+ const api_key_ai21 = $('#api_key_ai21').val().trim();
+
+ if (api_key_ai21.length) {
+ await writeSecret(SECRET_KEYS.AI21, api_key_ai21);
+ }
+
+ if (!secret_state[SECRET_KEYS.AI21]) {
+ console.log('No secret key saved for AI21');
+ return;
+ }
+ }
+
$("#api_loading_openai").css("display", 'inline-block');
$("#api_button_openai").css("display", 'none');
saveSettingsDebounced();
@@ -2766,7 +2875,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
$('#model_openrouter_select').trigger('change');
}
-
+ else if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
+ $('#model_ai21_select').trigger('change');
+ }
$('[data-source]').each(function () {
const validSources = $(this).data('source').split(',');
$(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -2824,7 +2935,12 @@ $(document).ready(async function () {
oai_settings.pres_pen_openai = $(this).val();
$('#pres_pen_counter_openai').text(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
+ });
+ $(document).on('input', '#count_pen', function () {
+ oai_settings.count_pen = $(this).val();
+ $('#count_pen_counter').text(Number($(this).val()).toFixed(2));
+ saveSettingsDebounced();
});
$(document).on('input', '#top_p_openai', function () {
@@ -2862,6 +2978,14 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
+ $('#use_ai21_tokenizer').on('change', function () {
+ oai_settings.use_ai21_tokenizer = !!$('#use_ai21_tokenizer').prop('checked');
+ oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200;
+ oai_settings.openai_max_context = Math.min(ai21_max, oai_settings.openai_max_context);
+ $('#openai_max_context').attr('max', ai21_max).val(oai_settings.openai_max_context).trigger('input');
+ saveSettingsDebounced();
+ });
+
$('#names_in_completion').on('change', function () {
oai_settings.names_in_completion = !!$('#names_in_completion').prop('checked');
saveSettingsDebounced();
@@ -3029,6 +3153,7 @@ $(document).ready(async function () {
$("#model_windowai_select").on("change", onModelChange);
$("#model_scale_select").on("change", onModelChange);
$("#model_openrouter_select").on("change", onModelChange);
+ $("#model_ai21_select").on("change", onModelChange);
$("#settings_perset_openai").on("change", onSettingsPresetChange);
$("#new_oai_preset").on("click", onNewPresetClick);
$("#delete_oai_preset").on("click", onDeletePresetClick);
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index d3e26bc58..aedc8285b 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -163,6 +163,7 @@ let power_user = {
trim_spaces: true,
relaxed_api_urls: false,
+ default_instruct: '',
instruct: {
enabled: false,
wrap: true,
@@ -177,6 +178,7 @@ let power_user = {
separator_sequence: '',
macro: false,
names_force_groups: true,
+ activation_regex: '',
},
context: {
@@ -938,6 +940,7 @@ function loadInstructMode() {
{ id: "instruct_macro", property: "macro", isCheckbox: true },
{ id: "instruct_names_force_groups", property: "names_force_groups", isCheckbox: true },
{ id: "instruct_last_output_sequence", property: "last_output_sequence", isCheckbox: false },
+ { id: "instruct_activation_regex", property: "activation_regex", isCheckbox: false },
];
if (power_user.instruct.names_force_groups === undefined) {
@@ -968,6 +971,19 @@ function loadInstructMode() {
$('#instruct_presets').append(option);
});
+ function highlightDefaultPreset() {
+ $('#instruct_set_default').toggleClass('default', power_user.default_instruct === power_user.instruct.preset);
+ }
+
+ $('#instruct_set_default').on('click', function () {
+ power_user.default_instruct = power_user.instruct.preset;
+ $(this).addClass('default');
+ toastr.success(`Default instruct preset set to ${power_user.default_instruct}`);
+ saveSettingsDebounced();
+ });
+
+ highlightDefaultPreset();
+
$('#instruct_presets').on('change', function () {
const name = $(this).find(':selected').val();
const preset = instruct_presets.find(x => x.name === name);
@@ -989,6 +1005,8 @@ function loadInstructMode() {
}
}
});
+
+ highlightDefaultPreset();
});
}
diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js
index 00203a106..202870c50 100644
--- a/public/scripts/secrets.js
+++ b/public/scripts/secrets.js
@@ -8,6 +8,7 @@ export const SECRET_KEYS = {
CLAUDE: 'api_key_claude',
OPENROUTER: 'api_key_openrouter',
SCALE: 'api_key_scale',
+ AI21: 'api_key_ai21',
}
const INPUT_MAP = {
@@ -18,6 +19,7 @@ const INPUT_MAP = {
[SECRET_KEYS.CLAUDE]: '#api_key_claude',
[SECRET_KEYS.OPENROUTER]: '#api_key_openrouter',
[SECRET_KEYS.SCALE]: '#api_key_scale',
+ [SECRET_KEYS.AI21]: '#api_key_ai21',
}
async function clearSecret() {
diff --git a/public/scripts/tags.js b/public/scripts/tags.js
index cd8339358..a6887c7c6 100644
--- a/public/scripts/tags.js
+++ b/public/scripts/tags.js
@@ -285,8 +285,8 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon);
}
- if (tag.excluded) {
- isGeneralList ? $(tagElement).addClass('excluded') : $(listElement).closest('.character_select, .group_select').addClass('hiddenByTag');
+ if (tag.excluded && isGeneralList) {
+ $(tagElement).addClass('excluded');
}
if (selectable) {
diff --git a/public/style.css b/public/style.css
index f9b5f576c..a29a3cf8c 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1351,7 +1351,8 @@ body.charListGrid #rm_print_characters_block .tags_inline {
overflow-y: auto;
}
-#floatingPrompt, #cfgConfig {
+#floatingPrompt,
+#cfgConfig {
overflow-y: auto;
max-width: 90svw;
max-height: 90svh;
@@ -1471,6 +1472,14 @@ select option:not(:checked) {
color: #4b9c00 !important;
}
+#instruct_set_default {
+ font-size: smaller;
+}
+
+#instruct_set_default.default {
+ color: #f44336 !important;
+}
+
.displayBlock {
display: block !important;
}
@@ -3278,6 +3287,7 @@ h5 {
justify-content: flex-start;
max-height: 66%;
overflow: hidden;
+ flex-basis: 100%;
}
.tag_name {
@@ -3967,13 +3977,6 @@ a {
display: none;
}
-.hiddenByTag,
-.hiddenByFav,
-.hiddenByGroup,
-.hiddenBySearch {
- display: none !important;
-}
-
/* Message images */
.mes .mes_img_container {
max-width: 100%;
@@ -4342,7 +4345,7 @@ input.extension_missing[type="checkbox"] {
}
.fillLeft .scrollableInner {
- padding: 0.5em 1em 0.5em 0.5em
+ padding: 0.5em 1em 0.5em 0.5em
}
.width100p {
@@ -4653,6 +4656,7 @@ toolcool-color-picker {
color: var(--SmartThemeQuoteColor);
font-weight: bold;
font-style: italic;
+ font-size: 0.8em;
}
.openai_restorable .right_menu_button img {
@@ -5663,10 +5667,13 @@ body.waifuMode .zoomed_avatar {
border: none;
outline: none;
color: white;
- display: inline-block; /* Change display to inline-block */
- vertical-align: middle; /* Align to middle if there's a height discrepancy */
+ display: inline-block;
+ /* Change display to inline-block */
+ vertical-align: middle;
+ /* Align to middle if there's a height discrepancy */
width: 200px;
font-size: 16px;
z-index: 10;
- margin-left: 10px; /* Give some space between the button and search box */
+ margin-left: 10px;
+ /* Give some space between the button and search box */
}
diff --git a/server.js b/server.js
index 67f62a249..4dfa9d741 100644
--- a/server.js
+++ b/server.js
@@ -633,7 +633,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
resolve(data, isBinary);
});
});
- } catch(err) {
+ } catch (err) {
console.error("Socket error:", err);
websocket.close();
yield "[SillyTavern] Streaming failed:\n" + err;
@@ -3323,6 +3323,10 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
return sendScaleRequest(request, response_generate_openai);
}
+ if (request.body.use_ai21) {
+ return sendAI21Request(request, response_generate_openai);
+ }
+
let api_url;
let api_key_openai;
let headers;
@@ -3512,6 +3516,94 @@ app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_op
response_tokenize_openai.send({ "token_count": num_tokens });
});
+async function sendAI21Request(request, response) {
+ if (!request.body) return response.sendStatus(400);
+ const controller = new AbortController();
+ console.log(request.body.messages)
+ request.socket.removeAllListeners('close');
+ request.socket.on('close', function () {
+ controller.abort();
+ });
+ const options = {
+ method: 'POST',
+ headers: {
+ accept: 'application/json',
+ 'content-type': 'application/json',
+ Authorization: `Bearer ${readSecret(SECRET_KEYS.AI21)}`
+ },
+ body: JSON.stringify({
+ numResults: 1,
+ maxTokens: request.body.max_tokens,
+ minTokens: 0,
+ temperature: request.body.temperature,
+ topP: request.body.top_p,
+ stopSequences: request.body.stop_tokens,
+ topKReturn: request.body.top_k,
+ frequencyPenalty: {
+ scale: request.body.frequency_penalty * 100,
+ applyToWhitespaces: false,
+ applyToPunctuations: false,
+ applyToNumbers: false,
+ applyToStopwords: false,
+ applyToEmojis: false
+ },
+ presencePenalty: {
+ scale: request.body.presence_penalty,
+ applyToWhitespaces: false,
+ applyToPunctuations: false,
+ applyToNumbers: false,
+ applyToStopwords: false,
+ applyToEmojis: false
+ },
+ countPenalty: {
+ scale: request.body.count_pen,
+ applyToWhitespaces: false,
+ applyToPunctuations: false,
+ applyToNumbers: false,
+ applyToStopwords: false,
+ applyToEmojis: false
+ },
+ prompt: request.body.messages
+ }),
+ signal: controller.signal,
+ };
+
+ fetch(`https://api.ai21.com/studio/v1/${request.body.model}/complete`, options)
+ .then(r => r.json())
+ .then(r => {
+ if (r.completions === undefined) {
+ console.log(r)
+ } else {
+ console.log(r.completions[0].data.text)
+ }
+ const reply = { choices: [{ "message": { "content": r.completions[0].data.text, } }] };
+ return response.send(reply)
+ })
+ .catch(err => {
+ console.error(err)
+ return response.send({ error: true })
+ });
+
+}
+
+app.post("/tokenize_ai21", jsonParser, function (request, response_tokenize_ai21 = response) {
+ if (!request.body) return response_tokenize_ai21.sendStatus(400);
+ const options = {
+ method: 'POST',
+ headers: {
+ accept: 'application/json',
+ 'content-type': 'application/json',
+ Authorization: `Bearer ${readSecret(SECRET_KEYS.AI21)}`
+ },
+ body: JSON.stringify({ text: request.body[0].content })
+ };
+
+ fetch('https://api.ai21.com/studio/v1/tokenize', options)
+ .then(response => response.json())
+ .then(response => response_tokenize_ai21.send({ "token_count": response.tokens.length }))
+ .catch(err => console.error(err));
+});
+
app.post("/save_preset", jsonParser, function (request, response) {
const name = sanitize(request.body.name);
if (!request.body.preset || !name) {
@@ -3825,6 +3917,7 @@ const SECRET_KEYS = {
DEEPL: 'deepl',
OPENROUTER: 'api_key_openrouter',
SCALE: 'api_key_scale',
+ AI21: 'api_key_ai21'
}
function migrateSecrets() {