commit 8d9504d2ade5c849cf6aec8a25109a28712d5290 Author: privet.fun Date: Sat May 27 18:43:21 2023 -0700 Initial commit diff --git a/privet/ailabs/README.md b/privet/ailabs/README.md new file mode 100644 index 0000000..cc0356a --- /dev/null +++ b/privet/ailabs/README.md @@ -0,0 +1,11 @@ +# AI Labs + +## Installation + +Copy the extension to phpBB/ext/privet/ailabs + +Go to "ACP" > "Customise" > "Extensions" and enable the "AI Labs" extension. + +## License + +[GPLv2](license.txt) diff --git a/privet/ailabs/acp/main_info.php b/privet/ailabs/acp/main_info.php new file mode 100644 index 0000000..5e020c8 --- /dev/null +++ b/privet/ailabs/acp/main_info.php @@ -0,0 +1,30 @@ + '\privet\ailabs\acp\main_module', + 'title' => 'ACP_AILABS_TITLE', + 'modes' => [ + 'settings' => [ + 'title' => 'ACP_AILABS_SETTINGS', + 'auth' => 'ext_privet/ailabs && acl_a_board', + 'cat' => ['ACP_AILABS_TITLE'] + ], + ], + ]; + } +} diff --git a/privet/ailabs/acp/main_module.php b/privet/ailabs/acp/main_module.php new file mode 100644 index 0000000..cbb4791 --- /dev/null +++ b/privet/ailabs/acp/main_module.php @@ -0,0 +1,82 @@ +phpbb_container = $phpbb_container; + $this->request = $request; + $this->template = $template; + + $this->tpl_name = 'acp_ailabs_body'; + + $action = $request->variable('action', ''); + $submit = $request->is_set_post('submit'); + $user_id = $request->variable('user_id', 0); + $username = utf8_normalize_nfc($request->variable('username', '', true)); + + $language = $phpbb_container->get('language'); + $language->add_lang('info_acp_ailabs', 'privet/ailabs'); + + $acp_controller = $this->phpbb_container->get('privet.ailabs.acp_controller'); + + add_form_key('privet_ailabs_settings'); + if ($submit && !check_form_key('privet_ailabs_settings')) { + trigger_error('FORM_INVALID' . adm_back_link($this->u_action), E_USER_WARNING); + } + + $acp_controller->get_acp_data($id, $mode, $action, $submit, $this->u_action); + + switch ($mode) { + case 'settings': + switch ($action) { + case 'add': + case 'edit': + + $this->page_title = ($action == 'add') ? 'ACP_AILABS_TITLE_ADD' : 'ACP_AILABS_TITLE_EDIT'; + $acp_controller->edit_add(); + + return; + break; + + case 'delete': + if (confirm_box(true)) { + $acp_controller->delete($user_id); + } else { + confirm_box(false, $language->lang('ACP_AILABS_DELETED_CONFIRM', $username), build_hidden_fields([ + 'user_id' => $user_id, + 'mode' => $mode, + 'action' => $action, + ])); + } + break; + } + + $this->page_title = 'ACP_AILABS_TITLE_VIEW'; + $acp_controller->acp_ailabs_main(); + break; + default; + } + } +} diff --git a/privet/ailabs/adm/style/acp_ailabs_body.html b/privet/ailabs/adm/style/acp_ailabs_body.html new file mode 100644 index 0000000..1f1d0d0 --- /dev/null +++ b/privet/ailabs/adm/style/acp_ailabs_body.html @@ -0,0 +1,267 @@ +{% INCLUDE 'overall_header.html' %} + +{% INCLUDEJS '@privet_ailabs/js/chosen.jquery.min.js' %} + +{% INCLUDECSS '@privet_ailabs/chosen.min.css' %} + + + +

{{ lang('ACP_AILABS_TITLE') }}

+ +

+ {{ lang('LBL_AILABS_SETTINGS_DESC') }} + + {% if U_AILABS_VEIW %} + {{ lang('ACP_AILABS_ADD') }} +
+
+ {% endif %} + + {% if U_AILABS_ADD_EDIT %} + « {{ lang('BACK') }} + {% endif %} +

+ +{% if S_ERROR %} +
+

{{ lang('WARNING') }}

+

{{ S_ERROR }}

+
+{% endif %} + +{% if U_AILABS_ADD_EDIT %} + +
+ + + +
+
+
+ +
+
+ + +
+
+
+
+
+
[ {L_FIND_USERNAME} ] +
+
+
+
+ +
+
+ +
+
+
+
+ +
{{ lang('LBL_AILABS_CONFIG_EXPLAIN') }} +

+
+
+ +
+
+
+
+ +
{{ lang('LBL_AILABS_TEMPLATE_EXPLAIN') }} +

+
+
+ +
+
+
+ +
+ {{ lang('LBL_AILABS_REPLY_POST_FORUMS') }} + {{ lang('LBL_AILABS_REPLY_POST_FORUMS_EXPLAIN') }} + +
+ +
+ {{ lang('LBL_AILABS_REPLY_QUOTE_FORUMS') }} + {{ lang('LBL_AILABS_REPLY_QUOTE_FORUMS_EXPLAIN') }} + +
+ +
+ {{ lang('ACP_SUBMIT_CHANGES') }} +

+ + + + + + {S_FORM_TOKEN} +

+
+
+ +{% endif %} + +{% if U_AILABS_VEIW %} + + + + + + + + + + + + + + {% for user in U_AILABS_USERS %} + + + + + + + + + {% endfor %} + +
{{ lang('LBL_AILABS_USERNAME') }}{{ lang('LBL_AILABS_CONTROLLER') }}{{ lang('LBL_AILABS_REPLY_POST_FORUMS') }}{{ lang('LBL_AILABS_REPLY_QUOTE_FORUMS') }}{{ lang('LBL_AILABS_ENABLED') }}
{{ user.username }}{{ user.controller }}{{ user.forums_post_names }}{{ user.forums_mention_names }} + + {{ ICON_EDIT }} + {{ ICON_DELETE }} +
+ +{% endif %} + +{% INCLUDE 'overall_footer.html' %} \ No newline at end of file diff --git a/privet/ailabs/composer.json b/privet/ailabs/composer.json new file mode 100644 index 0000000..8677265 --- /dev/null +++ b/privet/ailabs/composer.json @@ -0,0 +1,33 @@ +{ + "name": "privet/ailabs", + "type": "phpbb-extension", + "description": "AI Labs", + "homepage": "https://privet.fun", + "version": "1.0.0", + "time": "2023-02-12", + "keywords": [ + "phpbb", + "extension", + "ailabs", + "chatgpt", + "ai" + ], + "license": "GPL-2.0-only", + "authors": [ + { + "name": "privet.fun", + "homepage": "https://privet.fun" + } + ], + "require": { + "php": ">=7.1", + "phpbb/phpbb": ">=3.2", + "composer/installers": "~1.0.0" + }, + "extra": { + "display-name": "AI Labs", + "soft-require": { + "phpbb/phpbb": ">=3.2" + } + } +} \ No newline at end of file diff --git a/privet/ailabs/config/routing.yml b/privet/ailabs/config/routing.yml new file mode 100644 index 0000000..14226d2 --- /dev/null +++ b/privet/ailabs/config/routing.yml @@ -0,0 +1,21 @@ +privet_chatgpt_page: + path: /ailabs/chatgpt + defaults: { _controller: privet.ailabs.controller_chatgpt:execute } + +privet_dalle_page: + path: /ailabs/dalle + defaults: { _controller: privet.ailabs.controller_dalle:execute } + +privet_stablediffusion_page: + path: /ailabs/stablediffusion + defaults: { _controller: privet.ailabs.controller_stablediffusion:execute } + +privet_scriptexecute_page: + path: /ailabs/scriptexecute + defaults: { _controller: privet.ailabs.controller_scriptexecute:execute } + +privet_ailabs_view_log_controller_page: + path: /ailabs/log/{post_id} + defaults: { _controller: privet.ailabs.controller_log:view_log } + requirements: + post_id: \d+ diff --git a/privet/ailabs/config/services.yml b/privet/ailabs/config/services.yml new file mode 100644 index 0000000..b5745b4 --- /dev/null +++ b/privet/ailabs/config/services.yml @@ -0,0 +1,119 @@ +imports: + - { resource: tables.yml } + +services: + privet.ailabs.acp_controller: + class: privet\ailabs\controller\acp_controller + arguments: + - '@config' + - '@dbal.conn' + - '@language' + - '@log' + - '@notification_manager' + - '@pagination' + - '@request' + - '@template' + - '@user' + - '%core.root_path%' + - '%privet.ailabs.tables.users%' + + privet.ailabs.listener: + class: privet\ailabs\event\listener + arguments: + - '@user' + - '@auth' + - '@dbal.conn' + - '@controller.helper' + - '@language' + - '@request' + - '%core.root_path%' + - '%core.php_ext%' + - '%privet.ailabs.tables.users%' + - '%privet.ailabs.tables.jobs%' + tags: + - { name: event.listener } + + privet.ailabs.controller_chatgpt: + class: privet\ailabs\controller\chatgpt + arguments: + - '@auth' + - '@config' + - '@dbal.conn' + - '@controller.helper' + - '@language' + - '@request' + - '@template' + - '@user' + - '@service_container' + - '%core.php_ext%' + - '%core.root_path%' + - '%privet.ailabs.tables.users%' + - '%privet.ailabs.tables.jobs%' + + privet.ailabs.controller_dalle: + class: privet\ailabs\controller\dalle + arguments: + - '@auth' + - '@config' + - '@dbal.conn' + - '@controller.helper' + - '@language' + - '@request' + - '@template' + - '@user' + - '@service_container' + - '%core.php_ext%' + - '%core.root_path%' + - '%privet.ailabs.tables.users%' + - '%privet.ailabs.tables.jobs%' + + privet.ailabs.controller_stablediffusion: + class: privet\ailabs\controller\stablediffusion + arguments: + - '@auth' + - '@config' + - '@dbal.conn' + - '@controller.helper' + - '@language' + - '@request' + - '@template' + - '@user' + - '@service_container' + - '%core.php_ext%' + - '%core.root_path%' + - '%privet.ailabs.tables.users%' + - '%privet.ailabs.tables.jobs%' + + privet.ailabs.controller_scriptexecute: + class: privet\ailabs\controller\scriptexecute + arguments: + - '@auth' + - '@config' + - '@dbal.conn' + - '@controller.helper' + - '@language' + - '@request' + - '@template' + - '@user' + - '@service_container' + - '%core.php_ext%' + - '%core.root_path%' + - '%privet.ailabs.tables.users%' + - '%privet.ailabs.tables.jobs%' + + privet.ailabs.controller_log: + class: privet\ailabs\controller\log + arguments: + - '@auth' + - '@config' + - '@dbal.conn' + - '@controller.helper' + - '@language' + - '@request' + - '@template' + - '@user' + - '@service_container' + - '%core.php_ext%' + - '%core.root_path%' + - '%privet.ailabs.tables.users%' + - '%privet.ailabs.tables.jobs%' \ No newline at end of file diff --git a/privet/ailabs/config/tables.yml b/privet/ailabs/config/tables.yml new file mode 100644 index 0000000..024d5fc --- /dev/null +++ b/privet/ailabs/config/tables.yml @@ -0,0 +1,3 @@ +parameters: + privet.ailabs.tables.users: '%core.table_prefix%ailabs_users' + privet.ailabs.tables.jobs: '%core.table_prefix%ailabs_jobs' diff --git a/privet/ailabs/controller/acp_controller.php b/privet/ailabs/controller/acp_controller.php new file mode 100644 index 0000000..d02a5e7 --- /dev/null +++ b/privet/ailabs/controller/acp_controller.php @@ -0,0 +1,305 @@ +config = $config; + $this->db = $db; + $this->language = $language; + $this->log = $log; + $this->notification_manager = $notification_manager; + $this->pagination = $pagination; + $this->request = $request; + $this->template = $template; + $this->user = $user; + $this->root_path = $root_path; + $this->ailabs_users_table = $ailabs_users_table; + } + + public function get_acp_data($id, $mode, $action, $submit, $u_action) + { + $this->id = $id; + $this->mode = $mode; + $this->action = $action; + $this->submit = $submit; + $this->u_action = $u_action; + $this->user_id = $this->request->variable('user_id', 0); + $this->ailabs_enabled = !empty($this->config['ailabs_enabled']) ? true : false; + } + + public function edit_add() + { + $username = utf8_normalize_nfc($this->request->variable('ailabs_username', '', true)); + $new_user_id = $this->find_user_id($username); + + if ($this->action == 'edit' && empty($this->user_id)) { + trigger_error($this->language->lang('AILABS_USER_EMPTY') . adm_back_link($this->u_action), E_USER_WARNING); + } + + $edit = []; + + $data = [ + 'user_id' => $new_user_id, + 'controller' => $this->request->variable('ailabs_controller', ''), + 'config' => utf8_normalize_nfc($this->request->variable('ailabs_config', '', true)), + 'template' => utf8_normalize_nfc($this->request->variable('ailabs_template', '', true)), + 'forums_post' => $this->request->variable('ailabs_forums_post', ''), + 'forums_mention' => $this->request->variable('ailabs_forums_mention', ''), + 'enabled' => $this->request->variable('ailabs_enabled', true), + ]; + + if ($this->submit) { + + if (empty($new_user_id)) { + trigger_error($this->language->lang('AILABS_USER_NOT_FOUND', $username) . adm_back_link($this->u_action), E_USER_WARNING); + } + + $configs_count = $this->count_configs($new_user_id); + + if (($this->action == 'add' && $configs_count > 0) || + ($this->action == 'edit' && $new_user_id != $this->user_id && $configs_count > 0) + ) { + trigger_error($this->language->lang('AILABS_USER_ALREADY_CONFIGURED', $username) . adm_back_link($this->u_action), E_USER_WARNING); + } + + if (empty($data['forums_post']) && empty($data['forums_mention'])) { + trigger_error($this->language->lang('AILABS_SPECIFY_POST_OR_MENTION') . adm_back_link($this->u_action), E_USER_WARNING); + } + + if (!isset($error)) { + $sql_ary = [ + 'user_id' => (int) $data['user_id'], + 'controller' => (string) $data['controller'], + 'config' => (string) html_entity_decode($data['config']), + 'template' => (string) html_entity_decode($data['template']), + 'forums_post' => (string) html_entity_decode($data['forums_post']), + 'forums_mention' => (string) html_entity_decode($data['forums_mention']), + 'enabled' => (bool) $data['enabled'] + ]; + + if ($this->action == 'add') { + $sql = 'INSERT INTO ' . $this->ailabs_users_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary); + $this->db->sql_query($sql); + + $log_lang = 'LOG_ACP_AILABS_ADDED'; + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, $log_lang, false, [$data['user_id']]); + + trigger_error($this->language->lang('ACP_AILABS_ADDED') . adm_back_link($this->u_action), E_USER_NOTICE); + } else if ($this->action == 'edit') { + $sql = 'UPDATE ' . $this->ailabs_users_table . ' + SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' + WHERE user_id = ' . (int) $this->user_id; + $this->db->sql_query($sql); + + $log_lang = 'LOG_ACP_AILABS_EDITED'; + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, $log_lang, false, [$data['user_id']]); + + trigger_error($this->language->lang('ACP_AILABS_UPDATED') . adm_back_link($this->u_action), E_USER_NOTICE); + } + } + } else { + if ($this->action == 'edit') { + $sql = 'SELECT * FROM ' . $this->ailabs_users_table . ' WHERE user_id = ' . (int) $this->user_id; + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + + $edit = [ + 'ailabs_user_id' => (int) $row['user_id'], + 'ailabs_username' => (string) $this->find_user_name((int) $this->user_id), + 'ailabs_controller' => (string) $row['controller'], + 'ailabs_config' => (string) $row['config'], + 'ailabs_template' => (string) $row['template'], + 'ailabs_forums_post' => (string) $row['forums_post'], + 'ailabs_forums_mention' => (string) $row['forums_mention'], + 'ailabs_enabled' => (bool) $row['enabled'] + ]; + + $this->db->sql_freeresult($result); + } + } + + $sql = 'SELECT forum_id, forum_name FROM ' . FORUMS_TABLE . ' ORDER BY left_id'; + $result = $this->db->sql_query($sql); + while ($row = $this->db->sql_fetchrow($result)) { + $this->template->assign_block_vars('AILABS_FORUMS_LIST', [ + 'VALUE' => $row['forum_id'], + 'NAME' => $row['forum_name'] + ]); + } + $this->db->sql_freeresult($result); + + foreach ($this->desc_contollers as $key => $value) { + $controller = explode("/", $value); + $name = end($controller); + $this->template->assign_block_vars('AILABS_CONTROLLER_DESC', [ + 'NAME' => $name, + 'VALUE' => $value, + 'SELECTED' => (!empty($edit) && $value === $edit['ailabs_controller']) + ]); + } + + global $phpbb_root_path, $phpEx; + + $this->template->assign_vars( + array_merge( + $edit, + [ + 'S_ERROR' => isset($error) ? $error : '', + 'U_AILABS_ADD_EDIT' => true, + 'U_ACTION' => $this->action == 'add' ? $this->u_action . '&action=add' : $this->u_action . '&action=edit&user_id=' . $this->user_id, + 'U_BACK' => $this->u_action, + 'U_FIND_USERNAME' => append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=searchuser&form=ailabs_configuration&field=ailabs_username&select_single=true'), + ] + ) + ); + } + + public function delete($user_id) + { + if (empty($user_id)) { + trigger_error('AILABS_USER_EMPTY' . adm_back_link($this->u_action), E_USER_WARNING); + } + + $sql = 'DELETE FROM ' . $this->ailabs_users_table . ' WHERE user_id = ' . (int) $user_id; + $result = $this->db->sql_query($sql); + $this->db->sql_freeresult($result); + + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_ACP_AILABS_DELETED'); + + return $this; + } + + public function acp_ailabs_main() + { + $sql = 'SELECT a.*, u.username, ' . + '(SELECT GROUP_CONCAT(f.forum_name SEPARATOR ", ") FROM phpbblt_forums f WHERE INSTR(a.forums_post, CONCAT(\'"\',f.forum_id,\'"\')) > 0) as forums_post_names, ' . + '(SELECT GROUP_CONCAT(f.forum_name SEPARATOR ", ") FROM phpbblt_forums f WHERE INSTR(a.forums_mention, CONCAT(\'"\',f.forum_id,\'"\')) > 0) as forums_mention_names ' . + 'FROM ' . $this->ailabs_users_table . ' a ' . + 'LEFT JOIN ' . USERS_TABLE . ' u ON u.user_id = a.user_id ' . + 'ORDER BY u.username'; + + $result = $this->db->sql_query($sql); + + $ailabs_users = []; + + while ($row = $this->db->sql_fetchrow($result)) { + if (empty($row)) { + continue; + } + + $controller = explode("/", $row['controller']); + $row['controller'] = end($controller); + $row['U_EDIT'] = $this->u_action . '&action=edit&user_id=' . $row['user_id'] . '&hash=' . generate_link_hash('acp_ailabs'); + $row['U_DELETE'] = $this->u_action . '&action=delete&user_id=' . $row['user_id'] . '&username=' . $row['username'] . '&hash=' . generate_link_hash('acp_ailabs'); + + $ailabs_users[] = (array) $row; + } + + $this->db->sql_freeresult($result); + + $template_vars = [ + 'U_AILABS_USERS' => $ailabs_users, + 'U_ADD' => $this->u_action . '&action=add', + 'U_ACTION' => $this->u_action, + 'U_AILABS_VEIW' => true + ]; + + return $this->template->assign_vars($template_vars); + } + + protected function find_user_id($username) + { + $user_id = null; + if (!empty($username)) { + $where = ['username' => $username]; + $sql = 'SELECT user_id FROM ' . USERS_TABLE . ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + if (!empty($row) && !empty($row['user_id'])) + $user_id = $row['user_id']; + $this->db->sql_freeresult($result); + } + return $user_id; + } + + protected function find_user_name($user_id) + { + $username = null; + if (!empty($user_id)) { + $where = ['user_id' => $user_id]; + $sql = 'SELECT username FROM ' . USERS_TABLE . ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + if (!empty($row) && !empty($row['username'])) + $username = $row['username']; + $this->db->sql_freeresult($result); + } + return $username; + } + + protected function count_configs($user_id) + { + $count = 0; + if (!empty($user_id)) { + $where = ['user_id' => $user_id]; + $sql = 'SELECT count(*) as cnt FROM ' . $this->ailabs_users_table . ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + if (!empty($row) && !empty($row['cnt'])) + $count = $row['cnt']; + $this->db->sql_freeresult($result); + } + return $count; + } +} diff --git a/privet/ailabs/controller/chatgpt.php b/privet/ailabs/controller/chatgpt.php new file mode 100644 index 0000000..75b81f5 --- /dev/null +++ b/privet/ailabs/controller/chatgpt.php @@ -0,0 +1,291 @@ +", +"url_chat": "https://api.openai.com/v1/chat/completions", +"model": "gpt-3.5-turbo", +"temperature": 0.9, +"max_tokens": 4096, +"message_tokens": 1024, +"top_p": 1, +"frequency_penalty": 0, +"presence_penalty": 0.6, +"prefix": "This is optional field you can remove it or populate with something like this -> Pretend your are Bender from Futurma", +"prefix_tokens": 16 +} + +template +{info}[quote={poster_name} post_id={post_id} user_id={poster_id}]{request}[/quote]{response} +*/ + +class chatgpt extends AIController +{ + // https://platform.openai.com/docs/api-reference/chat/create#chat/create-max_tokens + // By default, the number of tokens the model can return will be (4096 - prompt tokens). + protected $max_tokens = 4096; + protected $message_tokens = 2048; + + protected function process() + { + $this->language->add_lang('common', 'privet/ailabs'); + + $this->job['status'] = 'exec'; + + $set = [ + 'status' => $this->job['status'], + 'log' => json_encode($this->log) + ]; + + $this->job_update($set); + $this->post_update($this->job); + + if (!empty($this->cfg->message_tokens)) { + $this->message_tokens = $this->cfg->message_tokens; + } + + if (!empty($this->cfg->max_tokens)) { + $this->max_tokens = (int)$this->cfg->max_tokens; + } + + $prefix_tokens = empty($this->cfg->prefix_tokens) ? 0 : $this->cfg->prefix_tokens; + + $api_key = $this->cfg->api_key; + $this->cfg->api_key = null; + $api = new GenericCurl($api_key); + + $this->job['status'] = 'fail'; + $response = $this->language->lang('AILABS_ERROR_CHECK_LOGS'); + $api_response = null; + $request_tokens = null; + $response_tokens = null; + + $total_replaced = 0; + $original_request = $this->job['request']; + + if ($total_replaced > 0) { + $this->job['request'] = trim(str_replace(' ', ' ', $this->job['request'])); + $this->log['request.original'] = $original_request; + $this->log['request.adjusted'] = $this->job['request']; + } + + $messages = []; + $info = null; + $posts = []; + $post_first_taken = null; + $post_first_discarded = null; + + if ($this->job['post_mode'] == 'quote') { + $history = ['post_text' => $this->job['post_text']]; + + $pattern = '/job['ailabs_username'] . '"\spost_id="(.*)"\stime="(.*)"\suser_id="' . $this->job['ailabs_user_id'] . '">/'; + + $this->log['history.pattern'] = $pattern; + $this->log_flush(); + + $history_tokens = 0; + $round = -1; + do { + $round++; + $matches = null; + preg_match_all( + $pattern, + $history['post_text'], + $matches + ); + + $history = null; + + if ($matches != null && !empty($matches) && !empty($matches[1][0])) { + $postid = (int) $matches[1][0]; + + $sql = 'SELECT j.job_id, j.post_id, j.response_post_id, j.request, j.response, p.post_text, j.request_tokens, j.response_tokens ' . + 'FROM ' . $this->jobs_table . ' j ' . + 'JOIN ' . POSTS_TABLE . ' p ON p.post_id = j.post_id ' . + 'WHERE ' . $this->db->sql_build_array('SELECT', ['response_post_id' => $postid]); + $result = $this->db->sql_query($sql); + $history = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + if (!empty($history)) { + $count_tokens = $history['request_tokens'] + $history['response_tokens']; + + $discard = $this->max_tokens < ($this->message_tokens + $history_tokens + $count_tokens); + + $posts[] = [ + 'postid' => $postid, + 'request_tokens' => $history['request_tokens'], + 'response_tokens' => $history['response_tokens'], + 'runnig_total_tokens' => $history_tokens + $count_tokens, + 'discard' => $discard + ]; + + if ($discard) { + $post_first_discarded = $postid; + break; + } + + $post_first_taken = $postid; + $history_tokens += $count_tokens; + + $post_messages = [ + ['role' => 'user', 'content' => trim($history['request'])], + ['role' => 'assistant', 'content' => trim($history['response'])], + ]; + + $messages = [ + ...$post_messages, + ...$messages + ]; + } + } + } while (!empty($history)); + + if (!empty($posts)) { + $this->log['history.posts'] = $posts; + $this->log_flush(); + } + } + + if (!empty($this->cfg->prefix)) { + $messages = [ + ['role' => 'system', 'content' => $this->cfg->prefix], + ...$messages + ]; + } + + $messages = [ + ...$messages, + ['role' => 'user', 'content' => trim($this->job['request'])] + ]; + + $this->log['request.messages'] = $messages; + $this->log_flush(); + + try { + // https://api.openai.com/v1/chat/completions + $api_result = $api->sendRequest($this->cfg->url_chat, 'POST', [ + 'model' => $this->cfg->model, + 'messages' => $messages, + 'temperature' => (float) $this->cfg->temperature, + // https://platform.openai.com/docs/api-reference/chat/create#chat/create-max_tokens + // By default, the number of tokens the model can return will be (4096 - prompt tokens). + // 'max_tokens' => (int) $this->cfg->max_tokens, + 'frequency_penalty' => (float) $this->cfg->frequency_penalty, + 'presence_penalty' => (float)$this->cfg->presence_penalty, + ]); + + /* + Response example: + { + 'id': 'chatcmpl-1p2RTPYSDSRi0xRviKjjilqrWU5Vr', + 'object': 'chat.completion', + 'created': 1677649420, + 'model': 'gpt-3.5-turbo', + 'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87}, + 'choices': [ + { + 'message': { + 'role': 'assistant', + 'content': 'The 2020 World Series was played in Arlington, Texas at the Globe Life Field, which was the new home stadium for the Texas Rangers.' + }, + 'finish_reason': 'stop', + 'index': 0 + } + ] + } + */ + + $json = json_decode($api_result); + $this->log['response'] = $json; + $this->log['response.codes'] = $api->responseCodes; + + $this->log_flush(); + + if ( + empty($json->object) || + empty($json->choices) || + $json->object != 'chat.completion' || + !in_array(200, $api->responseCodes) + ) { + } else { + $this->job['status'] = 'ok'; + $api_response = $json->choices[0]->message->content; + $response = $api_response; + $request_tokens = $json->usage->prompt_tokens; + $response_tokens = $json->usage->completion_tokens; + if ($history_tokens > 0 || $prefix_tokens > 0) { + $this->log['request.tokens.raw'] = $request_tokens; + $this->log['request.tokens.adjusted'] = $request_tokens - $history_tokens - $prefix_tokens; + } + } + } catch (\Exception $e) { + $this->log['exception'] = $e->getMessage(); + $this->log_flush(); + } + + $this->log['finish'] = date('Y-m-d H:i:s'); + + if (!empty($posts)) { + $discarded = ''; + if ($post_first_discarded != null) { + $discarded = $this->language->lang('AILABS_POSTS_DISCARDED', $post_first_discarded); + } + $total_posts_count = count($posts) * 2 + 2; + $total_tokens_used_count = $request_tokens + $response_tokens; + $info = $this->language->lang( + 'AILABS_DISCARDED_INFO', + $post_first_taken, + $total_posts_count, + $discarded, + $total_tokens_used_count, + $this->max_tokens + ); + } + + $resultParse = new resultParse(); + $resultParse->message = $response; + $resultParse->info = $info; + + $response = $this->replace_vars($this->job, $resultParse); + + $data = $this->post_response($this->job, $response); + + $this->job['response_time'] = time(); + $this->job['response_post_id'] = $data['post_id']; + + $set = [ + 'status' => $this->job['status'], + 'attempts' => $this->job['attempts'] + 1, + 'response_time' => $this->job['response_time'], + 'response' => $api_response, + 'request_tokens' => $request_tokens - $history_tokens - $prefix_tokens, + 'response_post_id' => $this->job['response_post_id'], + 'response_tokens' => $response_tokens, + 'log' => json_encode($this->log) + ]; + + $this->job_update($set); + $this->post_update($this->job); + + return new JsonResponse($this->log); + } +} diff --git a/privet/ailabs/controller/dalle.php b/privet/ailabs/controller/dalle.php new file mode 100644 index 0000000..d22b371 --- /dev/null +++ b/privet/ailabs/controller/dalle.php @@ -0,0 +1,176 @@ +", +"url_generations": "https://api.openai.com/v1/images/generations", +"url_variations": "https://api.openai.com/v1/images/variations", +"n": 1, +"size": "1024x1024", +"response_format": "url" +} + +template +[quote={poster_name} post_id={post_id} user_id={poster_id}]{request}[/quote] +{response}{attachments} + +*/ + +class dalle extends GenericController +{ + protected function init() + { + $opts = parent::init(); + + $opts += ['size' => $this->cfg->size]; + + $count_replaced = 0; + + // User can explicitly override image size. + // Comment out code below to disable this feature. + foreach (['256x256', '512x512', '1024x1024'] as $known_size) { + $count_replaced = 0; + $this->job['request'] = trim(str_replace($known_size, '', $this->job['request'], $count_replaced)); + if ($count_replaced > 0) { + if ($opts['size'] != $known_size) { + $this->log['size.adjusted'] = $known_size; + $opts['size'] = $known_size; + } + } + } + + if ($count_replaced > 0) { + $this->log['request.adjusted'] = $this->job['request']; + } + + return $opts; + } + + protected function prepare($opts) + { + if (filter_var($this->job['request'], FILTER_VALIDATE_URL)) { + // https://platform.openai.com/docs/api-reference/images/create-variation + // The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square. + $image = curl_file_create($this->job['request'], 'image/png'); + $opts += [ + 'image' => $image, + 'n' => $this->cfg->n, + 'response_format' => $this->cfg->response_format, + ]; + } else { + $opts += [ + 'prompt' => trim($this->job['request']), + 'n' => $this->cfg->n, + 'response_format' => $this->cfg->response_format, + ]; + } + + return $opts; + } + + protected function submit($opts): resultSubmit + { + $api = new GenericCurl($this->cfg->api_key); + $this->cfg->api_key = null; + + $result = new resultSubmit(); + + if (empty($opts['image'])) { + // https://api.openai.com/v1/images/generations + $result->response = $api->sendRequest($this->cfg->url_generations, 'POST', $opts); + } else { + // https://api.openai.com/v1/images/variations + $result->response = $api->sendRequest($this->cfg->url_variations, 'POST', $opts); + } + + $result->responseCodes = $api->responseCodes; + + return $result; + } + + protected function parse(resultSubmit $resultSubmit): resultParse + { + /* + Response example for response_format="url": + { + "created": 1589478378, + "data": [ + { + "url": "https://..." + }, + { + "url": "https://..." + } + ] + } + + Response example for response_format="b64_json": + { + "created": 1589478378, + "data": [ + { + "b64_json": "..." + }, + { + "b64_json": "..." + } + ] + } + */ + + $json = json_decode($resultSubmit->response); + $images = null; + $message = null; + + if ( + empty($json->data) || + !empty($json->error) || + !in_array(200, $resultSubmit->responseCodes) + ) { + if (!empty($json->error)) { + $message = $json->error->message; + } + } else { + $this->job['status'] = 'ok'; + $images = []; + $ind = 0; + foreach ($json->data as $item) { + // Image name returned back by Open AI API in url is not always can be parsed by internal phpBB routines. + // Use b64_json instead + if ($this->cfg->response_format == 'url') { + array_push($images, $item->url); + } else { + $filename = $this->save_base64_to_temp_file($item->b64_json, $ind); + $item->b64_json = ''; + array_push($images, $filename); + } + $ind++; + } + } + + $result = new resultParse(); + $result->json = $json; + $result->images = $images; + $result->message = $message; + + return $result; + } +} diff --git a/privet/ailabs/controller/log.php b/privet/ailabs/controller/log.php new file mode 100644 index 0000000..a966649 --- /dev/null +++ b/privet/ailabs/controller/log.php @@ -0,0 +1,51 @@ +user->data['user_id'] == ANONYMOUS || $this->user->data['is_bot']) { + throw new http_exception(401); + } + + $where = [ + 'post_id' => $post_id + ]; + + $sql = 'SELECT * ' . 'FROM ' . $this->jobs_table . ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $data = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + if (!empty($data)) { + foreach($data as &$row) + { + $row['poster_user_url'] = '/' . append_sid("memberlist.$this->php_ext", 'mode=viewprofile&u=' . $row['poster_id'], true, ''); + $row['ailabs_user_url'] = '/' . append_sid("memberlist.$this->php_ext", 'mode=viewprofile&u=' . $row['ailabs_user_id'], true, ''); + if (!empty($row['response_post_id'])) { + $row['response_url'] = '/viewtopic.php?p=' . $row['response_post_id'] . '#p' . $row['response_post_id']; + } + } + + $this->template->assign_block_vars('ailabs_log', [ + 'LOGS' => $data + ]); + } + + return $this->helper->render('post_ailabs_log.html', 'AI Labs Log'); + } +} diff --git a/privet/ailabs/controller/scriptexecute.php b/privet/ailabs/controller/scriptexecute.php new file mode 100644 index 0000000..c6e3776 --- /dev/null +++ b/privet/ailabs/controller/scriptexecute.php @@ -0,0 +1,125 @@ +", + "logs": "" + } +} + +template +[quote={poster_name} post_id={post_id} user_id={poster_id}]{request}[/quote] +{response}{attachments} + +*/ + +class scriptexecute extends GenericController +{ + protected function submit($opts): resultSubmit + { + $opts = (array)(clone $this->cfg); + $opts['prompt'] = trim($this->job['request']); + + if (array_key_exists('model', $opts) && property_exists($opts['model'], 'prompt') && empty($opts['model']->prompt)) + $opts['model']->prompt = $opts['prompt']; + + if (array_key_exists('config', $opts) && property_exists($opts['config'], 'prompt') && empty($opts['config']->prompt)) + $opts['config']->prompt = $opts['prompt']; + + $result = new resultSubmit(); + + $fileConfig = $this->cfg->config->logs . '/' . $this->job_id . '.json'; + $fileLog = $this->cfg->config->logs . '/' . $this->job_id . '.json.log'; + $fileResponse = $this->cfg->config->logs . '/' . $this->job_id . '.json.response'; + $execute = $this->cfg->config->script . ' ' . $this->job_id . ' ' . $fileConfig . ' > ' . $fileLog; + + $jsonConfig = json_encode($opts, JSON_PRETTY_PRINT); + + $result_put = file_put_contents($fileConfig, $jsonConfig); + + if (empty($result_put)) { + $result->responseCodes[] = $result_put; + $result->response = '{ "error" : "Unable to save .json config" }'; + } else { + $result_code = null; + try { + // Make sure that exe is enabled in /etc/php/8.2/fpm/pool.d/phpbb_pool.conf file + // See line php_admin_value[disable_functions] = + unset($output); + exec($execute, $output, $result_code); + } catch (\Exception $e) { + $result->response = '{ "error" : "' . $e . '" }'; + } + + $result->responseCodes[] = $result_code; + $result->response = file_get_contents($fileResponse); + + if ($result_code == 0) { + unlink($fileLog); + unlink($fileResponse); + unlink($fileConfig); + } + } + + return $result; + } + + protected function parse(resultSubmit $resultSubmit): resultParse + { + /* + Response + { + images: ['','',''], + agentName: '0'..'j', + subscriptionTokens: tokensLeft, + error: null | 'error message' + } + */ + + $json = empty($resultSubmit->response) ? false : json_decode($resultSubmit->response); + $images = []; + $message = null; + + if ( + empty($json) || + empty($json->images) || + !empty($json->error) + ) { + if (!empty($json->error)) { + $message = $json->error; + } + } else { + $this->job['status'] = 'ok'; + $images = []; + foreach ($json->images as $item) { + array_push($images, $item); + } + $json->images = $images; + } + + $result = new resultParse(); + $result->json = $json; + $result->images = $images; + $result->message = $message; + + return $result; + } +} diff --git a/privet/ailabs/controller/stablediffusion.php b/privet/ailabs/controller/stablediffusion.php new file mode 100644 index 0000000..63153fa --- /dev/null +++ b/privet/ailabs/controller/stablediffusion.php @@ -0,0 +1,148 @@ +", +"url_texttoimage": "https://api.stability.ai/v1/generation/stable-diffusion-xl-beta-v2-2-2/text-to-image", +"cfg_scale": 7.5, +"clip_guidance_preset": "FAST_BLUE", +"height": 512, +"width": 512, +"samples": 1, +"steps": 30 +} + +template +[quote={poster_name} post_id={post_id} user_id={poster_id}]{request}[/quote] +{response}{attachments} + +*/ + +class stablediffusion extends GenericController +{ + + protected function prepare($opts) + { + return [ + 'text_prompts' => [ + ['text' => trim($this->job['request'])], + ], + 'cfg_scale' => $this->cfg->cfg_scale, + 'clip_guidance_preset' => $this->cfg->clip_guidance_preset, + 'height' => $this->cfg->height, + 'width' => $this->cfg->width, + 'samples' => $this->cfg->samples, + 'steps' => $this->cfg->steps, + ]; + } + + protected function submit($opts): resultSubmit + { + $api = new GenericCurl($this->cfg->api_key); + $this->cfg->api_key = null; + + $result = new resultSubmit(); + // https://api.stability.ai/docs#tag/v1generation/operation/textToImage + // https://api.stability.ai/v1/generation/stable-diffusion-xl-beta-v2-2-2/text-to-image + $result->response = $api->sendRequest($this->cfg->url_texttoimage, 'POST', $opts); + $result->responseCodes = $api->responseCodes; + return $result; + } + + protected function parse(resultSubmit $resultSubmit): resultParse + { + /* + Response headers: + Content-Type required string + Enum: "application/json" | "image/png" + Finish-Reason string (FinishReason) + Enum: "SUCCESS" | "ERROR" | "CONTENT_FILTERED" + The result of the generation process. + + SUCCESS indicates success + ERROR indicates an error + CONTENT_FILTERED indicates the result affected by the content filter and may be blurred. + This header is only present when the Accept is set to image/png. Otherwise it is returned in the response body. + + Seed integer + Example: 3817857576 + The seed used to generate the image. This header is only present when the Accept is set to image/png. Otherwise it is returned in the response body. + + Response HTTP 200: + { + "artifacts":[ + { + "base64":"", + "seed":4188843142, + "finishReason":"SUCCESS" + } + ] + } + + Response HTTP 400, 401, 404, 500: + { + "id": "A unique identifier for this particular occurrence of the problem.", + "name": "The short-name of this class of errors e.g. bad_request.", + "message": "A human-readable explanation specific to this occurrence of the problem." + } + */ + + $json = json_decode($resultSubmit->response); + $images = null; + $message = null; + + if ( + empty($json->artifacts) || + !empty($json->name) || + !empty($json->message) || + !in_array(200, $resultSubmit->responseCodes) + ) { + if (!empty($json->message)) { + $message = $json->message; + } + } else { + $this->job['status'] = 'ok'; + + $images = []; + + $ind = 0; + foreach ($json->artifacts as $item) { + if ($item->finishReason !== 'SUCCESS') { + $message = $item->finishReason; + if (!empty($item->base64)) + $item->base64 = ''; + } else { + $filename = $this->save_base64_to_temp_file($item->base64, $ind); + array_push($images, $filename); + $item->base64 = ''; + } + $ind++; + } + } + + $result = new resultParse(); + $result->json = $json; + $result->images = $images; + $result->message = $message; + + return $result; + } +} diff --git a/privet/ailabs/event/listener.php b/privet/ailabs/event/listener.php new file mode 100644 index 0000000..284fa44 --- /dev/null +++ b/privet/ailabs/event/listener.php @@ -0,0 +1,312 @@ +user = $user; + $this->auth = $auth; + $this->db = $db; + $this->helper = $helper; + $this->language = $language; + $this->request = $request; + $this->root_path = $root_path; + $this->php_ext = $php_ext; + $this->users_table = $users_table; + $this->jobs_table = $jobs_table; + } + + static public function getSubscribedEvents() + { + return array( + 'core.posting_modify_submit_post_after' => 'post_ailabs_message', + 'core.viewtopic_post_rowset_data' => 'viewtopic_post_rowset_data', + 'core.viewtopic_modify_post_row' => 'viewtopic_modify_post_row', + ); + } + + /** + * Post a message + * + * @param \phpbb\event\data $event Event object + */ + public function post_ailabs_message($event, $forum_data) + { + $mode = $event['mode']; + + // Only for new topics and posts with mention (quote/reply/@mention) to AI Labs user posts + if (!in_array($mode, ['post', 'reply', 'quote'])) { + return false; + } + + $forum_id = $event['forum_id']; + + $ailabs_users_forum = $this->ailabs_users_forum($forum_id); + + if (empty($ailabs_users_forum)) { + return false; + } + + $post_id = $event['data']['post_id']; + + $ailabs_users_notified = $this->ailabs_users_notified($post_id); + + $ailabs_users = array(); + + foreach ($ailabs_users_forum as $user) { + if ($mode == 'post' && $user['post'] == 1) { + array_push($ailabs_users, $user); + } else { + if ($user['mention'] == 1 && in_array($user['user_id'], $ailabs_users_notified)) + array_push($ailabs_users, $user); + } + } + + if (empty($ailabs_users)) { + return false; + } + + $message_parser = new \parse_message($event['data']['message']); + $message_parser->remove_nested_quotes(0); + $message_parser->decode_message(); + + $request = $message_parser->message; + + // Remove all mentioned AI user names from request + foreach ($ailabs_users as $user) { + $count = 0; + $updated = preg_replace('/\[mention\][\s\t]?' . $user['username'] . '[\s\t]?\[\/mention\]/', '', $request, -1, $count); + if ($count > 0) { + $request = $updated; + } + } + + // Replace mention tags + $request = preg_replace(array('/\[mention\]/', '/\[\/mention\]/'), array('', ''), $request); + + // Replace size tags + $request = preg_replace(array('/\[size=[0-9]+\]/', '/\[\/size\]/'), array('', ''), $request); + + // Remove leading and trailing spaces as well as all doublespaces + $request = trim(str_replace(' ', ' ', $request)); + + // https://area51.phpbb.com/docs/dev/master/db/dbal.html + foreach ($ailabs_users as $user) { + $data = [ + 'ailabs_user_id' => $user['user_id'], + 'ailabs_username' => $user['username'], + 'request_time' => time(), + 'post_mode' => $mode, + 'post_id' => $post_id, + 'forum_id' => $forum_id, + 'poster_id' => $this->user->data['user_id'], + 'poster_name' => $this->user->data['username'], + 'request' => utf8_encode_ucr($request), + ]; + $sql = 'INSERT INTO ' . $this->jobs_table . ' ' . $this->db->sql_build_array('INSERT', $data); + $result = $this->db->sql_query($sql); + $this->db->sql_freeresult($result); + $data['job_id'] = $this->db->sql_nextid(); + + $this->update_post($data); + + $url = generate_board_url() . $user['controller'] . '?job_id=' . $data['job_id']; + get_headers($url); + unset($data); + } + } + + private function update_post($data) + { + $where = [ + 'post_id' => $data['post_id'] + ]; + + $set = [ + 'post_ailabs_data' => json_encode(array( + 'job_id' => $data['job_id'], + 'ailabs_user_id' => $data['ailabs_user_id'], + 'ailabs_username' => $data['ailabs_username'], + )) . ',' + ]; + + $sql = 'UPDATE ' . POSTS_TABLE . + ' SET ' . $this->db->sql_build_array('UPDATE', $set) . + ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $this->db->sql_freeresult($result); + } + + /** + * Check if forum enabled for ailabs users + * @param int $id + * @return array of found ailabs user_ids along with allowed actions for each [user_id, post, mention] + */ + private function ailabs_users_forum($id) + { + $return = array(); + $sql = 'SELECT c.user_id, ' . + 'c.forums_post LIKE \'%"' . $id . '"%\' as post, ' . + 'c.forums_mention LIKE \'%"' . $id . '"%\' as mention, ' . + 'c.controller, ' . + 'u.username ' . + 'FROM ' . $this->users_table . ' c ' . + 'JOIN ' . USERS_TABLE . ' u ON c.user_id = u.user_id ' . + 'WHERE c.enabled = 1'; + $result = $this->db->sql_query($sql); + while ($row = $this->db->sql_fetchrow($result)) { + array_push($return, array( + 'user_id' => $row['user_id'], + 'username' => $row['username'], + 'post' => $row['post'], + 'mention' => $row['mention'], + 'controller' => $row['controller'] + )); + } + $this->db->sql_freeresult($result); + return $return; + } + + /** + * Check if any of ailabs user notified in this post + * @param int $post_id + * @return array of notified ailabs users + */ + private function ailabs_users_notified($post_id) + { + $return = array(); + $sql = 'SELECT c.user_id FROM ' . $this->users_table . ' c ' . + 'JOIN ' . NOTIFICATIONS_TABLE . ' n ON n.user_id = c.user_id ' . + 'WHERE c.enabled = 1 AND n.item_id = ' . (int) $post_id; + $result = $this->db->sql_query($sql); + while ($row = $this->db->sql_fetchrow($result)) { + array_push($return, $row['user_id']); + } + $this->db->sql_freeresult($result); + return $return; + } + + private function get_status($status) + { + $this->language->add_lang('common', 'privet/ailabs'); + + switch ($status) { + case null: + return $this->language->lang('AILABS_THINKING'); + case 'exec': + return $this->language->lang('AILABS_REPLYING'); + case 'ok': + return $this->language->lang('AILABS_REPLIED'); + case 'fail': + return $this->language->lang('AILABS_UNABLE_TO_REPLY'); + } + + return $status; + } + + public function viewtopic_post_rowset_data($event) + { + $rowset_data = $event['rowset_data']; + $rowset_data = array_merge($rowset_data, [ + 'post_ailabs_data' => $event['row']['post_ailabs_data'], + ]); + $event['rowset_data'] = $rowset_data; + } + + public function viewtopic_modify_post_row($event) + { + $post_ailabs_data = $event['row']['post_ailabs_data']; + $post_id = $event['row']['post_id']; + + $jobs = array(); + + if (!empty($post_ailabs_data)) { + $json_data = json_decode('[' . rtrim($post_ailabs_data, ',') . ']'); + if (!empty($json_data)) { + /* + [ + job_id: , + ailabs_user_id: , + ailabs_username: , + response_time: , + status: , + response_post_id: + ] + */ + foreach ($json_data as $job) { + $ailabs_user_id = (string) $job->ailabs_user_id; + $response_time = empty($job->response_time) ? 0 : $job->response_time; + if ( + !in_array($ailabs_user_id, $jobs) || + $jobs[$ailabs_user_id]->$response_time < $response_time + ) { + $jobs[$ailabs_user_id] = $job; + } + } + } + unset($json_data); + } + + $ailabs = array(); + + foreach ($jobs as $key => $value) { + $value->user_url = '/' . append_sid("memberlist.$this->php_ext", 'mode=viewprofile&u=' . $value->ailabs_user_id, true, ''); + if (!empty($value->response_post_id)) { + $value->response_url = '/viewtopic.php?p=' . $value->response_post_id . '#p' . $value->response_post_id; + } + $value->status = $this->get_status(empty($value->status) ? null : $value->status); + array_push($ailabs, $value); + } + + if (!empty($ailabs)) { + $event['post_row'] = array_merge($event['post_row'], [ + 'U_AILABS' => $ailabs, + ]); + if ($this->auth->acl_get('a_', 'm_')) { + $event['post_row'] = array_merge($event['post_row'], [ + 'U_AILABS_VIEW_LOG' => $this->helper->route('privet_ailabs_view_log_controller_page', ['post_id' => $post_id]), + ]); + } + } + } +} diff --git a/privet/ailabs/includes/AIController.php b/privet/ailabs/includes/AIController.php new file mode 100644 index 0000000..9c95884 --- /dev/null +++ b/privet/ailabs/includes/AIController.php @@ -0,0 +1,451 @@ +auth = $auth; + $this->config = $config; + $this->db = $db; + $this->helper = $helper; + $this->language = $language; + $this->request = $request; + $this->template = $template; + $this->user = $user; + $this->phpbb_container = $phpbb_container; + $this->php_ext = $php_ext; + $this->root_path = $root_path; + $this->users_table = $users_table; + $this->jobs_table = $jobs_table; + } + + /** + * @return \Symfony\Component\HttpFoundation\Response A Symfony Response object + */ + public function execute() + { + $this->start = date('Y-m-d H:i:s'); + + // https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response + $streamedResponse = new StreamedResponse(); + $streamedResponse->headers->set('X-Accel-Buffering', 'no'); + $streamedResponse->setCallback(function () { + var_dump('Processing'); + flush(); + }); + $streamedResponse->send(); + + $this->job_id = utf8_clean_string($this->request->variable('job_id', '', true)); + + if (empty($this->job_id)) { + return new JsonResponse('job_id not provided'); + } + + $where = [ + 'job_id' => $this->job_id + ]; + + $sql = 'SELECT j.job_id, j.ailabs_user_id, j.status, j.attempts, j.post_mode, j.post_id, j.forum_id, j.poster_id, j.poster_name, j.request, c.config, c.template, u.username as ailabs_username, p.topic_id, p.post_subject, p.post_text, f.forum_name ' . + 'FROM ' . $this->jobs_table . ' j ' . + 'JOIN ' . $this->users_table . ' c ON c.user_id = j.ailabs_user_id ' . + 'JOIN ' . USERS_TABLE . ' u ON u.user_id = j.ailabs_user_id ' . + 'JOIN ' . POSTS_TABLE . ' p ON p.post_id = j.post_id ' . + 'JOIN ' . FORUMS_TABLE . ' f ON f.forum_id = j.forum_id ' . + 'WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $this->job = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + if (empty($this->job)) { + return new JsonResponse('job_id not found in the database'); + } + + if (!empty($this->job['status'])) { + return new JsonResponse('job_id already completed with ' . $this->job['status']); + } + + $this->log = array('start' => $this->start); + + $this->job['request'] = utf8_decode_ncr($this->job['request']); + + try { + $this->cfg = json_decode($this->job['config']); + } catch (\Exception $e) { + $this->cfg = null; + $this->log['exception'] = $e->getMessage(); + } + + if (empty($this->cfg)) { + $this->job['status'] = 'fail'; + $this->log['error'] = 'config not provided'; + + $set = [ + 'status' => $this->job['status'], + 'log' => json_encode($this->log) + ]; + + $this->job_update($set); + $this->post_update($this->job); + + return new JsonResponse($this->log); + } + + return $this->process(); + } + + protected function process() + { + return new JsonResponse($this->log); + } + + /** + * Parse message template + * @param string $template + */ + protected function replace_vars($job, resultParse $resultParse) + { + $images = null; + $attachments = null; + if (!empty($resultParse->images)) { + $images = []; + $attachments = []; + $ind = 0; + foreach ($resultParse->images as $item) { + array_push($images, '[img]' . $item . '[/img]' . PHP_EOL . PHP_EOL); + array_push($attachments, '[attachment=' . $ind . '][/attachment]' . PHP_EOL); + $ind = $ind + 1; + } + $images = implode("", $images); + $attachments = implode("", $attachments); + } + + $tokens = array( + '{post_id}' => $job['post_id'], + '{request}' => $job['request'], + '{info}' => $resultParse->info, + '{response}' => $resultParse->message, + '{images}' => $images, + '{attachments}' => $attachments, + '{poster_id}' => $job['poster_id'], + '{poster_name}' => $job['poster_name'], + '{ailabs_username}' => $job['ailabs_username'], + ); + + return str_ireplace(array_keys($tokens), array_values($tokens), $job['template']); + } + + protected function job_update($set) + { + $where = ['job_id' => $this->job_id]; + + $sql = 'UPDATE ' . $this->jobs_table . + ' SET ' . $this->db->sql_build_array('UPDATE', $set) . + ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + + $result = $this->db->sql_query($sql); + + $this->db->sql_freeresult($result); + } + + protected function log_flush() + { + $set = ['log' => json_encode($this->log)]; + + $this->job_update($set); + } + + protected function post_update($job) + { + /* + [ + job_id: , + ailabs_user_id: , + ailabs_username: , + response_time: , + status: , + response_post_id: + ] + */ + $data = array( + 'job_id' => $job['job_id'], + 'ailabs_user_id' => $job['ailabs_user_id'], + 'ailabs_username' => $job['ailabs_username'], + 'status' => $job['status'], + 'response_time' => empty($job['response_time']) ? time() : $job['response_time'], + ); + if (!empty($job['response_post_id'])) { + $data['response_post_id'] = $job['response_post_id']; + } + $where = [ + 'post_id' => $job['post_id'] + ]; + $set = '\'' . json_encode($data) . ',\''; + $sql = 'UPDATE ' . POSTS_TABLE . + ' SET post_ailabs_data = CONCAT(post_ailabs_data, ' . $set . ')' . + ' WHERE ' . $this->db->sql_build_array('SELECT', $where); + $result = $this->db->sql_query($sql); + $this->db->sql_freeresult($result); + } + + protected function post_response($job, $response) + { + // Prep posting + $poll = $uid = $bitfield = $options = ''; + $allow_bbcode = $allow_urls = $allow_smilies = true; + generate_text_for_storage($response, $uid, $bitfield, $options, $allow_bbcode, $allow_urls, $allow_smilies); + + $data = array( + 'poster_id' => $job['ailabs_user_id'], + // General Posting Settings + 'forum_id' => $job['forum_id'], // The forum ID in which the post will be placed. (int) + 'topic_id' => $job['topic_id'], // Post a new topic or in an existing one? Set to 0 to create a new one, if not, specify your topic ID here instead. + 'icon_id' => false, // The Icon ID in which the post will be displayed with on the viewforum, set to false for icon_id. (int) + // Defining Post Options + 'enable_bbcode' => true, // Enable BBcode in this post. (bool) + 'enable_smilies' => true, // Enabe smilies in this post. (bool) + 'enable_urls' => true, // Enable self-parsing URL links in this post. (bool) + 'enable_sig' => true, // Enable the signature of the poster to be displayed in the post. (bool) + // Message Body + 'message' => $response, // Your text you wish to have submitted. It should pass through generate_text_for_storage() before this. (string) + 'message_md5' => md5($response), // The md5 hash of your message + 'post_checksum' => md5($response), // The md5 hash of your message + // Values from generate_text_for_storage() + 'bbcode_bitfield' => $bitfield, // Value created from the generate_text_for_storage() function. + 'bbcode_uid' => $uid, // Value created from the generate_text_for_storage() function. + // Other Options + 'post_edit_locked' => 0, // Disallow post editing? 1 = Yes, 0 = No + 'topic_title' => $job['post_subject'], + 'notify_set' => true, // (bool) + 'notify' => true, // (bool) + 'post_time' => 0, // Set a specific time, use 0 to let submit_post() take care of getting the proper time (int) + 'forum_name' => $job['forum_name'], // For identifying the name of the forum in a notification email. (string) // Indexing + 'enable_indexing' => true, // Allow indexing the post? (bool) // 3.0.6 + ); + + // Post as designated user and then switch back to original one + $actual_user_id = $this->user->data['user_id']; + $this->switch_user($job['ailabs_user_id']); + $post_subject = ((strpos($job['post_subject'], 'Re: ') !== 0) ? 'Re: ' : '') . censor_text($job['post_subject']); + + include($this->root_path . 'includes/functions_posting.' . $this->php_ext); + submit_post('reply', $post_subject, $job['ailabs_username'], POST_NORMAL, $poll, $data); + + $this->switch_user($actual_user_id); + + return $data; + } + + /** + * Switch to the AI Labs user + * @param int $new_user_id + * @return bool + */ + protected function switch_user($new_user_id) + { + if ($this->user->data['user_id'] == $new_user_id) { + // Nothing to do + return true; + } + + $sql = 'SELECT * FROM ' . USERS_TABLE . ' WHERE user_id = ' . (int) $new_user_id; + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + $row['is_registered'] = true; + $this->user->data = array_merge($this->user->data, $row); + $this->auth->acl($this->user->data); + + return true; + } + + /* + Inspired by https://www.phpbb.com/community/viewtopic.php?t=2556226 + */ + protected function attach_to_post($isUrl, $urlOrFilename, $postId, $topicId = 0, $forumId = 0, $userId = 0, $realFileName = '', $comment = '', $dispatchEvent = true) + { + if ( + getType($isUrl) != 'boolean' || + getType($urlOrFilename) != 'string' || + getType($postId) != 'integer' || + getType($comment) != 'string' || + getType($dispatchEvent) != 'boolean' || + getType($topicId) != 'integer' || + getType($forumId) != 'integer' || + getType($userId) != 'integer' + ) + throw 'Type Missmatch'; + + if ($postId <= 0) + throw 'Post ID cannot be zero!'; + + // if not given, get missing IDs + if ($topicId == 0 || $forumId == 0 || $userId == 0) { + $idRow = $this->db->sql_fetchrow($this->db->sql_query('SELECT post_id, topic_id, forum_id, poster_id FROM ' . POSTS_TABLE . ' WHERE post_id = ' . $postId)); + if (!$idRow) + return ['POST_NOT_EXISTS']; + $topicId = intval($idRow['topic_id']); + $forumId = intval($idRow['topic_id']); + $userId = intval($idRow['poster_id']); + } + + // get required classes + $upload = $this->phpbb_container->get('files.upload'); + $cache = $this->phpbb_container->get('cache'); + $attach = $this->phpbb_container->get('attachment.upload'); + + // load file from remote location to local server + $upload->set_disallowed_content([]); + $extensions = $cache->obtain_attach_extensions($forumId); + $extensionArr = array_keys($extensions['_allowed_']); + $upload->set_allowed_extensions($extensionArr); + + $tempFile = null; + if ($isUrl) { + $upload->set_allowed_extensions($extensionArr); + $tempFile = $upload->handle_upload('files.types.remote', $urlOrFilename); + } else { + // TODO: There seems to be some kind of bug where phpBB unable to get extension for local file + array_push($extensionArr, ''); + $upload->set_allowed_extensions($extensionArr); + $local_filedata = array(); + $tempFile = $upload->handle_upload('files.types.local', $urlOrFilename, $local_filedata); + } + + if (count($tempFile->error) > 0) { + $ext = '.'; + if (strrpos($urlOrFilename, '.') !== false) + $ext = substr($urlOrFilename, strrpos($urlOrFilename, '.') + 1); + if ($tempFile->error[0] == 'URL_INVALID' && !in_array($ext, $extensionArr)) + return ['FILE_EXTENSION_NOT_ALLOWED', 'EXTENSION=.' . htmlspecialchars($ext)]; + return $tempFile->error; + } + + $realFileNameExt = $isUrl ? '.' . $tempFile->get('extension') : ''; + + $realFileName = $realFileName == '' ? $tempFile->get('realname') : htmlspecialchars($realFileName) . $realFileNameExt; + + $tempFileData = [ + 'realname' => $realFileName, + 'size' => $tempFile->get('filesize'), + 'type' => $tempFile->get('mimetype'), + ]; + + // create attachment from temp file + if (!function_exists('create_thumbnail')) + require($this->root_path . 'includes/functions_posting.php'); + + $attachFileName = $isUrl ? $tempFile->get('filename') : $urlOrFilename; + + $attachmentFileData = $attach->upload('', $forumId, true, $attachFileName, false, $tempFileData); + + if (!$attachmentFileData['post_attach']) + return ['FILE_ATTACH_ERROR', $realFileName, $attachFileName, $tempFileData]; + + if (count($attachmentFileData['error']) > 0) + return $attachmentFileData['error']; + + $sql_ary = array( + 'physical_filename' => $attachmentFileData['physical_filename'], + 'attach_comment' => $comment, + 'real_filename' => $attachmentFileData['real_filename'], + 'extension' => $attachmentFileData['extension'], + 'mimetype' => $attachmentFileData['mimetype'], + 'filesize' => $attachmentFileData['filesize'], + 'filetime' => $attachmentFileData['filetime'], + 'thumbnail' => $attachmentFileData['thumbnail'], + 'is_orphan' => 0, + 'in_message' => 0, + 'poster_id' => $userId, + 'post_msg_id' => $postId, + 'topic_id' => $topicId, + ); + + if ($dispatchEvent) { + $dispatcher = $this->phpbb_container->get('dispatcher'); + $vars = array('sql_ary'); + extract($dispatcher->trigger_event('core.modify_attachment_sql_ary_on_submit', compact($vars))); + } + + $this->db->sql_query('INSERT INTO ' . ATTACHMENTS_TABLE . ' ' . $this->db->sql_build_array('INSERT', $sql_ary)); + $newAttachmentID = intval($this->db->sql_nextid()); + + if ($newAttachmentID == 0) + return ['SQL_ATTACHMENT_INSERT_ERROR']; + + $this->db->sql_query('UPDATE ' . POSTS_TABLE . ' SET post_attachment = 1 WHERE post_id = ' . $postId); + + return ['SUCCESS', $newAttachmentID, $postId]; + } + + protected function image_filename($ind) + { + return 'ailabs_' . $this->job['ailabs_user_id'] . '_' . $this->job_id . '_' . $ind; + } + + protected function save_base64_to_temp_file($base64, $ind, $ext = '.png') + { + $temp_dir_path = sys_get_temp_dir(); + + if (substr($temp_dir_path, -1) != DIRECTORY_SEPARATOR) + $temp_dir_path .= DIRECTORY_SEPARATOR; + + $filename = $temp_dir_path . $this->image_filename($ind) . $ext; + + $handle = fopen($filename, 'wb'); + fwrite($handle, base64_decode($base64)); + fclose($handle); + + return $filename; + } +} diff --git a/privet/ailabs/includes/GenericController.php b/privet/ailabs/includes/GenericController.php new file mode 100644 index 0000000..c07e1ec --- /dev/null +++ b/privet/ailabs/includes/GenericController.php @@ -0,0 +1,159 @@ +job['request'] + // Return $opts or empty array + protected function init() + { + $this->job['status'] = 'exec'; + + $set = [ + 'status' => $this->job['status'], + 'log' => json_encode($this->log) + ]; + + $this->job_update($set); + $this->post_update($this->job); + + $total_replaced = 0; + $original_request = $this->job['request']; + $this->job['request'] = str_replace('@' . $this->job['ailabs_username'], '', $this->job['request'], $total_replaced); + + if ($total_replaced > 0) { + $this->job['request'] = str_replace(' ', ' ', $this->job['request']); + $this->log['request.original'] = $original_request; + $this->log['request.adjusted'] = $this->job['request']; + } + + return []; + } + + protected function prepare($opts) + { + return $opts; + } + + protected function submit($opts): resultSubmit + { + $result = new resultSubmit(); + $result->response = ''; + $result->responseCodes = []; + return $result; + } + + // Override this method to extract response image(s)/message(s) and set job status + protected function parse(resultSubmit $resultSubmit): resultParse + { + $result = new resultParse(); + $result->json = json_decode($resultSubmit->response); + return $result; + } + + protected function process() + { + $opts = $this->init(); + $this->log_flush(); + + $opts = $this->prepare($opts); + + $this->log['request.json'] = $opts; + $this->log_flush(); + + $this->job['status'] = 'fail'; + + $resultSubmit = null; + $resultParse = null; + + try { + $resultSubmit = $this->submit($opts); + + $this->log['response.length'] = strlen($resultSubmit->response); + $this->log['response.codes'] = $resultSubmit->responseCodes; + $this->log_flush(); + + $resultParse = $this->parse($resultSubmit); + + $this->log['response.json'] = $resultParse->json; + $this->log_flush(); + } catch (\Exception $e) { + $this->log['exception'] = $e->getMessage(); + $this->log_flush(); + + $this->log['response.raw'] = $resultSubmit->response; + $this->log_flush(); + } + + $this->log['finish'] = date('Y-m-d H:i:s'); + + $response = $this->replace_vars($this->job, $resultParse); + + $data = $this->post_response($this->job, $response); + + $this->job['response_time'] = time(); + $this->job['response_post_id'] = $data['post_id']; + + if (!empty($resultParse->images)) { + $this->log['attachments_start_time'] = date('Y-m-d H:i:s'); + $ind = 0; + foreach ($resultParse->images as $url_or_filename) { + $is_url = filter_var($url_or_filename, FILTER_VALIDATE_URL) !== false; + $attachemnt_name = $is_url ? $this->image_filename($ind) : basename($url_or_filename); + $attachment = $this->attach_to_post( + $is_url, + $url_or_filename, + +$data['post_id'], + +$this->job['topic_id'], + +$this->job['forum_id'], + +$this->job['ailabs_user_id'], + $attachemnt_name + ); + + // If you getting error REMOTE_UPLOAD_TIMEOUT try to adjust php.ini as described at https://stackoverflow.com/questions/52069439/upload-large-files-and-time-out-php-uploads + // Edit /etc/php/8.2/fpm/php.ini, eg + // max_execution_time = 120 + // max_input_time = 120 + // upload_max_filesize = 10M + + $this->log['attachment_' . $attachemnt_name] = [ + 'is_url' => $is_url, + 'url_or_filename' => $url_or_filename, + 'result' => $attachment + ]; + + $ind++; + } + $this->log['attachments_finish_time'] = date('Y-m-d H:i:s'); + } + + $set = [ + 'status' => $this->job['status'], + 'attempts' => $this->job['attempts'] + 1, + 'response_time' => $this->job['response_time'], + 'response' => empty($resultParse->images) ? $resultParse->message : implode(PHP_EOL, $resultParse->images), + 'response_post_id' => $this->job['response_post_id'], + 'log' => json_encode($this->log) + ]; + + $this->job_update($set); + $this->post_update($this->job); + + return new JsonResponse($this->log); + } +} diff --git a/privet/ailabs/includes/GenericCurl.php b/privet/ailabs/includes/GenericCurl.php new file mode 100644 index 0000000..a8c45b2 --- /dev/null +++ b/privet/ailabs/includes/GenericCurl.php @@ -0,0 +1,151 @@ +contentTypes = [ + "application/json" => "Content-Type: application/json", + "multipart/form-data" => "Content-Type: multipart/form-data", + ]; + + $this->headers = [ + $this->contentTypes["application/json"], + "Authorization: Bearer $API_KEY", + ]; + + $this->retryCount = $retryCount; + $this->timeoutBeforeRetrySec = $timeoutBeforeRetrySec; + } + + /** + * @return array + * Remove this method from your code before deploying + */ + public function getCURLInfo() + { + return $this->curlInfo; + } + + /** + * @param int $timeout + */ + public function setTimeout(int $timeout) + { + $this->timeout = $timeout; + } + + /** + * @param string $proxy + */ + public function setProxy(string $proxy) + { + if ($proxy && strpos($proxy, '://') === false) { + $proxy = 'https://' . $proxy; + } + $this->proxy = $proxy; + } + + /** + * @param array $header + * @return void + */ + public function setHeader(array $header) + { + if ($header) { + foreach ($header as $key => $value) { + $this->headers[$key] = $value; + } + } + } + + /** + * @param string $url + * @param string $method + * @param array $opts + * @return bool|string + */ + public function sendRequest(string $url, string $method, array $opts = []) + { + $this->responseCodes = []; + + $post_fields = json_encode($opts); + + if (array_key_exists('file', $opts) || array_key_exists('image', $opts)) { + $this->headers[0] = $this->contentTypes["multipart/form-data"]; + $post_fields = $opts; + } else { + $this->headers[0] = $this->contentTypes["application/json"]; + } + + $curl_info = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_POSTFIELDS => $post_fields, + CURLOPT_HTTPHEADER => $this->headers, + ]; + + if ($opts == []) { + unset($curl_info[CURLOPT_POSTFIELDS]); + } + + if (!empty($this->proxy)) { + $curl_info[CURLOPT_PROXY] = $this->proxy; + } + + if (array_key_exists('stream', $opts) && $opts['stream']) { + $curl_info[CURLOPT_WRITEFUNCTION] = $this->stream_method; + } + + $curl = curl_init(); + + curl_setopt_array($curl, $curl_info); + $retryCount = 0; + $responseCode = 0; + $response = null; + + do { + $retryCount++; + $response = curl_exec($curl); + $responseCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + array_push($this->responseCodes, $responseCode); + } while ( + $responseCode !== 200 && + $retryCount < $this->retryCount && + sleep($this->timeoutBeforeRetrySec) !== false + ); + + $this->curlInfo = curl_getinfo($curl); + + curl_close($curl); + + return $response; + } +} diff --git a/privet/ailabs/includes/resultParse.php b/privet/ailabs/includes/resultParse.php new file mode 100644 index 0000000..ba07e80 --- /dev/null +++ b/privet/ailabs/includes/resultParse.php @@ -0,0 +1,20 @@ + '[color=#FF0000]Error. Pelase check logs.[/color]', + 'AILABS_POSTS_DISCARDED' => ', posts starting from [url=/viewtopic.php?p=%1$d#p%1$d]this post[/url] were discarded', + 'AILABS_DISCARDED_INFO' => '[size=75][url=/viewtopic.php?p=%1$d#p%1$d]Beginning[/url] of a conversation containing %2$d posts%3$s (%4$d tokens of %5$d were used)[/size]', + 'AILABS_THINKING' => 'thinking', + 'AILABS_REPLYING' => 'replying…', + 'AILABS_REPLIED' => 'replied ↓', + 'AILABS_UNABLE_TO_REPLY' => 'unable to reply' +]); diff --git a/privet/ailabs/language/en/info_acp_ailabs.php b/privet/ailabs/language/en/info_acp_ailabs.php new file mode 100644 index 0000000..40637b4 --- /dev/null +++ b/privet/ailabs/language/en/info_acp_ailabs.php @@ -0,0 +1,58 @@ + 'AI Labs', + 'ACP_AILABS_TITLE_VIEW' => 'AI Labs View Configuration', + 'ACP_AILABS_TITLE_ADD' => 'AI Labs Add Configuration', + 'ACP_AILABS_TITLE_EDIT' => 'AI Labs Edit Configuration', + 'ACP_AILABS_SETTINGS' => 'Settings', + + 'ACP_AILABS_ADD' => 'Add Configuration', + + 'AILABS_USER_EMPTY' => 'Please select user', + 'AILABS_USER_NOT_FOUND' => 'Unable to locate user %1$s', + 'AILABS_USER_ALREADY_CONFIGURED' => 'User %1$s already configured, only one configuration per user supported', + 'AILABS_SPECIFY_POST_OR_MENTION' => 'Both Reply on a post and Reply when quoted can\'t be empty, please specify at least one', + + 'LOG_ACP_AILABS_ADDED' => 'AI Labs configuration added', + 'LOG_ACP_AILABS_EDITED' => 'AI Labs configuration updated', + 'LOG_ACP_AILABS_DELETED' => 'AI Labs configuration deleted', + + 'ACP_AILABS_ADDED' => 'Configuration successfully created', + 'ACP_AILABS_UPDATED' => 'Configuration successfully updated', + 'ACP_AILABS_DELETED_CONFIRM' => 'Are you sure that you wish to delete the configuration associated with user %1$s?', + + 'LBL_AILABS_SETTINGS_DESC' => 'Please visit 👉 https://github.com/privet-fun/phpbb_ailabs for detailed configuration instructions and examples', + 'LBL_AILABS_USERNAME' => 'User Name', + 'LBL_AILABS_CONTROLLER' => 'AI', + 'LBL_AILABS_CONFIG' => 'Configuration JSON', + 'LBL_AILABS_TEMPLATE' => 'Template', + 'LBL_AILABS_REPLY_POST_FORUMS' => 'Reply on a post', + 'LBL_AILABS_REPLY_QUOTE_FORUMS' => 'Reply when quoted', + 'LBL_AILABS_ENABLED' => 'Enabled', + 'LBL_AILABS_SELECT_FORUMS' => 'Select forums...', + + 'LBL_AILABS_CONFIG_EXPLAIN' => 'Must be valid JSON, please refer to documnetation for details', + 'LBL_AILABS_TEMPLATE_EXPLAIN' => 'Valid variables: {post_id}, {request}, {info}, {response}, {images}, {attachments}, {poster_id}, {poster_name}, {ailabs_username}', + 'LBL_AILABS_REPLY_POST_FORUMS_EXPLAIN' => 'Specify forums where AI will reply to new posts', + 'LBL_AILABS_REPLY_QUOTE_FORUMS_EXPLAIN' => 'Specify forums where AI will reply to quoted posts', + 'LBL_AILABS_CONFIG_DEFAULT' => 'Load default configuration', + 'LBL_AILABS_TEMPLATE_DEFAULT' => 'Load default template', +]); diff --git a/privet/ailabs/language/ru/common.php b/privet/ailabs/language/ru/common.php new file mode 100644 index 0000000..4cb49a5 --- /dev/null +++ b/privet/ailabs/language/ru/common.php @@ -0,0 +1,28 @@ + '[color=#FF0000]Ошибка. Лог содержит детальную информацию.[/color]', + 'AILABS_POSTS_DISCARDED' => ', сообщения начиная с [url=/viewtopic.php?p=%1$d#p%1$d]этого[/url] не включены', + 'AILABS_DISCARDED_INFO' => '[size=75][url=/viewtopic.php?p=%1$d#p%1$d]Начало[/url] беседы из %2$d сообщений%3$s (%4$d токенов из %5$d использовано)[/size]', + 'AILABS_THINKING' => 'думает', + 'AILABS_REPLYING' => 'отвечает…', + 'AILABS_REPLIED' => 'ответил ↓', + 'AILABS_UNABLE_TO_REPLY' => 'ответить не смог' +]); diff --git a/privet/ailabs/language/ru/info_acp_ailabs.php b/privet/ailabs/language/ru/info_acp_ailabs.php new file mode 100644 index 0000000..27fe095 --- /dev/null +++ b/privet/ailabs/language/ru/info_acp_ailabs.php @@ -0,0 +1,58 @@ + 'AI Labs', + 'ACP_AILABS_TITLE_VIEW' => 'Просмотр конфигурации AI Labs', + 'ACP_AILABS_TITLE_ADD' => 'Добавить конфигурацию AI Labs', + 'ACP_AILABS_TITLE_EDIT' => 'Изменить конфигурацию AI Labs', + 'ACP_AILABS_SETTINGS' => 'Настройки', + + 'ACP_AILABS_ADD' => 'Добавить конфигурацию', + + 'AILABS_USER_EMPTY' => 'Пожалуйста, выберите пользователя', + 'AILABS_USER_NOT_FOUND' => 'Не удалось найти пользователя %1$s', + 'AILABS_USER_ALREADY_CONFIGURED' => 'Пользователь %1$s уже настроен, поддерживается только одна конфигурация на пользователя', + 'AILABS_SPECIFY_POST_OR_MENTION' => 'Нельзя оставлять пустыми и "Ответ на сообщение" и "Ответ при цитировании", пожалуйста, укажите хотя бы одно значение', + + 'LOG_ACP_AILABS_ADDED' => 'Конфигурация AI Labs добавлена', + 'LOG_ACP_AILABS_EDITED' => 'Конфигурация AI Labs изменена', + 'LOG_ACP_AILABS_DELETED' => 'Конфигурация AI Labs удалена', + + 'ACP_AILABS_ADDED' => 'Конфигурация успешно создана', + 'ACP_AILABS_UPDATED' => 'Конфигурация успешно обновлена', + 'ACP_AILABS_DELETED_CONFIRM' => 'Вы уверены, что хотите удалить конфигурацию, связанную с пользователем %1$s?', + + 'LBL_AILABS_SETTINGS_DESC' => 'Пожалуйста, посетите 👉 https://github.com/privet-fun/phpbb_ailabs для получения подробных инструкций по настройке и примеров', + 'LBL_AILABS_USERNAME' => 'Имя пользователя', + 'LBL_AILABS_CONTROLLER' => 'AI', + 'LBL_AILABS_CONFIG' => 'Конфигурация в формате JSON', + 'LBL_AILABS_TEMPLATE' => 'Шаблон', + 'LBL_AILABS_REPLY_POST_FORUMS' => 'Ответ на сообщение', + 'LBL_AILABS_REPLY_QUOTE_FORUMS' => 'Ответ при цитировании', + 'LBL_AILABS_ENABLED' => 'Включено', + 'LBL_AILABS_SELECT_FORUMS' => 'Выберите форумы...', + + 'LBL_AILABS_CONFIG_EXPLAIN' => 'Пожалуйста, обратитесь к документации для получения подробных инструкций по настройке и примеров', + 'LBL_AILABS_TEMPLATE_EXPLAIN' => 'Допустимые переменные: {post_id}, {request}, {info}, {response}, {images}, {attachments}, {poster_id}, {poster_name}, {ailabs_username}', + 'LBL_AILABS_REPLY_POST_FORUMS_EXPLAIN' => 'Укажите форумы, на которых AI будет отвечать на новые сообщения', + 'LBL_AILABS_REPLY_QUOTE_FORUMS_EXPLAIN' => 'Укажите форумы, на которых AI будет отвечать на цитируемые сообщения', + 'LBL_AILABS_CONFIG_DEFAULT' => 'Загрузить конфигурацию по умолчанию', + 'LBL_AILABS_TEMPLATE_DEFAULT' => 'Загрузить шаблон по умолчанию', +]); diff --git a/privet/ailabs/license.txt b/privet/ailabs/license.txt new file mode 100644 index 0000000..b9f6fc2 --- /dev/null +++ b/privet/ailabs/license.txt @@ -0,0 +1,280 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 675 Mass Ave, Cambridge, MA 02139, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/privet/ailabs/migrations/v1x/release_1_0_0_schema.php b/privet/ailabs/migrations/v1x/release_1_0_0_schema.php new file mode 100644 index 0000000..7569915 --- /dev/null +++ b/privet/ailabs/migrations/v1x/release_1_0_0_schema.php @@ -0,0 +1,110 @@ + [ + // Config table + $this->table_prefix . 'ailabs_users' => [ + 'COLUMNS' => [ + 'user_id' => ['UINT', 0], + 'controller' => ['VCHAR', ''], // eg /ailabs/chatgpt + 'config' => ['TEXT_UNI', ''], // JSON + 'template' => ['TEXT_UNI', ''], // eg [quote={poster_name} post_id={post_id} user_id={poster_id}]{request}[/quote]{response}{attachments} + 'forums_post' => ['VCHAR', ''], // eg ["forum_id1","forum_id2"] + 'forums_mention' => ['VCHAR', ''], // eg ["forum_id1","forum_id2"] + 'enabled' => ['BOOL', 0], + ], + 'PRIMARY_KEY' => 'user_id', + ], + // Jobs table + $this->table_prefix . 'ailabs_jobs' => [ + 'COLUMNS' => [ + 'job_id' => ['UINT', null, 'auto_increment'], + 'ailabs_user_id' => ['UINT', 0], + 'ailabs_username' => ['VCHAR', ''], + 'status' => ['VCHAR:10', null], // exec, ok, fail (null -> exec -> ok | fail, null -> exec -> null) + 'attempts' => ['UINT', 0], + 'request_time' => ['UINT:11', 0], + 'response_time' => ['UINT:11', null], + 'post_mode' => ['VCHAR:10', ''], // post, reply, quote + 'post_id' => ['UINT:10', 0], + 'forum_id' => ['UINT:8', 0], + 'poster_id' => ['UINT:10', 0], + 'poster_name' => ['VCHAR', ''], + 'request' => ['TEXT_UNI', ''], + 'request_tokens' => ['UINT:8', null], + 'response' => ['TEXT_UNI', null], + 'response_tokens' => ['UINT:8', null], + 'response_post_id' => ['UINT:10', null], + 'log' => ['TEXT_UNI', null], + ], + 'PRIMARY_KEY' => 'job_id', + 'KEYS' => [ + 'idx_ailabs_jobs' => [null, ['response_post_id', 'status']], + 'idx_ailabs_post_id' => [null, ['post_id']], + ], + ], + ], + 'add_columns' => [ + $this->table_prefix . 'posts' => [ + 'post_ailabs_data' => ['TEXT_UNI', null], + ], + ], + ]; + } + + public function revert_schema() + { + return [ + 'drop_columns' => [ + $this->table_prefix . 'posts' => [ + 'post_ailabs_data', + ], + ], + 'drop_tables' => [ + $this->table_prefix . 'ailabs_users', + $this->table_prefix . 'ailabs_jobs', + ], + ]; + } + + // https://area51.phpbb.com/docs/dev/master/extensions/tutorial_modules.html + public function update_data() + { + return [ + ['module.add', [ + 'acp', + 'ACP_CAT_DOT_MODS', + 'ACP_AILABS_TITLE' + ]], + + ['module.add', [ + 'acp', + 'ACP_AILABS_TITLE', + [ + 'module_basename' => '\privet\ailabs\acp\main_module', + 'modes' => ['settings'], + ], + ]], + ]; + } +} diff --git a/privet/ailabs/styles/all/template/event/overall_header_head_append.html b/privet/ailabs/styles/all/template/event/overall_header_head_append.html new file mode 100644 index 0000000..a636ee1 --- /dev/null +++ b/privet/ailabs/styles/all/template/event/overall_header_head_append.html @@ -0,0 +1,8 @@ +{% INCLUDECSS '@privet_ailabs/ailabs.css' %} + diff --git a/privet/ailabs/styles/all/template/js/chosen.jquery.min.js b/privet/ailabs/styles/all/template/js/chosen.jquery.min.js new file mode 100644 index 0000000..4ad1647 --- /dev/null +++ b/privet/ailabs/styles/all/template/js/chosen.jquery.min.js @@ -0,0 +1,3 @@ +/* Chosen v1.8.7 | (c) 2011-2018 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */ + +(function(){var t,e,s,i,n=function(t,e){return function(){return t.apply(e,arguments)}},r=function(t,e){function s(){this.constructor=t}for(var i in e)o.call(e,i)&&(t[i]=e[i]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},o={}.hasOwnProperty;(i=function(){function t(){this.options_index=0,this.parsed=[]}return t.prototype.add_node=function(t){return"OPTGROUP"===t.nodeName.toUpperCase()?this.add_group(t):this.add_option(t)},t.prototype.add_group=function(t){var e,s,i,n,r,o;for(e=this.parsed.length,this.parsed.push({array_index:e,group:!0,label:t.label,title:t.title?t.title:void 0,children:0,disabled:t.disabled,classes:t.className}),o=[],s=0,i=(r=t.childNodes).length;s"+this.escape_html(t.group_label)+""+t.html:t.html},t.prototype.mouse_enter=function(){return this.mouse_on_container=!0},t.prototype.mouse_leave=function(){return this.mouse_on_container=!1},t.prototype.input_focus=function(t){if(this.is_multiple){if(!this.active_field)return setTimeout(function(t){return function(){return t.container_mousedown()}}(this),50)}else if(!this.active_field)return this.activate_field()},t.prototype.input_blur=function(t){if(!this.mouse_on_container)return this.active_field=!1,setTimeout(function(t){return function(){return t.blur_test()}}(this),100)},t.prototype.label_click_handler=function(t){return this.is_multiple?this.container_mousedown(t):this.activate_field()},t.prototype.results_option_build=function(t){var e,s,i,n,r,o,h;for(e="",h=0,n=0,r=(o=this.results_data).length;n=this.max_shown_results));n++);return e},t.prototype.result_add_option=function(t){var e,s;return t.search_match&&this.include_option_in_results(t)?(e=[],t.disabled||t.selected&&this.is_multiple||e.push("active-result"),!t.disabled||t.selected&&this.is_multiple||e.push("disabled-result"),t.selected&&e.push("result-selected"),null!=t.group_array_index&&e.push("group-option"),""!==t.classes&&e.push(t.classes),s=document.createElement("li"),s.className=e.join(" "),t.style&&(s.style.cssText=t.style),s.setAttribute("data-option-array-index",t.array_index),s.innerHTML=t.highlighted_html||t.html,t.title&&(s.title=t.title),this.outerHTML(s)):""},t.prototype.result_add_group=function(t){var e,s;return(t.search_match||t.group_match)&&t.active_options>0?((e=[]).push("group-result"),t.classes&&e.push(t.classes),s=document.createElement("li"),s.className=e.join(" "),s.innerHTML=t.highlighted_html||this.escape_html(t.label),t.title&&(s.title=t.title),this.outerHTML(s)):""},t.prototype.results_update_field=function(){if(this.set_default_text(),this.is_multiple||this.results_reset_cleanup(),this.result_clear_highlight(),this.results_build(),this.results_showing)return this.winnow_results()},t.prototype.reset_single_select_options=function(){var t,e,s,i,n;for(n=[],t=0,e=(s=this.results_data).length;t"+this.escape_html(s)+""+this.escape_html(p)),null!=a&&(a.group_match=!0)):null!=r.group_array_index&&this.results_data[r.group_array_index].search_match&&(r.search_match=!0)));return this.result_clear_highlight(),_<1&&h.length?(this.update_results_content(""),this.no_results(h)):(this.update_results_content(this.results_option_build()),(null!=t?t.skip_highlight:void 0)?void 0:this.winnow_results_set_highlight())},t.prototype.get_search_regex=function(t){var e,s;return s=this.search_contains?t:"(^|\\s|\\b)"+t+"[^\\s]*",this.enable_split_word_search||this.search_contains||(s="^"+s),e=this.case_sensitive_search?"":"i",new RegExp(s,e)},t.prototype.search_string_match=function(t,e){var s;return s=e.exec(t),!this.search_contains&&(null!=s?s[1]:void 0)&&(s.index+=1),s},t.prototype.choices_count=function(){var t,e,s;if(null!=this.selected_option_count)return this.selected_option_count;for(this.selected_option_count=0,t=0,e=(s=this.form_field.options).length;t0?this.keydown_backstroke():this.pending_backstroke||(this.result_clear_highlight(),this.results_search());break;case 13:t.preventDefault(),this.results_showing&&this.result_select(t);break;case 27:this.results_showing&&this.results_hide();break;case 9:case 16:case 17:case 18:case 38:case 40:case 91:break;default:this.results_search()}},t.prototype.clipboard_event_checker=function(t){if(!this.is_disabled)return setTimeout(function(t){return function(){return t.results_search()}}(this),50)},t.prototype.container_width=function(){return null!=this.options.width?this.options.width:this.form_field.offsetWidth+"px"},t.prototype.include_option_in_results=function(t){return!(this.is_multiple&&!this.display_selected_options&&t.selected)&&(!(!this.display_disabled_options&&t.disabled)&&!t.empty)},t.prototype.search_results_touchstart=function(t){return this.touch_started=!0,this.search_results_mouseover(t)},t.prototype.search_results_touchmove=function(t){return this.touch_started=!1,this.search_results_mouseout(t)},t.prototype.search_results_touchend=function(t){if(this.touch_started)return this.search_results_mouseup(t)},t.prototype.outerHTML=function(t){var e;return t.outerHTML?t.outerHTML:((e=document.createElement("div")).appendChild(t),e.innerHTML)},t.prototype.get_single_html=function(){return'\n '+this.default_text+'\n
\n
\n
\n \n
    \n
    '},t.prototype.get_multi_html=function(){return'
      \n
    • \n \n
    • \n
    \n
    \n
      \n
      '},t.prototype.get_no_results_html=function(t){return'
    • \n '+this.results_none_found+" "+this.escape_html(t)+"\n
    • "},t.browser_is_supported=function(){return"Microsoft Internet Explorer"===window.navigator.appName?document.documentMode>=8:!(/iP(od|hone)/i.test(window.navigator.userAgent)||/IEMobile/i.test(window.navigator.userAgent)||/Windows Phone/i.test(window.navigator.userAgent)||/BlackBerry/i.test(window.navigator.userAgent)||/BB10/i.test(window.navigator.userAgent)||/Android.*Mobile/i.test(window.navigator.userAgent))},t.default_multiple_text="Select Some Options",t.default_single_text="Select an Option",t.default_no_result_text="No results match",t}(),(t=jQuery).fn.extend({chosen:function(i){return e.browser_is_supported()?this.each(function(e){var n,r;r=(n=t(this)).data("chosen"),"destroy"!==i?r instanceof s||n.data("chosen",new s(this,i)):r instanceof s&&r.destroy()}):this}}),s=function(s){function n(){return n.__super__.constructor.apply(this,arguments)}return r(n,e),n.prototype.setup=function(){return this.form_field_jq=t(this.form_field),this.current_selectedIndex=this.form_field.selectedIndex},n.prototype.set_up_html=function(){var e,s;return(e=["chosen-container"]).push("chosen-container-"+(this.is_multiple?"multi":"single")),this.inherit_select_classes&&this.form_field.className&&e.push(this.form_field.className),this.is_rtl&&e.push("chosen-rtl"),s={"class":e.join(" "),title:this.form_field.title},this.form_field.id.length&&(s.id=this.form_field.id.replace(/[^\w]/g,"_")+"_chosen"),this.container=t("
      ",s),this.container.width(this.container_width()),this.is_multiple?this.container.html(this.get_multi_html()):this.container.html(this.get_single_html()),this.form_field_jq.hide().after(this.container),this.dropdown=this.container.find("div.chosen-drop").first(),this.search_field=this.container.find("input").first(),this.search_results=this.container.find("ul.chosen-results").first(),this.search_field_scale(),this.search_no_results=this.container.find("li.no-results").first(),this.is_multiple?(this.search_choices=this.container.find("ul.chosen-choices").first(),this.search_container=this.container.find("li.search-field").first()):(this.search_container=this.container.find("div.chosen-search").first(),this.selected_item=this.container.find(".chosen-single").first()),this.results_build(),this.set_tab_index(),this.set_label_behavior()},n.prototype.on_ready=function(){return this.form_field_jq.trigger("chosen:ready",{chosen:this})},n.prototype.register_observers=function(){return this.container.on("touchstart.chosen",function(t){return function(e){t.container_mousedown(e)}}(this)),this.container.on("touchend.chosen",function(t){return function(e){t.container_mouseup(e)}}(this)),this.container.on("mousedown.chosen",function(t){return function(e){t.container_mousedown(e)}}(this)),this.container.on("mouseup.chosen",function(t){return function(e){t.container_mouseup(e)}}(this)),this.container.on("mouseenter.chosen",function(t){return function(e){t.mouse_enter(e)}}(this)),this.container.on("mouseleave.chosen",function(t){return function(e){t.mouse_leave(e)}}(this)),this.search_results.on("mouseup.chosen",function(t){return function(e){t.search_results_mouseup(e)}}(this)),this.search_results.on("mouseover.chosen",function(t){return function(e){t.search_results_mouseover(e)}}(this)),this.search_results.on("mouseout.chosen",function(t){return function(e){t.search_results_mouseout(e)}}(this)),this.search_results.on("mousewheel.chosen DOMMouseScroll.chosen",function(t){return function(e){t.search_results_mousewheel(e)}}(this)),this.search_results.on("touchstart.chosen",function(t){return function(e){t.search_results_touchstart(e)}}(this)),this.search_results.on("touchmove.chosen",function(t){return function(e){t.search_results_touchmove(e)}}(this)),this.search_results.on("touchend.chosen",function(t){return function(e){t.search_results_touchend(e)}}(this)),this.form_field_jq.on("chosen:updated.chosen",function(t){return function(e){t.results_update_field(e)}}(this)),this.form_field_jq.on("chosen:activate.chosen",function(t){return function(e){t.activate_field(e)}}(this)),this.form_field_jq.on("chosen:open.chosen",function(t){return function(e){t.container_mousedown(e)}}(this)),this.form_field_jq.on("chosen:close.chosen",function(t){return function(e){t.close_field(e)}}(this)),this.search_field.on("blur.chosen",function(t){return function(e){t.input_blur(e)}}(this)),this.search_field.on("keyup.chosen",function(t){return function(e){t.keyup_checker(e)}}(this)),this.search_field.on("keydown.chosen",function(t){return function(e){t.keydown_checker(e)}}(this)),this.search_field.on("focus.chosen",function(t){return function(e){t.input_focus(e)}}(this)),this.search_field.on("cut.chosen",function(t){return function(e){t.clipboard_event_checker(e)}}(this)),this.search_field.on("paste.chosen",function(t){return function(e){t.clipboard_event_checker(e)}}(this)),this.is_multiple?this.search_choices.on("click.chosen",function(t){return function(e){t.choices_click(e)}}(this)):this.container.on("click.chosen",function(t){t.preventDefault()})},n.prototype.destroy=function(){return t(this.container[0].ownerDocument).off("click.chosen",this.click_test_action),this.form_field_label.length>0&&this.form_field_label.off("click.chosen"),this.search_field[0].tabIndex&&(this.form_field_jq[0].tabIndex=this.search_field[0].tabIndex),this.container.remove(),this.form_field_jq.removeData("chosen"),this.form_field_jq.show()},n.prototype.search_field_disabled=function(){return this.is_disabled=this.form_field.disabled||this.form_field_jq.parents("fieldset").is(":disabled"),this.container.toggleClass("chosen-disabled",this.is_disabled),this.search_field[0].disabled=this.is_disabled,this.is_multiple||this.selected_item.off("focus.chosen",this.activate_field),this.is_disabled?this.close_field():this.is_multiple?void 0:this.selected_item.on("focus.chosen",this.activate_field)},n.prototype.container_mousedown=function(e){var s;if(!this.is_disabled)return!e||"mousedown"!==(s=e.type)&&"touchstart"!==s||this.results_showing||e.preventDefault(),null!=e&&t(e.target).hasClass("search-choice-close")?void 0:(this.active_field?this.is_multiple||!e||t(e.target)[0]!==this.selected_item[0]&&!t(e.target).parents("a.chosen-single").length||(e.preventDefault(),this.results_toggle()):(this.is_multiple&&this.search_field.val(""),t(this.container[0].ownerDocument).on("click.chosen",this.click_test_action),this.results_show()),this.activate_field())},n.prototype.container_mouseup=function(t){if("ABBR"===t.target.nodeName&&!this.is_disabled)return this.results_reset(t)},n.prototype.search_results_mousewheel=function(t){var e;if(t.originalEvent&&(e=t.originalEvent.deltaY||-t.originalEvent.wheelDelta||t.originalEvent.detail),null!=e)return t.preventDefault(),"DOMMouseScroll"===t.type&&(e*=40),this.search_results.scrollTop(e+this.search_results.scrollTop())},n.prototype.blur_test=function(t){if(!this.active_field&&this.container.hasClass("chosen-container-active"))return this.close_field()},n.prototype.close_field=function(){return t(this.container[0].ownerDocument).off("click.chosen",this.click_test_action),this.active_field=!1,this.results_hide(),this.container.removeClass("chosen-container-active"),this.clear_backstroke(),this.show_search_field_default(),this.search_field_scale(),this.search_field.blur()},n.prototype.activate_field=function(){if(!this.is_disabled)return this.container.addClass("chosen-container-active"),this.active_field=!0,this.search_field.val(this.search_field.val()),this.search_field.focus()},n.prototype.test_active_click=function(e){var s;return(s=t(e.target).closest(".chosen-container")).length&&this.container[0]===s[0]?this.active_field=!0:this.close_field()},n.prototype.results_build=function(){return this.parsing=!0,this.selected_option_count=null,this.results_data=i.select_to_array(this.form_field),this.is_multiple?this.search_choices.find("li.search-choice").remove():(this.single_set_selected_text(),this.disable_search||this.form_field.options.length<=this.disable_search_threshold?(this.search_field[0].readOnly=!0,this.container.addClass("chosen-container-single-nosearch")):(this.search_field[0].readOnly=!1,this.container.removeClass("chosen-container-single-nosearch"))),this.update_results_content(this.results_option_build({first:!0})),this.search_field_disabled(),this.show_search_field_default(),this.search_field_scale(),this.parsing=!1},n.prototype.result_do_highlight=function(t){var e,s,i,n,r;if(t.length){if(this.result_clear_highlight(),this.result_highlight=t,this.result_highlight.addClass("highlighted"),i=parseInt(this.search_results.css("maxHeight"),10),r=this.search_results.scrollTop(),n=i+r,s=this.result_highlight.position().top+this.search_results.scrollTop(),(e=s+this.result_highlight.outerHeight())>=n)return this.search_results.scrollTop(e-i>0?e-i:0);if(s0)return this.form_field_label.on("click.chosen",this.label_click_handler)},n.prototype.show_search_field_default=function(){return this.is_multiple&&this.choices_count()<1&&!this.active_field?(this.search_field.val(this.default_text),this.search_field.addClass("default")):(this.search_field.val(""),this.search_field.removeClass("default"))},n.prototype.search_results_mouseup=function(e){var s;if((s=t(e.target).hasClass("active-result")?t(e.target):t(e.target).parents(".active-result").first()).length)return this.result_highlight=s,this.result_select(e),this.search_field.focus()},n.prototype.search_results_mouseover=function(e){var s;if(s=t(e.target).hasClass("active-result")?t(e.target):t(e.target).parents(".active-result").first())return this.result_do_highlight(s)},n.prototype.search_results_mouseout=function(e){if(t(e.target).hasClass("active-result")||t(e.target).parents(".active-result").first())return this.result_clear_highlight()},n.prototype.choice_build=function(e){var s,i;return s=t("
    • ",{"class":"search-choice"}).html(""+this.choice_label(e)+""),e.disabled?s.addClass("search-choice-disabled"):((i=t("",{"class":"search-choice-close","data-option-array-index":e.array_index})).on("click.chosen",function(t){return function(e){return t.choice_destroy_link_click(e)}}(this)),s.append(i)),this.search_container.before(s)},n.prototype.choice_destroy_link_click=function(e){if(e.preventDefault(),e.stopPropagation(),!this.is_disabled)return this.choice_destroy(t(e.target))},n.prototype.choice_destroy=function(t){if(this.result_deselect(t[0].getAttribute("data-option-array-index")))return this.active_field?this.search_field.focus():this.show_search_field_default(),this.is_multiple&&this.choices_count()>0&&this.get_search_field_value().length<1&&this.results_hide(),t.parents("li").first().remove(),this.search_field_scale()},n.prototype.results_reset=function(){if(this.reset_single_select_options(),this.form_field.options[0].selected=!0,this.single_set_selected_text(),this.show_search_field_default(),this.results_reset_cleanup(),this.trigger_form_field_change(),this.active_field)return this.results_hide()},n.prototype.results_reset_cleanup=function(){return this.current_selectedIndex=this.form_field.selectedIndex,this.selected_item.find("abbr").remove()},n.prototype.result_select=function(t){var e,s;if(this.result_highlight)return e=this.result_highlight,this.result_clear_highlight(),this.is_multiple&&this.max_selected_options<=this.choices_count()?(this.form_field_jq.trigger("chosen:maxselected",{chosen:this}),!1):(this.is_multiple?e.removeClass("active-result"):this.reset_single_select_options(),e.addClass("result-selected"),s=this.results_data[e[0].getAttribute("data-option-array-index")],s.selected=!0,this.form_field.options[s.options_index].selected=!0,this.selected_option_count=null,this.is_multiple?this.choice_build(s):this.single_set_selected_text(this.choice_label(s)),this.is_multiple&&(!this.hide_results_on_select||t.metaKey||t.ctrlKey)?t.metaKey||t.ctrlKey?this.winnow_results({skip_highlight:!0}):(this.search_field.val(""),this.winnow_results()):(this.results_hide(),this.show_search_field_default()),(this.is_multiple||this.form_field.selectedIndex!==this.current_selectedIndex)&&this.trigger_form_field_change({selected:this.form_field.options[s.options_index].value}),this.current_selectedIndex=this.form_field.selectedIndex,t.preventDefault(),this.search_field_scale())},n.prototype.single_set_selected_text=function(t){return null==t&&(t=this.default_text),t===this.default_text?this.selected_item.addClass("chosen-default"):(this.single_deselect_control_build(),this.selected_item.removeClass("chosen-default")),this.selected_item.find("span").html(t)},n.prototype.result_deselect=function(t){var e;return e=this.results_data[t],!this.form_field.options[e.options_index].disabled&&(e.selected=!1,this.form_field.options[e.options_index].selected=!1,this.selected_option_count=null,this.result_clear_highlight(),this.results_showing&&this.winnow_results(),this.trigger_form_field_change({deselected:this.form_field.options[e.options_index].value}),this.search_field_scale(),!0)},n.prototype.single_deselect_control_build=function(){if(this.allow_single_deselect)return this.selected_item.find("abbr").length||this.selected_item.find("span").first().after(''),this.selected_item.addClass("chosen-single-with-deselect")},n.prototype.get_search_field_value=function(){return this.search_field.val()},n.prototype.get_search_text=function(){return t.trim(this.get_search_field_value())},n.prototype.escape_html=function(e){return t("
      ").text(e).html()},n.prototype.winnow_results_set_highlight=function(){var t,e;if(e=this.is_multiple?[]:this.search_results.find(".result-selected.active-result"),null!=(t=e.length?e.first():this.search_results.find(".active-result").first()))return this.result_do_highlight(t)},n.prototype.no_results=function(t){var e;return e=this.get_no_results_html(t),this.search_results.append(e),this.form_field_jq.trigger("chosen:no_results",{chosen:this})},n.prototype.no_results_clear=function(){return this.search_results.find(".no-results").remove()},n.prototype.keydown_arrow=function(){var t;return this.results_showing&&this.result_highlight?(t=this.result_highlight.nextAll("li.active-result").first())?this.result_do_highlight(t):void 0:this.results_show()},n.prototype.keyup_arrow=function(){var t;return this.results_showing||this.is_multiple?this.result_highlight?(t=this.result_highlight.prevAll("li.active-result")).length?this.result_do_highlight(t.first()):(this.choices_count()>0&&this.results_hide(),this.result_clear_highlight()):void 0:this.results_show()},n.prototype.keydown_backstroke=function(){var t;return this.pending_backstroke?(this.choice_destroy(this.pending_backstroke.find("a").first()),this.clear_backstroke()):(t=this.search_container.siblings("li.search-choice").last()).length&&!t.hasClass("search-choice-disabled")?(this.pending_backstroke=t,this.single_backstroke_delete?this.keydown_backstroke():this.pending_backstroke.addClass("search-choice-focus")):void 0},n.prototype.clear_backstroke=function(){return this.pending_backstroke&&this.pending_backstroke.removeClass("search-choice-focus"),this.pending_backstroke=null},n.prototype.search_field_scale=function(){var e,s,i,n,r,o,h;if(this.is_multiple){for(r={position:"absolute",left:"-1000px",top:"-1000px",display:"none",whiteSpace:"pre"},s=0,i=(o=["fontSize","fontStyle","fontWeight","fontFamily","lineHeight","textTransform","letterSpacing"]).length;s").css(r)).text(this.get_search_field_value()),t("body").append(e),h=e.width()+25,e.remove(),this.container.is(":visible")&&(h=Math.min(this.container.outerWidth()-10,h)),this.search_field.width(h)}},n.prototype.trigger_form_field_change=function(t){return this.form_field_jq.trigger("input",t),this.form_field_jq.trigger("change",t)},n}()}).call(this); \ No newline at end of file diff --git a/privet/ailabs/styles/all/theme/chosen-sprite.png b/privet/ailabs/styles/all/theme/chosen-sprite.png new file mode 100644 index 0000000..c57da70 Binary files /dev/null and b/privet/ailabs/styles/all/theme/chosen-sprite.png differ diff --git a/privet/ailabs/styles/all/theme/chosen.min.css b/privet/ailabs/styles/all/theme/chosen.min.css new file mode 100644 index 0000000..1c68ebb --- /dev/null +++ b/privet/ailabs/styles/all/theme/chosen.min.css @@ -0,0 +1,11 @@ +/*! +Chosen, a Select Box Enhancer for jQuery and Prototype +by Patrick Filler for Harvest, http://getharvest.com + +Version 1.8.7 +Full source at https://github.com/harvesthq/chosen +Copyright (c) 2011-2018 Harvest http://getharvest.com + +MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md +This file is generated by `grunt build`, do not edit it by hand. +*/.chosen-container{position:relative;display:inline-block;vertical-align:middle;font-size:13px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.chosen-container *{-webkit-box-sizing:border-box;box-sizing:border-box}.chosen-container .chosen-drop{position:absolute;top:100%;z-index:1010;width:100%;border:1px solid #aaa;border-top:0;background:#fff;-webkit-box-shadow:0 4px 5px rgba(0,0,0,.15);box-shadow:0 4px 5px rgba(0,0,0,.15);clip:rect(0,0,0,0);-webkit-clip-path:inset(100% 100%);clip-path:inset(100% 100%)}.chosen-container.chosen-with-drop .chosen-drop{clip:auto;-webkit-clip-path:none;clip-path:none}.chosen-container a{cursor:pointer}.chosen-container .chosen-single .group-name,.chosen-container .search-choice .group-name{margin-right:4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-weight:400;color:#999}.chosen-container .chosen-single .group-name:after,.chosen-container .search-choice .group-name:after{content:":";padding-left:2px;vertical-align:top}.chosen-container-single .chosen-single{position:relative;display:block;overflow:hidden;padding:0 0 0 8px;height:25px;border:1px solid #aaa;border-radius:5px;background-color:#fff;background:-webkit-gradient(linear,left top,left bottom,color-stop(20%,#fff),color-stop(50%,#f6f6f6),color-stop(52%,#eee),to(#f4f4f4));background:linear-gradient(#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-clip:padding-box;-webkit-box-shadow:0 0 3px #fff inset,0 1px 1px rgba(0,0,0,.1);box-shadow:0 0 3px #fff inset,0 1px 1px rgba(0,0,0,.1);color:#444;text-decoration:none;white-space:nowrap;line-height:24px}.chosen-container-single .chosen-default{color:#999}.chosen-container-single .chosen-single span{display:block;overflow:hidden;margin-right:26px;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:6px;right:26px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-single .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single.chosen-disabled .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;width:18px;height:100%}.chosen-container-single .chosen-single div b{display:block;width:100%;height:100%;background:url(chosen-sprite.png) no-repeat 0 2px}.chosen-container-single .chosen-search{position:relative;z-index:1010;margin:0;padding:3px 4px;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{margin:1px 0;padding:4px 20px 4px 5px;width:100%;height:auto;outline:0;border:1px solid #aaa;background:url(chosen-sprite.png) no-repeat 100% -20px;font-size:1em;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-single .chosen-drop{margin-top:-1px;border-radius:0 0 4px 4px;background-clip:padding-box}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;clip:rect(0,0,0,0);-webkit-clip-path:inset(100% 100%);clip-path:inset(100% 100%)}.chosen-container .chosen-results{color:#444;position:relative;overflow-x:hidden;overflow-y:auto;margin:0 4px 4px 0;padding:0 0 0 4px;max-height:240px;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;margin:0;padding:5px 6px;list-style:none;line-height:15px;word-wrap:break-word;-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{background-color:#3875d7;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(20%,#3875d7),color-stop(90%,#2a62bc));background-image:linear-gradient(#3875d7 20%,#2a62bc 90%);color:#fff}.chosen-container .chosen-results li.no-results{color:#777;display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;overflow:hidden;margin:0;padding:0 5px;width:100%;height:auto;border:1px solid #aaa;background-color:#fff;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(1%,#eee),color-stop(15%,#fff));background-image:linear-gradient(#eee 1%,#fff 15%);cursor:text}.chosen-container-multi .chosen-choices li{float:left;list-style:none}.chosen-container-multi .chosen-choices li.search-field{margin:0;padding:0;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{margin:1px 0;padding:0;height:25px;outline:0;border:0!important;background:0 0!important;-webkit-box-shadow:none;box-shadow:none;color:#999;font-size:100%;font-family:sans-serif;line-height:normal;border-radius:0;width:25px}.chosen-container-multi .chosen-choices li.search-choice{position:relative;margin:3px 5px 3px 0;padding:3px 20px 3px 5px;border:1px solid #aaa;max-width:100%;border-radius:3px;background-color:#eee;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),to(#eee));background-image:linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-size:100% 19px;background-repeat:repeat-x;background-clip:padding-box;-webkit-box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);color:#333;line-height:13px;cursor:default}.chosen-container-multi .chosen-choices li.search-choice span{word-wrap:break-word}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:4px;right:3px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{background-position:-42px -10px}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;border:1px solid #ccc;background-color:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),to(#eee));background-image:linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);color:#666}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{margin:0;padding:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border:1px solid #5897fb;-webkit-box-shadow:0 0 5px rgba(0,0,0,.3);box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #aaa;border-bottom-right-radius:0;border-bottom-left-radius:0;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(20%,#eee),color-stop(80%,#fff));background-image:linear-gradient(#eee 20%,#fff 80%);-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset}.chosen-container-active.chosen-with-drop .chosen-single div{border-left:none;background:0 0}.chosen-container-active.chosen-with-drop .chosen-single div b{background-position:-18px 2px}.chosen-container-active .chosen-choices{border:1px solid #5897fb;-webkit-box-shadow:0 0 5px rgba(0,0,0,.3);box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#222!important}.chosen-disabled{opacity:.5!important;cursor:default}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-rtl{text-align:right}.chosen-rtl .chosen-single{overflow:visible;padding:0 8px 0 0}.chosen-rtl .chosen-single span{margin-right:0;margin-left:26px;direction:rtl}.chosen-rtl .chosen-single-with-deselect span{margin-left:38px}.chosen-rtl .chosen-single div{right:auto;left:3px}.chosen-rtl .chosen-single abbr{right:auto;left:26px}.chosen-rtl .chosen-choices li{float:right}.chosen-rtl .chosen-choices li.search-field input[type=text]{direction:rtl}.chosen-rtl .chosen-choices li.search-choice{margin:3px 5px 3px 0;padding:3px 5px 3px 19px}.chosen-rtl .chosen-choices li.search-choice .search-choice-close{right:auto;left:4px}.chosen-rtl.chosen-container-single .chosen-results{margin:0 0 4px 4px;padding:0 4px 0 0}.chosen-rtl .chosen-results li.group-option{padding-right:15px;padding-left:0}.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div{border-right:none}.chosen-rtl .chosen-search input[type=text]{padding:4px 5px 4px 20px;background:url(chosen-sprite.png) no-repeat -30px -20px;direction:rtl}.chosen-rtl.chosen-container-single .chosen-single div b{background-position:6px 2px}.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b{background-position:-12px 2px}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min-resolution:144dpi),only screen and (min-resolution:1.5dppx){.chosen-container .chosen-results-scroll-down span,.chosen-container .chosen-results-scroll-up span,.chosen-container-multi .chosen-choices .search-choice .search-choice-close,.chosen-container-single .chosen-search input[type=text],.chosen-container-single .chosen-single abbr,.chosen-container-single .chosen-single div b,.chosen-rtl .chosen-search input[type=text]{background-image:url(chosen-sprite@2x.png)!important;background-size:52px 37px!important;background-repeat:no-repeat!important}} \ No newline at end of file diff --git a/privet/ailabs/styles/prosilver/template/ailabs.css b/privet/ailabs/styles/prosilver/template/ailabs.css new file mode 100644 index 0000000..e5ab063 --- /dev/null +++ b/privet/ailabs/styles/prosilver/template/ailabs.css @@ -0,0 +1,43 @@ +.ailabs { + padding: 1px 0; + display: inline-flex; + float: left; +} + +.ailabs div { + margin-left: 3px; +} + +.ailabs-log div { + padding-bottom: 3px; + display: flex; +} + +.ailabs-log label { + width: 100px; + font-weight: bold; +} + +.ailabs-log textarea { + width: 100%; + resize: vertical; + /* Allow resizing in both directions */ + overflow: auto; + /* Add scrollbars when content overflows */ + /* pointer-events: none; */ + /* Make the textarea read-only */ + border: none; + /* Remove the border from the textarea */ + outline: none; + /* Remove the outline when focused */ + /* background: inherit; */ + /* Remove the background */ + font-family: inherit; + /* Inherit the font-family from the parent */ + font-size: inherit; + /* Inherit the font-size from the parent */ + line-height: inherit; + /* Inherit the line-height from the parent */ + color: inherit; + /* Inherit the text color from the parent */ +} \ No newline at end of file diff --git a/privet/ailabs/styles/prosilver/template/event/viewtopic_body_postrow_post_notices_before.html b/privet/ailabs/styles/prosilver/template/event/viewtopic_body_postrow_post_notices_before.html new file mode 100644 index 0000000..97b908a --- /dev/null +++ b/privet/ailabs/styles/prosilver/template/event/viewtopic_body_postrow_post_notices_before.html @@ -0,0 +1 @@ +{% INCLUDE 'post_ailabs.html' %} \ No newline at end of file diff --git a/privet/ailabs/styles/prosilver/template/post_ailabs.html b/privet/ailabs/styles/prosilver/template/post_ailabs.html new file mode 100644 index 0000000..4beb28b --- /dev/null +++ b/privet/ailabs/styles/prosilver/template/post_ailabs.html @@ -0,0 +1,19 @@ +{% if postrow.U_AILABS %} +
      + AI  + {% if postrow.U_AILABS_VIEW_LOG %} + + {% endif %} + {% for ailabs in postrow.U_AILABS %} +
      + {{ ailabs.ailabs_username }} + {% if ailabs.response_url %} + {{ ailabs.status }} + {% else %} + {{ ailabs.status }} + {% endif %} +
      + {% endfor %} +
      +{% endif %} \ No newline at end of file diff --git a/privet/ailabs/styles/prosilver/template/post_ailabs_log.html b/privet/ailabs/styles/prosilver/template/post_ailabs_log.html new file mode 100644 index 0000000..8e0f51a --- /dev/null +++ b/privet/ailabs/styles/prosilver/template/post_ailabs_log.html @@ -0,0 +1,88 @@ +{% INCLUDECSS '@privet_ailabs/ailabs.css' %} +{% INCLUDE 'simple_header.html' %} + + + + +{% for logs in ailabs_log %} +{% for log in logs['LOGS'] %} +
      +
      + +
      +
      {{ log.status }}
      +
      {{ log.attempts }}
      +
      {{ log.post_mode }}
      +
      +
      {{ log.request_time }}
      +
      + +
      +
      {{ log.response_time }}
      +
      + + +
      {{ log.request_tokens }}
      + + +
      {{ log.response_tokens }}
      + +
      + + +
      + + +
      + +
      +
      +{% endfor %} +{% endfor %} + + +
      + + + + +{% INCLUDE 'simple_footer.html' %} \ No newline at end of file