diff --git a/cache/timer.yaml b/cache/timer.yaml index 0c15a03..e55b790 100644 --- a/cache/timer.yaml +++ b/cache/timer.yaml @@ -1 +1 @@ -licenseupdate: 1743349357 +licenseupdate: 1743531622 diff --git a/system/typemill/Controllers/ControllerApiKixote.php b/system/typemill/Controllers/ControllerApiKixote.php index 8db2fc4..df6df88 100644 --- a/system/typemill/Controllers/ControllerApiKixote.php +++ b/system/typemill/Controllers/ControllerApiKixote.php @@ -15,6 +15,19 @@ class ControllerApiKixote extends Controller { private $error = false; + private function getSystemMessage() + { + $system = '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.'; + + return $system; + } + public function getKixoteSettings(Request $request, Response $response) { $settingsModel = new Settings(); @@ -35,7 +48,6 @@ class ControllerApiKixote extends Controller ])); return $response->withHeader('Content-Type', 'application/json')->withStatus(200); - } public function updateKixoteSettings(Request $request, Response $response) @@ -247,6 +259,199 @@ class ControllerApiKixote extends Controller return $jwt; } + public function prompt(Request $request, Response $response) + { + $params = $request->getParsedBody(); + + 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); + } + + $promptname = $params['name'] ?? ''; + $prompt = $params['prompt'] ?? ''; + $article = $params['article'] ?? ''; + $tone = $params['tone'] ?? ''; + + $aiservice = $this->settings['aiservice'] ?? false; + if(!$aiservice) + { + $response->getBody()->write(json_encode([ + 'message' => 'No ai service is selected.' + ])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + switch ($aiservice) { + case 'chatgpt': + $answer = $this->promptChatGPT($promptname, $prompt, $article, $tone); + break; + + case 'claude': + $answer = $this->promptClaude($promptname, $prompt, $article, $tone); + break; + + default: + $answer = false; + break; + } + + if(!isset($answer) or !$answer) + { + $response->getBody()->write(json_encode([ + 'message' => $this->error + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $response->getBody()->write(json_encode([ + 'message' => 'Success', + 'answer' => $answer, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function promptChatGPT($promptname, $prompt, $article, $tone) + { + # check if user has accepted + + $settingsModel = new Settings(); + $model = $this->settings['chatgptModel'] ?? false; + $apikey = $settingsModel->getSecret('chatgptKey'); + + if (!$model || !$apikey) + { + $this->error = 'Model or api key for chatgpt is missing, please add it in the system settings.'; + return false; + } + + $url = 'https://api.openai.com/v1/chat/completions'; + $authHeader = "Authorization: Bearer $apikey"; + + $postdata = [ + 'model' => $model, + 'messages' => [ + [ + 'role' => 'system', + 'content' => $this->getSystemMessage(), + ], + [ + 'role' => 'user', + 'content' => $prompt . "\n" . $article + ], + ], + 'temperature' => 0.7, + 'max_tokens' => 8000, + ]; + + $apiservice = new ApiCalls(); + $apiResponse = $apiservice->makePostCall($url, $postdata, $authHeader); + + if (!$apiResponse) + { + $this->error = 'Failed to communicate with ChatGPT: ' . $apiservice->getError(); + return false; + } + + $data = json_decode($apiResponse, true); + + if(isset($data['error'])) + { + $this->error = 'ChatGPT returned and error'; + if(isset($data['error']['message'])) + { + $this->error = $data['error']['message']; + } + + return false; + } + + if (!isset($data['choices'][0]['message']['content']) || !is_string($data['choices'][0]['message']['content'])) + { + $this->error = 'ChatGPT did not return a valid answer.'; + return false; + } + + $answer = trim($data['choices'][0]['message']['content']); + + return $answer; + } + + public function promptClaude($promptname, $prompt, $article, $tone) + { + # Check if user has accepted + $settingsModel = new Settings(); + $model = $this->settings['claudeModel'] ?? false; + $apikey = $settingsModel->getSecret('claudeKey'); + + if (!$model || !$apikey) + { + $this->error = 'Model or API key for Claude is missing, please add it in the system settings.'; + return false; + } + + $url = 'https://api.anthropic.com/v1/messages'; + $headers = [ + "x-api-key: $apikey", + "anthropic-version: 2023-06-01" + ]; + + $postdata = [ + 'model' => $model, + 'system' => $this->getSystemMessage(), + 'messages' => [ + [ + 'role' => 'user', + 'content' => $prompt . "\n" . $article + ], + ], + 'temperature' => 0.7, + 'max_tokens' => 8000, + ]; + + $apiservice = new ApiCalls(); + $apiResponse = $apiservice->makePostCall($url, $postdata, $headers); + + if (!$apiResponse) { + $this->error = 'Failed to communicate with Claude: ' . $apiservice->getError(); + return false; + } + + $data = json_decode($apiResponse, true); + + if (isset($data['error'])) + { + $this->error = 'Claude API returned an error'; + if (isset($data['error']['message'])) + { + $this->error = $data['error']['message']; + } + return false; + } + + if (!isset($data['content'][0]['text']) || !is_string($data['content'][0]['text'])) + { + $this->error = 'Claude did not return a valid answer.'; + return false; + } + + return trim($data['content'][0]['text']); + } + + # NOT READY YET public function promptKixote(Request $request, Response $response) { $jwt = $this->getKixoteJWT(); @@ -296,84 +501,4 @@ class ControllerApiKixote extends Controller 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/Models/ApiCalls.php b/system/typemill/Models/ApiCalls.php index eb58354..18ca6da 100644 --- a/system/typemill/Models/ApiCalls.php +++ b/system/typemill/Models/ApiCalls.php @@ -31,7 +31,7 @@ class ApiCalls return $this->makeFileGetContentsCall($url, 'GET', null, $authHeader); } - private function makeCurlCall($url, $method, $data = false, $authHeader = '') + private function makeCurlCall($url, $method, $data = false, $customHeader = '') { $this->error = null; @@ -39,10 +39,7 @@ class ApiCalls "Content-Type: application/json", ]; - if (!empty($authHeader)) - { - $headers[] = $authHeader; - } + $headers = $this->addCustomHeader($customHeader); $curl = curl_init($url); if (defined('CURLSSLOPT_NATIVE_CA') && version_compare(curl_version()['version'], '7.71', '>=')) @@ -78,7 +75,7 @@ class ApiCalls return $response; } - private function makeFileGetContentsCall($url, $method, $data = null, $authHeader = '') + private function makeFileGetContentsCall($url, $method, $data = null, $customHeader = '') { $this->error = null; @@ -86,10 +83,7 @@ class ApiCalls "Content-Type: application/json" ]; - if (!empty($authHeader)) - { - $headers[] = $authHeader; - } + $headers = $this->addCustomHeader($headers, $customHeader); $options = [ 'http' => [ @@ -133,4 +127,24 @@ class ApiCalls return $response; } -} + + private function addCustomHeader(array $header, $customHeader = '') + { + if(!empty($customHeader)) + { + if(is_array($customHeader)) + { + foreach($customHeader as $cHeader) + { + $header[] = $cHeader; + } + } + elseif(is_string($customHeader)) + { + $header[] = $customHeader; + } + } + + return $header; + } +} \ No newline at end of file diff --git a/system/typemill/author/js/vue-kixote.js b/system/typemill/author/js/vue-kixote.js index 7789352..2bc29c0 100644 --- a/system/typemill/author/js/vue-kixote.js +++ b/system/typemill/author/js/vue-kixote.js @@ -332,6 +332,7 @@ kixote.component('tab-generate', { activeversion: 0, versions: [], prompt: '', + promptError: false, showFocusButton: false, buttonPosition: { top: 0, left: 0 }, selection: { start: 0, end: 0, text: '' }, @@ -456,6 +457,7 @@ kixote.component('tab-generate', { +
{{ promptError }}
Ki> @@ -643,7 +645,6 @@ kixote.component('tab-generate', { currentTab(newTab, oldTab) { if (newTab === 'article') { - console.info('article'); this.$nextTick(() => { this.initAutosize(); // Trigger the resizing when switching back to the article tab }); @@ -710,12 +711,12 @@ kixote.component('tab-generate', { { eventBus.$emit('agreetoservice'); self.$nextTick(() => { - self.resizeAiEditor(); + self.initAutosize(); }); }) }, setCurrentTab(tabValue) - { + { this.currentTab = tabValue; if(tabValue == 'article') @@ -768,10 +769,12 @@ kixote.component('tab-generate', { }, submitPrompt() { + this.promptError = false; + var self = this; eventBus.$emit('switchLoading'); - tmaxios.post('/api/v1/chatgpt',{ + tmaxios.post('/api/v1/prompt',{ 'prompt': this.prompt, 'article': this.versions[this.activeversion] }) @@ -780,7 +783,7 @@ kixote.component('tab-generate', { eventBus.$emit('switchLoading'); if (response.data.message === 'Success') { - let answer = response.data.data.choices[0].message.content; + let answer = response.data.answer; answer = answer.replace(/<\/?focus>/g, ''); self.versions.push(answer); self.activeversion = self.versions.length-1; @@ -795,12 +798,11 @@ kixote.component('tab-generate', { if(error.response) { self.disabled = false; - self.message = handleErrorMessage(error); - self.messageClass = 'bg-rose-500'; + self.promptError = handleErrorMessage(error); self.licensemessage = error.response.data.message; if(error.response.data.errors !== undefined) { - self.errors = error.response.data.errors; + self.promptError = error.response.data.errors; } } }); diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index 0c01b5a..08ed8ea 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -99,7 +99,7 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { # 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 + $group->post('/prompt', ControllerApiKixote::class . ':prompt')->setName('api.kixote.prompt')->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 diff --git a/system/typemill/settings/system.yaml b/system/typemill/settings/system.yaml index e47928b..1a4da34 100644 --- a/system/typemill/settings/system.yaml +++ b/system/typemill/settings/system.yaml @@ -325,23 +325,35 @@ fieldsetai: none: 'None' # kixote: 'Kixote (not available yet)' chatgpt: 'ChatGPT' + claude: 'Claude' 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)' + gpt-4o-mini: 'gpt-4o-mini ($0.15 / $0.6)' + gpt-4o: 'gpt-4o ($2.50 / $10)' + gpt-3.5-turbo-0125: 'gpt-3.5-turbo ($0.50 / $1.50)' + o1-mini: 'o1-mini ($1.10 / 4.40)' + o1: 'o1 ($15 / $60.00)' 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." - customsmall: - type: customfields - label: 'Customfield Small' - description: "This is a small standard customfield." + claudeModel: + type: select + label: 'Claude model' + description: "Select the AI model you want to use. Pricing may vary over time, so please check the latest details on [Claude's pricing page](https://docs.anthropic.com/en/docs/about-claude/models/all-models)" + options: + claude-3-7-sonnet-20250219: 'Claude 3.7 Sonnet ($3 / $15)' + claude-3-5-haiku-20241022: 'Claude 3.5 Haiku ($0.80 / $4)' + claude-3-opus-20240229: 'Claude 3 Opus ($15.00 / $75)' + claude-3-haiku-20240307: 'Claude 3 Haiku ($0.25 / $1.25)' + claudeKey: + type: password + autocomplete: new-password + generator: false + label: 'Claude Api Key' + description: "Enter your Claude API key here. You can generate a new key on [Claude's console](https://console.anthropic.com). For security reasons, your API key is secret and will not be visible again after you leave this page."