Making completion utilities available in AI package (#39190)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Joel Thiessen 2023-07-17 10:25:14 -07:00 committed by GitHub
parent 722922191e
commit 7cb5cfed69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1807 additions and 2113 deletions

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ npm-debug.log
build/
build-module/
build-style/
build-style.js
build-types/
dist/

View File

@ -0,0 +1,12 @@
module.exports = {
extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
root: true,
overrides: [
{
files: [ '**/*.js', '**/*.jsx', '**/*.tsx' ],
rules: {
'react/react-in-jsx-scope': 'off',
},
},
],
};

1
packages/js/ai/.npmrc Normal file
View File

@ -0,0 +1 @@
package-lock=false

11
packages/js/ai/README.md Normal file
View File

@ -0,0 +1,11 @@
# Artificial Intelligence tools
A collection of WooCommerce tools and utilities to implement ai features.
## Installation
Install the module
```bash
pnpm install @woocommerce/ai --save
```

View File

@ -0,0 +1,3 @@
module.exports = {
extends: '../internal-js-tests/babel.config.js',
};

View File

@ -0,0 +1,3 @@
# Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Moving text completion hooks into @woocommerce/ai package for reuse.

View File

@ -0,0 +1,32 @@
{
"name": "woocommerce/ai",
"description": "WooCommerce AI library",
"type": "library",
"license": "GPL-3.0-or-later",
"minimum-stability": "dev",
"require-dev": {
"automattic/jetpack-changelogger": "3.3.0"
},
"config": {
"platform": {
"php": "7.2"
}
},
"extra": {
"changelogger": {
"formatter": {
"filename": "../../../tools/changelogger/class-package-formatter.php"
},
"types": {
"fix": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "CHANGELOG.md"
}
}
}

483
packages/js/ai/composer.lock generated Normal file
View File

@ -0,0 +1,483 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c8190a953aa0a440f30a24c71e0fef6d",
"packages": [],
"packages-dev": [
{
"name": "automattic/jetpack-changelogger",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-changelogger.git",
"reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959",
"reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959",
"shasum": ""
},
"require": {
"php": ">=5.6",
"symfony/console": "^3.4 || ^5.2 || ^6.0",
"symfony/process": "^3.4 || ^5.2 || ^6.0",
"wikimedia/at-ease": "^1.2 || ^2.0"
},
"require-dev": {
"wikimedia/testing-access-wrapper": "^1.0 || ^2.0",
"yoast/phpunit-polyfills": "1.0.4"
},
"bin": [
"bin/changelogger"
],
"type": "project",
"extra": {
"autotagger": true,
"branch-alias": {
"dev-trunk": "3.3.x-dev"
},
"mirror-repo": "Automattic/jetpack-changelogger",
"version-constants": {
"::VERSION": "src/Application.php"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}"
}
},
"autoload": {
"psr-4": {
"Automattic\\Jetpack\\Changelog\\": "lib",
"Automattic\\Jetpack\\Changelogger\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.",
"support": {
"source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0"
},
"time": "2022-12-26T13:49:01+00:00"
},
{
"name": "psr/log",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/1.1.4"
},
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "symfony/console",
"version": "3.4.x-dev",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81",
"reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/debug": "~2.8|~3.0|~4.0",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/dependency-injection": "<3.4",
"symfony/process": "<3.3"
},
"provide": {
"psr/log-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "~3.3|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/event-dispatcher": "~2.8|~3.0|~4.0",
"symfony/lock": "~3.4|~4.0",
"symfony/process": "~3.3|~4.0"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/console/tree/3.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
},
{
"name": "symfony/debug",
"version": "4.4.x-dev",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "1a692492190773c5310bc7877cb590c04c2f05be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be",
"reference": "1a692492190773c5310bc7877cb590c04c2f05be",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/http-kernel": "<3.4"
},
"require-dev": {
"symfony/http-kernel": "^3.4|^4.0|^5.0"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Debug\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools to ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/debug/tree/v4.4.44"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"abandoned": "symfony/error-handler",
"time": "2022-07-28T16:29:46+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f9c7affe77a00ae32ca127ca6833d034e6d33f25",
"reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/main"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-01-30T17:25:47+00:00"
},
{
"name": "symfony/process",
"version": "3.4.x-dev",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca",
"reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/3.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
},
{
"name": "wikimedia/at-ease",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/wikimedia/at-ease.git",
"reference": "013ac61929797839c80a111a3f1a4710d8248e7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a",
"reference": "013ac61929797839c80a111a3f1a4710d8248e7a",
"shasum": ""
},
"require": {
"php": ">=5.6.99"
},
"require-dev": {
"jakub-onderka/php-console-highlighter": "0.3.2",
"jakub-onderka/php-parallel-lint": "1.0.0",
"mediawiki/mediawiki-codesniffer": "22.0.0",
"mediawiki/minus-x": "0.3.1",
"ockcyp/covers-validator": "0.5.1 || 0.6.1",
"phpunit/phpunit": "4.8.36 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/Wikimedia/Functions.php"
],
"psr-4": {
"Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"authors": [
{
"name": "Tim Starling",
"email": "tstarling@wikimedia.org"
},
{
"name": "MediaWiki developers",
"email": "wikitech-l@lists.wikimedia.org"
}
],
"description": "Safe replacement to @ for suppressing warnings.",
"homepage": "https://www.mediawiki.org/wiki/at-ease",
"support": {
"source": "https://github.com/wikimedia/at-ease/tree/master"
},
"time": "2018-10-10T15:39:06+00:00"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.2"
},
"plugin-api-version": "2.3.0"
}

View File

@ -0,0 +1,4 @@
{
"rootDir": "./src",
"preset": "../node_modules/@woocommerce/internal-js-tests/jest-preset.js"
}

View File

@ -0,0 +1,95 @@
{
"name": "@woocommerce/ai",
"version": "0.1.0-beta.0",
"description": "Utilities for usage in AI features across WooCommerce.",
"author": "Automattic",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"woocommerce"
],
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/ai/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
},
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},
"main": "build/index.js",
"module": "build-module/index.js",
"types": "build-types",
"react-native": "src/index",
"sideEffects": [
"build-style/**",
"src/**/*.scss"
],
"private": true,
"dependencies": {
"@wordpress/api-fetch": "wp-6.0",
"@wordpress/compose": "wp-6.0",
"@wordpress/core-data": "wp-6.0",
"@wordpress/element": "wp-6.0",
"classnames": "^2.3.1",
"debug": "^4.3.3",
"dompurify": "^2.3.6",
"prop-types": "^15.8.1",
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/runtime": "^7.17.2",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/debug": "^4.1.7",
"@types/dompurify": "^2.3.3",
"@types/jest": "^27.4.1",
"@types/react": "^17.0.2",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/wordpress__core-data": "^2.4.5",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-js-tests": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/browserslist-config": "wp-6.0",
"concurrently": "^7.0.0",
"copy-webpack-plugin": "^9.1.0",
"css-loader": "^3.6.0",
"eslint": "^8.32.0",
"jest": "^27.5.1",
"jest-cli": "^27.5.1",
"postcss": "^8.4.7",
"postcss-loader": "^4.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hooks^8.0.1": "link:@testing-library/react-hooks^8.0.1",
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.9.5",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},
"scripts": {
"turbo:build": "pnpm run build:js && pnpm run build:css",
"turbo:test": "jest --config ./jest.config.json",
"prepare": "composer install",
"changelog": "composer exec -- changelogger",
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
"build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name",
"test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
"lint": "eslint src",
"build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json",
"build:css": "webpack",
"start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"",
"prepack": "pnpm run clean && pnpm run build",
"lint:fix": "eslint src --fix"
},
"peerDependencies": {
"@types/react": "^17.0.2",
"@wordpress/data": "wp-6.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

View File

@ -0,0 +1 @@
export * from './useCompletion';

View File

@ -6,23 +6,27 @@ import { useRef, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { getCompletion } from '../utils';
import { getCompletion, createExtendedError } from '../utils/';
type StopReason = 'abort' | 'finished' | 'error' | 'interrupted';
export type UseCompletionError = Error & { code?: string; cause?: Error };
type UseCompletionProps = {
onStreamMessage?: ( message: string, chunk: string ) => void;
onCompletionFinished?: (
reason: StopReason,
previousContent: string
) => void;
onStreamError?: ( error: string ) => void;
onStreamError?: ( error: UseCompletionError ) => void;
feature?: string;
};
export const useCompletion = ( {
onStreamMessage = () => {},
onCompletionFinished = () => {},
onStreamError = () => {},
feature,
}: UseCompletionProps ) => {
const completionSource = useRef< EventSource | null >( null );
const previousContent = useRef< string >( '' );
@ -51,47 +55,68 @@ export const useCompletion = ( {
}
};
const onCompletionError = ( error: string ) => {
const onCompletionError = ( error: string | Error ) => {
stopCompletion( 'error' );
onStreamError( error );
onStreamError( typeof error === 'object' ? error : new Error( error ) );
};
const requestCompletion = async ( prompt: string ) => {
const requestCompletion = async (
prompt: string,
featureOverride?: string
) => {
if (
! window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive
) {
throw createExtendedError(
'You must be connected to Jetpack for text completion',
'no_jetpack_connection'
);
}
const completionFeature = featureOverride ?? feature;
if ( completionSource.current ) {
stopCompletion( 'interrupted' );
}
previousContent.current = '';
let suggestionsSource;
try {
suggestionsSource = await getCompletion( prompt );
} catch ( e ) {
// eslint-disable-next-line no-console
console.debug( 'Completion connection error encountered', e );
onCompletionError( 'connection_error' );
return;
if ( typeof completionFeature !== 'string' ) {
throw createExtendedError(
'You must provide a feature when requesting a completion',
'missing_feature'
);
}
setCompletionActive( true );
try {
const suggestionsSource = await getCompletion(
prompt,
completionFeature
);
suggestionsSource.addEventListener( 'message', ( e ) => {
onMessage( e );
} );
suggestionsSource.addEventListener(
'error',
( event: MessageEvent ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', event );
onCompletionError(
typeof event === 'string' ? event : event.data
);
}
);
setCompletionActive( true );
completionSource.current = suggestionsSource;
suggestionsSource.addEventListener( 'message', ( e ) => {
onMessage( e );
} );
return suggestionsSource;
suggestionsSource.addEventListener(
'error',
( event: MessageEvent ) => {
onCompletionError(
typeof event === 'string' ? event : event.data
);
}
);
completionSource.current = suggestionsSource;
return suggestionsSource;
} catch ( e ) {
throw createExtendedError(
'An error occurred while connecting to the completion service',
'connection_error',
e as Error
);
}
};
return {

View File

@ -0,0 +1,16 @@
/**
* Hooks
*/
export {
useCompletion as __experimentalUseCompletion,
UseCompletionError,
} from './hooks';
/**
* Utils
*/
export {
requestJetpackToken as __experimentalRequestJetpackToken,
getCompletion as __experimentalgetCompletion,
createExtendedError,
} from './utils';

View File

View File

@ -0,0 +1,11 @@
export type UseCompletionError = Error & { code?: string; cause?: Error };
export const createExtendedError = (
msg: string,
code?: string,
cause?: Error
) =>
Object.assign( new Error( msg ), {
code,
cause,
} );

View File

@ -0,0 +1,2 @@
export * from './text-completion';
export * from './create-extended-error';

View File

@ -7,7 +7,7 @@ import debugFactory from 'debug';
/**
* Internal dependencies
*/
import { WOO_AI_PLUGIN_FEATURE_NAME } from '../constants';
import { createExtendedError } from './create-extended-error';
const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
@ -19,6 +19,7 @@ declare global {
JP_CONNECTION_INITIAL_STATE: {
apiNonce: string;
siteSuffix: string;
connectionStatus: { isActive: boolean };
};
}
}
@ -37,7 +38,10 @@ export async function requestJetpackToken() {
tokenData = JSON.parse( token );
} catch ( err ) {
debugToken( 'Error parsing token', err );
throw new Error( 'Error parsing cached token' );
throw createExtendedError(
'Error parsing cached token',
'token_parse_error'
);
}
}
@ -64,7 +68,7 @@ export async function requestJetpackToken() {
blogId: siteSuffix,
/**
* Let's expire the token in 5 minutes
* Let's expire the token in 2 minutes
*/
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
};
@ -74,7 +78,10 @@ export async function requestJetpackToken() {
return newTokenData;
} catch ( e ) {
throw new Error( 'Error fetching new token' );
throw createExtendedError(
'Error fetching new token',
'token_fetch_error'
);
}
}
@ -83,7 +90,7 @@ export async function requestJetpackToken() {
*
* @param {string} prompt - The query to send to the API
*/
export async function getCompletion( prompt: string ) {
export async function getCompletion( prompt: string, feature: string ) {
const { token } = await requestJetpackToken();
const url = new URL(
@ -92,7 +99,7 @@ export async function getCompletion( prompt: string ) {
url.searchParams.append( 'prompt', prompt );
url.searchParams.append( 'token', token );
url.searchParams.append( 'feature', WOO_AI_PLUGIN_FEATURE_NAME );
url.searchParams.append( 'feature', feature );
return new EventSource( url.toString() );
}

View File

@ -0,0 +1,16 @@
{
"extends": "../tsconfig-cjs",
"include": [
"**/*.d.ts",
"src/**/*",
"src/**/*.json"
],
"compilerOptions": {
"outDir": "build",
"resolveJsonModule": true,
"typeRoots": [
"./typings",
"./node_modules/@types"
]
}
}

View File

@ -0,0 +1,19 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"rootDir": "src",
"outDir": "build-module",
"declaration": true,
"declarationMap": true,
"declarationDir": "./build-types",
"resolveJsonModule": true,
"typeRoots": [
"./node_modules/@types"
]
},
"include": [
"**/*.d.ts",
"src/**/*",
"src/**/*.json"
]
}

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
const WebpackRTLPlugin = require( 'webpack-rtl-plugin' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
/**
* Internal dependencies
*/
const { webpackConfig } = require( '@woocommerce/internal-style-build' );
const NODE_ENV = process.env.NODE_ENV || 'development';
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: {
'build-style': __dirname + '/src/style.scss',
},
output: {
path: __dirname,
},
module: {
parser: webpackConfig.parser,
rules: webpackConfig.rules,
},
plugins: [
new MiniCssExtractPlugin( {
filename: ( data ) => {
return data.chunk.name.startsWith( '/build/blocks' )
? `[name].css`
: `[name]/style.css`;
},
chunkFilename: 'chunks/[id].style.css',
} ),
new WebpackRTLPlugin( {
test: /(?<!style)\.css$/,
filename: '[name]-rtl.css',
minify: NODE_ENV === 'development' ? false : { safe: true },
} ),
new WebpackRTLPlugin( {
test: /style\.css$/,
filename: '[name]/style-rtl.css',
minify: NODE_ENV === 'development' ? false : { safe: true },
} ),
],
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Moving text completion hooks into @woocommerce/ai package for reuse.

View File

@ -30,6 +30,7 @@
"@emotion/react": "^11.10.4",
"@types/prop-types": "^15.7.4",
"@types/react-outside-click-handler": "^1.3.1",
"@woocommerce/ai": "workspace:0.1.0-beta.0",
"@woocommerce/components": "workspace:*",
"@woocommerce/tracks": "workspace:*",
"@wordpress/api-fetch": "wp-6.0",

View File

@ -1,5 +1,3 @@
export * from './useTinyEditor';
export * from './useCompletion';
export * from './useFeedbackSnackbar';
export * from './useProductSlug';
export * from './useProductDataSuggestions';

View File

@ -1,66 +0,0 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import {
ProductDataSuggestion,
ProductDataSuggestionRequest,
ApiErrorResponse,
} from '../utils/types';
import { requestJetpackToken } from '../utils';
type ProductDataSuggestionSuccessResponse = {
suggestions: ProductDataSuggestion[];
};
type ProductDataSuggestionErrorResponse = ApiErrorResponse;
export const useProductDataSuggestions = () => {
const fetchSuggestions = async (
request: ProductDataSuggestionRequest
): Promise< ProductDataSuggestion[] > => {
try {
const token = await requestJetpackToken();
const { suggestions } =
await apiFetch< ProductDataSuggestionSuccessResponse >( {
path: '/wooai/product-data-suggestions',
method: 'POST',
data: { ...request, token },
} );
return suggestions;
} catch ( error ) {
/* eslint-disable-next-line no-console */
console.error( error );
const errorResponse = error as ProductDataSuggestionErrorResponse;
const hasStatus = errorResponse?.data?.status;
const hasMessage = errorResponse?.message;
// Check if the status is 500 or greater.
const isStatusGte500 =
errorResponse?.data?.status && errorResponse.data.status >= 500;
// If the error response doesn't have a status or message, or if the status is 500 or greater, throw a generic error.
if ( ! hasStatus || ! hasMessage || isStatusGte500 ) {
throw new Error(
__(
`Apologies, this is an experimental feature and there was an error with this service. Please try again.`,
'woocommerce'
)
);
}
throw new Error( errorResponse.message );
}
};
return {
fetchSuggestions,
} as const;
};

View File

@ -1,9 +1,12 @@
/**
* External dependencies
*/
import React from 'react';
import { __ } from '@wordpress/i18n';
import { useState, useEffect, useRef } from '@wordpress/element';
import {
__experimentalUseCompletion as useCompletion,
UseCompletionError,
} from '@woocommerce/ai';
/**
* Internal dependencies
@ -11,9 +14,10 @@ import { useState, useEffect, useRef } from '@wordpress/element';
import {
MAX_TITLE_LENGTH,
MIN_TITLE_LENGTH_FOR_DESCRIPTION,
WOO_AI_PLUGIN_FEATURE_NAME,
} from '../constants';
import { StopCompletionBtn, WriteItForMeBtn } from '../components';
import { useCompletion, useFeedbackSnackbar, useTinyEditor } from '../hooks';
import { useFeedbackSnackbar, useTinyEditor } from '../hooks';
import {
getProductName,
getPostId,
@ -57,9 +61,14 @@ export function WriteItForMeButtonContainer() {
titleEl.current?.value || ''
);
const tinyEditor = useTinyEditor();
const handleCompletionError = ( error: UseCompletionError ) =>
tinyEditor.setContent( getApiError( error.code ?? '' ) );
const { showSnackbar, removeSnackbar } = useFeedbackSnackbar();
const { requestCompletion, completionActive, stopCompletion } =
useCompletion( {
feature: WOO_AI_PLUGIN_FEATURE_NAME,
onStreamMessage: ( content ) => {
// This prevents printing out incomplete HTML tags.
const ignoreRegex = new RegExp( /<\/?\w*[^>]*$/g );
@ -67,12 +76,7 @@ export function WriteItForMeButtonContainer() {
tinyEditor.setContent( content );
}
},
onStreamError: ( error ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', error );
tinyEditor.setContent( getApiError( error ) );
},
onStreamError: handleCompletionError,
onCompletionFinished: ( reason, content ) => {
recordDescriptionTracks( 'stop', {
reason,
@ -174,7 +178,7 @@ export function WriteItForMeButtonContainer() {
].join( ' ' );
};
const onWriteItForMeClick = () => {
const onWriteItForMeClick = async () => {
setFetching( true );
removeSnackbar();
@ -182,7 +186,12 @@ export function WriteItForMeButtonContainer() {
recordDescriptionTracks( 'start', {
prompt,
} );
requestCompletion( prompt );
try {
await requestCompletion( prompt );
} catch ( err ) {
handleCompletionError( err as UseCompletionError );
}
};
return completionActive ? (

View File

@ -4,6 +4,7 @@
import React from 'react';
import { __ } from '@wordpress/i18n';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { __experimentalUseCompletion as useCompletion } from '@woocommerce/ai';
/**
* Internal dependencies
@ -20,10 +21,11 @@ import {
getTags,
getAttributes,
} from '../utils';
import { useCompletion, useProductSlug } from '../hooks';
import { useProductSlug } from '../hooks';
import { ProductDataSuggestion } from '../utils/types';
import { SuggestionItem, PoweredByLink, recordNameTracks } from './index';
import { RandomLoadingMessage } from '../components';
import { WOO_AI_PLUGIN_FEATURE_NAME } from '../constants';
const MIN_TITLE_LENGTH = 10;
@ -64,12 +66,13 @@ export const ProductNameSuggestions = () => {
);
const { updateProductSlug } = useProductSlug();
const { requestCompletion } = useCompletion( {
feature: WOO_AI_PLUGIN_FEATURE_NAME,
onStreamError: ( error ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', error );
recordNameTracks( 'stop', {
reason: 'error',
error: ( error as { message?: string } )?.message || '',
error: error.code ?? error.message,
} );
setSuggestionsState( SuggestionsState.Failed );
},
@ -127,14 +130,13 @@ export const ProductNameSuggestions = () => {
const onBodyClick = ( e: MouseEvent ) => {
const target = e.target as HTMLElement;
if (
! (
nameInput?.ownerDocument.activeElement === nameInput ||
// Need to capture errant handlediv click that happens on load as well
Boolean( target.querySelector( ':scope > .handlediv' ) ) ||
target?.matches(
'#woocommerce-ai-app-product-name-suggestions *, #title'
'#woocommerce-ai-app-product-name-suggestions *, #title, .woo-ai-get-suggestions-btn__content'
)
)
) {
@ -259,7 +261,11 @@ export const ProductNameSuggestions = () => {
current_title: getProductName(),
} );
requestCompletion( buildPrompt() );
try {
await requestCompletion( buildPrompt() );
} catch ( e ) {
setSuggestionsState( SuggestionsState.Failed );
}
};
const shouldRenderSuggestionsButton = useCallback( () => {

View File

@ -1,6 +1,5 @@
export * from './productData';
export * from './shuffleArray';
export * from './text-completion';
export * from './recordTracksFactory';
export * from './get-post-id';
export * from './tiny-tools';

View File

@ -7,24 +7,3 @@ export type ProductDataSuggestion = {
reason: string;
content: string;
};
export type ProductDataSuggestionRequest = {
requested_data: string;
name: string;
description: string;
categories: string[];
tags: string[];
attributes: Attribute[];
};
// This is the standard API response data when an error is returned.
export type ApiErrorResponse = {
code: string;
message: string;
data?: ApiErrorResponseData | undefined;
};
// API errors contain data with the status, and more in-depth error details. This may be null.
export type ApiErrorResponseData = {
status: number;
} | null;

File diff suppressed because it is too large Load Diff