diff --git a/bin/set-settings.php b/bin/set-settings.php index 15db2a8..e6516d4 100644 --- a/bin/set-settings.php +++ b/bin/set-settings.php @@ -3,7 +3,6 @@ $settings = [ 'api_key' => 'sk_XXXXXXXXX', // Dummy license key. - 'assistant_id' => 'asst_TtalCGxTygMEqb7g3vNy6q8h', // Dummy assistant key. 'qdrant_api_key' => '', 'qdrant_endpoint' => '', 'chat_enabled' => false, diff --git a/composer.lock b/composer.lock index f44dea9..ab178bf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "085f400101febf3dd7c3edad8bdbfb0c", + "content-hash": "2e446c5d147699bcb595b4c696ba4f3a", "packages": [ { "name": "codeinwp/themeisle-sdk", @@ -91,22 +91,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -197,7 +197,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -213,20 +213,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -234,7 +234,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -280,7 +280,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -296,20 +296,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -325,7 +325,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -396,7 +396,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -412,7 +412,7 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "psr/http-client", @@ -905,28 +905,28 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -946,9 +946,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -956,7 +956,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -977,9 +976,28 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-07-17T20:45:56+00:00" }, { "name": "doctrine/instantiator", @@ -1053,16 +1071,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -1101,7 +1119,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -1109,20 +1127,20 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -1141,7 +1159,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1165,9 +1183,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "phar-io/manifest", @@ -1289,16 +1307,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.8.1", + "version": "v6.8.2", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "92e444847d94f7c30f88c60004648f507688acd5" + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/92e444847d94f7c30f88c60004648f507688acd5", - "reference": "92e444847d94f7c30f88c60004648f507688acd5", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", "shasum": "" }, "conflict": { @@ -1306,7 +1324,7 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^5.4", + "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", "phpdocumentor/reflection-docblock": "^5.4.1", @@ -1334,9 +1352,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.1" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" }, - "time": "2025-05-02T12:33:34+00:00" + "time": "2025-07-16T06:41:00+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -1402,29 +1420,29 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.3.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd" + "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/46d08eb86eec622b96c466adec3063adfed280dd", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/882b8c947ada27eb002870fe77fee9ce0a454cdb", + "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.12.1" + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -1480,33 +1498,33 @@ "type": "thanks_dev" } ], - "time": "2025-04-20T23:35:32+00:00" + "time": "2025-09-05T06:54:52+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.12", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", + "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -1543,6 +1561,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -1566,22 +1585,26 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-05-20T13:34:27+00:00" + "time": "2025-09-05T00:00:03+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.17", + "version": "2.1.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" + "reference": "578fa296a166605d97b94091f724f1257185d278" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", + "reference": "578fa296a166605d97b94091f724f1257185d278", "shasum": "" }, "require": { @@ -1626,7 +1649,7 @@ "type": "github" } ], - "time": "2025-05-21T20:55:28+00:00" + "time": "2025-09-19T08:58:49+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1949,16 +1972,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.23", + "version": "9.6.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + "reference": "a8017241a554a259997a5285eee5d10c69ff7187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8017241a554a259997a5285eee5d10c69ff7187", + "reference": "a8017241a554a259997a5285eee5d10c69ff7187", "shasum": "" }, "require": { @@ -1969,7 +1992,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -1980,11 +2003,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.7", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -2032,7 +2055,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.28" }, "funding": [ { @@ -2056,7 +2079,7 @@ "type": "tidelift" } ], - "time": "2025-05-02T06:40:34+00:00" + "time": "2025-09-23T06:20:12+00:00" }, { "name": "sebastian/cli-parser", @@ -2227,16 +2250,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -2289,15 +2312,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -2487,16 +2522,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "eb49b981ef0817890129cb70f774506bebe57740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/eb49b981ef0817890129cb70f774506bebe57740", + "reference": "eb49b981ef0817890129cb70f774506bebe57740", "shasum": "" }, "require": { @@ -2552,28 +2587,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.7" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-22T05:18:21+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -2616,15 +2663,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -2797,16 +2856,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -2848,15 +2907,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -3080,16 +3151,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.0", + "version": "3.13.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186" + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", "shasum": "" }, "require": { @@ -3160,7 +3231,7 @@ "type": "thanks_dev" } ], - "time": "2025-05-11T03:36:00+00:00" + "time": "2025-09-05T05:47:09+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -3276,16 +3347,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/d2421de7cec3274ae622c22c744de9a62c7925af", + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af", "shasum": "" }, "require": { @@ -3294,13 +3365,13 @@ "ext-tokenizer": "*", "ext-xmlreader": "*", "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.10", - "squizlabs/php_codesniffer": "^3.9.0" + "phpcsstandards/phpcsextra": "^1.4.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" @@ -3338,7 +3409,7 @@ "type": "custom" } ], - "time": "2024-03-25T16:39:00+00:00" + "time": "2025-07-24T20:08:31+00:00" }, { "name": "yoast/phpunit-polyfills", @@ -3411,5 +3482,8 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, + "platform-overrides": { + "php": "7.4" + }, "plugin-api-version": "2.6.0" } diff --git a/inc/API.php b/inc/API.php index 8c6a727..f1a41b9 100644 --- a/inc/API.php +++ b/inc/API.php @@ -187,35 +187,6 @@ public function register_routes() { ], ], 'chat' => [ - [ - 'methods' => \WP_REST_Server::READABLE, - 'args' => [ - 'run_id' => [ - 'required' => true, - 'type' => 'string', - ], - 'thread_id' => [ - 'required' => true, - 'type' => 'string', - ], - 'record_id' => [ - 'required' => true, - 'type' => [ - 'string', - 'integer', - ], - ], - 'message' => [ - 'required' => false, - 'type' => 'string', - ], - ], - 'callback' => [ $this, 'get_chat' ], - 'permission_callback' => function ( $request ) { - $nonce = $request->get_header( 'x_wp_nonce' ); - return wp_verify_nonce( $nonce, 'wp_rest' ); - }, - ], [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ @@ -229,13 +200,10 @@ public function register_routes() { ], 'record_id' => [ 'required' => false, - 'type' => [ - 'string', - 'integer', - ], + 'type' => 'string', ], ], - 'callback' => [ $this, 'send_chat' ], + 'callback' => [ $this, 'stream_chat' ], 'permission_callback' => function ( $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); return wp_verify_nonce( $nonce, 'wp_rest' ); @@ -404,13 +372,11 @@ function ( $carry, $item ) { if ( 'api_key' === $key && ! empty( $value ) ) { $openai = new OpenAI( $value ); - $valid_api = $openai->setup_assistant(); + $valid_api = $openai->moderate( 'This is a test message.' ); if ( is_wp_error( $valid_api ) ) { return rest_ensure_response( [ 'error' => $this->get_error_message( $valid_api ) ] ); } - - $settings['assistant_id'] = $valid_api; } if ( 'telemetry_enabled' === $key ) { @@ -545,6 +511,119 @@ public function get_data( $request ) { return rest_ensure_response( $posts ); } + /** + * Stream chat response using Server-Sent Events. + * + * @param \WP_REST_Request $request Request object. + * @return void + */ + public function stream_chat( $request ) { + header( 'Content-Type: text/event-stream' ); + header( 'Cache-Control: no-cache' ); + header( 'Connection: keep-alive' ); + + if ( ob_get_level() ) { + ob_end_clean(); + } + + $message = $request->get_param( 'message' ); + $moderation = OpenAI::instance()->moderate_chunks( $message ); + + if ( true !== $moderation ) { + $this->send_sse_message( 'error', [ 'error' => __( 'Message was flagged.', 'hyve-lite' ) ] ); + exit; + } + + $openai = OpenAI::instance(); + $message_vector = $openai->create_embeddings( $message ); + $message_vector = reset( $message_vector ); + $message_vector = $message_vector->embedding; + + if ( is_wp_error( $message_vector ) ) { + $this->send_sse_message( 'error', [ 'error' => __( 'No embeddings found.', 'hyve-lite' ) ] ); + exit; + } + + if ( $request->get_param( 'thread_id' ) ) { + $thread_id = $request->get_param( 'thread_id' ); + } else { + $thread_id = $openai->create_conversation(); + } + + if ( is_wp_error( $thread_id ) ) { + $this->send_sse_message( 'error', [ 'error' => $this->get_error_message( $thread_id ) ] ); + exit; + } + + $similarity_score_threshold = apply_filters( 'hyve_similarity_score_threshold', 0.4 ); + $article_context = $this->search_knowledge_base( $message_vector, $similarity_score_threshold ); + + $this->send_sse_message( + 'init', + [ + 'thread_id' => $thread_id, + 'message' => 'Starting response...', + ] + ); + + $messages = []; + + if ( ! empty( $article_context ) ) { + $messages[] = [ + 'type' => 'message', + 'role' => 'user', + 'content' => 'START CONTEXT: ' . $article_context . ' :END CONTEXT', + ]; + } else { + $messages[] = [ + 'type' => 'message', + 'role' => 'user', + 'content' => 'START CONTEXT: No specific documentation context found. Please provide a general helpful response if possible. :END CONTEXT', + ]; + } + + $messages[] = [ + 'type' => 'message', + 'role' => 'user', + 'content' => 'START QUESTION: ' . $message . ' :END QUESTION', + ]; + + try { + $openai->create_response( + $messages, + $thread_id + ); + } catch ( Exception $e ) { + $this->send_sse_message( + 'error', + [ + 'error' => 'Failed to generate response. Please try again.', + 'details' => $e->getMessage(), + ] + ); + } + + exit; + } + + /** + * Send Server-Sent Events message. + * + * @param string $type Event type. + * @param mixed $data Event data. + */ + private function send_sse_message( $type, $data ) { + echo 'event: ' . esc_html( $type ) . "\n"; + echo 'data: ' . wp_json_encode( $data ) . "\n\n"; + + if ( function_exists( 'fastcgi_finish_request' ) ) { + fastcgi_finish_request(); + } else { + ob_flush(); + flush(); + } + } + /** * Add data. * @@ -740,68 +819,6 @@ public function qdrant_deactivate() { return rest_ensure_response( __( 'Qdrant deactivated.', 'hyve-lite' ) ); } - /** - * Get chat. - * - * @param \WP_REST_Request> $request Request object. - * - * @return \WP_REST_Response - */ - public function get_chat( $request ) { - $run_id = $request->get_param( 'run_id' ); - $thread_id = $request->get_param( 'thread_id' ); - $query = $request->get_param( 'message' ); - $record_id = $request->get_param( 'record_id' ); - - $openai = OpenAI::instance(); - - $status = $openai->get_status( $run_id, $thread_id ); - - if ( is_wp_error( $status ) ) { - return rest_ensure_response( [ 'error' => $this->get_error_message( $status ) ] ); - } - - if ( 'completed' !== $status ) { - return rest_ensure_response( [ 'status' => $status ] ); - } - - $messages = $openai->get_messages( $thread_id ); - - if ( is_wp_error( $messages ) ) { - return rest_ensure_response( [ 'error' => $this->get_error_message( $messages ) ] ); - } - - $messages = array_filter( - $messages, - function ( $message ) use ( $run_id ) { - return $message->run_id === $run_id; - } - ); - - $message = reset( $messages )->content[0]->text->value; - - $message = json_decode( $message, true ); - - if ( json_last_error() !== JSON_ERROR_NONE ) { - return rest_ensure_response( [ 'error' => __( 'No messages found.', 'hyve-lite' ) ] ); - } - - Main::add_labels_to_default_settings(); - $settings = Main::get_settings(); - - $response = ( isset( $message['success'] ) && true === $message['success'] && isset( $message['response'] ) ) ? $message['response'] : esc_html( $settings['default_message'] ); - - do_action( 'hyve_chat_response', $run_id, $thread_id, $query, $record_id, $message, $response ); - - return rest_ensure_response( - [ - 'status' => $status, - 'success' => isset( $message['success'] ) ? $message['success'] : false, - 'message' => $response, - ] - ); - } - /** * Search knowledge base using Qdrant vector database. * @@ -955,111 +972,4 @@ public function search_knowledge_base( $message_vector, $similarity_score_thresh return $this->search_knowledge_base_wp( $message_vector, $similarity_score_threshold, $tokens_threshold ); } - - /** - * Send chat. - * - * @param \WP_REST_Request> $request Request object. - * - * @return \WP_REST_Response - */ - public function send_chat( $request ) { - $message = $request->get_param( 'message' ); - $record_id = $request->get_param( 'record_id' ); - $moderation = OpenAI::instance()->moderate_chunks( $message ); - - if ( true !== $moderation ) { - return rest_ensure_response( [ 'error' => __( 'Message was flagged.', 'hyve-lite' ) ] ); - } - - $openai = OpenAI::instance(); - $message_vector = $openai->create_embeddings( $message ); - $message_vector = reset( $message_vector ); - $message_vector = $message_vector->embedding; - - if ( is_wp_error( $message_vector ) ) { - return rest_ensure_response( [ 'error' => __( 'No embeddings found.', 'hyve-lite' ) ] ); - } - - if ( $request->get_param( 'thread_id' ) ) { - $thread_id = $request->get_param( 'thread_id' ); - } else { - $thread_id = $openai->create_thread(); - } - - if ( is_wp_error( $thread_id ) ) { - return rest_ensure_response( [ 'error' => $this->get_error_message( $thread_id ) ] ); - } - - /** - * Filters the similarity score threshold for knowledge base search. - * - * The similarity score threshold determines the minimum cosine similarity - * required for an article to be considered relevant to the user's query. - * A higher value means stricter matching, while a lower value allows for - * broader results. - * - * @since 1.4.0 - * - * @param float $similarity_score_threshold The similarity score threshold. Default 0.4. - */ - $similarity_score_threshold = apply_filters( 'hyve_similarity_score_threshold', 0.4 ); - - $article_context = $this->search_knowledge_base( $message_vector, $similarity_score_threshold ); - $query_run = $openai->create_run( - [ - [ - 'role' => 'user', - 'content' => 'START QUESTION: ' . $message . ' :END QUESTION', - ], - [ - 'role' => 'user', - 'content' => 'START CONTEXT: ' . $article_context . ' :END CONTEXT', - ], - ], - $thread_id - ); - - if ( is_wp_error( $query_run ) ) { - if ( strpos( $this->get_error_message( $query_run ), 'No thread found with id' ) !== false ) { - $thread_id = $openai->create_thread(); - - if ( is_wp_error( $thread_id ) ) { - return rest_ensure_response( [ 'error' => $this->get_error_message( $thread_id ) ] ); - } - - $query_run = $openai->create_run( - [ - [ - 'role' => 'user', - 'content' => 'Question: ' . $message, - ], - [ - 'role' => 'user', - 'content' => 'Context: ' . $article_context, - ], - ], - $thread_id - ); - - if ( is_wp_error( $query_run ) ) { - return rest_ensure_response( [ 'error' => $this->get_error_message( $query_run ) ] ); - } - } - } - - $hash = hash( 'md5', strtolower( $message ) ); - set_transient( 'hyve_message_' . $hash, $message_vector, MINUTE_IN_SECONDS ); - - $record_id = apply_filters( 'hyve_chat_request', $thread_id, $record_id, $message ); - - return rest_ensure_response( - [ - 'thread_id' => $thread_id, - 'query_run' => $query_run, - 'record_id' => $record_id ? $record_id : null, - 'content' => $article_context, - ] - ); - } } diff --git a/inc/Main.php b/inc/Main.php index 0a9665e..d673c3c 100644 --- a/inc/Main.php +++ b/inc/Main.php @@ -77,8 +77,7 @@ public function __construct() { } if ( - isset( $settings['api_key'] ) && isset( $settings['assistant_id'] ) && - ! empty( $settings['api_key'] ) && ! empty( $settings['assistant_id'] ) + isset( $settings['api_key'] ) && ! empty( $settings['api_key'] ) ) { add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } @@ -349,7 +348,8 @@ public function enqueue_assets() { apply_filters( 'hyve_frontend_data', [ - 'api' => $this->api->get_endpoint(), + 'api' => rest_url( $this->api->get_endpoint() . '/chat' ), + 'nonce' => wp_create_nonce( 'wp_rest' ), 'audio' => [ 'click' => HYVE_LITE_URL . 'assets/audio/click.mp3', 'ping' => HYVE_LITE_URL . 'assets/audio/ping.mp3', @@ -750,6 +750,8 @@ public function plugin_usage( $data ) { unset( $settings['qdrant_endpoint'] ); } + // We no longer use assistant_id but in case the setting exists, + // it is private and we omit it from the usage data. if ( isset( $settings['assistant_id'] ) ) { unset( $settings['assistant_id'] ); } diff --git a/inc/OpenAI.php b/inc/OpenAI.php index a697ba4..874bef9 100644 --- a/inc/OpenAI.php +++ b/inc/OpenAI.php @@ -42,11 +42,11 @@ class OpenAI { private $api_key; /** - * Assistant ID. + * Current event type being processed. * * @var string */ - private $assistant_id; + private $current_event = ''; /** * The single instance of the class. @@ -81,172 +81,9 @@ public static function instance() { * @param string $api_key API Key. */ public function __construct( $api_key = '' ) { - $settings = Main::get_settings(); - $this->api_key = ! empty( $api_key ) ? $api_key : ( isset( $settings['api_key'] ) ? $settings['api_key'] : '' ); - $this->assistant_id = isset( $settings['assistant_id'] ) ? $settings['assistant_id'] : ''; - $this->chat_model = isset( $settings['chat_model'] ) ? $settings['chat_model'] : $this->chat_model; - - if ( $this->assistant_id && version_compare( $this->prompt_version, get_option( 'hyve_prompt_version', '1.0.0' ), '>' ) ) { - $this->update_assistant(); - } - } - - /** - * Get Assistant Properties. - * - * @return array - */ - public function get_properties() { - $props = [ - 'instructions' => "You are a Support Assistant tasked with providing precise, to-the-point answers based on the context provided for each query, as well as maintaining awareness of previous context for follow-up questions.\r\n\r\nCore Principles:\r\n\r\n1. Context and Question Analysis\r\n- Identify the context given in each message.\r\n- Determine the specific question to be answered based on the current context and previous interactions.\r\n\r\n2. Relevance Check\r\n- Assess if the current context or previous context contains information directly relevant to the question.\r\n- Proceed based on the following scenarios:\r\na) If current context addresses the question: Formulate a response using current context.\r\nb) If current context is empty but previous context is relevant: Use previous context to answer.\r\nc) If the input is a greeting: Respond appropriately.\r\nd) If neither current nor previous context addresses the question: Respond with an empty response and success: false.\r\n\r\n3. Response Formulation\r\n- Use information from the current context primarily. If current context is insufficient, refer to previous context for follow-up questions.\r\n- Include all relevant details, including any code snippets or links if present.\r\n- Avoid including unnecessary information.\r\n- Format the response in HTML using only these allowed tags: h2, h3, p, img, a, pre, strong, em.\r\n\r\n4. Context Reference\r\n- Do not explicitly mention or refer to the context in your answer.\r\n- Provide a straightforward response that directly answers the question.\r\n\r\n5. Response Structure\r\n- Always structure your response as a JSON object with 'response' and 'success' fields.\r\n- The 'response' field should contain the HTML-formatted answer.\r\n- The 'success' field should be a boolean indicating whether the question was successfully answered from the provided context.\r\n\r\n6. Handling Follow-up Questions\r\n- Maintain awareness of previous context to answer follow-up questions.\r\n- If current context is empty but the question seems to be a follow-up, attempt to answer using previous context.\r\n\r\nExamples:\r\n\r\n1. Initial Question with Full Answer\r\nContext: The price of XYZ product is $99.99 USD.\r\nQuestion: How much does XYZ cost?\r\nResponse:\r\n{\r\n\"response\": \"

The price of XYZ product is $99.99 USD.

\",\r\n\"success\": true\r\n}\r\n\r\n2. Follow-up Question with Empty Current Context\r\nContext: [Empty]\r\nQuestion: What currency is that in?\r\nResponse:\r\n{\r\n\"response\": \"

The price is in USD (United States Dollars).

\",\r\n\"success\": true\r\n}\r\n\r\n3. No Relevant Information in Current or Previous Context\r\nContext: [Empty]\r\nQuestion: Do you offer gift wrapping?\r\nResponse:\r\n{\r\n\"response\": \"\",\r\n\"success\": false\r\n}\r\n\r\n4. Greeting\r\nQuestion: Hello!\r\nResponse:\r\n{\r\n\"response\": \"

Hello! How can I assist you today?

\",\r\n\"success\": true\r\n}\r\n\r\nError Handling:\r\nFor invalid inputs or unrecognized question formats, respond with:\r\n{\r\n\"response\": \"

I apologize, but I couldn't understand your question. Could you please rephrase it?

\",\r\n\"success\": false\r\n}\r\n\r\nHTML Usage Guidelines:\r\n- Use

for main headings and

for subheadings.\r\n- Wrap paragraphs in

tags.\r\n- Use

 for code snippets or formatted text.\r\n- Apply  for bold and  for italic emphasis sparingly.\r\n- Include  only if specific image information is provided in the context.\r\n- Use  for links, ensuring they are relevant and from the provided context.\r\n\r\nRemember:\r\n- Prioritize using the current context for answers.\r\n- For follow-up questions with empty current context, refer to previous context if relevant.\r\n- If information isn't available in current or previous context, indicate this with an empty response and success: false.\r\n- Always strive to provide the most accurate and relevant information based on available context.",
-			'model'        => $this->chat_model,
-		];
-
-		if ( 'gpt-4o-mini' === $this->chat_model ) {
-			$props['response_format'] = [
-				'type'        => 'json_schema',
-				'json_schema' => [
-					'name'   => 'chatbot_response',
-					'strict' => false,
-					'schema' => [
-						'type'                 => 'object',
-						'properties'           => [
-							'response' => [
-								'type'        => 'string',
-								'description' => 'The HTML-formatted response to the user\'s question.',
-							],
-							'success'  => [
-								'type'        => 'boolean',
-								'description' => 'Indicates whether the question was successfully answered from the provided context.',
-							],
-						],
-						'required'             => [ 'success' ],
-						'additionalProperties' => false,
-					],
-				],
-			];
-		}
-
-		return $props;
-	}
-
-	/**
-	 * Setup Assistant.
-	 * 
-	 * @return string|\WP_Error
-	 */
-	public function setup_assistant() {
-		$assistant = $this->retrieve_assistant();
-
-		if ( is_wp_error( $assistant ) ) {
-			return $assistant;
-		}
-
-		if ( ! $assistant ) {
-			return $this->create_assistant();
-		}
-
-		return $assistant;
-	}
-
-	/**
-	 * Create Assistant.
-	 * 
-	 * @return string|\WP_Error
-	 */
-	public function create_assistant() {
-		$response = $this->request(
-			'assistants',
-			array_merge(
-				$this->get_properties(),
-				[
-					'name' => 'Chatbot by Hyve',
-				]
-			)
-		);
-
-		if ( is_wp_error( $response ) ) {
-			return $response;
-		}
-
-		if ( isset( $response->id ) ) {
-			$this->assistant_id = $response->id;
-			return $response->id;
-		}
-
-		return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the assistant.', 'hyve-lite' ) );
-	}
-
-	/**
-	 * Update Assistant.
-	 * 
-	 * @return bool|\WP_Error
-	 */
-	public function update_assistant() {
-		$assistant    = $this->retrieve_assistant();
-		$settings     = Main::get_settings();
-		$assistant_id = '';
-
-		if ( is_wp_error( $assistant ) ) {
-			return $assistant;
-		}
-
-		if ( ! $assistant ) {
-			$assistant_id = $this->create_assistant();
-
-			if ( is_wp_error( $assistant_id ) ) {
-				return $assistant_id;
-			}
-		} else {
-			$response = $this->request(
-				'assistants/' . $this->assistant_id,
-				$this->get_properties()
-			);
-
-			if ( is_wp_error( $response ) ) {
-				return $response;
-			}
-
-			if ( ! isset( $response->id ) ) {
-				return false;
-			}
-
-			$this->assistant_id = $response->id;
-			$assistant_id       = $response->id;
-		}
-
-		$settings['assistant_id'] = $assistant_id;
-		update_option( 'hyve_settings', $settings );
-		update_option( 'hyve_prompt_version', $this->prompt_version );
-
-		return true;
-	}
-
-	/**
-	 * Retrieve Assistant.
-	 * 
-	 * @return string|\WP_Error|false
-	 */
-	public function retrieve_assistant() {
-		if ( ! $this->assistant_id ) {
-			return false;
-		}
-
-		$response = $this->request( 'assistants/' . $this->assistant_id );
-
-		if ( is_wp_error( $response ) ) {
-			if ( strpos( $response->get_error_message(), 'No assistant found' ) !== false ) {
-				return false;
-			}
-
-			return $response;
-		}
-
-		if ( isset( $response->id ) ) {
-			return $response->id;
-		}
-
-		return false;
+		$settings         = Main::get_settings();
+		$this->api_key    = ! empty( $api_key ) ? $api_key : ( isset( $settings['api_key'] ) ? $settings['api_key'] : '' );
+		$this->chat_model = isset( $settings['chat_model'] ) ? $settings['chat_model'] : $this->chat_model;
 	}
 
 	/**
@@ -278,15 +115,15 @@ public function create_embeddings( $content, $model = 'text-embedding-3-small' )
 	}
 
 	/**
-	 * Create a Thread.
+	 * Create a Conversation.
 	 * 
 	 * @param array $params Parameters.
 	 * 
 	 * @return string|\WP_Error
 	 */
-	public function create_thread( $params = [] ) {
+	public function create_conversation( $params = [] ) {
 		$response = $this->request(
-			'threads',
+			'conversations',
 			$params
 		);
 
@@ -298,115 +135,53 @@ public function create_thread( $params = [] ) {
 			return $response->id;
 		}
 
-		return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the thread.', 'hyve-lite' ) );
+		return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the conversation.', 'hyve-lite' ) );
 	}
 
 	/**
-	 * Send Message.
+	 * Create a Response.
 	 * 
-	 * @param string $message Message.
-	 * @param string $thread  Thread.
-	 * @param string $role    Role.
+	 * @param array> $items        Items.
+	 * @param string                      $conversation Conversation.
 	 * 
-	 * @return true|\WP_Error
+	 * @return object|\WP_Error
 	 */
-	public function send_message( $message, $thread, $role = 'assistant' ) {
-		$response = $this->request(
-			'threads/' . $thread . '/messages',
-			[
-				'role'    => $role,
-				'content' => $message,
-			]
-		);
-
-		if ( is_wp_error( $response ) ) {
-			return $response;
-		}
-
-		if ( isset( $response->id ) ) {
-			return true;
-		}
-
-		return new \WP_Error( 'unknown_error', __( 'An error occurred while sending the message.', 'hyve-lite' ) );
-	}
-
-	/**
-	 * Create a run
-	 * 
-	 * @param array> $messages Messages.
-	 * @param string                      $thread  Thread.
-	 * 
-	 * @return string|\WP_Error
-	 */
-	public function create_run( $messages, $thread ) {
+	public function create_response( $items, $conversation ) {
 		$settings = Main::get_settings();
 
-		$response = $this->request(
-			'threads/' . $thread . '/runs',
-			[
-				'assistant_id'        => $this->assistant_id,
-				'additional_messages' => $messages,
-				'model'               => $this->chat_model,
-				'temperature'         => $settings['temperature'],
-				'top_p'               => $settings['top_p'],
-				'response_format'     => [
-					'type' => 'json_object',
+		$params = [
+			'conversation' => $conversation,
+			'model'        => $this->chat_model,
+			'temperature'  => $settings['temperature'],
+			'top_p'        => $settings['top_p'],
+			'input'        => $items,
+			'stream'       => true,
+			'instructions' => "You are a Support Assistant tasked with providing precise, to-the-point answers based on the context provided for each query, as well as maintaining awareness of previous context for follow-up questions.\r\n\r\nCore Principles:\r\n\r\n1. Context and Question Analysis\r\n- Identify the context given in each message.\r\n- Determine the specific question to be answered based on the current context and previous interactions.\r\n\r\n2. Relevance Check\r\n- Assess if the current context or previous context contains information directly relevant to the question.\r\n- Proceed based on the following scenarios:\r\na) If current context addresses the question: Formulate a response using current context.\r\nb) If current context is empty but previous context is relevant: Use previous context to answer.\r\nc) If the input is a greeting: Respond appropriately.\r\nd) If neither current nor previous context addresses the question: Respond with an empty response and success: false.\r\n\r\n3. Response Formulation\r\n- Use information from the current context primarily. If current context is insufficient, refer to previous context for follow-up questions.\r\n- Include all relevant details, including any code snippets or links if present.\r\n- Avoid including unnecessary information.\r\n- Format the response in HTML using only these allowed tags: h2, h3, p, img, a, pre, strong, em.\r\n\r\n4. Context Reference\r\n- Do not explicitly mention or refer to the context in your answer.\r\n- Provide a straightforward response that directly answers the question.\r\n\r\n5. Response Structure\r\n- Always structure your response as a JSON object with 'response' and 'success' fields.\r\n- The 'response' field should contain the HTML-formatted answer.\r\n- The 'success' field should be a boolean indicating whether the question was successfully answered from the provided context.\r\n\r\n6. Handling Follow-up Questions\r\n- Maintain awareness of previous context to answer follow-up questions.\r\n- If current context is empty but the question seems to be a follow-up, attempt to answer using previous context.\r\n\r\nExamples:\r\n\r\n1. Initial Question with Full Answer\r\nContext: The price of XYZ product is $99.99 USD.\r\nQuestion: How much does XYZ cost?\r\nResponse:\r\n{\r\n\"response\": \"

The price of XYZ product is $99.99 USD.

\",\r\n\"success\": true\r\n}\r\n\r\n2. Follow-up Question with Empty Current Context\r\nContext: [Empty]\r\nQuestion: What currency is that in?\r\nResponse:\r\n{\r\n\"response\": \"

The price is in USD (United States Dollars).

\",\r\n\"success\": true\r\n}\r\n\r\n3. No Relevant Information in Current or Previous Context\r\nContext: [Empty]\r\nQuestion: Do you offer gift wrapping?\r\nResponse:\r\n{\r\n\"response\": \"\",\r\n\"success\": false\r\n}\r\n\r\n4. Greeting\r\nQuestion: Hello!\r\nResponse:\r\n{\r\n\"response\": \"

Hello! How can I assist you today?

\",\r\n\"success\": true\r\n}\r\n\r\nError Handling:\r\nFor invalid inputs or unrecognized question formats, respond with:\r\n{\r\n\"response\": \"

I apologize, but I couldn't understand your question. Could you please rephrase it?

\",\r\n\"success\": false\r\n}\r\n\r\nHTML Usage Guidelines:\r\n- Use

for main headings and

for subheadings.\r\n- Wrap paragraphs in

tags.\r\n- Use

 for code snippets or formatted text.\r\n- Apply  for bold and  for italic emphasis sparingly.\r\n- Include  only if specific image information is provided in the context.\r\n- Use  for links, ensuring they are relevant and from the provided context.\r\n\r\nRemember:\r\n- Prioritize using the current context for answers.\r\n- For follow-up questions with empty current context, refer to previous context if relevant.\r\n- If information isn't available in current or previous context, indicate this with an empty response and success: false.\r\n- Always strive to provide the most accurate and relevant information based on available context.",
+			'text'         => [
+				'format' => [
+					'type'   => 'json_schema',
+					'name'   => 'chatbot_response',
+					'strict' => false,
+					'schema' => [
+						'type'                 => 'object',
+						'properties'           => [
+							'response' => [
+								'type'        => 'string',
+								'description' => 'The HTML-formatted response to the user\'s question.',
+							],
+							'success'  => [
+								'type'        => 'boolean',
+								'description' => 'Indicates whether the question was successfully answered from the provided context.',
+							],
+						],
+						'required'             => [ 'success' ],
+						'additionalProperties' => false,
+					],
 				],
-			]
-		);
-
-		if ( is_wp_error( $response ) ) {
-			return $response;
-		}
-
-		if ( ! isset( $response->id ) || ( isset( $response->status ) && 'queued' !== $response->status ) ) {
-			return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the run.', 'hyve-lite' ) );
-		}
-
-		return $response->id;
-	}
-
-	/**
-	 * Get Run Status.
-	 * 
-	 * @param string $run_id Run ID.
-	 * @param string $thread Thread.
-	 * 
-	 * @return string|\WP_Error
-	 */
-	public function get_status( $run_id, $thread ) {
-		$response = $this->request( 'threads/' . $thread . '/runs/' . $run_id, [], 'GET' );
-
-		if ( is_wp_error( $response ) ) {
-			return $response;
-		}
-
-		if ( isset( $response->status ) ) {
-			return $response->status;
-		}
-
-		return new \WP_Error( 'unknown_error', __( 'An error occurred while getting the run status.', 'hyve-lite' ) );
-	}
-
-	/**
-	 * Get Thread Messages.
-	 * 
-	 * @param string $thread Thread.
-	 * 
-	 * @return mixed
-	 */
-	public function get_messages( $thread ) {
-		$response = $this->request( 'threads/' . $thread . '/messages', [], 'GET' );
-
-		if ( is_wp_error( $response ) ) {
-			return $response;
-		}
-
-		if ( isset( $response->data ) ) {
-			return $response->data;
-		}
+			],
+		];
 
-		return new \WP_Error( 'unknown_error', __( 'An error occurred while getting the messages.', 'hyve-lite' ) );
+		return $this->stream_request( 'responses', $params );
 	}
 
 	/**
@@ -555,7 +330,6 @@ private function request( $endpoint, $params = [], $method = 'POST' ) {
 					'headers'     => [
 						'Content-Type'  => 'application/json',
 						'Authorization' => 'Bearer ' . $this->api_key,
-						'OpenAI-Beta'   => 'assistants=v2',
 					],
 					'body'        => $body,
 					'method'      => 'POST',
@@ -570,7 +344,6 @@ private function request( $endpoint, $params = [], $method = 'POST' ) {
 				'headers' => [
 					'Content-Type'  => 'application/json',
 					'Authorization' => 'Bearer ' . $this->api_key,
-					'OpenAI-Beta'   => 'assistants=v2',
 				],
 			];
 
@@ -605,6 +378,172 @@ private function request( $endpoint, $params = [], $method = 'POST' ) {
 		}
 	}
 
+	/**
+	 * Create streaming request to OpenAI API.
+	 * 
+	 * @param string               $endpoint Endpoint.
+	 * @param array $params   Parameters.
+	 * 
+	 * @return void
+	 */
+	private function stream_request( $endpoint, $params = [] ) {
+		if ( ! $this->api_key ) {
+			$this->send_sse_error( __( 'API key is missing.', 'hyve-lite' ) );
+			return;
+		}
+
+		$body = wp_json_encode( $params );
+		if ( false === $body ) {
+			$this->send_sse_error( __( 'Invalid parameters.', 'hyve-lite' ) );
+			return;
+		}
+
+		$url = self::$base_url . $endpoint;
+		
+		// Using cURL for streaming since wp_remote_request doesn't handle streaming well.
+		$curl = curl_init(); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
+		
+		curl_setopt_array( // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt_array
+			$curl,
+			[
+				CURLOPT_URL            => $url,
+				CURLOPT_POST           => true,
+				CURLOPT_POSTFIELDS     => $body,
+				CURLOPT_HTTPHEADER     => [
+					'Content-Type: application/json',
+					'Authorization: Bearer ' . $this->api_key,
+				],
+				CURLOPT_WRITEFUNCTION  => [ $this, 'handle_stream_chunk' ],
+				CURLOPT_TIMEOUT        => 120,
+				CURLOPT_CONNECTTIMEOUT => 30,
+				CURLOPT_SSL_VERIFYPEER => true,
+				CURLOPT_FOLLOWLOCATION => true,
+				CURLOPT_MAXREDIRS      => 3,
+			] 
+		);
+		
+		$result    = curl_exec( $curl ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
+		$http_code = curl_getinfo( $curl, CURLINFO_HTTP_CODE ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
+		
+		if ( curl_errno( $curl ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_errno
+			$error = curl_error( $curl ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_error
+			$errno = curl_errno( $curl ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_errno
+			curl_close( $curl ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
+			
+			$user_message = 'Connection error. Please try again.';
+			if ( CURLE_OPERATION_TIMEOUTED === $errno ) {
+				$user_message = 'Request timed out. Please try again.';
+			} elseif ( CURLE_COULDNT_CONNECT === $errno ) {
+				$user_message = 'Could not connect to the service. Please check your connection.';
+			}
+			
+			$this->send_sse_error( $user_message );
+			return;
+		}
+
+		curl_close( $curl ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
+
+		if ( 200 !== $http_code ) {
+			$error_message = 'Service unavailable. Please try again.';
+			if ( 401 === $http_code ) {
+				$error_message = 'Authentication failed.';
+			} elseif ( 429 === $http_code ) {
+				$error_message = 'Too many requests. Please wait a moment.';
+			} elseif ( 500 <= $http_code ) {
+				$error_message = 'Server error. Please try again shortly.';
+			}
+			
+			$this->send_sse_error( $error_message );
+			return;
+		}
+	}
+
+	/**
+	 * Handle streaming chunks from OpenAI.
+	 * 
+	 * @param resource $curl cURL resource.
+	 * @param string   $data Chunk data.
+	 * 
+	 * @return int Length of data processed.
+	 */
+	private function handle_stream_chunk( $curl, $data ) {
+		static $buffer = '';
+		
+		$buffer .= $data;
+		$lines   = explode( "\n", $buffer );
+		
+		// Keep the last line in buffer in case it's incomplete.
+		$buffer = array_pop( $lines );
+		
+		foreach ( $lines as $line ) {
+			$line = trim( $line );
+			
+			if ( empty( $line ) ) {
+				continue;
+			}
+			
+			if ( strpos( $line, 'event: ' ) === 0 ) {
+				$this->current_event = substr( $line, 7 );
+			} elseif ( strpos( $line, 'data: ' ) === 0 ) {
+				$data_content = substr( $line, 6 );
+				
+				if ( '[DONE]' === $data_content ) {
+					continue;
+				}
+				
+				$parsed_data = json_decode( $data_content, true );
+				if ( json_last_error() === JSON_ERROR_NONE ) {
+					$this->process_stream_event( ! empty( $this->current_event ) ? $this->current_event : 'data', $parsed_data );
+				}
+			}
+		}
+		
+		return strlen( $data );
+	}
+
+	/**
+	 * Process streaming events from OpenAI and forward them.
+	 * 
+	 * @param string $event_type Event type.
+	 * @param array  $data       Event data.
+	 */
+	private function process_stream_event( $event_type, $data ) {
+		$allowed_events = [
+			'response.output_text.delta',
+			'response.completed',
+			'error',
+		];
+		
+		if ( in_array( $event_type, $allowed_events, true ) ) {
+			echo 'event: ' . esc_html( $event_type ) . "\n";
+			echo 'data: ' . wp_json_encode( $data ) . "\n\n";
+			
+			if ( function_exists( 'fastcgi_finish_request' ) ) {
+				fastcgi_finish_request();
+			} else {
+				ob_flush();
+				flush();
+			}
+		}
+	}
+
+	/**
+	 * Send SSE error message.
+	 * 
+	 * @param string $message Error message.
+	 */
+	private function send_sse_error( $message ) {
+		echo "event: error\n";
+		echo 'data: ' . wp_json_encode( [ 'error' => $message ] ) . "\n\n";
+		
+		if ( function_exists( 'fastcgi_finish_request' ) ) {
+			fastcgi_finish_request();
+		} else {
+			ob_flush();
+			flush();
+		}
+	}
+
 	/**
 	 * Check the type of error returner by OpenAI and save if it is of interest.
 	 * 
@@ -622,7 +561,6 @@ private function check_and_save_error( $error ) {
 		$code = $error['code'];
 		
 		$errors_codes = [
-			// API Key Errors.
 			'invalid_api_key',
 			'insufficient_quota',
 			'invalid_authentication',
@@ -631,8 +569,6 @@ private function check_and_save_error( $error ) {
 			'organization_not_found',
 			'organization_deactivated',
 			'permission_denied',
-
-			// Rate Limiting Errors.
 			'rate_limit_exceeded',
 			'quota_exceeded ',
 		];
diff --git a/package-lock.json b/package-lock.json
index bcdc361..fcf1a56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "hyve-lite",
-  "version": "1.3.0",
+  "version": "1.3.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "hyve-lite",
-      "version": "1.3.0",
+      "version": "1.3.1",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@wordpress/icons": "^10.13.0",
diff --git a/src/backend/parts/settings/Assistant.js b/src/backend/parts/settings/Assistant.js
index af6db57..384479c 100644
--- a/src/backend/parts/settings/Assistant.js
+++ b/src/backend/parts/settings/Assistant.js
@@ -127,7 +127,7 @@ const Assistant = () => {
 					 {
 					 {
-					await this.getResponse( message );
-				}, 2000 );
+		try {
+			// Create AbortController for timeout handling
+			const controller = new AbortController();
+			const timeoutId = setTimeout( () => {
+				controller.abort();
+			}, 60000 ); // 60 second timeout
+
+			try {
+				const response = await fetch(
+					this.addCacheProtection( window.hyveClient.api ),
+					{
+						method: 'POST',
+						headers: {
+							'Content-Type': 'application/json',
+							'X-WP-Nonce': window.hyveClient.nonce,
+						},
+						body: JSON.stringify( {
+							message,
+							...( this.threadID
+								? { thread_id: this.threadID }
+								: {} ),
+						} ),
+						signal: controller.signal,
+					}
+				);
+
+				// Clear timeout on successful response
+				clearTimeout( timeoutId );
+
+				if ( ! response.ok ) {
+					throw new Error(
+						`HTTP error! status: ${ response.status }`
+					);
+				}
 
-				return;
-			}
+				if ( ! response.body ) {
+					throw new Error( 'Response body is null' );
+				}
 
-			this.removeMessage( this.runID );
+				const reader = response.body.getReader();
+				const decoder = new TextDecoder();
+				let buffer = '';
+				let currentEventType = null;
+
+				while ( true ) {
+					const { done, value } = await reader.read();
+
+					if ( done ) {
+						break;
+					}
+
+					// Decode the chunk and add to buffer
+					buffer += decoder.decode( value, { stream: true } );
+
+					// Process complete Server-Sent Event messages
+					const lines = buffer.split( '\n' );
+					buffer = lines.pop() || ''; // Keep incomplete line in buffer
+
+					for ( const line of lines ) {
+						if ( line.trim() === '' ) {
+							// Empty line indicates end of event, reset currentEventType
+							currentEventType = null;
+							continue;
+						}
+
+						// Parse Server-Sent Event format
+						if ( line.startsWith( 'event: ' ) ) {
+							currentEventType = line.substring( 7 ); // Remove 'event: '
+							continue;
+						}
+
+						if ( line.startsWith( 'data: ' ) ) {
+							const eventData = line.substring( 6 ); // Remove 'data: '
+
+							try {
+								const data = JSON.parse( eventData );
+								// Add the event type to the data object
+								data.type = currentEventType;
+								this.handleStreamEvent( data );
+							} catch ( parseError ) {
+								// SSE parsing failed - continue processing other events
+							}
+						}
+					}
+				}
 
-			if ( 'completed' === response.status ) {
-				this.add( response.message, 'bot' );
 				this.setLoading( false );
-			}
+			} catch ( fetchError ) {
+				// Clear timeout on error
+				clearTimeout( timeoutId );
 
-			if ( 'failed' === response.status ) {
-				this.add( strings.tryAgain, 'bot' );
-				this.setLoading( false );
+				// Handle AbortError from timeout differently
+				if ( fetchError.name === 'AbortError' ) {
+					throw new Error( 'Request timeout - please try again' );
+				}
+				throw fetchError;
 			}
 		} catch ( error ) {
-			this.add( strings.tryAgain, 'bot' );
+			// Enhanced error handling with user-friendly messages
+			this.removeMessage( 'streaming-response' );
+			this.handleStreamingError( error );
 			this.setLoading( false );
 		}
 	}
 
-	async sendRequest( message ) {
-		try {
-			this.setLoading( true );
+	/**
+	 * Handle individual stream events
+	 * @param {Object} data The event data
+	 */
+	handleStreamEvent( data ) {
+		if ( data.type === 'init' ) {
+			// Update thread_id if provided
+			if ( data.thread_id && data.thread_id !== this.threadID ) {
+				this.setThreadID( data.thread_id );
+			}
 
-			const preloaderId = 'hyve-preloader';
-			this.addPreloaderMessage( preloaderId );
+			// Remove the typing indicator and add streaming message
+			this.removeMessage( 'streaming-preloader' );
+			this.add(
+				strings.typing + ' (streaming)',
+				'bot',
+				'streaming-response'
+			);
+		} else if ( data.type === 'response.output_text.delta' ) {
+			// Handle OpenAI streaming text delta
+			const streamingMessage = this.messages.find(
+				( message ) => message.id === 'streaming-response'
+			);
+			const streamingElement = document.getElementById(
+				'hyve-message-streaming-response'
+			);
 
-			const response = await apiFetch( {
-				path: this.addCacheProtection(
-					`${ window.hyveClient.api }/chat`
-				),
-				method: 'POST',
-				data: {
-					message,
-					...( null !== this.threadID
-						? { thread_id: this.threadID }
-						: {} ),
-					...( null !== this.recordID
-						? { record_id: this.recordID }
-						: {} ),
-				},
-				headers: this.getDefaultHeaders(),
-			} );
+			if ( streamingMessage && streamingElement ) {
+				// Initialize fullText if not exists
+				if ( ! streamingMessage.fullText ) {
+					streamingMessage.fullText = '';
+				}
 
-			this.removeMessage( preloaderId );
+				// Accumulate all deltas to build the complete JSON response
+				streamingMessage.fullText += data.delta;
+
+				// Try to extract and display the response content as it builds
+				let displayText = strings.typing + '...';
+
+				// Look for the start of the response content
+				const responseStartMatch =
+					streamingMessage.fullText.match( /"response":"/ );
+				if ( responseStartMatch ) {
+					// Find where the response content starts
+					const contentStart =
+						responseStartMatch.index +
+						responseStartMatch[ 0 ].length;
+					let responseContent =
+						streamingMessage.fullText.substring( contentStart );
+
+					// Try to find the end of the response field (look for the closing quote before success)
+					// We need to be careful with escaped quotes in HTML content
+					let contentEnd = responseContent.length;
+
+					// Look for the pattern ","success" which indicates end of response field
+					const successMatch = responseContent.match( /","success"/ );
+					if ( successMatch ) {
+						contentEnd = successMatch.index;
+					}
+
+					// Extract the content (still may have escaped characters)
+					responseContent = responseContent.substring(
+						0,
+						contentEnd
+					);
+
+					// Try to decode JSON escapes for display
+					try {
+						// Attempt to parse the JSON-escaped string
+						displayText = JSON.parse( '"' + responseContent + '"' );
+
+						// If we got valid content, process it for streaming display
+						if ( displayText && displayText.trim() ) {
+							displayText =
+								this.processStreamingHTML( displayText );
+						} else {
+							displayText = strings.typing + '...';
+						}
+					} catch ( e ) {
+						// If JSON parsing fails, show raw content (still building)
+						if ( responseContent && responseContent.length > 0 ) {
+							// Clean up escaped characters for display
+							const rawContent = responseContent
+								.replace( /\\"/g, '"' )
+								.replace( /\\\//g, '/' );
+
+							displayText =
+								this.processStreamingHTML( rawContent );
+						} else {
+							displayText = strings.typing + '...';
+						}
+					}
+				}
 
-			if ( response.error ) {
-				this.add( strings.tryAgain, 'bot' );
-				this.setLoading( false );
-				return;
+				// Update the streaming message display
+				streamingMessage.message = displayText;
+				streamingElement.querySelector( 'div' ).innerHTML = displayText;
+
+				// Auto-scroll to bottom during streaming
+				this.scrollToBottom();
+
+				// Add subtle typing animation class for smoother feel
+				const messageDiv = streamingElement.querySelector( 'div' );
+				if ( messageDiv ) {
+					messageDiv.classList.add( 'hyve-typing-animation' );
+					// Remove animation class briefly to retrigger it
+					setTimeout( () => {
+						if ( messageDiv ) {
+							messageDiv.classList.remove(
+								'hyve-typing-animation'
+							);
+						}
+					}, 500 );
+				}
 			}
+		} else if ( data.type === 'response.completed' ) {
+			// Handle OpenAI response completion
+			const streamingMessage = this.messages.find(
+				( message ) => message.id === 'streaming-response'
+			);
 
-			if ( response.thread_id !== this.threadID ) {
-				this.setThreadID( response.thread_id );
+			if ( streamingMessage && streamingMessage.fullText ) {
+				try {
+					const parsed = JSON.parse( streamingMessage.fullText );
+
+					// Remove streaming message and add final response
+					this.removeMessage( 'streaming-response' );
+
+					if ( parsed.success && parsed.response ) {
+						// Successful response with content
+						this.add( parsed.response, 'bot' );
+					} else if ( parsed.success === false ) {
+						// OpenAI couldn't answer - show helpful message
+						this.add(
+							"

I don't have enough information to answer that question. Please provide more context or ask about something else.

", + 'bot' + ); + } else { + // Fallback - show whatever we have + this.add( + parsed.response || 'No response available', + 'bot' + ); + } + } catch ( e ) { + // JSON parsing failed - show raw text + this.removeMessage( 'streaming-response' ); + this.add( + streamingMessage.fullText || 'Error parsing response', + 'bot' + ); + } + } else { + // No streaming message found + this.removeMessage( 'streaming-response' ); + this.add( 'Response completed but no content received', 'bot' ); } + } else if ( data.type === 'data' ) { + // Legacy handling for non-OpenAI streaming + this.removeMessage( 'streaming-response' ); + this.add( data.message || JSON.stringify( data ), 'bot' ); + } else if ( data.type === 'error' ) { + this.removeMessage( 'streaming-response' ); + this.add( data.error || strings.tryAgain, 'bot' ); + } else if ( data.type === 'end' ) { + // Stream completed successfully + // Stream completed successfully + } + } + + /** + * Process HTML content for streaming display, handling incomplete tags gracefully + * @param {string} content The HTML content to process + * @return {string} Processed content safe for streaming display + */ + processStreamingHTML( content ) { + if ( ! content || ! content.trim() ) { + return strings.typing + '...'; + } + + // Find all complete HTML tags and text content + const parts = []; + const currentPos = 0; + let insideTag = false; + let tagStart = -1; + let textBuffer = ''; + + for ( let i = 0; i < content.length; i++ ) { + const char = content[ i ]; + + if ( char === '<' && ! insideTag ) { + // Starting a new tag - save any accumulated text first + if ( textBuffer ) { + parts.push( { type: 'text', content: textBuffer } ); + textBuffer = ''; + } + insideTag = true; + tagStart = i; + } else if ( char === '>' && insideTag ) { + // Ending a tag - check if it's complete + const tagContent = content.substring( tagStart, i + 1 ); + + // Check if it's a complete, valid tag + if ( this.isCompleteHTMLTag( tagContent ) ) { + parts.push( { type: 'tag', content: tagContent } ); + } else { + // Incomplete or invalid tag - treat as text for now + textBuffer += tagContent; + } - if ( response.record_id !== this.recordID ) { - this.setRecordID( response.record_id ); + insideTag = false; + tagStart = -1; + } else if ( ! insideTag ) { + // Regular text content + textBuffer += char; } + // If we're inside a tag, we wait until it's complete + } - this.setRunID( response.query_run ); + // Handle any remaining content + if ( insideTag && tagStart !== -1 ) { + // We have an incomplete tag at the end - don't show it + const incompleteTag = content.substring( tagStart ); + // Only show if it looks like it might be text that was mistaken for a tag + if ( + incompleteTag.length > 10 && + ! incompleteTag.match( /^<[a-zA-Z]/ ) + ) { + textBuffer += incompleteTag; + } + } else if ( textBuffer ) { + parts.push( { type: 'text', content: textBuffer } ); + } - this.add( strings.typing, 'bot', response.query_run ); + // Rebuild the content with complete elements only + let result = ''; + const openTags = []; + + for ( const part of parts ) { + if ( part.type === 'text' ) { + result += part.content; + } else if ( part.type === 'tag' ) { + const tag = part.content; + + // Check if it's a closing tag + if ( tag.startsWith( ']*?)(\s*\/?)>/ + ); + if ( tagMatch ) { + const tagName = tagMatch[ 1 ]; + const isSelfClosing = + tagMatch[ 3 ] === '/' || + [ + 'br', + 'hr', + 'img', + 'input', + 'meta', + 'link', + ].includes( tagName.toLowerCase() ); + + result += tag; + + if ( ! isSelfClosing ) { + openTags.push( tagName ); + } + } + } + } + } - await this.getResponse( message ); + // If we have no result yet, show typing indicator + if ( ! result.trim() ) { + return strings.typing + '...'; + } + + return result; + } + + /** + * Check if an HTML tag is complete and valid + * @param {string} tag The tag to check + * @return {boolean} True if the tag is complete and valid + */ + isCompleteHTMLTag( tag ) { + if ( ! tag.startsWith( '<' ) || ! tag.endsWith( '>' ) ) { + return false; + } + + // Basic validation for common HTML tags + const tagPattern = /^<\/?[a-zA-Z][a-zA-Z0-9]*([^<>]*?)\/?>$/; + return tagPattern.test( tag ); + } + + async sendRequest( message ) { + try { + this.setLoading( true ); + + // Clean up any previous streaming state before starting new request + this.removeMessage( 'streaming-response' ); + this.removeMessage( 'streaming-preloader' ); + + // Close any existing streaming connections + if ( this.currentEventSource ) { + this.currentEventSource.close(); + this.currentEventSource = null; + } + + // Always use streaming response + const preloaderId = 'streaming-preloader'; + this.addPreloaderMessage( preloaderId ); + await this.handleStreamingResponse( message ); } catch ( error ) { - this.removeMessage( 'hyve-preloader' ); + this.removeMessage( 'streaming-preloader' ); + this.removeMessage( 'streaming-response' ); this.add( strings.tryAgain, 'bot' ); + // Error handled - consider implementing error reporting service this.setLoading( false ); } } + /** + * Enhanced error handling for streaming requests + * @param {Error} error The error object + */ + handleStreamingError( error ) { + let errorMessage = strings.tryAgain; + + // Provide user-friendly error messages based on error type + if ( error.name === 'TypeError' && error.message.includes( 'fetch' ) ) { + errorMessage = + 'Connection failed. Please check your internet connection and try again.'; + } else if ( error.message.includes( 'HTTP error! status: 429' ) ) { + errorMessage = + 'Too many requests. Please wait a moment and try again.'; + } else if ( error.message.includes( 'HTTP error! status: 401' ) ) { + errorMessage = + 'Authentication failed. Please refresh the page and try again.'; + } else if ( error.message.includes( 'HTTP error! status: 500' ) ) { + errorMessage = 'Server error. Please try again in a few moments.'; + } else if ( error.message.includes( 'timeout' ) ) { + errorMessage = + 'Request timed out. The server may be busy. Please try again.'; + } else if ( error.message.includes( 'Response body is null' ) ) { + errorMessage = + 'No response received. Please check your connection and try again.'; + } + + this.add( errorMessage, 'bot' ); + } + + /** + * Smoothly scroll chat to bottom + */ + scrollToBottom() { + const chatMessageBox = document.getElementById( 'hyve-message-box' ); + if ( chatMessageBox ) { + // Use smooth scrolling if supported, instant fallback + chatMessageBox.scrollTo( { + top: chatMessageBox.scrollHeight, + behavior: 'smooth', + } ); + } + } + addAudioPlayback( audioElement ) { audioElement.play(); } @@ -367,6 +754,17 @@ class App { if ( message ) { message.remove(); } + + // Remove from messages array + const messageIndex = this.messages.findIndex( + ( msg ) => msg.id === id + ); + if ( messageIndex !== -1 ) { + this.messages.splice( messageIndex, 1 ); + } + + // Update storage after removing message + this.updateStorage(); } toggleChatWindow( isOpen ) { @@ -474,6 +872,14 @@ class App { } clearConversation() { + // Close any active streaming connection + if ( this.currentEventSource ) { + if ( typeof this.currentEventSource.close === 'function' ) { + this.currentEventSource.close(); + } + this.currentEventSource = null; + } + // Clear messages from UI const chatMessageBox = document.getElementById( 'hyve-message-box' ); chatMessageBox.innerHTML = ''; @@ -481,7 +887,6 @@ class App { // Reset state this.messages = []; this.threadID = null; - this.runID = null; this.recordID = null; this.hasSuggestions = false; this.isInitialToggle = true; @@ -602,12 +1007,6 @@ class App { } ); } - getDefaultHeaders() { - return { - 'Cache-Control': 'no-cache', - }; - } - async renderUI() { const chatOpenButton = this.createElement( 'button', { className: 'collapsible open', @@ -802,5 +1201,4 @@ class App { chatMessageBox.scrollTop = chatMessageBox.scrollHeight; } } - export default App;