diff --git a/content/01-publish-status/index.md b/content/01-publish-status/index.md index 1500d60..0105137 100644 --- a/content/01-publish-status/index.md +++ b/content/01-publish-status/index.md @@ -1,14 +1,13 @@ -# Publish Status +# Publish Status -In Typemill you can save drafts, publish pages, unpublish pages, and delete pages with a sticky publish panel at the bottom of each page. For published pages, you can also save modifications, discard modifications and publish modifications. +In Typemill, you can save drafts, publish pages, unpublish pages, and delete pages using a sticky publish panel at the bottom of each page. For published pages, you can also save modifications, discard modifications, and publish modifications. The current status of the page is indicated with colors in the publish panel and in the navigation. This way, an author can always see the status of each page and the current state of the whole website. -* **Save a draft**: The page is not published and not accessible in the frontend. The status color is red. -* **Publish a page**: The page is published and accessible in the frontend. The status color is green. -* **Unpublish a page**: The page is not accessible in the frontend and only accessible in the author interface. The status color is red again. -* **Delete a page**: The page is completely deleted in frontend and in the author interface. -* **Save modivications**: The page is published and accessible in the frontend. The modified draft is only visible in the author interface. The status color is orange. -* **Discard modifications**: The modifications are deleted and the live version is restored in the author interface. The status color is green again. -* **Publish modifications**: The modifications are published to the live version. The status color is green again. - +- **Save a draft**: The page is not published and not accessible on the frontend. The status color is red. +- **Publish a page**: The page is published and accessible on the frontend. The status color is green. +- **Unpublish a page**: The page is not accessible on the frontend and only accessible in the author interface. The status color is red again. +- **Delete a page**: The page is completely deleted from the frontend and the author interface. +- **Save modifications**: The page is published and accessible on the frontend. The modified draft is only visible in the author interface. The status color is orange. +- **Discard modifications**: The modifications are deleted, and the live version is restored in the author interface. The status color returns to green. +- **Publish modifications**: The modifications are published to the live version. The status color returns to green. \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiKixote.php b/system/typemill/Controllers/ControllerApiKixote.php new file mode 100644 index 0000000..94ea913 --- /dev/null +++ b/system/typemill/Controllers/ControllerApiKixote.php @@ -0,0 +1,377 @@ +getKixoteSettings(); + + if(!$kixoteSettings) + { + $response->getBody()->write(json_encode([ + 'message' => 'could not load kixote settings.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # send to Kixote + $response->getBody()->write(json_encode([ + 'settings' => $kixoteSettings + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + + } + + public function updateKixoteSettings(Request $request, Response $response) + { + $params = $request->getParsedBody(); + $kixoteSettings = $params['kixotesettings'] ?? false; + $validate = new Validation(); + + if(isset($kixoteSettings['promptlist'])) + { + $promptErrors = false; + foreach($kixoteSettings['promptlist'] as $name => $values) + { + $validInput = $validate->kixotePrompt($values); + if($validInput !== true) + { + $promptErrors = true; + $kixoteSettings['promptlist'][$name]['errors'] = $validInput; + } + else + { + unset($kixoteSettings['promptlist'][$name]['errors']); + } + } + + if($promptErrors) + { + $response->getBody()->write(json_encode([ + 'message' => 'please correct the errors in the form', + 'kixotesettings' => $kixoteSettings + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + } + + $settingsModel = new Settings(); + $result = $settingsModel->updateKixoteSettings($kixoteSettings); + + if(!$result) + { + # restore the current kixote-settings + $kixoteSettings = $settingsModel->getKixoteSettings(); + + $response->getBody()->write(json_encode([ + 'message' => 'error while saving settings.', + 'kixotesettings' => $kixoteSettings + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # send to Kixote + $response->getBody()->write(json_encode([ + 'kixotesettings' => $kixoteSettings + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + # initial token statistics + public function getTokenStats(Request $request, Response $response): Response + { + $aiservice = false; + $tokenstats = 0; + $useragreement = false; + $user = new User(); + $username = $request->getAttribute('c_username'); + + if(!$user->setUser($username)) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('We did not find the a user.') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + if(isset($this->settings['aiservice']) && $this->settings['aiservice'] !== 'none') + { + $aiservice = $this->settings['aiservice']; + } + + if($aiservice) + { + $userdata = $user->getUserData(); + if(isset($userdata['aiservices']) && in_array($aiservice, $userdata['aiservices'])) + { + $useragreement = true; + } + } + + # get toke stats for AI service + if($aiservice && $useragreement) + { + switch ($aiservice) + { + case 'chatgpt': + $tokenstats = [ + 'url' => 'https://platform.openai.com/settings/organization/billing/overview', + 'label' => 'ChatGPT Billing' + ]; + break; + + default: + $tokenstats = 0; + break; + } + } + + if($tokenstats === false) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('Could not get tokenstats.') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $response->getBody()->write(json_encode([ + 'message' => 'Success', + 'aiservice' => $aiservice, + 'useragreement' => $useragreement, + 'tokenstats' => $tokenstats + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + # initial token statistics + public function agreeToAiService(Request $request, Response $response): Response + { + $aiservice = false; + $user = new User(); + $username = $request->getAttribute('c_username'); + + if(!$user->setUserWithPassword($username)) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('We did not find the a user or usermail.') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + if(isset($this->settings['aiservice']) && $this->settings['aiservice'] !== 'none') + { + $aiservice = $this->settings['aiservice']; + } + else + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('No valid ai service has been selected.') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $agreements = $user->getValue('aiservices'); + + if(!$agreements) + { + $agreements = [$aiservice]; + } + elseif(!isset($agreements[$aiservice])) + { + $agreements[] = $aiservice; + } + + $user->setValue('aiservices', $agreements); + if($user->updateUser() !== true) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('We could not update your user settings, please try again or agree to ' . $aiservice . ' in your user profile.') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $response->getBody()->write(json_encode([ + 'message' => 'Success' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + private function getKixoteJWT(Request $request, Response $response) + { + # this will authenticate from service.typemill.net (e.g. for template service) + $license = new License(); + $jwt = $license->getToken(); + if($jwt) + { + $this->error = $license->getMessage(); + return false; + } + + # if no agb-confirmation + $confirm = $settings['kixote_confirm'] ?? false; + if(!$confirm) + { + $this->error = 'Please read and accept the AGB before you start with our service.'; + return false; + } + + return $jwt; + } + + public function promptKixote(Request $request, Response $response) + { + $jwt = $this->getKixoteJWT(); + if(!$jwt) + { + $response->getBody()->write(json_encode([ + 'message' => $this->error + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $params = $request->getParsedBody(); + + $params['name'] = ''; # will trigger some cool stuff in kixote + $params['prompt'] = ''; # the prompt itself + $params['article'] = ''; # the current article + $params['tone'] = ''; # the tone + + if(!isset($params['prompt']) OR !is_array($params['article'])) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('Prompt or article missing.') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # validate input + $validate = new Validation(); + $validationresult = $validate->newLicense($params['license']); + if($validationresult !== true) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('Please correct your input.'), + 'errors' => $validate->returnFirstValidationErrors($validationresult) + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # send to Kixote + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('Licence has been stored'), + 'licensedata' => $licensedata + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function promptChatGPT(Request $request, Response $response): Response + { + # check if user has accepted + + $params = $request->getParsedBody(); + + $params['name'] = $params['name'] ?? ''; + $params['prompt'] = $params['prompt'] ?? ''; + $params['article'] = $params['article'] ?? ''; + $params['tone'] = $params['tone'] ?? ''; + + $settingsModel = new Settings(); + $model = $this->settings['chatgptModel'] ?? false; + $apikey = $settingsModel->getSecret('chatgptKey'); + + if (empty($params['prompt']) || !is_string($params['prompt'])) + { + $response->getBody()->write(json_encode([ + 'message' => 'Prompt is missing or invalid.' + ])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + if (empty($params['article']) || !is_string($params['article'])) + { + $response->getBody()->write(json_encode([ + 'message' => 'Article is missing or invalid.' + ])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + if (!$model || !$apikey) + { + $response->getBody()->write(json_encode([ + 'message' => 'Model or api key for chatgpt is missing, please add it in the system settings.' + ])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $url = 'https://api.openai.com/v1/chat/completions'; + $authHeader = "Authorization: Bearer $apikey"; + + $postdata = [ + 'model' => $model, + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'You are a content editor and writing assistant. If the user prompt does not explicitly specify otherwise, apply the prompt to the provided article and return only the updated article in Markdown syntax, without any extra comments or explanations. If you find the tag , modify only the content inside these tags and leave everything else unchanged. Always return the full article.' + ], + [ + 'role' => 'user', + 'content' => $params['prompt'] . "\n" . $params['article'] + ], + ], + 'temperature' => 0.7, + 'max_tokens' => 2000, + ]; + + $apiservice = new ApiCalls(); + $apiResponse = $apiservice->makePostCall($url, $postdata, $authHeader); + + if (!$apiResponse) + { + $response->getBody()->write(json_encode([ + 'message' => 'Failed to communicate with ChatGPT', + 'error' => $apiservice->getError() + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $data = json_decode($apiResponse, true); + $response->getBody()->write(json_encode([ + 'message' => 'Success', + 'data' => $data, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiSystemPlugins.php b/system/typemill/Controllers/ControllerApiSystemPlugins.php index 8940f93..d79fb7e 100644 --- a/system/typemill/Controllers/ControllerApiSystemPlugins.php +++ b/system/typemill/Controllers/ControllerApiSystemPlugins.php @@ -20,7 +20,7 @@ class ControllerApiSystemPlugins extends Controller $extension = new Extension(); $formdefinitions = $extension->getPluginDefinition($pluginname); $formdefinitions = $this->addDatasets($formdefinitions['forms']['fields']); - $plugindata = []; +# $plugindata = []; # validate input $validator = new Validation(); @@ -43,8 +43,20 @@ class ControllerApiSystemPlugins extends Controller } # store updated settings here - $settings = new Settings(); - $updatedSettings = $settings->updateSettings($validatedOutput, 'plugins', $pluginname); + $settingsModel = new Settings(); + $securityFields = $settingsModel->findSecurityDefinitions($formdefinitions); + if(!empty($securityFields)) + { + $splitSettings = $settingsModel->extractSecuritySettings($validatedOutput, $securityFields); + $validatedOutput = $splitSettings['settings']; + + if($splitSettings['securitySettings'] && !empty($splitSettings['securitySettings'])) + { + $settingsModel->updateSecuritySettings($splitSettings['securitySettings'], 'plugins', $pluginname); + } + } + + $updatedSettings = $settingsModel->updateSettings($validatedOutput, 'plugins', $pluginname); $response->getBody()->write(json_encode([ 'message' => Translations::translate('settings have been saved') diff --git a/system/typemill/Controllers/ControllerApiSystemSettings.php b/system/typemill/Controllers/ControllerApiSystemSettings.php index 6f11877..043b600 100644 --- a/system/typemill/Controllers/ControllerApiSystemSettings.php +++ b/system/typemill/Controllers/ControllerApiSystemSettings.php @@ -64,7 +64,19 @@ class ControllerApiSystemSettings extends Controller } } - # store updated settings here + $securityFields = $settingsModel->findSecurityDefinitions($formdefinitions); + + if(!empty($securityFields)) + { + $splitSettings = $settingsModel->extractSecuritySettings($validatedOutput, $securityFields); + $validatedOutput = $splitSettings['settings']; + + if($splitSettings['securitySettings'] && !empty($splitSettings['securitySettings'])) + { + $settingsModel->updateSecuritySettings($splitSettings['securitySettings']); + } + } + $updatedSettings = $settingsModel->updateSettings($validatedOutput); $response->getBody()->write(json_encode([ diff --git a/system/typemill/Controllers/ControllerApiSystemThemes.php b/system/typemill/Controllers/ControllerApiSystemThemes.php index e8bc5bf..9048e74 100644 --- a/system/typemill/Controllers/ControllerApiSystemThemes.php +++ b/system/typemill/Controllers/ControllerApiSystemThemes.php @@ -40,8 +40,20 @@ class ControllerApiSystemThemes extends Controller unset($validatedOutput['customcss']); # store updated settings here - $settings = new Settings(); - $updatedSettings = $settings->updateSettings($validatedOutput, 'themes', $themename); + $settingsModel = new Settings(); + $securityFields = $settingsModel->findSecurityDefinitions($formdefinitions); + if(!empty($securityFields)) + { + $splitSettings = $settingsModel->extractSecuritySettings($validatedOutput, $securityFields); + $validatedOutput = $splitSettings['settings']; + + if($splitSettings['securitySettings'] && !empty($splitSettings['securitySettings'])) + { + $settingsModel->updateSecuritySettings($splitSettings['securitySettings'], 'themes', $themename); + } + } + + $updatedSettings = $settingsModel->updateSettings($validatedOutput, 'themes', $themename); $response->getBody()->write(json_encode([ 'message' => Translations::translate('settings have been saved') diff --git a/system/typemill/Models/ApiCalls.php b/system/typemill/Models/ApiCalls.php new file mode 100644 index 0000000..70cdf59 --- /dev/null +++ b/system/typemill/Models/ApiCalls.php @@ -0,0 +1,106 @@ +error; + } + + public function makePostCall(string $url, array $data, $authHeader = '') + { + if (in_array('curl', get_loaded_extensions())) { + return $this->makeCurlCall($url, 'POST', $data, $authHeader); + } + + return $this->makeFileGetContentsCall($url, 'POST', $data, $authHeader); + } + + public function makeGetCall($url, $authHeader = '') + { + if (in_array('curl', get_loaded_extensions())) { + return $this->makeCurlCall($url, 'GET', null, $authHeader); + } + + return $this->makeFileGetContentsCall($url, 'GET', null, $authHeader); + } + + private function makeCurlCall($url, $method, $data = false, $authHeader = '') + { + $this->error = null; + + $headers = [ + "Content-Type: application/json", + ]; + + if (!empty($authHeader)) { + $headers[] = $authHeader; + } + + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + if ($method === 'POST' && $data) { + $postdata = json_encode($data); + if ($postdata === false) { + $this->error = "JSON encoding error: " . json_last_error_msg(); + return false; + } + curl_setopt($curl, CURLOPT_POSTFIELDS, $postdata); + curl_setopt($curl, CURLOPT_POST, true); + } + curl_setopt($curl, CURLOPT_FAILONERROR, true); + + $response = curl_exec($curl); + + if ($response === false) { + $this->error = curl_error($curl); + } + curl_close($curl); + + return $response !== false ? $response : false; + } + + private function makeFileGetContentsCall($url, $method, $data = null, $authHeader = '') + { + $this->error = null; + + $headers = [ + "Content-Type: application/json" + ]; + + if (!empty($authHeader)) { + $headers[] = $authHeader; + } + + $options = [ + 'http' => [ + 'method' => $method, + 'ignore_errors' => true, + 'header' => implode("\r\n", $headers), + ] + ]; + + if ($method === 'POST' && $data !== null) { + $postdata = json_encode($data); + if ($postdata === false) { + $this->error = "JSON encoding error: " . json_last_error_msg(); + return false; + } + $options['http']['content'] = $postdata; + } + + $context = stream_context_create($options); + $response = file_get_contents($url, false, $context); + + if ($response === false) { + $this->error = 'file_get_contents failed for ' . $method . ' request.'; + } + + return $response !== false ? $response : false; + } +} diff --git a/system/typemill/Models/License.php b/system/typemill/Models/License.php index 86ace69..0141ad2 100644 --- a/system/typemill/Models/License.php +++ b/system/typemill/Models/License.php @@ -87,7 +87,6 @@ class License # check if license data are valid and not manipulated $licenseStatus = $this->validateLicense($licensedata); - if($licenseStatus !== true) { $this->message = Translations::translate('The license data are invalid. ') . $this->message; @@ -97,7 +96,6 @@ class License # check if website uses licensed domain $licenseDomain = $this->checkLicenseDomain($licensedata['domain'], $urlinfo); - if(!$licenseDomain) { $this->message = Translations::translate('The website is running not under the domain of your license.'); @@ -107,7 +105,6 @@ class License # check if subscription period is paid $subscriptionPaid = $this->checkLicenseDate($licensedata['payed_until']); - if(!$subscriptionPaid) { $storage = new StorageWrapper('\Typemill\Models\Storage'); diff --git a/system/typemill/Models/Settings.php b/system/typemill/Models/Settings.php index dc611ec..44be506 100644 --- a/system/typemill/Models/Settings.php +++ b/system/typemill/Models/Settings.php @@ -107,6 +107,47 @@ class Settings return false; } + public function getKixoteSettings() + { + $defaultSettings = $this->storage->getYaml('systemSettings', '', 'kixote.yaml'); + $userSettings = $this->storage->getYaml('settingsFolder', '', 'kixote.yaml'); + + if ($userSettings) + { + foreach ($defaultSettings['promptlist'] as $key => $prompt) + { + if (isset($userSettings['promptlist'][$key])) + { + # Use active setting from user but keep system settings intact + $active = $userSettings['promptlist'][$key]['active']; + $userSettings['promptlist'][$key] = $prompt; + $userSettings['promptlist'][$key]['active'] = $active; + } + else + { + # New prompt from system settings, add it to user settings + $userSettings['promptlist'][$key] = $prompt; + } + } + } + else + { + $userSettings = $defaultSettings; + } + + return $userSettings; + } + + public function updateKixoteSettings($kixoteSettings) + { + if($this->storage->updateYaml('settingsFolder', '', 'kixote.yaml', $kixoteSettings)) + { + return true; + } + + return false; + } + public function getObjectSettings($objectType, $objectName) { $objectSettings = $this->storage->getYaml($objectType, $objectName, $objectName . '.yaml'); @@ -245,4 +286,101 @@ class Settings return false; } + public function findSecurityDefinitions($definitions, $securityDefinitions = []) + { + foreach ($definitions as $fieldname => $definition) + { + if (isset($definition['fields'])) + { + $securityDefinitions = $this->findSecurityDefinitions($definition['fields'], $securityDefinitions); + } + + if (isset($definition['type']) && $definition['type'] === 'password') + { + $securityDefinitions[] = $fieldname; + } + } + + return $securityDefinitions; + } + + public function extractSecuritySettings($settings, $securityFields) + { + $securitySettings = []; + + foreach ($securityFields as $fieldname) + { + if (isset($settings[$fieldname])) + { + $securitySettings[$fieldname] = $settings[$fieldname]; + unset($settings[$fieldname]); + } + } + + return [ + 'settings' => $settings, + 'securitySettings' => $securitySettings + ]; + } + + public function updateSecuritySettings($newSecuritySettings, $themeorplugin = null, $themeorpluginname = null) + { + # problem that settings with same name will overwrite (e.g. from theme and plugins) + $securitySettings = $this->getSecuritySettings(); + foreach($newSecuritySettings as $fieldname => $value) + { + if($themeorplugin && $themeorpluginname) + { + $securitySettings[$themeorplugin][$themeorpluginname][$fieldname] = $value; + } + else + { + $securitySettings[$fieldname] = $value; + } + } + + $secrets = $this->storage->updateYaml('settingsFolder', '', 'secrets.yaml', $securitySettings); + if($secrets) + { + return true; + } + + return false; + } + + private function getSecuritySettings() + { + $secrets = $this->storage->getYaml('settingsFolder', '', 'secrets.yaml'); + + if($secrets) + { + return $secrets; + } + return []; + } + + public function getSecret(string $fieldname, $objecttype = null, $objectname = null) + { + $secrets = $this->storage->getYaml('settingsFolder', '', 'secrets.yaml'); + + if(!$secrets) + { + return false; + } + + if($fieldname && $objecttype && $objectname) + { + if(isset($secrets[$objecttype][$objectname][$fieldname])) + { + return $secrets[$objecttype][$objectname][$fieldname]; + } + } + + if($fieldname && isset($secrets[$fieldname])) + { + return $secrets[$fieldname]; + } + + return false; + } } \ No newline at end of file diff --git a/system/typemill/Models/Validation.php b/system/typemill/Models/Validation.php index c786a43..0e14332 100644 --- a/system/typemill/Models/Validation.php +++ b/system/typemill/Models/Validation.php @@ -682,6 +682,26 @@ class Validation return $v->errors(); } + public function kixotePrompt(array $params) + { + $v = new Validator($params); + + $v->rule('required', 'title', 'content', 'active', 'system'); + $v->rule('regex', 'title', '/^[a-z0-9 ]+$/i'); + $v->rule('lengthBetween', 'title', 2,20); + $v->rule('noHTML', 'content'); + $v->rule('lengthBetween', 'content',2,5000); + $v->rule('boolean', 'active'); + $v->rule('boolean', 'system'); + + if($v->validate()) + { + return true; + } + + return $v->errors(); + } + /** * validation for password recovery * @@ -848,7 +868,7 @@ class Validation $v->rule('lengthMax', $fieldName, 10000); break; case "password": - $v->rule('lengthMax', $fieldName, 100); + $v->rule('lengthMax', $fieldName, 500); break; case "radio": $v->rule('in', $fieldName, $fieldDefinitions['options']); diff --git a/system/typemill/author/css/custom.css b/system/typemill/author/css/custom.css index 95f52ec..12c18b5 100644 --- a/system/typemill/author/css/custom.css +++ b/system/typemill/author/css/custom.css @@ -1,5 +1,79 @@ +.iconwrapper { + position:relative; + height:60px; + width:60px; + overflow: hidden; + border-radius: 30%; +} +.magicicon{ + position: absolute; + top:10px; + left: 10px; + bottom: 10px; + right: 10px; + color: white; + animation: glow .5s infinite alternate; +} +@keyframes glow { + 0% { + fill: black; + filter: drop-shadow(0 0 5px black); + } + 100% { + fill: rgb(15, 50, 46); + filter: drop-shadow(0 0 15px rgb(15, 50, 46)); + } +} + +.loader { + width: 1px; + height:80px; + background: #ccc; + box-shadow: 0 0 60px 10px #eee; + transform: translate(-20px); + clip-path: inset(0); + animation: + l4-1 1s ease-in-out infinite alternate, + l4-2 2s ease-in-out infinite; +} +@keyframes l4-1 { + 100% {transform: translateX(80px)} +} +@keyframes l4-2 { + 33% {clip-path: inset(0 0 0 -100px)} + 50% {clip-path: inset(0 0 0 0) } + 83% {clip-path: inset(0 -100px 0 0)} +} + + +@keyframes wobble-line-with-trailing-shadow { + 0% { + transform: translateY(0); + box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.8), + 0 20px 30px rgba(255, 255, 255, 0.6), + 0 40px 60px rgba(255, 255, 255, 0.4); + } + 50% { + transform: translateY(60px); + box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.8), + 0 20px 30px rgba(255, 255, 255, 0.6), + 0 40px 60px rgba(255, 255, 255, 0.4); + } + 100% { + transform: translateY(0); + box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.8), + 0 20px 30px rgba(255, 255, 255, 0.6), + 0 40px 60px rgba(255, 255, 255, 0.4); + } +} + +#loading-overlay .animate-wobble-line-with-trailing-shadow { + animation: wobble-line-with-trailing-shadow 2s ease-in-out infinite; + background: white; +} + /******************** -* SVG ICONS * +* SVG ICONS * ********************/ .icon { @@ -12,7 +86,7 @@ } .icon.baseline{ top: 0.125em; - position: relative; + position: relative; } diff --git a/system/typemill/author/css/output.css b/system/typemill/author/css/output.css index 453aded..3e52b95 100644 --- a/system/typemill/author/css/output.css +++ b/system/typemill/author/css/output.css @@ -862,6 +862,10 @@ video { margin-top: 0.5rem; } +.mt-20 { + margin-top: 5rem; +} + .mt-3 { margin-top: 0.75rem; } @@ -1026,18 +1030,10 @@ video { width: 5rem; } -.w-24 { - width: 6rem; -} - .w-3\/4 { width: 75%; } -.w-3\/5 { - width: 60%; -} - .w-32 { width: 8rem; } @@ -1054,10 +1050,6 @@ video { width: 83.333333%; } -.w-54rem { - width: 54rem; -} - .w-6 { width: 1.5rem; } @@ -1082,10 +1074,6 @@ video { width: 100%; } -.w-48 { - width: 12rem; -} - .max-w-4xl { max-width: 56rem; } @@ -1153,6 +1141,10 @@ video { list-style-position: inside; } +.list-decimal { + list-style-type: decimal; +} + .list-disc { list-style-type: disc; } @@ -1201,12 +1193,24 @@ video { justify-content: space-around; } +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); } +.space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + .overflow-auto { overflow: auto; } @@ -1225,6 +1229,14 @@ video { white-space: nowrap; } +.rounded { + border-radius: 0.25rem; +} + +.rounded-lg { + border-radius: 0.5rem; +} + .border { border-width: 1px; } @@ -1309,6 +1321,11 @@ video { border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.border-gray-500 { + --tw-border-opacity: 1; + border-color: rgb(107 114 128 / var(--tw-border-opacity)); +} + .border-red-500 { --tw-border-opacity: 1; border-color: rgb(239 68 68 / var(--tw-border-opacity)); @@ -1349,6 +1366,11 @@ video { border-color: rgb(250 250 249 / var(--tw-border-opacity)); } +.border-stone-500 { + --tw-border-opacity: 1; + border-color: rgb(120 113 108 / var(--tw-border-opacity)); +} + .border-stone-700 { --tw-border-opacity: 1; border-color: rgb(68 64 60 / var(--tw-border-opacity)); @@ -1399,6 +1421,11 @@ video { border-top-color: rgb(255 255 255 / var(--tw-border-opacity)); } +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} + .bg-black\/75 { background-color: rgb(0 0 0 / 0.75); } @@ -1453,6 +1480,11 @@ video { background-color: rgb(13 148 136 / var(--tw-bg-opacity)); } +.bg-teal-700 { + --tw-bg-opacity: 1; + background-color: rgb(15 118 110 / var(--tw-bg-opacity)); +} + .bg-transparent { background-color: transparent; } @@ -1467,6 +1499,10 @@ video { background-color: rgb(234 179 8 / var(--tw-bg-opacity)); } +.bg-opacity-80 { + --tw-bg-opacity: 0.8; +} + .bg-opacity-90 { --tw-bg-opacity: 0.9; } @@ -1517,11 +1553,6 @@ video { padding-right: 3rem; } -.px-16 { - padding-left: 4rem; - padding-right: 4rem; -} - .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1562,16 +1593,6 @@ video { padding-bottom: 0.375rem; } -.py-10 { - padding-top: 2.5rem; - padding-bottom: 2.5rem; -} - -.py-16 { - padding-top: 4rem; - padding-bottom: 4rem; -} - .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; @@ -1597,6 +1618,10 @@ video { padding-bottom: 2rem; } +.pb-0 { + padding-bottom: 0px; +} + .pb-3 { padding-bottom: 0.75rem; } @@ -1753,6 +1778,10 @@ video { font-weight: 400; } +.font-semibold { + font-weight: 600; +} + .uppercase { text-transform: uppercase; } @@ -1929,6 +1958,18 @@ video { transition-duration: 150ms; } +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -1993,6 +2034,11 @@ video { background-color: rgb(250 250 249 / var(--tw-bg-opacity)); } +.hover\:bg-stone-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(87 83 78 / var(--tw-bg-opacity)); +} + .hover\:bg-stone-700:hover { --tw-bg-opacity: 1; background-color: rgb(68 64 60 / var(--tw-bg-opacity)); @@ -2028,6 +2074,11 @@ video { background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } +.hover\:text-rose-500:hover { + --tw-text-opacity: 1; + color: rgb(244 63 94 / var(--tw-text-opacity)); +} + .hover\:text-stone-100:hover { --tw-text-opacity: 1; color: rgb(245 245 244 / var(--tw-text-opacity)); @@ -2048,6 +2099,16 @@ video { color: rgb(41 37 36 / var(--tw-text-opacity)); } +.hover\:text-teal-300:hover { + --tw-text-opacity: 1; + color: rgb(94 234 212 / var(--tw-text-opacity)); +} + +.hover\:text-teal-500:hover { + --tw-text-opacity: 1; + color: rgb(20 184 166 / var(--tw-text-opacity)); +} + .hover\:text-white:hover { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -2314,27 +2375,22 @@ video { right: 0px; } - .lg\:my-8 { - margin-top: 2rem; - margin-bottom: 2rem; + .lg\:ml-2 { + margin-left: 0.5rem; } .lg\:mr-2 { margin-right: 0.5rem; } - .lg\:mt-0 { - margin-top: 0px; - } - - .lg\:ml-2 { - margin-left: 0.5rem; - } - .lg\:mr-3 { margin-right: 0.75rem; } + .lg\:mt-0 { + margin-top: 0px; + } + .lg\:block { display: block; } @@ -2359,14 +2415,42 @@ video { width: 50%; } + .lg\:w-1\/3 { + width: 33.333333%; + } + .lg\:w-1\/4 { width: 25%; } + .lg\:w-2\/5 { + width: 40%; + } + + .lg\:w-24 { + width: 6rem; + } + .lg\:w-3\/4 { width: 75%; } + .lg\:w-3\/5 { + width: 60%; + } + + .lg\:w-32 { + width: 8rem; + } + + .lg\:w-48 { + width: 12rem; + } + + .lg\:w-54rem { + width: 54rem; + } + .lg\:w-80 { width: 20rem; } @@ -2375,34 +2459,6 @@ video { width: 48%; } - .lg\:w-48 { - width: 12rem; - } - - .lg\:w-2\/5 { - width: 40%; - } - - .lg\:w-3\/5 { - width: 60%; - } - - .lg\:w-1\/3 { - width: 33.333333%; - } - - .lg\:w-24 { - width: 6rem; - } - - .lg\:w-32 { - width: 8rem; - } - - .lg\:w-54rem { - width: 54rem; - } - .lg\:flex-row { flex-direction: row; } @@ -2420,11 +2476,6 @@ video { padding: 0.75rem; } - .lg\:px-4 { - padding-left: 1rem; - padding-right: 1rem; - } - .lg\:px-12 { padding-left: 3rem; padding-right: 3rem; @@ -2435,9 +2486,9 @@ video { padding-right: 4rem; } - .lg\:py-16 { - padding-top: 4rem; - padding-bottom: 4rem; + .lg\:px-4 { + padding-left: 1rem; + padding-right: 1rem; } .lg\:px-6 { @@ -2445,22 +2496,27 @@ video { padding-right: 1.5rem; } - .lg\:pb-3 { - padding-bottom: 0.75rem; - } - - .lg\:pt-4 { - padding-top: 1rem; + .lg\:py-16 { + padding-top: 4rem; + padding-bottom: 4rem; } .lg\:pb-0 { padding-bottom: 0px; } + .lg\:pb-3 { + padding-bottom: 0.75rem; + } + .lg\:pr-3 { padding-right: 0.75rem; } + .lg\:pt-4 { + padding-top: 1rem; + } + .lg\:text-black { --tw-text-opacity: 1; color: rgb(0 0 0 / var(--tw-text-opacity)); diff --git a/system/typemill/author/js/vue-forms.js b/system/typemill/author/js/vue-forms.js index 7f2c849..6a66d7f 100644 --- a/system/typemill/author/js/vue-forms.js +++ b/system/typemill/author/js/vue-forms.js @@ -1,3 +1,12 @@ +app.component('component-paragraph', { + props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'], + template: `
+

{{ $filters.translate(label) }}

+

+
`, +}) + + app.component('component-text', { props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'], template: `
@@ -15,7 +24,7 @@ app.component('component-text', { :value="value" @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -40,7 +49,7 @@ app.component('component-textarea', { :value="value" @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -84,7 +93,7 @@ app.component('component-codearea', {

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, mounted: function() { @@ -152,7 +161,7 @@ app.component('component-select', {

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -184,7 +193,7 @@ app.component('component-checkbox', { {{ $filters.translate(checkboxlabel) }}

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, mounted: function() { @@ -220,7 +229,7 @@ app.component('component-checkboxlist', { {{ $filters.translate(option) }}

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, mounted: function() { @@ -260,7 +269,7 @@ app.component('component-radio', { {{ $filters.translate(option) }}

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function(picked, name) @@ -288,7 +297,7 @@ app.component('component-number', { :value="value" @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -320,7 +329,7 @@ app.component('component-date', { @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -354,7 +363,7 @@ app.component('component-email', { @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -387,7 +396,7 @@ app.component('component-tel', { @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -421,7 +430,7 @@ app.component('component-url', { @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -454,7 +463,7 @@ app.component('component-color', { @input="update($event, name)">

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

`, methods: { update: function($event, name) @@ -509,7 +518,7 @@ app.component('component-password', {

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

@@ -791,7 +800,7 @@ app.component('component-image', {

{{ errors[name] }}

-

{{ $filters.translate(description) }}

+

@@ -1026,7 +1035,7 @@ app.component('component-file', {

{{ error }}

-

{{ $filters.translate(description) }}

+

diff --git a/system/typemill/author/js/vue-kixote.js b/system/typemill/author/js/vue-kixote.js index 0debc74..4bc1b02 100644 --- a/system/typemill/author/js/vue-kixote.js +++ b/system/typemill/author/js/vue-kixote.js @@ -1,7 +1,5 @@ const getKixoteError = function(error) { - console.info(error); - if(error.response.data.error != undefined) { if(Array.isArray(error.response.data.error)) @@ -17,6 +15,981 @@ const getKixoteError = function(error) return ['something went wrong']; } +/* ++ If you change page and open kixote, then the old version without changes is loaded ++ store kixoteSettings +*/ + +const kixote = Vue.createApp({ + template: `
+ +
+
+
+ +
+ + + + +
+ +
+
+
+
+ + + + {{ tab }} +
+
+
+
+ + + + + + Usage +
+
+ 0 + Token +
+
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + + +
+

Generating, please be patient ...

+
+
+
`, + data() { + return { + showKixote: false, + currentTab: "Admin", + tabs: [ + "Admin", + "Generate", +/* "Automate", + "Translate", + "SEO", + "RAG" */ + ], + icons: { + Admin: "M11.366 22.564l1.291-1.807-1.414-1.414-1.807 1.291c-0.335-0.187-0.694-0.337-1.071-0.444l-0.365-2.19h-2l-0.365 2.19c-0.377 0.107-0.736 0.256-1.071 0.444l-1.807-1.291-1.414 1.414 1.291 1.807c-0.187 0.335-0.337 0.694-0.443 1.071l-2.19 0.365v2l2.19 0.365c0.107 0.377 0.256 0.736 0.444 1.071l-1.291 1.807 1.414 1.414 1.807-1.291c0.335 0.187 0.694 0.337 1.071 0.444l0.365 2.19h2l0.365-2.19c0.377-0.107 0.736-0.256 1.071-0.444l1.807 1.291 1.414-1.414-1.291-1.807c0.187-0.335 0.337-0.694 0.444-1.071l2.19-0.365v-2l-2.19-0.365c-0.107-0.377-0.256-0.736-0.444-1.071zM7 27c-1.105 0-2-0.895-2-2s0.895-2 2-2 2 0.895 2 2-0.895 2-2 2zM32 12v-2l-2.106-0.383c-0.039-0.251-0.088-0.499-0.148-0.743l1.799-1.159-0.765-1.848-2.092 0.452c-0.132-0.216-0.273-0.426-0.422-0.629l1.219-1.761-1.414-1.414-1.761 1.219c-0.203-0.149-0.413-0.29-0.629-0.422l0.452-2.092-1.848-0.765-1.159 1.799c-0.244-0.059-0.492-0.109-0.743-0.148l-0.383-2.106h-2l-0.383 2.106c-0.251 0.039-0.499 0.088-0.743 0.148l-1.159-1.799-1.848 0.765 0.452 2.092c-0.216 0.132-0.426 0.273-0.629 0.422l-1.761-1.219-1.414 1.414 1.219 1.761c-0.149 0.203-0.29 0.413-0.422 0.629l-2.092-0.452-0.765 1.848 1.799 1.159c-0.059 0.244-0.109 0.492-0.148 0.743l-2.106 0.383v2l2.106 0.383c0.039 0.251 0.088 0.499 0.148 0.743l-1.799 1.159 0.765 1.848 2.092-0.452c0.132 0.216 0.273 0.426 0.422 0.629l-1.219 1.761 1.414 1.414 1.761-1.219c0.203 0.149 0.413 0.29 0.629 0.422l-0.452 2.092 1.848 0.765 1.159-1.799c0.244 0.059 0.492 0.109 0.743 0.148l0.383 2.106h2l0.383-2.106c0.251-0.039 0.499-0.088 0.743-0.148l1.159 1.799 1.848-0.765-0.452-2.092c0.216-0.132 0.426-0.273 0.629-0.422l1.761 1.219 1.414-1.414-1.219-1.761c0.149-0.203 0.29-0.413 0.422-0.629l2.092 0.452 0.765-1.848-1.799-1.159c0.059-0.244 0.109-0.492 0.148-0.743l2.106-0.383zM21 15.35c-2.402 0-4.35-1.948-4.35-4.35s1.948-4.35 4.35-4.35 4.35 1.948 4.35 4.35c0 2.402-1.948 4.35-4.35 4.35z", + Generate: "M8 6l-4-4h-2v2l4 4zM10 0h2v4h-2zM18 10h4v2h-4zM20 4v-2h-2l-4 4 2 2zM0 10h4v2h-4zM10 18h2v4h-2zM2 18v2h2l4-4-2-2zM31.563 27.563l-19.879-19.879c-0.583-0.583-1.538-0.583-2.121 0l-1.879 1.879c-0.583 0.583-0.583 1.538 0 2.121l19.879 19.879c0.583 0.583 1.538 0.583 2.121 0l1.879-1.879c0.583-0.583 0.583-1.538 0-2.121zM15 17l-6-6 2-2 6 6-2 2z", + Automate: "M0.001 16.051l-0.001 0c0 0 0 0.003 0.001 0.007 0.003 0.121 0.017 0.24 0.041 0.355 0.006 0.055 0.013 0.114 0.021 0.18 0.007 0.059 0.014 0.122 0.022 0.19 0.012 0.080 0.024 0.165 0.037 0.256 0.027 0.18 0.056 0.379 0.091 0.592 0.042 0.201 0.088 0.419 0.136 0.652 0.022 0.116 0.055 0.235 0.087 0.356s0.065 0.247 0.099 0.375c0.018 0.064 0.032 0.129 0.053 0.194s0.041 0.131 0.062 0.197 0.085 0.268 0.129 0.406c0.011 0.035 0.022 0.069 0.033 0.104 0.013 0.034 0.025 0.069 0.038 0.104 0.026 0.069 0.052 0.139 0.078 0.21 0.053 0.14 0.107 0.284 0.162 0.429 0.061 0.143 0.124 0.288 0.188 0.435 0.032 0.073 0.064 0.147 0.096 0.222s0.071 0.147 0.107 0.221c0.073 0.147 0.146 0.297 0.221 0.448 0.077 0.15 0.163 0.297 0.245 0.448 0.042 0.075 0.084 0.15 0.126 0.226s0.091 0.148 0.136 0.223c0.092 0.148 0.185 0.298 0.279 0.448 0.395 0.59 0.834 1.174 1.319 1.727 0.491 0.549 1.023 1.070 1.584 1.55 0.568 0.473 1.165 0.903 1.773 1.285 0.613 0.376 1.239 0.697 1.856 0.973 0.156 0.064 0.311 0.127 0.465 0.19 0.077 0.030 0.152 0.064 0.229 0.091s0.154 0.054 0.23 0.081 0.302 0.108 0.453 0.156c0.151 0.045 0.3 0.089 0.447 0.133 0.074 0.021 0.146 0.045 0.219 0.063s0.146 0.036 0.218 0.053c0.144 0.035 0.286 0.069 0.425 0.103 0.141 0.027 0.279 0.054 0.415 0.080 0.068 0.013 0.135 0.026 0.201 0.038 0.033 0.006 0.066 0.012 0.099 0.019 0.033 0.005 0.066 0.009 0.099 0.014 0.131 0.018 0.259 0.036 0.384 0.053 0.062 0.009 0.124 0.017 0.185 0.026s0.122 0.012 0.182 0.018c0.119 0.011 0.236 0.021 0.349 0.031s0.222 0.021 0.329 0.023c0.007 0 0.014 0 0.021 0.001 0.019 1.088 0.906 1.964 1.999 1.964 0.017 0 0.034-0.001 0.051-0.001v0.001c0 0 0.003-0 0.007-0.001 0.121-0.003 0.24-0.017 0.355-0.041 0.055-0.006 0.114-0.013 0.18-0.021 0.059-0.007 0.122-0.014 0.19-0.022 0.080-0.012 0.165-0.024 0.256-0.037 0.18-0.027 0.379-0.056 0.592-0.091 0.201-0.042 0.419-0.088 0.652-0.136 0.116-0.022 0.235-0.056 0.356-0.087s0.247-0.065 0.375-0.099c0.064-0.018 0.129-0.032 0.194-0.053s0.13-0.041 0.197-0.062 0.268-0.085 0.406-0.129c0.035-0.011 0.069-0.022 0.104-0.033 0.034-0.013 0.069-0.025 0.104-0.038 0.069-0.026 0.139-0.052 0.21-0.078 0.14-0.053 0.284-0.107 0.429-0.162 0.143-0.061 0.288-0.124 0.436-0.188 0.073-0.032 0.147-0.064 0.222-0.096s0.147-0.071 0.221-0.107c0.147-0.073 0.297-0.146 0.448-0.221 0.15-0.077 0.297-0.163 0.448-0.245 0.075-0.042 0.15-0.084 0.226-0.126s0.148-0.091 0.223-0.136c0.148-0.092 0.298-0.185 0.448-0.279 0.59-0.395 1.174-0.834 1.727-1.319 0.549-0.491 1.070-1.023 1.55-1.584 0.473-0.568 0.903-1.165 1.285-1.773 0.376-0.613 0.697-1.239 0.973-1.855 0.064-0.156 0.127-0.311 0.19-0.465 0.030-0.077 0.064-0.152 0.091-0.229s0.054-0.154 0.081-0.23 0.108-0.302 0.156-0.453c0.045-0.151 0.089-0.3 0.133-0.447 0.021-0.074 0.045-0.146 0.063-0.219s0.036-0.146 0.053-0.218c0.035-0.144 0.069-0.286 0.103-0.425 0.027-0.141 0.054-0.279 0.080-0.415 0.013-0.068 0.026-0.135 0.038-0.201 0.006-0.033 0.012-0.066 0.019-0.099 0.005-0.033 0.009-0.066 0.014-0.099 0.018-0.131 0.036-0.259 0.053-0.384 0.009-0.062 0.017-0.124 0.026-0.185s0.012-0.122 0.018-0.182c0.011-0.119 0.021-0.236 0.031-0.349s0.021-0.222 0.023-0.329c0.001-0.017 0.001-0.033 0.002-0.049 1.101-0.005 1.992-0.898 1.992-2 0-0.017-0.001-0.034-0.001-0.051h0.001c0 0-0-0.003-0.001-0.007-0.003-0.121-0.017-0.24-0.041-0.355-0.006-0.055-0.013-0.114-0.021-0.181-0.007-0.059-0.014-0.122-0.022-0.19-0.012-0.080-0.024-0.165-0.037-0.255-0.027-0.18-0.056-0.379-0.091-0.592-0.042-0.201-0.088-0.419-0.136-0.652-0.022-0.116-0.055-0.235-0.087-0.357s-0.065-0.247-0.099-0.375c-0.018-0.064-0.032-0.129-0.053-0.194s-0.041-0.13-0.062-0.197-0.085-0.268-0.129-0.406c-0.011-0.034-0.022-0.069-0.033-0.104-0.013-0.034-0.025-0.069-0.038-0.104-0.026-0.069-0.052-0.139-0.078-0.21-0.053-0.141-0.107-0.284-0.162-0.429-0.061-0.143-0.124-0.288-0.188-0.435-0.032-0.073-0.064-0.147-0.096-0.222s-0.071-0.147-0.107-0.221c-0.073-0.147-0.146-0.297-0.221-0.448-0.077-0.15-0.163-0.297-0.245-0.448-0.042-0.075-0.084-0.15-0.126-0.226s-0.091-0.148-0.136-0.223c-0.092-0.148-0.185-0.298-0.279-0.448-0.395-0.59-0.834-1.174-1.319-1.727-0.491-0.549-1.023-1.070-1.584-1.55-0.568-0.473-1.165-0.903-1.773-1.285-0.613-0.376-1.239-0.697-1.855-0.973-0.156-0.064-0.311-0.127-0.465-0.19-0.077-0.030-0.152-0.063-0.229-0.091s-0.154-0.054-0.23-0.081-0.302-0.108-0.453-0.156c-0.151-0.045-0.3-0.089-0.447-0.133-0.074-0.021-0.146-0.045-0.219-0.063s-0.146-0.036-0.218-0.053c-0.144-0.035-0.286-0.069-0.425-0.103-0.141-0.027-0.279-0.054-0.415-0.080-0.068-0.013-0.135-0.026-0.201-0.038-0.033-0.006-0.066-0.012-0.099-0.019-0.033-0.005-0.066-0.009-0.099-0.014-0.131-0.018-0.259-0.036-0.384-0.053-0.062-0.009-0.124-0.017-0.185-0.026s-0.122-0.012-0.182-0.018c-0.119-0.010-0.236-0.021-0.349-0.031s-0.222-0.021-0.329-0.023c-0.027-0.001-0.052-0.002-0.078-0.003-0.020-1.087-0.907-1.962-1.999-1.962-0.017 0-0.034 0.001-0.051 0.001l-0-0.001c0 0-0.003 0-0.007 0.001-0.121 0.003-0.24 0.017-0.355 0.041-0.055 0.006-0.114 0.013-0.181 0.021-0.059 0.007-0.122 0.014-0.19 0.022-0.080 0.012-0.165 0.024-0.255 0.037-0.18 0.027-0.379 0.056-0.592 0.091-0.201 0.042-0.419 0.088-0.652 0.136-0.116 0.022-0.235 0.056-0.356 0.087s-0.247 0.065-0.375 0.099c-0.064 0.018-0.129 0.032-0.194 0.053s-0.13 0.041-0.197 0.062-0.268 0.085-0.406 0.129c-0.034 0.011-0.069 0.022-0.104 0.033-0.034 0.013-0.069 0.025-0.104 0.038-0.069 0.026-0.139 0.052-0.21 0.078-0.14 0.053-0.284 0.107-0.429 0.162-0.143 0.061-0.288 0.124-0.435 0.188-0.073 0.032-0.147 0.064-0.222 0.096s-0.147 0.071-0.221 0.107c-0.147 0.073-0.297 0.146-0.448 0.221-0.15 0.077-0.297 0.163-0.448 0.245-0.075 0.042-0.15 0.084-0.226 0.126s-0.148 0.091-0.223 0.136c-0.148 0.092-0.298 0.185-0.448 0.279-0.59 0.395-1.174 0.834-1.727 1.319-0.549 0.491-1.070 1.023-1.55 1.584-0.473 0.568-0.903 1.165-1.285 1.773-0.376 0.613-0.697 1.239-0.973 1.855-0.064 0.156-0.127 0.311-0.19 0.465-0.030 0.077-0.063 0.152-0.091 0.229s-0.054 0.154-0.081 0.23-0.108 0.302-0.156 0.453c-0.045 0.151-0.089 0.3-0.132 0.447-0.021 0.074-0.045 0.146-0.063 0.219s-0.036 0.146-0.053 0.218c-0.035 0.144-0.069 0.286-0.103 0.425-0.027 0.141-0.054 0.279-0.080 0.415-0.013 0.068-0.026 0.135-0.038 0.201-0.006 0.033-0.012 0.066-0.019 0.099-0.005 0.033-0.009 0.066-0.014 0.099-0.018 0.131-0.036 0.259-0.053 0.384-0.009 0.062-0.017 0.124-0.026 0.185s-0.012 0.122-0.018 0.182c-0.010 0.119-0.021 0.236-0.031 0.349s-0.021 0.222-0.023 0.329c-0.001 0.017-0.001 0.034-0.002 0.051-1.074 0.035-1.934 0.916-1.934 1.998 0 0.017 0.001 0.034 0.001 0.051zM2.297 14.022c0.001-0.006 0.003-0.012 0.004-0.018 0.020-0.101 0.051-0.204 0.080-0.311s0.059-0.215 0.090-0.327c0.016-0.056 0.029-0.113 0.048-0.169s0.038-0.113 0.057-0.171 0.077-0.233 0.117-0.353c0.010-0.030 0.020-0.060 0.030-0.090 0.012-0.030 0.023-0.060 0.035-0.090 0.023-0.060 0.047-0.121 0.071-0.182 0.047-0.122 0.096-0.246 0.145-0.373 0.055-0.124 0.111-0.25 0.168-0.377 0.028-0.064 0.057-0.128 0.086-0.192s0.064-0.127 0.095-0.191c0.065-0.128 0.13-0.257 0.197-0.388 0.069-0.129 0.145-0.257 0.219-0.387 0.037-0.065 0.074-0.13 0.112-0.195s0.081-0.128 0.121-0.193c0.082-0.128 0.164-0.257 0.247-0.388 0.351-0.509 0.739-1.012 1.167-1.489 0.434-0.472 0.901-0.919 1.394-1.33 0.499-0.404 1.021-0.77 1.552-1.094 0.535-0.319 1.081-0.589 1.617-0.821 0.136-0.053 0.271-0.106 0.404-0.158 0.067-0.025 0.132-0.053 0.199-0.076s0.134-0.045 0.2-0.067 0.262-0.090 0.392-0.129c0.131-0.037 0.26-0.073 0.387-0.109 0.064-0.017 0.126-0.037 0.189-0.052s0.126-0.029 0.189-0.043c0.124-0.028 0.247-0.056 0.367-0.084 0.121-0.021 0.241-0.043 0.358-0.063 0.058-0.010 0.116-0.021 0.173-0.031 0.029-0.005 0.057-0.010 0.085-0.015 0.029-0.003 0.057-0.007 0.085-0.010 0.113-0.014 0.223-0.028 0.331-0.041 0.054-0.007 0.107-0.013 0.159-0.020s0.105-0.008 0.157-0.013c0.103-0.007 0.203-0.015 0.3-0.022s0.191-0.016 0.283-0.016c0.183-0.004 0.354-0.008 0.512-0.012 0.146 0.005 0.28 0.010 0.401 0.014 0.060 0.002 0.116 0.003 0.17 0.005 0.066 0.004 0.128 0.008 0.186 0.012 0.067 0.004 0.127 0.008 0.182 0.012 0.102 0.016 0.206 0.024 0.312 0.024 0.015 0 0.029-0.001 0.044-0.001 0.004 0 0.007 0 0.007 0v-0.001c0.973-0.024 1.773-0.743 1.924-1.68 0.017 0.004 0.033 0.007 0.050 0.011 0.101 0.020 0.204 0.051 0.311 0.080s0.215 0.059 0.327 0.090c0.056 0.016 0.113 0.029 0.169 0.048s0.113 0.038 0.171 0.057 0.233 0.077 0.353 0.117c0.030 0.010 0.060 0.020 0.090 0.030 0.030 0.012 0.060 0.023 0.090 0.035 0.060 0.023 0.121 0.047 0.182 0.071 0.122 0.047 0.246 0.096 0.373 0.145 0.124 0.055 0.25 0.111 0.378 0.168 0.064 0.028 0.128 0.057 0.192 0.086s0.127 0.064 0.191 0.095c0.128 0.065 0.257 0.13 0.388 0.197 0.13 0.069 0.257 0.145 0.387 0.219 0.065 0.037 0.13 0.074 0.195 0.112s0.128 0.081 0.193 0.121c0.128 0.082 0.257 0.164 0.388 0.247 0.509 0.351 1.012 0.739 1.489 1.167 0.472 0.434 0.919 0.901 1.33 1.394 0.404 0.499 0.77 1.021 1.094 1.552 0.319 0.535 0.589 1.081 0.821 1.617 0.053 0.136 0.106 0.271 0.158 0.404 0.025 0.067 0.053 0.132 0.076 0.199s0.045 0.134 0.067 0.2 0.090 0.262 0.129 0.392c0.037 0.131 0.073 0.26 0.109 0.387 0.017 0.064 0.037 0.126 0.052 0.189s0.029 0.126 0.043 0.189c0.028 0.124 0.056 0.247 0.084 0.367 0.021 0.121 0.043 0.241 0.063 0.358 0.010 0.058 0.020 0.116 0.031 0.173 0.005 0.029 0.010 0.057 0.015 0.085 0.003 0.029 0.007 0.057 0.010 0.085 0.014 0.113 0.028 0.223 0.041 0.331 0.007 0.054 0.014 0.107 0.020 0.159s0.008 0.105 0.013 0.157c0.007 0.103 0.015 0.203 0.022 0.3s0.016 0.191 0.016 0.283c0.004 0.183 0.008 0.354 0.012 0.512-0.005 0.146-0.010 0.28-0.014 0.401-0.002 0.060-0.003 0.116-0.005 0.17-0.004 0.066-0.008 0.128-0.012 0.186-0.004 0.067-0.008 0.127-0.012 0.182-0.016 0.102-0.024 0.206-0.024 0.312 0 0.015 0.001 0.029 0.001 0.044-0 0.004-0 0.007-0 0.007h0.001c0.024 0.961 0.726 1.754 1.646 1.918-0.002 0.009-0.004 0.018-0.006 0.028-0.020 0.102-0.051 0.204-0.080 0.311s-0.059 0.215-0.090 0.327c-0.016 0.056-0.029 0.113-0.048 0.169s-0.038 0.113-0.057 0.171-0.077 0.233-0.117 0.353c-0.010 0.030-0.020 0.060-0.030 0.090-0.012 0.030-0.023 0.060-0.035 0.090-0.023 0.060-0.047 0.121-0.071 0.182-0.047 0.122-0.096 0.246-0.145 0.373-0.055 0.124-0.111 0.25-0.169 0.378-0.028 0.064-0.057 0.128-0.086 0.192s-0.064 0.127-0.095 0.191c-0.065 0.128-0.13 0.257-0.197 0.388-0.069 0.129-0.145 0.257-0.219 0.387-0.037 0.065-0.074 0.13-0.112 0.195s-0.081 0.128-0.121 0.193c-0.082 0.128-0.164 0.257-0.247 0.388-0.351 0.509-0.738 1.012-1.167 1.489-0.434 0.472-0.901 0.919-1.394 1.33-0.499 0.404-1.021 0.77-1.552 1.094-0.535 0.319-1.081 0.589-1.617 0.821-0.136 0.053-0.271 0.106-0.404 0.158-0.067 0.025-0.132 0.053-0.199 0.076s-0.134 0.045-0.2 0.067-0.262 0.090-0.392 0.129c-0.131 0.037-0.26 0.073-0.387 0.109-0.064 0.017-0.126 0.037-0.189 0.052s-0.126 0.029-0.189 0.043c-0.124 0.028-0.247 0.056-0.367 0.084-0.122 0.021-0.241 0.043-0.358 0.063-0.058 0.010-0.116 0.021-0.173 0.031-0.029 0.005-0.057 0.010-0.085 0.015-0.029 0.003-0.057 0.007-0.085 0.010-0.113 0.014-0.223 0.028-0.331 0.041-0.054 0.007-0.107 0.014-0.159 0.020s-0.105 0.008-0.157 0.013c-0.103 0.007-0.203 0.015-0.3 0.022s-0.191 0.016-0.283 0.016c-0.183 0.004-0.354 0.008-0.512 0.012-0.146-0.005-0.28-0.010-0.401-0.014-0.060-0.002-0.116-0.003-0.17-0.005-0.066-0.004-0.128-0.008-0.186-0.012-0.067-0.004-0.127-0.008-0.182-0.012-0.102-0.016-0.206-0.024-0.312-0.024-0.015 0-0.029 0.001-0.044 0.001-0.004-0-0.007-0-0.007-0v0.001c-0.969 0.024-1.766 0.737-1.921 1.668-0.1-0.020-0.201-0.050-0.306-0.079-0.106-0.029-0.215-0.059-0.327-0.090-0.056-0.016-0.113-0.029-0.169-0.048s-0.113-0.038-0.171-0.057-0.233-0.077-0.353-0.117c-0.030-0.010-0.060-0.020-0.090-0.030-0.030-0.012-0.060-0.023-0.090-0.035-0.060-0.023-0.121-0.047-0.182-0.071-0.122-0.048-0.246-0.096-0.373-0.145-0.124-0.055-0.25-0.111-0.377-0.168-0.064-0.028-0.128-0.057-0.192-0.086s-0.127-0.064-0.191-0.095c-0.128-0.065-0.257-0.13-0.388-0.197-0.13-0.069-0.257-0.145-0.387-0.219-0.065-0.037-0.13-0.074-0.195-0.112s-0.128-0.081-0.193-0.121c-0.128-0.082-0.257-0.164-0.388-0.247-0.509-0.351-1.012-0.738-1.489-1.166-0.472-0.434-0.919-0.901-1.33-1.394-0.404-0.499-0.77-1.021-1.094-1.552-0.319-0.535-0.589-1.081-0.821-1.617-0.053-0.136-0.106-0.271-0.158-0.404-0.025-0.067-0.053-0.132-0.076-0.199s-0.045-0.134-0.067-0.2-0.090-0.262-0.129-0.392c-0.037-0.131-0.073-0.26-0.109-0.387-0.017-0.064-0.037-0.126-0.052-0.189s-0.029-0.126-0.043-0.189c-0.028-0.124-0.056-0.247-0.084-0.367-0.021-0.121-0.043-0.241-0.063-0.358-0.010-0.058-0.021-0.116-0.031-0.173-0.005-0.029-0.010-0.057-0.015-0.085-0.003-0.029-0.007-0.057-0.010-0.085-0.014-0.113-0.028-0.223-0.041-0.331-0.007-0.054-0.013-0.107-0.020-0.159s-0.008-0.105-0.013-0.157c-0.007-0.103-0.015-0.203-0.022-0.3s-0.016-0.191-0.016-0.283c-0.004-0.183-0.008-0.354-0.012-0.512 0.005-0.146 0.010-0.28 0.014-0.401 0.002-0.060 0.003-0.116 0.005-0.17 0.004-0.066 0.008-0.128 0.012-0.186 0.004-0.067 0.008-0.127 0.012-0.182 0.015-0.102 0.024-0.206 0.024-0.312 0-0.015-0.001-0.029-0.001-0.044 0-0.004 0.001-0.007 0.001-0.007h-0.001c-0.024-0.981-0.754-1.786-1.701-1.927z", + Translate: "M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 30c-1.967 0-3.84-0.407-5.538-1.139l7.286-8.197c0.163-0.183 0.253-0.419 0.253-0.664v-3c0-0.552-0.448-1-1-1-3.531 0-7.256-3.671-7.293-3.707-0.188-0.188-0.442-0.293-0.707-0.293h-4c-0.552 0-1 0.448-1 1v6c0 0.379 0.214 0.725 0.553 0.894l3.447 1.724v5.871c-3.627-2.53-6-6.732-6-11.489 0-2.147 0.484-4.181 1.348-6h3.652c0.265 0 0.52-0.105 0.707-0.293l4-4c0.188-0.188 0.293-0.442 0.293-0.707v-2.419c1.268-0.377 2.61-0.581 4-0.581 2.2 0 4.281 0.508 6.134 1.412-0.13 0.109-0.256 0.224-0.376 0.345-1.133 1.133-1.757 2.64-1.757 4.243s0.624 3.109 1.757 4.243c1.139 1.139 2.663 1.758 4.239 1.758 0.099 0 0.198-0.002 0.297-0.007 0.432 1.619 1.211 5.833-0.263 11.635-0.014 0.055-0.022 0.109-0.026 0.163-2.541 2.596-6.084 4.208-10.004 4.208z", + SEO: "M16 2c8.837 0 16 7.163 16 16 0 6.025-3.331 11.271-8.25 14h-15.499c-4.92-2.729-8.25-7.975-8.25-14 0-8.837 7.163-16 16-16zM25.060 27.060c2.42-2.42 3.753-5.637 3.753-9.060h-2.813v-2h2.657c-0.219-1.406-0.668-2.755-1.33-4h-3.327v-2h2.009c-0.295-0.368-0.611-0.722-0.949-1.060-1.444-1.444-3.173-2.501-5.060-3.119v2.178h-2v-2.658c-0.656-0.102-1.324-0.155-2-0.155s-1.344 0.053-2 0.155v2.658h-2v-2.178c-1.887 0.617-3.615 1.674-5.060 3.119-0.338 0.338-0.654 0.692-0.949 1.060h2.009v2h-3.327c-0.662 1.245-1.111 2.594-1.33 4h2.657v2h-2.813c0 3.422 1.333 6.64 3.753 9.060 0.335 0.335 0.685 0.648 1.049 0.94h6.011l1.143-16h1.714l1.143 16h6.011c0.364-0.292 0.714-0.606 1.049-0.94z", + RAG: "M32 10l-16-8-16 8 16 8 16-8zM16 4.655l10.689 5.345-10.689 5.345-10.689-5.345 10.689-5.345zM28.795 14.398l3.205 1.602-16 8-16-8 3.205-1.602 12.795 6.398zM28.795 20.398l3.205 1.602-16 8-16-8 3.205-1.602 12.795 6.398z", + }, + aiservice: false, + tokenstats: 0, + useragreement: false, + loading: false, + item: data.item, + content: data.content, + urlinfo: data.urlinfo, + labels: data.labels, + settings: data.settings, + kixoteSettings: {}, + settingsSaved: false, + command: '', + } + }, + mounted() { + + eventBus.$on('kiExit', this.stopKixote); + + eventBus.$on('kiScrollBottom', this.scrollToBottom); + + eventBus.$on('switchLoading', this.switchLoading); + + eventBus.$on('updateKixoteSettings', this.updateKixoteSettings); + + eventBus.$on('storeKixoteSettings', this.storeKixoteSettings); + + eventBus.$on('agreetoservice', this.switchAgreement); + }, + watch: { + showKixote(newValue) { + if (newValue) { + this.loadKixoteSettings(); + this.loadContent(); + this.loadTokenStats(); + } + } + }, + computed: { + currentTabComponent: function () + { + return 'tab-' + this.currentTab.toLowerCase() + } + }, + methods: { + loadKixoteSettings() + { + self = this; + + tmaxios.get('/api/v1/kixotesettings',{ + params: { + 'url': data.urlinfo.route + } + }) + .then(function (response) + { + if (response.data.settings) + { + self.kixoteSettings = response.data.settings; + } + }) + .catch(function (error) + { + if(error.response) + { + } + }); + }, + loadContent() + { + self = this; + + tmaxios.get('/api/v1/article/content',{ + params: { + 'url': data.urlinfo.route, + 'draft': true + } + }) + .then(function (response) + { + if (response.data.content) + { + self.content = response.data.content; + } + }) + .catch(function (error) + { + if(error.response) + { + } + }); + }, + loadTokenStats() + { + self = this; + + tmaxios.get('/api/v1/tokenstats',{ + params: { + 'url': data.urlinfo.route, + } + }) + .then(function (response) + { + if (response.data) + { + self.aiservice = response.data.aiservice; + self.tokenstats = response.data.tokenstats; + self.useragreement = response.data.useragreement; + } + }) + .catch(function (error) + { + if(error.response) + { + console.info(response); + } + }); + }, + startKixote() + { + this.showKixote = true; + }, + stopKixote() + { + this.showKixote = false; + this.currentTab = 'Admin'; + this.content = ''; + }, + switchLoading() + { + this.loading = !this.loading; + }, + switchAgreement() + { + this.useragreement = true; + }, + selectTab(tab) + { + alert("Select Tab") + }, + scrollToBottom() + { + this.$nextTick(() => { + const displayRef = this.$refs.kdisplay; + displayRef.scrollTop = displayRef.scrollHeight; + }); + }, + updateKixoteSettings(newSettings) + { + this.settingsSaved = false; + this.kixoteSettings = { ...this.kixoteSettings, ...newSettings }; // ✅ Merge settings + }, + storeKixoteSettings() + { + self = this; + + tmaxios.put('/api/v1/kixotesettings',{ + 'url': data.urlinfo.route, + 'kixotesettings': this.kixoteSettings + }) + .then(function (response) + { + self.settingsSaved = true; + self.kixoteSettings = response.data.kixotesettings; + }) + .catch(function (error) + { + if(error.response) + { + self.kixoteSettings = error.response.data.kixotesettings; + } + }); + } + }, +}) + +kixote.component('tab-generate', { + props: ['content', 'item', 'labels', 'urlinfo', 'settings', 'kixoteSettings', 'settingsSaved', 'aiservice', 'useragreement', 'tokenstats'], + data: function () { + return { + tabs: [ + { value: 'article', name: 'Article' }, + { value: 'prompts', name: 'Prompts' }, +/* { value: 'tone', name: 'Tone' }, */ + ], + currentTab: 'article', + originalmd: '', + activeversion: 0, + versions: [], + prompt: '', + showFocusButton: false, + buttonPosition: { top: 0, left: 0 }, + selection: { start: 0, end: 0, text: '' }, + editPrompt: '', + addNewPrompt: false, + newPrompt: { + title: '', + content: '', + active: true, + system: false + }, + titleError: false, + bodyError: false, + currentFilter: 'all', + article: '', + index: '', + }; + }, + template: `
+ +
+
+

Your AI Assistant for Typemill

+

+ To get started with AI-powered assistance, go to System Settings, open the AI tab, and follow these steps: +

+
    +
  1. Select an AI service.
  2. +
  3. Choose a model.
  4. +
  5. Enter your API key.
  6. +
+

Once set up, you can start using AI assistance right away!

+
+
+ +
+ +
+
+ +
+
Activate {{aiservice}}
+ +
+ +
+
+ +
+ +
    +
  • + {{ action.name }} +
  • +
+ +
+ + +
+ + + + + + +
+
    +
  • + +
  • +
+
+ +
+
+
+ + +
+
+ Ki> + + + | + +
+
+ + +
+
    +
  • + +
  • +
+
+ +
+ +
+
+
+ +
+ Filter: + + +
+ +
+ +
+
+
+ +
+ {{ titleError }} + + {{ bodyError }} + +
+
+
+
+
+
+ +
+
+ + + ✔ saved + + +
+
+ + +
+
+
+ {{ prompttemplate.errors.title }} + + {{ prompttemplate.errors.body }} +
+
+
+
+ +
+

Mimic your tone tab content here.

+
+ +
+
+ +
+

Content Generation only works on content pages. You are currently in the settings area.

+
+ +
`, + mounted: function() + { + this.initAutosize(); + + if(this.versions.length == 0) + { + this.initializeContent() + } + }, + computed: { + promptlistactive() + { + return Object.values(this.kixoteSettings?.promptlist || {}).filter(prompt => prompt.active); + }, + promptlistsystem() + { + return Object.fromEntries( + Object.entries(this.kixoteSettings?.promptlist || {}).filter(([key, prompt]) => prompt.system) + ); + }, + promptlistuser() + { + return Object.fromEntries( + Object.entries(this.kixoteSettings?.promptlist || {}).filter(([key, prompt]) => !prompt.system) + ); + }, + filteredPrompts() + { + if(this.currentFilter === 'system') + { + return this.promptlistsystem; + } + else if (this.currentFilter === 'user') + { + return this.promptlistuser; + } + else + { + return this.kixoteSettings.promptlist; + } + } + + }, + methods: { + initAutosize() + { + let kieditor = this.$refs["kieditor"]; + let prompteditor = this.$refs["prompteditor"]; + + if (kieditor) + { + autosize(kieditor); + } + if (prompteditor) + { + autosize(prompteditor); + } + }, + agreeTo(aiservice) + { + var self = this; + + tmaxios.post('/api/v1/agreetoaiservice',{ + 'aiservice': aiservice + }) + .then(function (response) + { + eventBus.$emit('agreetoservice'); + self.$nextTick(() => { + self.resizeAiEditor(); + }); + }) + }, + setCurrentTab(tabValue) + { + this.currentTab = tabValue; + + if(tabValue == 'article') + { + this.resizeAiEditor(); + } + }, + initializeContent() + { + let markdown = ''; + + for(block in this.content) + { + markdown += this.content[block].markdown + '\n\n'; + } + this.originalmd = markdown; + this.versions.push(markdown); + this.resizeAiEditor(); + }, + resizeAiEditor() + { + this.$nextTick(() => { + let kieditor = this.$refs["kieditor"]; + if (kieditor) + { + autosize.update(kieditor); + } + }); + }, + resizePromptEditor() + { + this.$nextTick(() => { + let prompteditor = this.$refs["prompteditor"]; + if (prompteditor) + { + autosize.update(prompteditor); + } + }); + }, + usePrompt(index) + { + this.prompt = this.promptlistactive[index].content; + this.resizePromptEditor(); + }, + switchVersion(index) + { + this.activeversion = index; + this.resizeAiEditor(); + }, + submitPrompt() + { + var self = this; + eventBus.$emit('switchLoading'); + + tmaxios.post('/api/v1/chatgpt',{ + 'prompt': this.prompt, + 'article': this.versions[this.activeversion] + }) + .then(function (response) + { + eventBus.$emit('switchLoading'); + if (response.data.message === 'Success') + { + let answer = response.data.data.choices[0].message.content; + answer = answer.replace(/<\/?focus>/g, ''); + self.versions.push(answer); + self.activeversion = self.versions.length-1; + self.prompt = ''; + self.resizePromptEditor(); + self.resizeAiEditor(); + } + }) + .catch(function (error) + { + eventBus.$emit('switchLoading'); + if(error.response) + { + self.disabled = false; + self.message = handleErrorMessage(error); + self.messageClass = 'bg-rose-500'; + self.licensemessage = error.response.data.message; + if(error.response.data.errors !== undefined) + { + self.errors = error.response.data.errors; + } + } + }); + }, + handleKeydown(event) + { + if (event.key === 'Enter' && !event.shiftKey) + { + event.preventDefault(); + this.submitPrompt(); + } + else if (event.key === 'Enter' && event.shiftKey) + { + // Allow line break + const textarea = event.target; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + textarea.value = textarea.value.slice(0, start) + '\n' + textarea.value.slice(end); + textarea.selectionStart = textarea.selectionEnd = start + 1; + + let prompteditor = this.$refs["prompteditor"]; + autosize.update(prompteditor); + + event.preventDefault(); + } + }, + detectSelection(event) + { + const textarea = this.$refs.kieditor; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value.substring(start, end); + + if (text.length > 0) + { + this.selection = { start, end, text }; + this.showFocusButton = true; + this.buttonPosition = { + top: event.offsetY-20, + left: event.offsetX, + }; + } + else + { + this.showFocusButton = false; + } + }, + wrapInFocus() + { + const textarea = this.$refs.kieditor; + const { start, end, text } = this.selection; + + if (!text) return; + + const newText = + textarea.value.substring(0, start) + + `${text}` + + textarea.value.substring(end); + + this.versions[this.activeversion] = newText; + this.showFocusButton = false; + }, + storeArticle(append = false) + { + var self = this; + + var content = this.versions[this.activeversion]; + var title = 'Title missing'; + + var regex = /^#(?!#)([^#\n]+)/m; + var match = content.match(regex); + if (match) + { + var title = '# ' + match[1]; + var content = content.replace(regex, ''); + } + + tmaxios.put('/api/v1/draft',{ + 'url': data.urlinfo.route, + 'item_id': this.item.keyPath, + 'title': title.trim(), + 'body': content.trim() + }) + .then(function (response) + { + location.reload(); + }) + .catch(function (error) + { + if(error.response) + { + let message = handleErrorMessage(error); + if(message) + { + eventBus.$emit('publishermessage', message); + } + } + }); + }, + slugify(text) + { + return text + .toLowerCase() // Convert to lowercase + .replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric characters with '-' + .replace(/-+/g, '-') // Remove multiple dashes + .trim(); // Trim leading/trailing dashes + }, + updateSettings(newSettings) + { + eventBus.$emit('updateKixoteSettings', newSettings); // Emit event + }, + validateTitle(title) + { + const titleRegex = /^[a-zA-Z0-9 ]{0,20}$/; + if (!titleRegex.test(title)) + { + this.titleError = "Title can only contain letters, numbers, and spaces (max 20 chars)."; + } + else + { + this.titleError = false; + } + }, + validateBody(body) + { + const bodyRegex = /<\/?[^>]+(>|$)/g; + if (bodyRegex.test(body)) + { + this.bodyError = "HTML and script tags are not allowed."; + } + else + { + this.bodyError = false; + } + }, + savePrompts() + { + if (!this.titleError && !this.bodyError) + { + eventBus.$emit('storeKixoteSettings'); + } + }, + updatePrompt(kixoteSettings, promptname) + { + if(kixoteSettings.promptlist[promptname] != undefined) + { + kixoteSettings.promptlist[promptname].errors = {}; + + this.validateTitle(kixoteSettings.promptlist[promptname].title); + kixoteSettings.promptlist[promptname].errors.title = this.titleError; + + this.validateBody(kixoteSettings.promptlist[promptname].content); + kixoteSettings.promptlist[promptname].errors.body = this.bodyError; + + this.updateSettings(kixoteSettings); + } + }, + deletePrompt(name) + { + var promptlist = this.kixoteSettings.promptlist; + + delete promptlist[name]; + + this.updateSettings(promptlist); + + eventBus.$emit('storeKixoteSettings'); + + }, + saveNewPrompt() + { + if (this.titleError || this.bodyError) + { + return false; + } + + var promptlist = this.kixoteSettings.promptlist; + var promptkey = this.slugify(this.newPrompt.title); + promptlist[promptkey] = { + title: this.newPrompt.title, + content: this.newPrompt.content, + active: this.newPrompt.active, + system: this.newPrompt.system + }; + + this.newPrompt = { + title: '', + content: '', + active: true, + system: false + }; + + this.addNewPrompt = false; + this.updateSettings(promptlist); + eventBus.$emit('storeKixoteSettings'); + }, + exit() + { + eventBus.$emit('kiExit'); + }, + } +}) // publish tree // unpublish tree @@ -172,52 +1145,52 @@ const kixoteCommands = [ }, answer: ['Save content...'], }, -*/ ]; +*/ + ]; -const kixote = Vue.createApp({ - template: `
- -
-
-

Hello, I am Kixote from Typemill. How can I help?

-
-
-
-
-
-
-
- -
-
-
-
-
-
-

- Ki> - - -

-
-
-

Enter "help" to see a list of commands

-
-
-
-
`, - data() { + + +kixote.component('tab-admin', { + props: [], + data: function () { return { - showKixote: false, messenger: [], messengerIndex: false, command: '', params: false, } }, - mounted() { - - this.clear(); + template: `
+
+

Hello, I am Kixote from Typemill. How can I help?

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+

+ Ki> + + +

+
+
+

Enter "help" to see a list of commands

+
+
+
`, + mounted: function() + { +// this.clear(); eventBus.$on('answer', messages => { let lastKey = this.messenger.length - 1; @@ -239,47 +1212,32 @@ const kixote = Vue.createApp({ let lastKey = this.messenger.length - 1; this.messenger[lastKey].storable = data; }); + + this.focusOnInput(); }, methods: { + exit() + { + eventBus.$emit('kiExit'); + }, clear() { this.messenger = []; this.params = false; this.command = ''; }, - startKixote() - { - this.clear(); - this.showKixote = true; - this.focusOnInput(); - }, - stopKixote() - { - this.clear(); - this.showKixote = false; - }, focusOnInput() { this.$nextTick(() => { - if(this.showKixote) - { - const inputRef = this.$refs.kinput; - inputRef.focus(); - } + const inputRef = this.$refs.kinput; + inputRef.focus(); }); - }, - scrollToBottom() - { - this.$nextTick(() => { - const displayRef = this.$refs.kdisplay; - displayRef.scrollTop = displayRef.scrollHeight; - }); - }, + }, finishCommand() { this.command = ''; this.focusOnInput(); - this.scrollToBottom(); + eventBus.$emit('kiScrollBottom'); }, submitInlineCommand(command, index) { @@ -302,7 +1260,7 @@ const kixote = Vue.createApp({ if(this.command == 'exit') { - this.stopKixote(); + this.exit(); return; } @@ -351,7 +1309,7 @@ const kixote = Vue.createApp({ if(!commandObject) { - message.answer.push('Command not found. Type "help" to see a list of available commands. With a KI plugin, you can also use prompts.'); + message.answer.push('Command not found. Type "help" to see a list of available commands.'); this.messenger.push(message); @@ -459,14 +1417,6 @@ const kixote = Vue.createApp({ { if(!params[index].value) { - // TODO: validate the param - console.info("validate params"); - - console.info("return error message"); - - console.info("repeat current question (set next to index)."); - - console.info("if valid, set value and set next to index +1"); // set param if valid this.params[index].value = this.command; @@ -480,6 +1430,102 @@ const kixote = Vue.createApp({ } return false; - }, + } + } +}) + + +kixote.component('tab-translate', { + props: [], + data: function () { + return { + } }, + template: `
+

Translation Component

+
`, + mounted: function() + { + }, + methods: { + selectComponent: function(type) + { + } + } +}) + +kixote.component('tab-automate', { + props: [], + data: function () { + return { + } + }, + template: `
+

Automation Component

+
`, + mounted: function() + { + }, + methods: { + selectComponent: function(type) + { + } + } +}) + +kixote.component('tab-seo', { + props: [], + data: function () { + return { + } + }, + template: `
+

SEO Component

+
`, + mounted: function() + { + }, + methods: { + selectComponent: function(type) + { + } + } +}) + +kixote.component('tab-rag', { + props: [], + data: function () { + return { + } + }, + template: `
+

Retrieval Augmented Generation

+
`, + mounted: function() + { + }, + methods: { + selectComponent: function(type) + { + } + } +}) + +kixote.component('tab-token', { + props: [], + data: function () { + return { + } + }, + template: `
+

Token Overview Component

+
`, + mounted: function() + { + }, + methods: { + selectComponent: function(type) + { + } + } }) \ No newline at end of file diff --git a/system/typemill/author/js/vue-shared.js b/system/typemill/author/js/vue-shared.js index 48fed1c..ac2c501 100644 --- a/system/typemill/author/js/vue-shared.js +++ b/system/typemill/author/js/vue-shared.js @@ -51,15 +51,18 @@ const translatefilter = { .replace(/^_+|_+$/g, "") // Trim underscores from the start and end of the string .toUpperCase(); // Convert to uppercase + let translation_value = data.labels[translation_key]; + if(!translation_value || translation_value.length === 0) { - return value - } - else - { - return data.labels[translation_key] + translation_value = value; } + + /* process markdown links */ + translation_value = translation_value.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + return translation_value; } } diff --git a/system/typemill/author/js/vue-user.js b/system/typemill/author/js/vue-user.js index fa160b5..eabcffd 100644 --- a/system/typemill/author/js/vue-user.js +++ b/system/typemill/author/js/vue-user.js @@ -2,6 +2,7 @@ const app = Vue.createApp({ template: `
+ {{formdata}}
{{ $filters.translate(fieldDefinition.legend) }} diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index f25e0ba..0c01b5a 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -20,6 +20,7 @@ use Typemill\Controllers\ControllerApiAuthorBlock; use Typemill\Controllers\ControllerApiAuthorMeta; use Typemill\Controllers\ControllerApiAuthorShortcode; use Typemill\Controllers\ControllerApiTestmail; +use Typemill\Controllers\ControllerApiKixote; $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { @@ -88,11 +89,18 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { $group->post('/meta', ControllerApiAuthorMeta::class . ':updateMeta')->setName('api.metadata.update')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # KIXOTE + $group->get('/kixotesettings', ControllerApiKixote::class . ':getKixoteSettings')->setName('api.kixotesettings.get')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author + $group->put('/kixotesettings', ControllerApiKixote::class . ':updateKixoteSettings')->setName('api.kixotesettings.put')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author $group->get('/securitylog', ControllerApiGlobals::class . ':showSecurityLog')->setName('api.securitylog.show')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->delete('/securitylog', ControllerApiGlobals::class . ':deleteSecurityLog')->setName('api.securitylog.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->delete('/cache', ControllerApiGlobals::class . ':deleteCache')->setName('api.cache.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->delete('/clearnavigation', ControllerApiGlobals::class . ':clearNavigation')->setName('api.navigation.clear')->add(new ApiAuthorization($acl, 'system', 'update')); # manager + # KIXOTE Remote Services + $group->get('/tokenstats', ControllerApiKixote::class . ':getTokenStats')->setName('api.kixote.tokenstats')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author + $group->post('/agreetoaiservice', ControllerApiKixote::class . ':agreeToAiService')->setName('api.kixote.serviceagreement')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author + $group->post('/chatgpt', ControllerApiKixote::class . ':promptChatGPT')->setName('api.kixote.chatgpt')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author + # API USED ONLY EXTERNALLY $group->get('/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member $group->get('/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member @@ -104,6 +112,7 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { })->add(new ApiAuthentication($settings)); + # api-routes from plugins if(isset($routes['api']) && !empty($routes['api'])) { diff --git a/system/typemill/settings/kixote.yaml b/system/typemill/settings/kixote.yaml new file mode 100644 index 0000000..ed8e9d6 --- /dev/null +++ b/system/typemill/settings/kixote.yaml @@ -0,0 +1,41 @@ +promptlist: + brainstorm: + title: 'brainstorm' + content: "Act as an experienced copywriter and brainstorm 10 compelling article ideas based on the provided topic. The goal is to create content that resonates with the target audience and ranks well on search engines. Identify relevant keywords and trends to align with user interests. Each idea should provide valuable insights, answer common questions, or solve specific problems. Craft engaging headlines that capture attention and match user search intent." + active: true + system: true + outline: + title: 'outline' + content: "You are an experienced copywriter skilled in crafting compelling articles tailored to a specific target audience. Conduct in-depth research on the following article idea. Identify the most relevant information, authoritative sources, and key insights. Analyze search intent to determine what readers are looking for when they search for this topic. Define the article’s target audience, ideal length, tone, and structure.\n\nBased on your research, create a detailed structured outline for the article, including:\n\n* A compelling title and meta description.\n* Main headlines (H2, H3) that logically structure the content.\n* Bullet points under each headline, summarizing the key points to be covered.\n\nAfter the outline add a divider list, the headline `Research` and add your research findings." + active: true + system: true + outline2: + title: 'outline2' + content: "You are an experienced copywriter skilled in crafting compelling, well-structured articles tailored to a specific target audience. Based on the provided article idea, develop a comprehensive content outline that includes:\n\n- Title: A compelling, SEO-optimized headline.\n- User Intent: A one-sentence description of the intent, followed by the category in brackets (Informational, Navigational, Transactional, or Commercial).\n- Keywords: A comma-separated list, with the main keyword first, followed by up to five secondary keywords.\n- Target Audience: One sentence who the article is for and what they expect to gain.\n- Ideal Tone & Style: Concise, authoritative, engaging, personal, etc.\n\nBelow that, provide a detailed article structure:\n- Headings (H2, H3): A logical outline that organizes the content.\n- Bullet points under each heading: Summarizing key points to cover." + active: true + system: true + write: + title: 'write' + content: "Act as an experienced copywriter tasked with creating a comprehensive and engaging article based on the provided outline or draft. If details such as keywords, user intent, target audience, tone, or style are included, ensure the article aligns with them. If no outline is given, structure the article logically based on best practices for the topic.\n\nCraft a compelling article with:\n- **An eye-catching, SEO-optimized title** featuring the primary keyword.\n- **A strong introduction** that immediately hooks the reader—use a compelling question, statistic, expert quote, or relatable scenario. Clearly state what the article will cover.\n- **A well-structured body** with clear subheadings (H2, H3) incorporating relevant keywords. Ensure smooth transitions between sections and provide thorough, engaging explanations.\n- **Bullet points or lists** where appropriate to enhance readability but prioritize a natural flow of ideas.\n- **A unique perspective** by integrating expert insights, statistics, case studies, or real-life examples.\n- **Natural keyword integration** that enhances SEO without disrupting readability.\n\nMaintain a tone and style that align with the article’s goals and audience, ensuring clarity, engagement, and credibility." + active: true + system: true + proofread: + title: 'proofread' + content: "Review the text for grammar, spelling, and correct word usage. Correct any errors and ensure that the appropriate words are used, while maintaining the original tone, style, and phrasing. At the end of the article, provide a list of all corrections made." + active: true + system: true + refine: + title: 'refine' + content: "Refine the text to enhance its wording and style. Ensure that the content is readable, clear, concise, and flows naturally. Feel free to rephrase sentences where necessary, but retain the original tone and unique language usage to avoid making the text sound generic." + active: true + system: true + review: + title: 'review' + content: "Review the article below. Evaluate it based on readability, clarity, SEO best practices, engagement, and search intent. Provide actionable suggestions for improvement, focusing on areas where the text can be refined for better flow, clarity, and alignment with SEO goals." + active: true + system: true + mermaid: + title: 'mermaid' + content: "Return pure mermaid syntax for a [pie, x, y] diagram." + active: true + system: true \ No newline at end of file diff --git a/system/typemill/settings/system.yaml b/system/typemill/settings/system.yaml index ad87928..dc765df 100644 --- a/system/typemill/settings/system.yaml +++ b/system/typemill/settings/system.yaml @@ -309,4 +309,35 @@ fieldsetdeveloper: description: "If activated, you can allow login-links with a checkbox in the user profile. This is only available for guest-roles since guests do not have any rights. Login with a link can be helpful if you link from your software to a non-public documentation. Be aware of the low protection that this kind of logins has. If you integrate such links in a SaaS-software, then you should restrict access with login-links to your ips." trustedloginreferrer: type: text - label: "Trusted IPs for the login-link-referrer (comma separated)" \ No newline at end of file + label: "Trusted IPs for the login-link-referrer (comma separated)" +fieldsetai: + type: fieldset + legend: AI + fields: + aiprivacy: + type: paragraph + label: "Privacy Notice" + description: "You can use the following AI features in the Kixote interface. If you enable any AI integrations, user inputs will be sent to external services for processing. Please review the details for each service and ensure compliance with your local data privacy regulations. Each user will be asked for confirmation before using a model in the Kixote AI interface." + aiservice: + type: radio + label: 'Choose an AI service' + options: + none: 'None' +# kixote: 'Kixote (not available yet)' + chatgpt: 'ChatGPT' + chatgptModel: + type: select + label: 'ChatGPT model' + description: "Select the AI model you want to use. Pricing may vary over time, so please check the latest details on [OpenAI's pricing page](https://platform.openai.com/docs/pricing)" + options: + gpt-4o-mini: 'gpt-4o-mini ($0.6 / 1M output tokens)' + gpt-3.5-turbo-0125: 'gpt-3.5-turbo-0125 ($1.50 / 1M output tokens)' + gpt-4o: 'gpt-4o ($10.00 / 1M output tokens)' + o1-mini: 'o1-mini ($12.00 / 1M output tokens)' + o1: 'o1 ($60.00 / 1M output tokens)' + chatgptKey: + type: password + autocomplete: new-password + generator: false + label: 'ChatGPT Api Key' + description: "Enter your ChatGPT API key here. You can generate a new key on [OpenAI's platform](https://platform.openai.com/docs/pricing). For security reasons, your API key is secret and will not be visible again after you leave this page." \ No newline at end of file diff --git a/system/typemill/settings/user.yaml b/system/typemill/settings/user.yaml index de4b4f4..69c739f 100644 --- a/system/typemill/settings/user.yaml +++ b/system/typemill/settings/user.yaml @@ -26,6 +26,12 @@ darkmode: label: 'Darkmode' checkboxlabel: 'Activate the darkmode for me' type: 'checkbox' +aiservices: + name: aiservices + label: 'AI Services' + type: 'checkboxlist' + options: + chatgpt: "Use ChatGPT and accept their terms and conditions." password: name: password label: 'Actual Password' @@ -36,4 +42,4 @@ newpassword: label: 'New Password' type: 'password' autocomplete: 'new-password' - generator: true \ No newline at end of file + generator: true