Merge branch 'staging' into parser-followup-2

This commit is contained in:
LenAnderson 2024-07-08 16:34:02 -04:00
commit 67dfe7354b
12 changed files with 1125 additions and 37 deletions

828
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1696,16 +1696,20 @@
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_none">
<input type="radio" id="continue_postfix_none" name="continue_postfix" value="0">
<span data-i18n="None">None</span>
</label>
<label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_space"> <label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_space">
<input type="radio" id="continue_postfix_space" name="continue_postfix" value="0"> <input type="radio" id="continue_postfix_space" name="continue_postfix" value="1">
<span data-i18n="Space">Space</span> <span data-i18n="Space">Space</span>
</label> </label>
<label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_newline"> <label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_newline">
<input type="radio" id="continue_postfix_newline" name="continue_postfix" value="1"> <input type="radio" id="continue_postfix_newline" name="continue_postfix" value="2">
<span data-i18n="Newline">Newline</span> <span data-i18n="Newline">Newline</span>
</label> </label>
<label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_double_newline"> <label class="checkbox_label flexWrap alignItemsCenter" for="continue_postfix_double_newline">
<input type="radio" id="continue_postfix_double_newline" name="continue_postfix" value="2"> <input type="radio" id="continue_postfix_double_newline" name="continue_postfix" value="3">
<span data-i18n="Double Newline">Double Newline</span> <span data-i18n="Double Newline">Double Newline</span>
</label> </label>
<!-- Hidden input for loading radio buttons from presets. Don't remove! --> <!-- Hidden input for loading radio buttons from presets. Don't remove! -->
@ -2348,6 +2352,16 @@
<small data-i18n="Example: 127.0.0.1:5000">Example: http://127.0.0.1:5000</small> <small data-i18n="Example: 127.0.0.1:5000">Example: http://127.0.0.1:5000</small>
<input id="tabby_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="tabby"> <input id="tabby_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="tabby">
</div> </div>
<div class="flex1">
<h4>
<span data-i18n="Tabby Model">Tabby Model</span>
</h4>
</h4>
<div id="tabby_download_model" class="menu_button menu_button_icon">
<i class="fa-solid fa-download"></i>
<span data-i18n="Download">Download</span>
</div>
</div>
</div> </div>
<div data-tg-type="koboldcpp"> <div data-tg-type="koboldcpp">
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
@ -2450,17 +2464,6 @@
<span data-i18n="Alternative server URL (leave empty to use the default value)."> <span data-i18n="Alternative server URL (leave empty to use the default value).">
Alternative server URL (leave empty to use the default value).<br> Alternative server URL (leave empty to use the default value).<br>
</span> </span>
<div id="ReverseProxyWarningMessage" class="reverse_proxy_warning">
<b data-i18n="Remove your real OAI API Key from the API panel BEFORE typing anything into this box">
Remove your real OAI API Key from the API panel BEFORE typing anything
into this box.
</b>
<hr>
<b data-i18n="We cannot provide support for problems encountered while using an unofficial OpenAI proxy">
We cannot provide support for problems encountered while using an
unofficial OpenAI proxy.
</b>
</div>
</div> </div>
<div class="wide100p"> <div class="wide100p">
<input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" maxlength="5000" /> <input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" maxlength="5000" />
@ -2482,6 +2485,20 @@
</div> </div>
</div> </div>
</div> </div>
<div id="ReverseProxyWarningMessage" class="reverse_proxy_warning">
<b>
<div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
Using a proxy that you're not running yourself is a risk to your data privacy.
</div>
<div data-i18n="ANY support requests will be REFUSED if you are using a proxy.">
ANY support requests will be REFUSED if you are using a proxy.
</div>
<hr>
<i data-i18n="Do not proceed if you do not agree to this!">
Do not proceed if you do not agree to this!
</i>
</b>
</div>
<form id="openai_form" data-source="openai" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="openai_form" data-source="openai" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<h4><span data-i18n="OpenAI API key">OpenAI API key</span></h4> <h4><span data-i18n="OpenAI API key">OpenAI API key</span></h4>
<div> <div>
@ -2837,6 +2854,7 @@
<option value="llama3-70b-8192">llama3-70b-8192</option> <option value="llama3-70b-8192">llama3-70b-8192</option>
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option> <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<option value="gemma-7b-it">gemma-7b-it</option> <option value="gemma-7b-it">gemma-7b-it</option>
<option value="gemma2-9b-it">gemma2-9b-it</option>
</select> </select>
</div> </div>
<div id="perplexity_form" data-source="perplexity"> <div id="perplexity_form" data-source="perplexity">

View File

@ -155,6 +155,7 @@ import {
ensureImageFormatSupported, ensureImageFormatSupported,
flashHighlight, flashHighlight,
isTrueBoolean, isTrueBoolean,
debouncedThrottle,
} from './scripts/utils.js'; } from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js'; import { debounce_timeout } from './scripts/constants.js';
@ -9236,7 +9237,7 @@ jQuery(async function () {
e.style.height = `${e.scrollHeight + 4}px`; e.style.height = `${e.scrollHeight + 4}px`;
is_use_scroll_holder = true; is_use_scroll_holder = true;
} }
const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); const autoFitEditTextAreaDebounced = debouncedThrottle(autoFitEditTextArea, debounce_timeout.standard);
document.addEventListener('input', e => { document.addEventListener('input', e => {
if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) {
const immediately = e.target.scrollHeight > e.target.offsetHeight || e.target.value === ''; const immediately = e.target.scrollHeight > e.target.offsetHeight || e.target.value === '';

View File

@ -191,6 +191,7 @@ const character_names_behavior = {
}; };
const continue_postfix_types = { const continue_postfix_types = {
NONE: '',
SPACE: ' ', SPACE: ' ',
NEWLINE: '\n', NEWLINE: '\n',
DOUBLE_NEWLINE: '\n\n', DOUBLE_NEWLINE: '\n\n',
@ -3138,6 +3139,9 @@ function setNamesBehaviorControls() {
function setContinuePostfixControls() { function setContinuePostfixControls() {
switch (oai_settings.continue_postfix) { switch (oai_settings.continue_postfix) {
case continue_postfix_types.NONE:
$('#continue_postfix_none').prop('checked', true);
break;
case continue_postfix_types.SPACE: case continue_postfix_types.SPACE:
$('#continue_postfix_space').prop('checked', true); $('#continue_postfix_space').prop('checked', true);
break; break;
@ -4112,7 +4116,7 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) { if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max); $('#openai_max_context').attr('max', unlocked_max);
} }
else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it'].includes(oai_settings.groq_model)) { else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
$('#openai_max_context').attr('max', max_8k); $('#openai_max_context').attr('max', max_8k);
} }
else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) { else if (['mixtral-8x7b-32768'].includes(oai_settings.groq_model)) {
@ -5078,6 +5082,12 @@ $(document).ready(async function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#continue_postfix_none').on('input', function () {
oai_settings.continue_postfix = continue_postfix_types.NONE;
setContinuePostfixControls();
saveSettingsDebounced();
});
$('#continue_postfix_space').on('input', function () { $('#continue_postfix_space').on('input', function () {
oai_settings.continue_postfix = continue_postfix_types.SPACE; oai_settings.continue_postfix = continue_postfix_types.SPACE;
setContinuePostfixControls(); setContinuePostfixControls();

View File

@ -332,6 +332,16 @@ export function initDefaultSlashCommands() {
name: 'continue', name: 'continue',
callback: continueChatCallback, callback: continueChatCallback,
aliases: ['cont'], aliases: ['cont'],
namedArgumentList: [
new SlashCommandNamedArgument(
'await',
'Whether to await for the continued generation before proceeding',
[ARGUMENT_TYPE.BOOLEAN],
false,
false,
'false',
),
],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(
'prompt', [ARGUMENT_TYPE.STRING], false, 'prompt', [ARGUMENT_TYPE.STRING], false,
@ -341,16 +351,19 @@ export function initDefaultSlashCommands() {
<div> <div>
Continues the last message in the chat, with an optional additional prompt. Continues the last message in the chat, with an optional additional prompt.
</div> </div>
<div>
If <code>await=true</code> named argument is passed, the command will await for the continued generation before proceeding.
</div>
<div> <div>
<strong>Example:</strong> <strong>Example:</strong>
<ul> <ul>
<li> <li>
<pre><code>/continue</code></pre> <pre><code>/continue</code></pre>
Continues the chat with no additional prompt. Continues the chat with no additional prompt and immediately proceeds to the next command.
</li> </li>
<li> <li>
<pre><code>/continue Let's explore this further...</code></pre> <pre><code>/continue await=true Let's explore this further...</code></pre>
Continues the chat with the provided prompt. Continues the chat with the provided prompt and waits for the generation to finish.
</li> </li>
</ul> </ul>
</div> </div>
@ -2635,19 +2648,35 @@ async function openChat(id) {
await reloadCurrentChat(); await reloadCurrentChat();
} }
function continueChatCallback(_, prompt) { async function continueChatCallback(args, prompt) {
setTimeout(async () => { const shouldAwait = isTrueBoolean(args?.await);
const outerPromise = new Promise(async (resolve, reject) => {
try { try {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100); await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch { } catch {
console.warn('Timeout waiting for generation unlock'); console.warn('Timeout waiting for generation unlock');
toastr.warning('Cannot run /continue command while the reply is being generated.'); toastr.warning('Cannot run /continue command while the reply is being generated.');
return reject();
} }
// Prevent infinite recursion try {
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); // Prevent infinite recursion
$('#option_continue').trigger('click', { fromSlashCommand: true, additionalPrompt: prompt }); $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
}, 1);
const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
await Generate('continue', options);
resolve();
} catch (error) {
console.error('Error running /continue command:', error);
reject();
}
});
if (shouldAwait) {
await outerPromise;
}
return ''; return '';
} }

View File

@ -9,24 +9,21 @@ import {
eventSource, eventSource,
event_types, event_types,
DEFAULT_PRINT_TIMEOUT, DEFAULT_PRINT_TIMEOUT,
substituteParams,
printCharacters, printCharacters,
} from '../script.js'; } from '../script.js';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js'; import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js'; import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';
import { power_user } from './power-user.js'; import { power_user } from './power-user.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { debounce_timeout } from './constants.js'; import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js'; import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { renderTemplateAsync } from './templates.js'; import { renderTemplateAsync } from './templates.js';
@ -53,6 +50,7 @@ export {
removeTagFromMap, removeTagFromMap,
}; };
/** @typedef {import('../scripts/popup.js').Popup} Popup */
/** @typedef {import('../script.js').Character} Character */ /** @typedef {import('../script.js').Character} Character */
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@ -717,7 +715,7 @@ async function importTags(character, { forceShow = false } = {}) {
// Gather the tags to import based on the selected setting // Gather the tags to import based on the selected setting
const tagNamesToImport = await handleTagImport(character, { forceShow }); const tagNamesToImport = await handleTagImport(character, { forceShow });
if (!tagNamesToImport?.length) { if (!tagNamesToImport?.length) {
toastr.info('No tags to import', 'Importing Tags'); console.debug('No tags to import');
return; return;
} }
@ -815,7 +813,7 @@ async function showTagImportPopup(character, existingTags, newTags, folderTags)
wider: true, okButton: 'Import', cancelButton: true, wider: true, okButton: 'Import', cancelButton: true,
customButtons: Object.values(importButtons), customButtons: Object.values(importButtons),
customInputs: [{ id: 'import_remember_option', label: 'Remember my choice', tooltip: 'Remember the chosen import option\nIf anything besides \'Cancel\' is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify "Tag Import Option".\n\nIf the "Import" option is chosen, the global setting will stay on "Ask".' }], customInputs: [{ id: 'import_remember_option', label: 'Remember my choice', tooltip: 'Remember the chosen import option\nIf anything besides \'Cancel\' is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify "Tag Import Option".\n\nIf the "Import" option is chosen, the global setting will stay on "Ask".' }],
onClose: onCloseRemember onClose: onCloseRemember,
}); });
if (!result) { if (!result) {
return []; return [];

View File

@ -0,0 +1,61 @@
<div id="tabby_downloader_popup">
<div>
<h3><strong data-i18n="">Download Model</strong>
<a href="https://github.com/theroyallab/async-hf-downloader" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
</h3>
<small class="flex-container extensions_info justifyCenter">
Download a HuggingFace model with TabbyAPI
</small>
<small class="flex-container extensions_info justifyCenter">
(Requires an admin key)
</small>
<hr />
<!-- Model parameter textboxes -->
Repo ID
<div class="flex-container">
<input name="hf_repo_id" class="text_pole" type="text" placeholder="Ex. turboderp/Llama-3-8B-exl2" />
</div>
<div class="range-block-title justifyCenter">
<span data-i18n="Downloader Options">Downloader Options</span>
<div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]Extra parameters for downloading/HuggingFace API" title="Extra parameters for downloading/HuggingFace API.&#13;If unsure, leave these blank."></div>
</div>
<div class="flex-container">
<div class="flex1">
<label for="revision">
<small data-i18n="Revision">Revision</small>
</label>
<input name="revision" class="text_pole" type="text" placeholder="Ex. 6.0bpw" />
</div>
<div class="flex1">
<label for="folder_name">
<small data-i18n="Folder Name">Output Folder Name</small>
</label>
<input name="folder_name" class="text_pole" type="text" />
</div>
<div class="flex1">
<label for="hf_token">
<small data-i18n="HF Token">HF Token</small>
</label>
<input name="hf_token" class="text_pole" type="text" placeholder="For gated models" />
</div>
</div>
<div class="range-block-title justifyCenter">
<span data-i18n="Include Patterns">Include Patterns</span>
<div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]Glob patterns of files to include in the download." title="Glob patterns of files to include in the download.&#13;Separate each pattern by a newline."></div>
</div>
<div class="flex-container">
<textarea class="text_pole textarea_compact" name="tabby_download_include" placeholder="Ex. *.txt"></textarea>
</div>
<div class="range-block-title justifyCenter">
<span data-i18n="Exclude Patterns">Exclude Patterns</span>
<div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]Glob patterns of files to exclude in the download." title="Glob patterns of files to exclude in the download.&#13;Separate each pattern by a newline."></div>
</div>
<div class="flex-container">
<textarea class="text_pole textarea_compact" name="tabby_download_exclude" placeholder="Ex. *.txt"></textarea>
</div>
</div>
</div>

View File

@ -1,7 +1,9 @@
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, setGenerationParamsFromPreset } from '../script.js'; import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, online_status, setGenerationParamsFromPreset } from '../script.js';
import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js'; import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';
import { tokenizers } from './tokenizers.js'; import { tokenizers } from './tokenizers.js';
import { renderTemplateAsync } from './templates.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js';
let mancerModels = []; let mancerModels = [];
let togetherModels = []; let togetherModels = [];
@ -470,6 +472,74 @@ async function downloadOllamaModel() {
} }
} }
async function downloadTabbyModel() {
try {
const serverUrl = textgen_settings.server_urls[textgen_types.TABBY];
if (online_status === 'no_connection' || !serverUrl) {
toastr.info('Please connect to a TabbyAPI server first.');
return;
}
const downloadHtml = $(await renderTemplateAsync('tabbyDownloader'));
const popupResult = await callGenericPopup(downloadHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Download', cancelButton: 'Cancel' });
// User cancelled the download
if (!popupResult) {
return;
}
const repoId = downloadHtml.find('input[name="hf_repo_id"]').val().toString();
if (!repoId) {
toastr.error('A HuggingFace repo ID must be provided. Skipping Download.');
return;
}
if (repoId.split('/').length !== 2) {
toastr.error('A HuggingFace repo ID must be formatted as Author/Name. Please try again.');
return;
}
const params = {
repo_id: repoId,
folder_name: downloadHtml.find('input[name="folder_name"]').val() || undefined,
revision: downloadHtml.find('input[name="revision"]').val() || undefined,
token: downloadHtml.find('input[name="hf_token"]').val() || undefined,
};
for (const suffix of ['include', 'exclude']) {
const patterns = downloadHtml.find(`textarea[name="tabby_download_${suffix}"]`).val().toString();
if (patterns) {
params[suffix] = patterns.split('\n');
}
}
// Params for the server side of ST
params['api_server'] = serverUrl;
params['api_type'] = textgen_settings.type;
toastr.info('Downloading. Check the Tabby console for progress reports.');
const response = await fetch('/api/backends/text-completions/tabby/download', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(params),
});
if (response.status === 403) {
toastr.error('The provided key has invalid permissions. Please use an admin key for downloading.');
return;
} else if (!response.ok) {
throw new Error(response.statusText);
}
toastr.success('Download complete.');
} catch (err) {
console.error(err);
toastr.error('Failed to download HuggingFace model in TabbyAPI. Please try again.');
}
}
function calculateOpenRouterCost() { function calculateOpenRouterCost() {
if (textgen_settings.type !== textgen_types.OPENROUTER) { if (textgen_settings.type !== textgen_types.OPENROUTER) {
return; return;
@ -538,6 +608,7 @@ jQuery(function () {
$('#vllm_model').on('change', onVllmModelSelect); $('#vllm_model').on('change', onVllmModelSelect);
$('#aphrodite_model').on('change', onAphroditeModelSelect); $('#aphrodite_model').on('change', onAphroditeModelSelect);
$('#featherless_model').on('change', onFeatherlessModelSelect); $('#featherless_model').on('change', onFeatherlessModelSelect);
$('#tabby_download_model').on('click', downloadTabbyModel);
const providersSelect = $('.openrouter_providers'); const providersSelect = $('.openrouter_providers');
for (const provider of OPENROUTER_PROVIDERS) { for (const provider of OPENROUTER_PROVIDERS) {

View File

@ -301,6 +301,32 @@ export function throttle(func, limit = 300) {
}; };
} }
/**
* Creates a debounced throttle function that only invokes func at most once per every limit milliseconds.
* @param {function} func The function to throttle.
* @param {number} [limit=300] The limit in milliseconds.
* @returns {function} The throttled function.
*/
export function debouncedThrottle(func, limit = 300) {
let last, deferTimer;
let db = debounce(func);
return function() {
let now = +new Date, args = arguments;
if(!last || (last && now < last + limit)) {
clearTimeout(deferTimer);
db.apply(this, args);
deferTimer = setTimeout(function() {
last = now;
func.apply(this, args);
}, limit);
} else {
last = now;
func.apply(this, args);
}
};
}
/** /**
* Checks if an element is in the viewport. * Checks if an element is in the viewport.
* @param {Element} el The element to check. * @param {Element} el The element to check.

View File

@ -138,6 +138,7 @@ body {
/*fallback for JS load*/ /*fallback for JS load*/
height: 100vh; height: 100vh;
height: 100svh; height: 100svh;
height: 100dvh;
/*defaults as 100%, then reassigned via JS as pixels, will work on PC and Android*/ /*defaults as 100%, then reassigned via JS as pixels, will work on PC and Android*/
/*height: calc(var(--doc-height) - 1px);*/ /*height: calc(var(--doc-height) - 1px);*/
background-color: var(--greyCAIbg); background-color: var(--greyCAIbg);

View File

@ -588,7 +588,53 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
} }
}); });
const tabby = express.Router();
tabby.post('/download', jsonParser, async function (request, response) {
try {
const baseUrl = String(request.body.api_server).replace(/\/$/, '');
const args = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request.body),
timeout: 0,
};
setAdditionalHeaders(request, args, baseUrl);
// Check key permissions
const permissionResponse = await fetch(`${baseUrl}/v1/auth/permission`, {
headers: args.headers,
});
if (permissionResponse.ok) {
const permissionJson = await permissionResponse.json();
if (permissionJson['permission'] !== 'admin') {
return response.status(403).send({ error: true });
}
} else {
console.log('API Permission error:', permissionResponse.status, permissionResponse.statusText);
return response.status(permissionResponse.status).send({ error: true });
}
const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
if (!fetchResponse.ok) {
console.log('Download error:', fetchResponse.status, fetchResponse.statusText);
return response.status(fetchResponse.status).send({ error: true });
}
return response.send({ ok: true });
} catch (error) {
console.error(error);
return response.status(500);
}
});
router.use('/ollama', ollama); router.use('/ollama', ollama);
router.use('/llamacpp', llamacpp); router.use('/llamacpp', llamacpp);
router.use('/tabby', tabby);
module.exports = { router }; module.exports = { router };

View File

@ -94,7 +94,11 @@ function getModelForTask(task) {
*/ */
async function getPipeline(task, forceModel = '') { async function getPipeline(task, forceModel = '') {
if (tasks[task].pipeline) { if (tasks[task].pipeline) {
return tasks[task].pipeline; if (forceModel === '' || tasks[task].currentModel === forceModel) {
return tasks[task].pipeline;
}
console.log('Disposing transformers.js pipeline for for task', task, 'with model', tasks[task].currentModel);
await tasks[task].pipeline.dispose();
} }
const cache_dir = path.join(process.cwd(), 'cache'); const cache_dir = path.join(process.cwd(), 'cache');
@ -103,6 +107,7 @@ async function getPipeline(task, forceModel = '') {
console.log('Initializing transformers.js pipeline for task', task, 'with model', model); console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
const instance = await pipeline(task, model, { cache_dir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly }); const instance = await pipeline(task, model, { cache_dir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
tasks[task].pipeline = instance; tasks[task].pipeline = instance;
tasks[task].currentModel = model;
return instance; return instance;
} }