[feature] Add info/plugin handler

This commit is contained in:
Sergey Konovalov
2025-05-28 18:21:46 +03:00
parent 15338bb458
commit 875350bb9d
561 changed files with 84969 additions and 166 deletions

View File

@ -5,99 +5,19 @@
"port": "8125",
"prefix": "ds."
},
"ai-api": {
"providers": [
{
"enable": true,
"name": "OpenAI",
"url": "https://api.openai.com",
"key": "",
"models": []
},
{
"enable": true,
"name": "Google Gemini",
"url": "https://generativelanguage.googleapis.com",
"key": "",
"models": []
},
{
"enable": true,
"name": "Anthropic",
"url": "https://api.anthropic.com",
"key": "",
"models": []
},
{
"enable": true,
"name": "Azure OpenAI",
"url": "https://{region}.api.cognitive.microsoft.com",
"key": "",
"models": []
},
{
"enable": true,
"name": "Cohere",
"url": "https://api.cohere.ai",
"key": "",
"models": []
},
{
"enable": true,
"name": "Mistral AI",
"url": "https://api.mistral.ai",
"key": "",
"models": []
},
{
"enable": true,
"name": "Aleph Alpha",
"url": "https://api.aleph-alpha.com",
"key": "",
"models": []
},
{
"enable": true,
"name": "Stability AI",
"url": "https://api.stability.ai",
"key": "",
"models": [
"Stable Diffusion",
"Stable Image Core",
"Stable Image Ultra"
]
}
],
"aiSettings": {
"actions": {
"Chat": {
"name": "Ask AI",
"icon": "ask-ai",
"model": "llama-3.2-90b-vision-preview",
"capabilities": 1
},
"Summarization": {
"name": "Summarization",
"icon": "summarization",
"model": "llama3.2:latest",
"capabilities": 1
},
"Translation": {
"name": "Translation",
"icon": "translation",
"model": "gemini-1.5-pro-latest",
"capabilities": 1
},
"TextAnalyze": {
"name": "Text analysis",
"icon": "",
"model": "claude-3-sonnet-20240229",
"capabilities": 1
}
},
"models": [
],
"providers": {
},
"version": 3,
"timeout": "30s",
"allowedCorsOrigins": [
"https://onlyoffice.github.io"
],
"pluginDir" : "../branding/info/ai",
"cache": {
"enable": true,
"ttl": 300,

View File

@ -41,7 +41,10 @@ const path = require('path');
const vm = require('vm');
// Configuration constants
const cfgAiApiTimeout = config.get('ai-api.timeout');
const cfgAiApiTimeout = config.get('aiSettings.timeout');
const cfgAiApiModels = config.get('aiSettings.models');
const cfgAiApiActions = config.get('aiSettings.actions');
const cfgAiPluginDir = config.get('aiSettings.pluginDir');
function setCtx(ctx) {
sandbox.ctx = ctx;
@ -53,6 +56,14 @@ function setCtx(ctx) {
const sandbox = {
ctx: null,
window: {AI: {}},
Asc: {
plugin: {
tr: function(text) {
// Just return the original text in the stub
return text;
}
}
},
/**
* Implementation of fetch that delegates to utils.httpRequest
@ -98,7 +109,7 @@ const sandbox = {
};
// Initialize minimal AI object with required functionality
sandbox.AI = sandbox.window.AI;
const AI = sandbox.AI = sandbox.window.AI;
setCtx(operationContext.global);
/**
@ -106,7 +117,7 @@ setCtx(operationContext.global);
*/
function loadInternalProviders() {
// Add simple provider loading logic
const enginePath = path.join(__dirname, 'engine', 'providers', 'internal');
const enginePath = path.join(cfgAiPluginDir, 'scripts/engine/providers/internal');
try {
// Read providers directory
@ -138,21 +149,97 @@ function loadInternalProviders() {
sandbox.ctx.logger.error('Error loading internal providers:', error);
}
}
/**
* Simple loadInternalProviders implementation
*/
function fillConfigObjects() {
AI.Models = cfgAiApiModels;
for (let i in cfgAiApiActions)
{
if (AI.Actions[i] && cfgAiApiActions[i].model) {
AI.Actions[i].model = cfgAiApiActions[i].model;
}
}
}
// Load engine.js
let engineCode = '';
engineCode += fs.readFileSync(path.join(__dirname, 'engine', 'storage.js'), 'utf8');
engineCode += fs.readFileSync(path.join(__dirname, 'engine', 'local_storage.js'), 'utf8');
engineCode += fs.readFileSync(path.join(__dirname, 'engine', 'providers', 'base.js'), 'utf8');
engineCode += fs.readFileSync(path.join(__dirname, 'engine', 'providers', 'provider.js'), 'utf8');
engineCode += fs.readFileSync(path.join(__dirname, 'engine', 'engine.js'), 'utf8');
engineCode += fs.readFileSync(path.join(cfgAiPluginDir, 'scripts/engine/storage.js'), 'utf8');
engineCode += fs.readFileSync(path.join(cfgAiPluginDir, 'scripts/engine/local_storage.js'), 'utf8');
engineCode += fs.readFileSync(path.join(cfgAiPluginDir, 'scripts/engine/providers/base.js'), 'utf8');
engineCode += fs.readFileSync(path.join(cfgAiPluginDir, 'scripts/engine/providers/provider.js'), 'utf8');
engineCode += fs.readFileSync(path.join(cfgAiPluginDir, 'scripts/engine/engine.js'), 'utf8');
vm.runInNewContext(engineCode, sandbox);
//start from engine/register.js
(function() {
const AI = sandbox.AI;
const Asc = sandbox.Asc;
AI.ActionType = {
Chat : "Chat",
Summarization : "Summarization",
Translation : "Translation",
TextAnalyze : "TextAnalyze",
ImageGeneration : "ImageGeneration",
OCR : "OCR",
Vision : "Vision"
};
AI.Actions = {};
function ActionUI(name, icon, modelId, capabilities) {
this.name = name || "";
this.icon = icon || "";
this.model = modelId || "";
this.capabilities = (capabilities === undefined) ? AI.CapabilitiesUI.Chat : capabilities;
}
AI.Actions[AI.ActionType.Chat] = new ActionUI("Chatbot", "ask-ai");
AI.Actions[AI.ActionType.Summarization] = new ActionUI("Summarization", "summarization");
AI.Actions[AI.ActionType.Translation] = new ActionUI("Translation", "translation");
AI.Actions[AI.ActionType.TextAnalyze] = new ActionUI("Text analysis", "text-analysis-ai");
AI.Actions[AI.ActionType.ImageGeneration] = new ActionUI("Image generation", "image-ai", "", AI.CapabilitiesUI.Image);
AI.Actions[AI.ActionType.OCR] = new ActionUI("OCR", "text-analysis-ai", "", AI.CapabilitiesUI.Vision);
AI.Actions[AI.ActionType.Vision] = new ActionUI("Vision", "vision-ai", "", AI.CapabilitiesUI.Vision);
AI.ActionsGetKeys = function()
{
return [
AI.ActionType.Chat,
AI.ActionType.Summarization,
AI.ActionType.Translation,
AI.ActionType.TextAnalyze,
AI.ActionType.ImageGeneration,
AI.ActionType.OCR,
AI.ActionType.Vision
];
};
AI.ActionsGetSorted = function()
{
let keys = AI.ActionsGetKeys();
let count = keys.length;
let actions = new Array(count);
for (let i = 0; i < count; i++)
{
let src = AI.Actions[keys[i]];
actions[i] = {
id : keys[i],
name : Asc.plugin.tr(src.name),
icon : src.icon,
model : src.model,
capabilities : src.capabilities
}
}
return actions;
};
//end from engine/register.js
})();
sandbox.AI.loadInternalProviders = loadInternalProviders;
loadInternalProviders();
fillConfigObjects();
exports.setCtx = setCtx;
exports.AI = sandbox.AI;

View File

@ -43,9 +43,9 @@ const docsCoServer = require('./../DocsCoServer');
// Import the new aiEngine module
const aiEngine = require('./aiEngineWrapper');
const cfgAiApiAllowedOrigins = config.get('ai-api.allowedCorsOrigins');
const cfgAiApiTimeout = config.get('ai-api.timeout');
const cfgAiApiCache = config.get('ai-api.cache');
const cfgAiApiAllowedOrigins = config.get('aiSettings.allowedCorsOrigins');
const cfgAiApiTimeout = config.get('aiSettings.timeout');
const cfgAiApiCache = config.get('aiSettings.cache');
const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser');
const AI = aiEngine.AI;
@ -216,41 +216,6 @@ async function proxyRequest(req, res) {
}
}
/**
* Process AI actions from configuration
*
* @param {Object} ctx - Operation context
* @param {Object} actions - The actions from configuration
* @returns {Object} Processed actions object
*/
function processActions(ctx, actions) {
const logger = ctx.logger;
if (!actions || typeof actions !== 'object') {
return {};
}
try {
const processedActions = Object.entries(actions).reduce((acc, [key, value]) => {
if (value) {
acc[key] = {
name: value.name || key,
icon: value.icon || '',
model: value.model || '',
capabilities: Array.isArray(value.capabilities) ? value.capabilities : []
};
}
return acc;
}, {});
logger.info(`Processed ${Object.keys(processedActions).length} AI actions`);
return processedActions;
} catch (error) {
logger.error('Error processing AI actions:', error);
return {};
}
}
/**
* Process a single AI provider and its models
*
@ -261,17 +226,17 @@ function processActions(ctx, actions) {
async function processProvider(ctx, provider) {
const logger = ctx.logger;
if (!provider.url || !provider.key) {
if (!provider.url) {
return null;
}
let engineModels = [];
let engineModelsUI = [];
try {
if (provider.url && provider.key) {
// Call getModels from engine.js
if (provider.key) {
AI.Providers[provider.name].key = provider.key;
// Call getModels from engine.js
aiEngine.setCtx(ctx);
const result = await AI.getModels(provider);
await AI.getModels(provider);
// Process result
if (AI.TmpProviderForModels.models) {
engineModels = AI.TmpProviderForModels.models;
@ -314,41 +279,61 @@ async function getPluginSettings(ctx) {
};
try {
// Get AI API configuration
const aiApi = config.get('ai-api');
const aiApi = config.get('aiSettings');
// Process providers and their models if configuration exists
if (aiApi?.providers && Array.isArray(aiApi.providers)) {
if (aiApi?.providers && typeof aiApi.providers === 'object') {
// Create an array of promises for each provider
const providerPromises = aiApi.providers
.filter(provider => provider.enable !== false || !provider.key || !provider.url)
.map(provider => processProvider(ctx, provider));
// const providerPromises = aiApi.providers
// .filter(provider => provider.enable !== false || !provider.key || !provider.url)
// .map(provider => processProvider(ctx, provider));
try {
let providers = await Promise.allSettled(providerPromises);
providers = providers.filter(provider => provider.status === 'fulfilled' && provider.value && provider.value.name && provider.value.models?.length > 0);
// try {
// let providers = await Promise.allSettled(providerPromises);
// // providers = providers.filter(provider => provider.status === 'fulfilled' && provider.value && provider.value.name && provider.value.models?.length > 0);
// providers = providers.filter(provider => provider.status === 'fulfilled' && provider.value && provider.value.name);
const providerCount = providers.length;
let totalModels = 0;
// Convert providers array to object by provider name
result.providers = {};
for(let i = 0; i < providers.length; i++) {
const provider = providers[i].value;
totalModels += provider.models.length;
result.models.push(...provider.modelsUI);
delete provider.modelsUI;//todo remove
// const providerCount = providers.length;
// let totalModels = 0;
// // Convert providers array to object by provider name
// result.providers = {};
// for(let i = 0; i < providers.length; i++) {
// const provider = providers[i].value;
// totalModels += provider.models.length;
// // result.models.push(...provider.modelsUI);
// delete provider.modelsUI;//todo remove
// //result.providers[provider.name] = provider;
// }
// logger.info(`Successfully processed ${providerCount} providers with a total of ${totalModels} models`);
// } catch (error) {
// logger.error('Error resolving provider promises:', error);
// }
if (true) {
const providers = AI.serializeProviders();
for (let i = 0; i < providers.length; i++) {
const provider = providers[i];
const cfgProvider = aiApi.providers[provider.name];
if (cfgProvider) {
//todo clone
provider.key = cfgProvider.key;
}
result.providers[provider.name] = provider;
}
logger.info(`Successfully processed ${providerCount} providers with a total of ${totalModels} models`);
} catch (error) {
logger.error('Error resolving provider promises:', error);
}
}
// Process AI actions
if (aiApi?.models && typeof aiApi.models === 'object') {
// result.actions = aiApi.actions;
result.models = AI.Storage.serializeModels();
}
// Process AI actions
if (aiApi?.actions && typeof aiApi.actions === 'object') {
result.actions = processActions(ctx, aiApi.actions);
// result.actions = aiApi.actions;
result.actions = AI.ActionsGetSorted();
}
nodeCache.set(ctx.tenant, result);
result.version = aiApi.version;
// nodeCache.set(ctx.tenant, result);
} catch (error) {
logger.error('Error retrieving AI models from config:', error);
}
@ -358,7 +343,36 @@ async function getPluginSettings(ctx) {
return result;
}
async function requestSettings(req, res) {
const ctx = new operationContext.Context();
ctx.initFromRequest(req);
try {
await ctx.initTenantCache();
const result = await getPluginSettings(ctx);
res.json(result);
} catch (error) {
ctx.logger.error('getSettings error: %s', error.stack);
res.sendStatus(400);
}
}
async function requestModels(req, res) {
const ctx = new operationContext.Context();
ctx.initFromRequest(req);
try {
await ctx.initTenantCache();
let body = JSON.parse(req.body);
let models = await AI.getModels(body);
res.json(models);
} catch (error) {
ctx.logger.error('getModels error: %s', error.stack);
res.sendStatus(400);
}
}
module.exports = {
proxyRequest,
getPluginSettings
getPluginSettings,
requestSettings,
requestModels
};

View File

@ -37,6 +37,7 @@ const express = require('express');
const bodyParser = require('body-parser');
const tenantManager = require('../../../Common/sources/tenantManager');
const operationContext = require('../../../Common/sources/operationContext');
const aiProxyHandler = require('../ai/aiProxyHandler');
const router = express.Router();
@ -49,17 +50,32 @@ router.get('/', async (req, res) => {
try {
ctx.initFromRequest(req);
await ctx.initTenantCache();
ctx.logger.debug('config get start');
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
//todo
}
let configPath = path.join(process.env.NODE_CONFIG_DIR, 'local.json');
result = await readFile(configPath);
try {
result = await readFile(configPath, {encoding: 'utf8'});
} catch (e) {
ctx.logger.debug('config get error: %s', e.stack);
}
//todo get default setting in separate request
let config = JSON.parse(result);
if (!(config.aiSettings && Object.keys(config.aiSettings?.actions).length > 0)) {
let pluginSettings = await aiProxyHandler.getPluginSettings(ctx);
config.aiSettings = pluginSettings;
result = JSON.stringify(config);
}
} catch (error) {
ctx.logger.error('baseurl error: %s', error.stack);
ctx.logger.error('config get error: %s', error.stack);
}
finally {
res.setHeader('Content-Type', 'application/json');
res.send(result);
ctx.logger.debug('config end');
}
});

View File

@ -238,6 +238,8 @@ docsCoServer.install(server, () => {
});
app.get('/info/info.json', utils.checkClientIp, docsCoServer.licenseInfo);
app.use('/info/config', utils.checkClientIp, configRouter);
app.get('/info/plugin/settings', utils.checkClientIp, aiProxyHandler.requestSettings);
app.post('/info/plugin/models', utils.checkClientIp, rawFileParser, aiProxyHandler.requestModels);
app.put('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
app.delete('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
app.get('/internal/connections/edit', docsCoServer.getEditorConnectionsCount);

View File

@ -0,0 +1,91 @@
# Change Log
## 1.0.0
* Initial release.
## 1.0.1
* Add new model.
## 1.0.2
* Change plugin structure. Now it's non visual plugin and it can work by context menu.
* Add new model for chat variation.
## 1.1.0
* Commont improvements.
## 1.1.1
* Add new languages for translating and add translations for it.
* Change plugin type from "system" to "background".
* Increase minimal editor version to "7.5.0".
* Change type for chat window (now it's not a modal window).
## 1.1.2
* Disable plugin for IE.
## 1.1.3
* Add notification, that service isn't enable in user region (if we can't load list of models).
## 1.1.4
* Change list of models for custom requests (add new models and remove old).
* Change default model for request by context menu (now we use gpt-3.5-turbo-16k model for chat and gpt-4 for other request).
* Add new functions: "Fix spelling & grammar", "Rewrite differently", "Make longer", "Make shorter", "Make simpler" (it has restriction by gpt-4 model: 8k tokens)
## 2.0.0
* The plugin has been completely redesigned.
## 2.1.0
* Bug fix.
## 2.1.1
* Bug fix.
## 2.1.2
* Fix add provider.
## 2.1.3
* Bug fix. Remove v1 suffix for endpoints.
## 2.1.4
* Add proxy for together.ai.
* Add Groc as internal provider.
* Bug fix
## 2.1.5
* Bug fix
## 2.2.0
* Refactoring
## 2.2.1
* Bug fix
## 2.2.2
* Add xAI as internal provider.
## 2.2.3
* Fix translations.
## 2.2.4
* Refactoring chat. Add docked mode for chat window.
## 2.2.5
* Bug fix
## 2.2.6
* Add interface for system role detection.
* Fixed the work of providers antropic and gemini when working with a system role.
* Fix bug with custom functions in spreadsheets editor.
## 2.2.7
* Add image actions.
* Add Stability AI provider.
## 2.2.8
* Fix image actions. Add "OCR" and "Image to text" support.

View File

@ -0,0 +1,114 @@
<!--
(c) Copyright Ascensio System SIA 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Edit action</title>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
<script type="text/javascript" src="scripts/engine/providers/base.js"></script>
<script type="text/javascript" src="components/Tooltip/script.js"></script>
<script type="text/javascript" src="../js/plugin-stub.js"></script>
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
<script src="vendor/select2-4.0.6-rc.1/dist/js/select2.js"></script>
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
<link rel="stylesheet" href="vendor/select2-4.0.6-rc.1/dist/css/select2.css"/>
<link rel="stylesheet" href="./resources/styles/common.css">
<link rel="stylesheet" href="./resources/styles/aiModelEdit.css">
<link rel="stylesheet" href="components/Tooltip/style.css">
</head>
<body class="noselect">
<div class="section">
<div class="form-vertical-item">
<div class="section-label">
<label class="d-flex-center">
<span class="i18n">Model name</span>
<span class="required-mark">*</span>
</label>
</div>
<input id="name-input" type="text" class="form-control" maxlength="80"/>
</div>
</div>
<div class="section">
<div class="section-label" id="provider-section-label">
<label class="i18n">Provider</label>
</div>
<div class="form-horizontal-item">
<label class="d-flex-center">
<span class="i18n">Name</span>
<span class="required-mark">*</span>
</label>
<select id="provider-name-cmb" class="form-control"></select>
</div>
<div class="form-horizontal-item">
<label class="d-flex-center">
<span class="i18n">URL</span>
<span class="required-mark">*</span>
</label>
<input id="provider-url-input" type="text" class="form-control"/>
</div>
<div class="form-horizontal-item">
<label class="i18n">Key</label>
<input id="provider-key-input" type="text" class="form-control"/>
</div>
</div>
<div class="section">
<div class="form-vertical-item">
<div class="section-label">
<label class="d-flex-center">
<span class="i18n">Model</span>
<span class="required-mark">*</span>
</label>
<div id="update-models-row">
<div id="update-models-loader-container" class="asc-loader-container"></div>
<button id="update-models-btn" class="action-btn i18n">Update models list</button>
<img id="update-models-error" class="icon" src="resources/icons/light/error.png" style="display: none;"/>
</div>
</div>
<select id="model-name-cmb" class="form-control"></select>
</div>
</div>
<div class="section" style="margin-bottom: 0;">
<div class="form-vertical-item">
<div class="section-label">
<label class="i18n">Use model for</label>
</div>
</div>
<div class="form-horizontal-item" id="block-use-for" style="margin-bottom: 0;">
<div id="use-for-text"></div>
<div id="use-for-image"></div>
<div id="use-for-embeddings"></div>
<div id="use-for-audio"></div>
<div id="use-for-moderations"></div>
<div id="use-for-realtime"></div>
<div id="use-for-code"></div>
<div id="use-for-vision"></div>
</div>
</div>
<div id="custom-providers-button">
<label class="i18n">
Custom providers
</label>
</div>
<script type="text/javascript" src="scripts/aiModelEdit.js"></script>
</body>
</html>

View File

@ -0,0 +1,50 @@
<!--
(c) Copyright Ascensio System SIA 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>AI Models list</title>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
<script type="text/javascript" src="./components/ListView/script.js"></script>
<script type="text/javascript" src="../js/plugin-stub.js"></script>
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
<link rel="stylesheet" href="./resources/styles/common.css">
<link rel="stylesheet" href="./components/ListView/style.css">
<link rel="stylesheet" href="./resources/styles/aiModelsList.css">
</head>
<body class="noselect">
<div id="ai-models-list" class="list">
<!-- Dynamic render items -->
</div>
<div id="buttons-block">
<button id="add-btn" class="btn-text-default">
<img src="resources/icons/light/btn-zoomup.png" class="icon"/>
</button>
<button id="edit-btn" class="btn-text-default">
<img src="resources/icons/light/btn-edit.png" class="icon"/>
</button>
<button id="delete-btn" class="btn-text-default">
<img src="resources/icons/light/btn-remove.png" class="icon"/>
</button>
</div>
<script type="text/javascript" src="scripts/aiModelsList.js"></script>
</body>
</html>

View File

@ -0,0 +1,87 @@
<!--
(c) Copyright Ascensio System SIA 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>OpenAI</title>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="vendor/GPT-3-Encoder/encoder.js"></script>
<script src="vendor/md/markdown-it.js"></script>
<script type="text/javascript" src="components/Tooltip/script.js"></script>
<link rel="stylesheet" href="components/Tooltip/style.css">
<link rel="stylesheet" href="./resources/styles/chat.css">
<script type="text/javascript" src="scripts/chat.js"></script>
</head>
<body style="display: flex;">
<div id="chat_window" class="chat_window hidden">
<div id="chat_wrapper" class="form-control empty">
<div id="chat" class="noselect">
<div id="start_panel">
<div id="welcome_text">
<br/>
<br/>
<span class="i18n">I'm here to assist you with all your text creation and editing needs. Feel free to ask me anything about your document or anything else that's on your mind.</span>
<br/>
<br/>
<span class="i18n">Lets make your content shine together!</span>
</div>
<div id="welcome_buttons_list"></div>
</div>
</div>
</div>
<div id="tokens_info" class="tokens_info">
<div class="form-control div_title_chat hidden">
<span class="i18n">Maximum 16000 tokens are available.</span>
<br>
<span class="i18n">For work with this model we should save chat history and sent it into a request.</span>
<span class="i18n">But we have a limit on the number of tokens in the request.</span>
<span class="i18n">That's why sometimes you should clear your chat history.</span>
<div class="text_align_end">
<button id="clear_history" style="margin-top: 5px;" class="form-control btn-text-default i18n">Clear history</button>
</div>
</div>
<div class="text_align_end">
<span class="i18n">Tokens in the request about: </span>
<span id="cur_tokens">0</span>
</div>
<div class="text_align_end">
<span class="i18n">Total tokens are used in last request: </span>
<span id="total_tokens">0</span>
</div>
</div>
<div id="attached_text_wrapper" class="hidden">
<div id="attached_text"></div>
<img id="attached_text_close" draggable="false" src="resources/icons/light/close.png" class="icon"/>
</div>
<div id="input_message_wrapper">
<textarea id="input_message" rows="1" class="form-control" spellcheck="false"></textarea>
<div id="input_message_submit" class="noselect">
<img src="resources/icons/light/btn-demote.png" draggable="false" class="icon"/>
</div>
</div>
</div>
<div id="loader-container" class="loader hidden"></div>
<div id="div_err" class="hidden div_error err_background">
<label class="lb_err i18n" style="font-weight: bold;">Error:</label>
<label id="lb_err" class="lb_err"></label>
</div>
</body>
</html>

View File

@ -0,0 +1,175 @@
function ListView($el, options) {
this._init = function() {
var defaults = {
emptyText: '',
renderItem: function(item, index) {
var itemEl = document.createElement('div');
itemEl.classList.add('item');
itemEl.innerText = item.label;
return itemEl;
}
};
this.options = Object.assign({}, defaults, options);
$el.classList.add('list-view');
this.$el = $el;
this.list = [];
this.selectedItem = null;
this.events = {
set: [],
add: [],
edit: [],
delete: [],
select: [],
deselect: [],
};
this._render();
};
this._render = function() {
var me = this;
this.$el.innerHTML = '';
if(this.list.length > 0) {
this.list.forEach(function(item, index) {
let itemEl = me.options.renderItem(item, index);
itemEl.addEventListener('click', function() {
me.setSelected(index);
});
me.$el.appendChild(itemEl);
});
} else if(this.options.emptyText) {
let emptyEl = document.createElement('div');
emptyEl.innerText = this.options.emptyText;
emptyEl.classList.add('empty-text');
this.$el.appendChild(emptyEl);
}
};
this._trigger = function(event, data) {
if(this.events[event]) {
this.events[event].forEach(function(callback) {
callback(data);
});
}
};
this.getList = function() {
return this.list;
};
this.set = function(list) {
this.list = list;
this._render();
if(list.length > 0) {
this.setSelected(0);
} else {
this.deselect();
}
scrollbarList.update();
scrollbarList.update();
this._trigger('set', list);
};
this.add = function(item) {
this.list.push(item);
this._render();
this.setSelected(this.list.length - 1);
this.$el.scrollTop = this.$el.scrollHeight;
scrollbarList.update();
scrollbarList.update();
this._trigger('add', item);
};
this.edit = function(item) {
var findedItem = this.list.filter(function(el) {
return el.id == item.id;
})[0];
if(!findedItem) return;
for (var key in item) {
if (findedItem[key]) {
findedItem[key] = item[key];
}
}
this._render();
this._trigger('edit', findedItem, item);
};
this.delete = function(item) {
var indexDeletedItem = this.list.indexOf(item);
if(indexDeletedItem != -1) {
this.list = this.list.filter(function(el) {
return el != item;
});
this._render();
if(this.list.length == 0) {
this.deselect();
} else if(indexDeletedItem < this.list.length) {
this.setSelected(indexDeletedItem);
} else {
this.setSelected(this.list.length - 1);
}
this._trigger('delete', item);
}
};
this.deleteByIndex = function(index) {
if(!this.list[index]) return;
this.delete(this.list[index]);
};
this.setSelected = function(index) {
if(this.list.length == 0) return;
if(index == -1) {
index = this.list.length - 1;
}
if(!this.list[index]) return;
this.deselect();
this.selectedItem = this.list[index];
this.$el.children[index].classList.add('selected');
this._trigger('select', this.selectedItem);
};
this.getSelected = function() {
return this.selectedItem;
};
this.deselect = function() {
var previouslySelectedItem = this.selectedItem;
this.selectedItem = null;
var itemsEl = this.$el.getElementsByClassName('item');
for (var i = 0; i < itemsEl.length; i++) {
itemsEl[i].classList.remove('selected');
}
if (previouslySelectedItem) {
this._trigger('deselect', previouslySelectedItem);
}
};
this.setEmptyText = function(text) {
this.options.emptyText = text;
this._render();
};
this.on = function(event, callback) {
if(this.events[event]) {
this.events[event].push(callback);
}
};
this._init();
}

View File

@ -0,0 +1,85 @@
.list-view {
flex: 1;
border: 1px solid;
position: relative;
}
.list-view .item {
padding: 6px;
cursor: pointer;
border-bottom: 1px solid;
}
.list-view .empty-text {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* Themes style for Models List */
body.theme-classic-light .list-view {
border-color: #cfcfcf;
}
body.theme-light .list-view,
body.theme-gray .list-view {
border-color: #c0c0c0;
}
body.theme-dark .list-view {
border-color: #666;
}
body.theme-contrast-dark .list-view {
border-color: #696969;
}
/* Themes style for Item Models List */
body.theme-classic-light .list-view .item {
border-color: #cfcfcf;
}
body.theme-classic-light .list-view .item:hover {
background-color: #d8dadc;
color: #444;
}
body.theme-classic-light .list-view .item.selected {
background-color: #7d858c;
color: #fff;
}
body.theme-light .list-view .item,
body.theme-gray .list-view .item {
border-color: #c0c0c0;
}
body.theme-light .list-view .item:hover,
body.theme-gray .list-view .item:hover {
background-color: #e0e0e0;
color: rgba(0, 0, 0, 0.8);
}
body.theme-light .list-view .item.selected,
body.theme-gray .list-view .item.selected {
background-color: #cbcbcb;
color: rgba(0, 0, 0, 0.8);
}
body.theme-dark .list-view .item {
border-color: #666;
}
body.theme-dark .list-view .item:hover {
background-color: #555;
color: rgba(255, 255, 255, 0.8);
}
body.theme-dark .list-view .item.selected {
background-color: #707070;
color: rgba(255, 255, 255, 0.8);
}
body.theme-contrast-dark .list-view .item {
border-color: #696969;
}
body.theme-contrast-dark .list-view .item:hover {
background-color: #424242;
color: #e8e8e8;
}
body.theme-contrast-dark .list-view .item.selected {
background-color: #666666;
color: #e8e8e8;
}

View File

@ -0,0 +1,128 @@
function Tooltip(targetEl, options) {
this._init = function() {
var me = this;
var defaults = {
renderInner: null,
text: '',
xAnchor: 'center',
yAnchor: 'bottom',
xOffset: 0,
yOffset: 3,
align: 'center',
width: null,
hasShadow: false,
keepAliveOnHover: false,
delay: 0,
hideDelay: 0
};
this.options = Object.assign({}, defaults, options);
targetEl.addEventListener('mouseover', function() {
// Set a timer for the delay before showing
me.showTimeout = setTimeout(function() {
me._createTooltipElement();
if(me.options.renderInner) {
me.tooltipEl.appendChild(me.options.renderInner());
} else {
me.tooltipEl.innerText = me.options.text;
}
if (me.options.width) {
me.tooltipEl.style.width = me.options.width + 'px';
me.tooltipEl.style.whiteSpace = '';
} else {
me.tooltipEl.style.width = '';
me.tooltipEl.style.whiteSpace = 'nowrap';
}
if (me.options.hasShadow) {
me.tooltipEl.classList.add('has-shadow');
} else {
me.tooltipEl.classList.remove('has-shadow');
}
me._updatePosition();
}, me.options.delay);
});
targetEl.addEventListener('mouseleave', function() {
clearTimeout(me.showTimeout); // Clear the show timer if the user leaves the element
// Set a timer for the delay before hiding
me.hideTimeout = setTimeout(function() {
me._deleteTooltipElement();
}, me.options.hideDelay);
});
};
this._createTooltipElement = function () {
if (!this.tooltipEl) {
let me = this;
this.tooltipEl = document.createElement("div");
this.tooltipEl.classList.add("tooltip");
this.tooltipEl.addEventListener('mouseenter', function() {
if (me.options.keepAliveOnHover) {
clearTimeout(me.hideTimeout); // Clear the hide timer when hovering over the tooltip
}
});
this.tooltipEl.addEventListener('mouseleave', function() {
if (me.options.keepAliveOnHover) {
// Set a timer for the delay before hiding the tooltip
me.hideTimeout = setTimeout(function() {
me._deleteTooltipElement();
}, me.options.hideDelay);
}
});
document.body.appendChild(this.tooltipEl);
}
};
this._deleteTooltipElement = function() {
if (this.tooltipEl) {
this.tooltipEl.remove();
this.tooltipEl = null;
}
};
this._updatePosition = function() {
var rectTooltip = this.tooltipEl.getBoundingClientRect();
var rectEl = targetEl.getBoundingClientRect();
var yOffset = this.options.yOffset;
var xOffset = this.options.xOffset;
if (this.options.align == 'right') {
xOffset = -rectTooltip.width;
} else if (this.options.align == 'center') {
xOffset = -rectTooltip.width / 2;
}
if (this.options.xAnchor == 'right') {
this.tooltipEl.style.left = rectEl.right + xOffset + 'px';
} else if (this.options.xAnchor == 'left') {
this.tooltipEl.style.left = rectEl.left + xOffset + 'px';
} else if (this.options.xAnchor == 'center') {
this.tooltipEl.style.left = rectEl.left + rectEl.width / 2 + xOffset + 'px';
}
if (this.options.yAnchor == 'bottom') {
this.tooltipEl.style.top = rectEl.bottom + yOffset + 'px';
} else if (this.options.yAnchor == 'top') {
this.tooltipEl.style.top = rectEl.top - yOffset - rectTooltip.height + 'px';
}
};
this.getText = function() {
return this.options.text;
};
this.setText = function(text) {
this.options.text = text;
if(this.tooltipEl) {
this.tooltipEl.innerText = text;
this._updatePosition();
}
};
this._init();
}

View File

@ -0,0 +1,29 @@
.tooltip {
line-height: 12px;
word-break: break-word;
position: absolute;
z-index: 10;
padding: 4px 8px;
border: 1px solid;
border-radius: 4px;
}
.tooltip.has-shadow {
box-shadow: 0px 2px 5px #00000033;
}
body.theme-light .tooltip,
body.theme-gray .tooltip {
background-color: #fff;
border-color: #c0c0c0;
}
body.theme-classic-light .tooltip {
background-color: #fff;
border-color: #cfcfcf;
}
body.theme-dark .tooltip {
background-color: #333;
border-color: #666;
}
body.theme-contrast-dark .tooltip {
background-color: #1e1e1e;
border-color: #696969;
}

View File

@ -0,0 +1,73 @@
{
"name" : "AI",
"nameLocale": {
"ru": "ИИ",
"fr": "AI",
"es": "AI",
"de": "AI",
"cs": "AI",
"zh": "AI",
"pt-BR": "AI",
"sr-Cyrl-RS": "AI",
"sr-Latn-RS": "AI",
"ja-JA": "AI",
"sq-AL": "AI",
"it": "IA"
},
"guid" : "asc.{9DC93CDB-B576-4F0C-B55E-FCC9C48DD007}",
"version": "2.2.8",
"minVersion" : "8.2.0",
"variations" : [
{
"description": "Use the AI chatbot to perform tasks which involve understanding or generating natural language or code.",
"descriptionLocale": {
"ru": "Используйте чат-бот AI для выполнения задач, связанных с пониманием или генерацией естественного языка или кода.",
"fr": "Utilisez le chatbot AI pour effectuer des tâches qui impliquent la compréhension ou la génération de langage naturel ou de code.",
"es": "Utilice el chatbot AI para realizar tareas que impliquen la comprensión o generación de lenguaje natural o de código.",
"pt-BR": "Use o chatbot AI para realizar tarefas que envolvam compreensão ou geração de linguagem ou código natural.",
"de": "Verwenden Sie den AI-Chatbot, um Aufgaben auszuführen, die das Verstehen oder Generieren von natürlicher Sprache oder Code beinhalten.",
"cs": "Použijte chatbota AI k provádění úkolů, který zahrnuje porozumění nebo generování přirozeného jazyka nebo kódu.",
"zh": "使用 AI 聊天机器人完成有关理解、生成自然语言或代码的任务。",
"sr-Cyrl-RS": "Користите AI чет робота за обављање задатака који укључују разумевање или генерисање природног језика или кода.",
"sr-Latn-RS": "Koristite AI čet robota za obavljanje zadataka koji uključuju razumevanje ili generisanje prirodnog jezika ili koda.",
"ja-JA": "自然言語やコードの理解または生成が必要なタスクを行うには、AIチャットボットを使用できます。",
"sq-AL": "Shtoni dhe selektoni modele AI për detyra të ndryshme.",
"it": "Utilizza il chatbot dell'IA per eseguire attività che implicano la comprensione o la generazione di codice o linguaggio naturale."
},
"url" : "index.html",
"icons": "resources/%theme-type%(light|dark)/icon%scale%(default).%extension%(png)",
"isViewer" : false,
"EditorsSupport" : ["word", "cell", "slide", "pdf"],
"type" : "background",
"initDataType" : "none",
"buttons" : [],
"events" : ["onContextMenuShow", "onContextMenuClick", "onToolbarMenuClick"],
"store": {
"background": {
"light" : "linear-gradient(90deg, #F9B6FF 0%, #E370EE 102.01%)",
"dark" : "linear-gradient(90deg, #F9B6FF 0%, #E370EE 102.01%)"
},
"screenshots" :
[
"resources/store/screenshots/screen_1.png",
"resources/store/screenshots/screen_2.png",
"resources/store/screenshots/screen_3.png",
"resources/store/screenshots/screen_4.png",
"resources/store/screenshots/screen_5.png",
"resources/store/screenshots/screen_6.png"
],
"icons" : {
"light" : "resources/store/icons",
"dark" : "resources/store/icons"
},
"categories": ["specAbilities", "work", "recommended"]
}
}
]
}

View File

@ -0,0 +1,56 @@
<!--
(c) Copyright Ascensio System SIA 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Custom providers</title>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
<script type="text/javascript" src="components/Tooltip/script.js"></script>
<script type="text/javascript" src="components/ListView/script.js"></script>
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
<link rel="stylesheet" href="./resources/styles/common.css">
<link rel="stylesheet" href="./resources/styles/customProviders.css">
<link rel="stylesheet" href="components/Tooltip/style.css">
<link rel="stylesheet" href="components/ListView/style.css">
</head>
<body class="noselect">
<div id="label-row">
<label class="i18n">Connected custom providers</label>
<img id="alert-icon" class="icon" src="resources/icons/light/info.png"/>
</div>
<div id="list-row">
<div id="providers-list" class="empty">
<!-- Dynamic render items -->
</div>
<div id="buttons-block">
<button id="add-btn" class="btn-text-default">
<img src="resources/icons/light/btn-zoomup.png" class="icon"/>
</button>
<button id="delete-btn" class="btn-text-default" disabled>
<img src="resources/icons/light/btn-remove.png" class="icon"/>
</button>
</div>
</div>
<label id="error-label" class="hide"></label>
<input id="file-input" type="file" multiple accept=".js"/>
<script type="text/javascript" src="scripts/customProviders.js"></script>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,35 @@
<!--
(c) Copyright Ascensio System SIA 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ChatGPT</title>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="scripts/hyperlink.js"></script>
<style>
html,body,iframe {
margin: 0px;
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<iframe id="iframe"></iframe>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!--
(c) Copyright Ascensio System SIA 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>AI Constructor</title>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
<script type="text/javascript" src="vendor/GPT-3-Encoder/encoder.js"></script>
<script type="text/javascript" src="scripts/engine/storage.js"></script>
<script type="text/javascript" src="scripts/engine/local_storage.js"></script>
<script type="text/javascript" src="scripts/engine/providers/base.js"></script>
<script type="text/javascript" src="scripts/engine/providers/provider.js"></script>
<script type="text/javascript" src="scripts/engine/engine.js"></script>
<script type="text/javascript" src="scripts/engine/library.js"></script>
<script type="text/javascript" src="scripts/engine/buttons.js"></script>
<script type="text/javascript" src="scripts/engine/register.js"></script>
<script type="text/javascript" src="scripts/code.js"></script>
<script src="vendor/md/markdown-it.js"></script>
</head>
<body>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Some files were not shown because too many files have changed in this diff Show More