diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index d1781844..fcb25fe7 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -1,7 +1,7 @@ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs -name: 300+ Unit Tests +name: 400+ Unit Tests on: push: diff --git a/package.json b/package.json index 1e7b5516..e1796449 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "symlink-dir": "^5.2.1", "jsdom": "^22.1.0", "typescript": "^5.3.3", - "vite": "^5.0.13" + "vite": "^5.1.0" }, "bugs": "https://github.com/nluxai/nlux/issues", "repository": { diff --git a/packages/css/themes/rollup.config.ts b/packages/css/themes/rollup.config.ts index 1b5cd730..f9f82e4a 100644 --- a/packages/css/themes/rollup.config.ts +++ b/packages/css/themes/rollup.config.ts @@ -37,6 +37,7 @@ const cssEntry = (input: string, output: string) => ({ const packageConfig: () => Promise = async () => ([ cssEntry('./src/nova/theme.css', `../../../dist/${folder}/themes/nova.css`), + cssEntry('./src/luna/theme.css', `../../../dist/${folder}/themes/luna.css`), ]); export default packageConfig; diff --git a/packages/css/themes/src/common/components/chat-room.css b/packages/css/themes/src/common/components/chat-room.css index 4efadffc..d0ffe811 100644 --- a/packages/css/themes/src/common/components/chat-room.css +++ b/packages/css/themes/src/common/components/chat-room.css @@ -1,26 +1,26 @@ -.nluxc-root { - > .nluxc-chat-room-container { +.nlux-AiChat-root { + > .nlux-chat-room-container { display: flex; flex-direction: column; flex-grow: 1; - gap: var(--nluxc-flex-gap); + gap: var(--nlux-flex-gap); - > .nluxc-chat-room-conversation-container { + > .nlux-chat-room-conversation-container { display: flex; overflow: auto; flex: 1; flex-direction: column; padding: 0; - gap: var(--nluxc-flex-gap); + gap: var(--nlux-flex-gap); - > .nluxc-conversation-messages-container { + > .nlux-conversation-messages-container { overflow: scroll; flex: 1; - padding-right: var(--nluxc-padding); - padding-left: var(--nluxc-padding); + padding-right: var(--nlux-padding); + padding-left: var(--nlux-padding); - > .nluxc-conversation-welcome-message { + > .nlux-conversation-welcome-message { display: flex; align-items: center; flex-direction: column; @@ -28,13 +28,13 @@ width: 100%; height: 100%; - > .nluxc-conversation-welcome-message-photo-container { + > .nlux-conversation-welcome-message-photo-container { position: relative; display: inline-block; overflow: hidden; width: 80px; height: 80px; - margin-bottom: var(--nluxc-padding-l); + margin-bottom: var(--nlux-padding-l); border-radius: 50%; > * { @@ -45,8 +45,8 @@ left: 0 !important; } - > .nluxc-conversation-welcome-message-letter { - font-size: var(--nluxc-font-size-l); + > .nlux-conversation-welcome-message-letter { + font-size: var(--nlux-font-size-l); font-weight: 400; z-index: 888888; display: flex; @@ -54,41 +54,41 @@ justify-content: center; margin: 5px; text-align: center; - color: var(--nluxc-text-color); + color: var(--nlux-text-color); border-radius: 50%; - background-color: var(--nluxc-message-received-background-color); + background-color: var(--nlux-message-received-background-color); } - > .nluxc-conversation-welcome-message-rendered-photo { + > .nlux-conversation-welcome-message-rendered-photo { z-index: 999999; background-position: center center; background-size: cover; } } - > .nluxc-conversation-welcome-message-name-and-tagline { + > .nlux-conversation-welcome-message-name-and-tagline { font-weight: 400; max-width: 80%; - padding: var(--nluxc-padding-l); + padding: var(--nlux-padding-l); text-align: center; - color: var(--nluxc-text-color); - border-color: var(--nluxc-message-received-border-color); - border-radius: var(--nluxc-border-radius-m); + color: var(--nlux-text-color); + border-color: var(--nlux-message-received-border-color); + border-radius: var(--nlux-border-radius-m); outline: none; - background-color: var(--nluxc-message-received-background-color); + background-color: var(--nlux-message-received-background-color); - > .nluxc-conversation-welcome-message-name { - font-size: var(--nluxc-font-size-l); + > .nlux-conversation-welcome-message-name { + font-size: var(--nlux-font-size-l); font-weight: 700; - margin-bottom: var(--nluxc-padding-m); + margin-bottom: var(--nlux-padding-m); } } } } } - > .nluxc-chat-room-prompt-box, - > .nluxc-chat-room-footer { + > .nlux-chat-room-prompt-box, + > .nlux-chat-room-footer { flex: 0; } } diff --git a/packages/css/themes/src/common/components/exceptions-box.css b/packages/css/themes/src/common/components/exceptions-box.css index e991c73e..f37acbec 100644 --- a/packages/css/themes/src/common/components/exceptions-box.css +++ b/packages/css/themes/src/common/components/exceptions-box.css @@ -1,7 +1,7 @@ -.nluxc-root { +.nlux-AiChat-root { position: relative; - > .nluxc-exceptions-box-container { + > .nlux-exceptions-box-container { position: absolute; top: 0; display: flex; @@ -14,16 +14,16 @@ width: 100%; gap: 0; - > .nluxc-exceptions-box-exception-container { + > .nlux-exceptions-box-exception-container { max-width: 85%; - padding: var(--nluxc-padding-s) var(--nluxc-border-radius); - color: var(--nluxc-exceptions-box-error-text-color); - border-width: var(--nluxc-border-width); - border-color: var(--nluxc-exceptions-box-error-border-color); - border-radius: var(--nluxc-border-radius-s); - background-color: var(--nluxc-exceptions-box-error-background-color); + padding: var(--nlux-padding-s) var(--nlux-border-radius); + color: var(--nlux-exceptions-box-error-text-color); + border-width: var(--nlux-border-width); + border-color: var(--nlux-exceptions-box-error-border-color); + border-radius: var(--nlux-border-radius-s); + background-color: var(--nlux-exceptions-box-error-background-color); - > .nluxc-exceptions-box-message { + > .nlux-exceptions-box-message { text-align: center; } } diff --git a/packages/css/themes/src/common/components/prompt-box.css b/packages/css/themes/src/common/components/prompt-box.css index 1949892a..d3684a71 100644 --- a/packages/css/themes/src/common/components/prompt-box.css +++ b/packages/css/themes/src/common/components/prompt-box.css @@ -1,17 +1,17 @@ -.nluxc-root { - .nluxc-prompt-box-container { +.nlux-AiChat-root { + .nlux-prompt-box-container { display: flex; align-items: stretch; flex-direction: row; - gap: var(--nluxc-flex-gap); + gap: var(--nlux-flex-gap); - > .nluxc-prompt-box-text-input { + > .nlux-prompt-box-text-input { flex: 1; resize: none; } - > .nluxc-prompt-box-send-button { + > .nlux-prompt-box-send-button { display: flex; align-items: center; justify-content: center; @@ -25,7 +25,7 @@ display: none; } - &.nluxc-prompt-box-send-button-loading { + &.nlux-prompt-box-send-button-loading { > svg { display: none; } diff --git a/packages/css/themes/src/common/components/spinning-loader.css b/packages/css/themes/src/common/components/spinning-loader.css index 6d1d28a1..3df1e878 100644 --- a/packages/css/themes/src/common/components/spinning-loader.css +++ b/packages/css/themes/src/common/components/spinning-loader.css @@ -1,4 +1,4 @@ -.nluxc-root { +.nlux-AiChat-root { .spinning-loader { display: inline-block; transform: rotateZ(45deg); diff --git a/packages/css/themes/src/common/components/text-message.css b/packages/css/themes/src/common/components/text-message.css index 104d1b55..9b6b9f60 100644 --- a/packages/css/themes/src/common/components/text-message.css +++ b/packages/css/themes/src/common/components/text-message.css @@ -1,11 +1,11 @@ -.nluxc-root { - .nluxc-text-message-container { +.nlux-AiChat-root { + .nlux-text-message-container { display: flex; align-items: stretch; justify-content: stretch; - padding-bottom: var(--nluxc-padding-m); + padding-bottom: var(--nlux-padding-m); - > .nluxc-text-message-content { + > .nlux-text-message-content { display: flex; flex-direction: column; flex-grow: 1; @@ -13,15 +13,15 @@ min-width: 80px; min-height: 20px; - padding: var(--nluxc-padding) var(--nluxc-padding-m); + padding: var(--nlux-padding) var(--nlux-padding-m); text-align: left; - border-width: var(--nluxc-border-width); + border-width: var(--nlux-border-width); border-style: solid; - border-radius: var(--nluxc-border-radius-m); + border-radius: var(--nlux-border-radius-m); outline: none; - gap: var(--nluxc-flex-gap); + gap: var(--nlux-flex-gap); :is(p, pre, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, table, hr) { margin: 0; @@ -29,35 +29,35 @@ } } - > .nluxc-text-message-content .code-block { - font-family: var(--nluxc-mono-font-family); + > .nlux-text-message-content .code-block { + font-family: var(--nlux-mono-font-family); - font-size: var(--nluxc-font-size-s); + font-size: var(--nlux-font-size-s); position: relative; overflow: scroll; - padding: var(--nluxc-padding) 0; + padding: var(--nlux-padding) 0; color: var(--nlux-code-block-text-color); border: none; - border-radius: var(--nluxc-border-radius-s); + border-radius: var(--nlux-border-radius-s); background-color: var(--nlux-code-block-background-color); - box-shadow: var(--nluxc-box-shadow); + box-shadow: var(--nlux-box-shadow); } - > .nluxc-text-message-content .code-block > pre { + > .nlux-text-message-content .code-block > pre { width: fit-content; min-width: 100%; } - > .nluxc-text-message-content .code-block > pre > div { - padding: 0 var(--nluxc-padding-m); + > .nlux-text-message-content .code-block > pre > div { + padding: 0 var(--nlux-padding-m); } - > .nluxc-text-message-content .code-block > pre > div:hover { + > .nlux-text-message-content .code-block > pre > div:hover { background-color: var(--nlux-code-block-hover-background-color); } - > .nluxc-text-message-content button.copy-button { + > .nlux-text-message-content button.copy-button { position: relative; z-index: 999999; width: 25px; @@ -72,15 +72,15 @@ cursor: pointer; color: var(--nlux-code-block-background-color); border: 1px solid var(--nlux-code-block-background-color); - border-radius: var(--nluxc-border-radius-xs); - background-color: var(--nluxc-message-received-background-color); + border-radius: var(--nlux-border-radius-xs); + background-color: var(--nlux-message-received-background-color); } - > .nluxc-text-message-content { + > .nlux-text-message-content { button.copy-button { &.clicked, &.clicked:hover { - color: var(--nluxc-message-received-background-color); + color: var(--nlux-message-received-background-color); border-color: var(--nlux-code-block-background-color); background-color: var(--nlux-code-block-background-color); } @@ -95,12 +95,12 @@ } code { - font-family: var(--nluxc-mono-font-family); - font-size: var(--nluxc-font-size-s); - padding: var(--nluxc-padding-xs) var(--nluxc-padding-s); + font-family: var(--nlux-mono-font-family); + font-size: var(--nlux-font-size-s); + padding: var(--nlux-padding-xs) var(--nlux-padding-s); color: var(--nlux-inline-code-text-color); - border-radius: var(--nluxc-border-radius-xs); + border-radius: var(--nlux-border-radius-xs); background-color: var(--nlux-inline-code-background-color); } @@ -109,11 +109,11 @@ } } - > .nluxc-text-message-persona { + > .nlux-text-message-persona { flex-grow: 0; flex-shrink: 0; - > .nluxc-text-message-persona-photo-container { + > .nlux-text-message-persona-photo-container { position: relative; overflow: hidden; width: 40px; @@ -134,8 +134,8 @@ left: 0 !important; } - > .nluxc-text-message-persona-letter { - font-size: var(--nluxc-font-size-l); + > .nlux-text-message-persona-letter { + font-size: var(--nlux-font-size-l); z-index: 888888; display: flex; align-items: center; @@ -144,7 +144,7 @@ border-radius: 50%; } - > .nluxc-text-message-persona-rendered-photo { + > .nlux-text-message-persona-rendered-photo { z-index: 999999; background-position: center center; background-size: cover; @@ -154,28 +154,28 @@ &.message-status-loading, &.message-status-connecting { - > .nluxc-text-message-content { + > .nlux-text-message-content { display: none; } - > .nluxc-text-message-loader { + > .nlux-text-message-loader { display: flex; align-items: center; justify-content: center; height: 40px; - padding: 0 var(--nluxc-padding-m); - color: var(--nluxc-message-received-text-color); + padding: 0 var(--nlux-padding-m); + color: var(--nlux-message-received-text-color); - border-width: var(--nluxc-border-width); + border-width: var(--nlux-border-width); border-style: solid; - border-color: var(--nluxc-message-received-border-color); - border-radius: var(--nluxc-border-radius-m); + border-color: var(--nlux-message-received-border-color); + border-radius: var(--nlux-border-radius-m); outline: none; - background-color: var(--nluxc-message-received-background-color); - gap: var(--nluxc-flex-gap); + background-color: var(--nlux-message-received-background-color); + gap: var(--nlux-flex-gap); > .spinning-loader-container { width: 17px; @@ -190,57 +190,57 @@ } &:not(.message-status-loading, .message-status-connecting) { - > .nluxc-text-message-content { + > .nlux-text-message-content { display: flex; } - > .nluxc-text-message-loader { + > .nlux-text-message-loader { display: none; } } - &.nluxc-text-message-received { + &.nlux-text-message-received { flex-direction: row; padding-right: 65px; - > .nluxc-text-message-content, - > .nluxc-text-message-loader, - > .nluxc-text-message-persona > .nluxc-text-message-persona-photo-container { - margin-left: var(--nluxc-padding-s); - color: var(--nluxc-message-received-text-color); - border-color: var(--nluxc-message-received-border-color); - background-color: var(--nluxc-message-received-background-color); + > .nlux-text-message-content, + > .nlux-text-message-loader, + > .nlux-text-message-persona > .nlux-text-message-persona-photo-container { + margin-left: var(--nlux-padding-s); + color: var(--nlux-message-received-text-color); + border-color: var(--nlux-message-received-border-color); + background-color: var(--nlux-message-received-background-color); } - > .nluxc-text-message-content:focus, - > .nluxc-text-message-content:active, - > .nluxc-text-message-loader:focus, - > .nluxc-text-message-loader:active { - color: var(--nluxc-message-received-active-text-color); - border-color: var(--nluxc-message-received-active-border-color); - background-color: var(--nluxc-message-received-active-background-color); - box-shadow: var(--nluxc-box-shadow); + > .nlux-text-message-content:focus, + > .nlux-text-message-content:active, + > .nlux-text-message-loader:focus, + > .nlux-text-message-loader:active { + color: var(--nlux-message-received-active-text-color); + border-color: var(--nlux-message-received-active-border-color); + background-color: var(--nlux-message-received-active-background-color); + box-shadow: var(--nlux-box-shadow); } } - &.nluxc-text-message-sent { + &.nlux-text-message-sent { flex-direction: row-reverse; padding-left: 65px; - > .nluxc-text-message-content, - > .nluxc-text-message-persona > .nluxc-text-message-persona-photo-container { - margin-right: var(--nluxc-padding-s); - color: var(--nluxc-message-sent-text-color); - border-color: var(--nluxc-message-sent-border-color); - background-color: var(--nluxc-message-sent-background-color); + > .nlux-text-message-content, + > .nlux-text-message-persona > .nlux-text-message-persona-photo-container { + margin-right: var(--nlux-padding-s); + color: var(--nlux-message-sent-text-color); + border-color: var(--nlux-message-sent-border-color); + background-color: var(--nlux-message-sent-background-color); } - > .nluxc-text-message-content:focus, - > .nluxc-text-message-content:active { - color: var(--nluxc-message-sent-active-text-color); - border-color: var(--nluxc-message-sent-active-border-color); - background-color: var(--nluxc-message-sent-active-background-color); - box-shadow: var(--nluxc-box-shadow); + > .nlux-text-message-content:focus, + > .nlux-text-message-content:active { + color: var(--nlux-message-sent-active-text-color); + border-color: var(--nlux-message-sent-active-border-color); + background-color: var(--nlux-message-sent-active-background-color); + box-shadow: var(--nlux-box-shadow); } } } diff --git a/packages/css/themes/src/common/format.css b/packages/css/themes/src/common/format.css index e1839b4a..85a4d8c2 100644 --- a/packages/css/themes/src/common/format.css +++ b/packages/css/themes/src/common/format.css @@ -1,8 +1,8 @@ -.nluxc-root { - font-family: var(--nluxc-font-family); - font-size: var(--nluxc-font-size); - font-weight: var(--nluxc-font-weight); - line-height: var(--nluxc-line-height); +.nlux-AiChat-root { + font-family: var(--nlux-font-family); + font-size: var(--nlux-font-size); + font-weight: var(--nlux-font-weight); + line-height: var(--nlux-line-height); - color: var(--nluxc-text-color); + color: var(--nlux-text-color); } diff --git a/packages/css/themes/src/common/layout.css b/packages/css/themes/src/common/layout.css index fc120edb..21954af0 100644 --- a/packages/css/themes/src/common/layout.css +++ b/packages/css/themes/src/common/layout.css @@ -1,4 +1,4 @@ -.nluxc-root { +.nlux-AiChat-root { display: flex; flex-direction: row; justify-content: center; @@ -15,7 +15,7 @@ padding: 0; } - > .nluxc-chat-room-container { + > .nlux-chat-room-container { min-height: 200px; } } diff --git a/packages/css/themes/src/common/ui/button.css b/packages/css/themes/src/common/ui/button.css index 57b86e4b..c4e5a7a9 100644 --- a/packages/css/themes/src/common/ui/button.css +++ b/packages/css/themes/src/common/ui/button.css @@ -1,48 +1,48 @@ -.nluxc-root { +.nlux-AiChat-root { .bt-primary-filled { - font-family: var(--nluxc-font-family); - font-size: var(--nluxc-font-size); - font-weight: var(--nluxc-font-weight); - line-height: var(--nluxc-line-height); + font-family: var(--nlux-font-family); + font-size: var(--nlux-font-size); + font-weight: var(--nlux-font-weight); + line-height: var(--nlux-line-height); - padding: var(--nluxc-padding) var(--nluxc-padding-l); + padding: var(--nlux-padding) var(--nlux-padding-l); cursor: pointer; - color: var(--nluxc-button-text-color); + color: var(--nlux-button-text-color); - border-width: var(--nluxc-border-width); + border-width: var(--nlux-border-width); border-style: solid; - border-color: var(--nluxc-button-border-color); - border-radius: var(--nluxc-border-radius-m); - background-color: var(--nluxc-button-background-color); + border-color: var(--nlux-button-border-color); + border-radius: var(--nlux-border-radius-m); + background-color: var(--nlux-button-background-color); > .loader, > .loader:after { - color: var(--nluxc-button-text-color); + color: var(--nlux-button-text-color); } &:active, &:hover { - color: var(--nluxc-button-active-text-color); - border-color: var(--nluxc-button-active-border-color); - background-color: var(--nluxc-button-active-background-color); + color: var(--nlux-button-active-text-color); + border-color: var(--nlux-button-active-border-color); + background-color: var(--nlux-button-active-background-color); } &:focus-visible, &:focus-within, &:focus { - color: var(--nluxc-button-active-text-color); - border-color: var(--nluxc-button-active-border-color); + color: var(--nlux-button-active-text-color); + border-color: var(--nlux-button-active-border-color); outline: none; - background-color: var(--nluxc-button-active-background-color); - box-shadow: var(--nluxc-box-shadow); + background-color: var(--nlux-button-active-background-color); + box-shadow: var(--nlux-box-shadow); } &:disabled { - color: var(--nluxc-button-disabled-text-color); - border-color: var(--nluxc-button-disabled-border-color); - background-color: var(--nluxc-button-disabled-background-color); + color: var(--nlux-button-disabled-text-color); + border-color: var(--nlux-button-disabled-border-color); + background-color: var(--nlux-button-disabled-background-color); } } } diff --git a/packages/css/themes/src/common/ui/input.css b/packages/css/themes/src/common/ui/input.css index f4df3932..180c70f8 100644 --- a/packages/css/themes/src/common/ui/input.css +++ b/packages/css/themes/src/common/ui/input.css @@ -1,51 +1,51 @@ -.nluxc-root { +.nlux-AiChat-root { :is(input, select, textarea) { - font-family: var(--nluxc-font-family); - font-size: var(--nluxc-font-size); + font-family: var(--nlux-font-family); + font-size: var(--nlux-font-size); font-weight: 400; line-height: 1.5; padding: 0.375rem 0.75rem; cursor: pointer; transition: color 0.1s ease-in-out, background-color 0.1s ease-in-out, border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out; - color: var(--nluxc-input-text-color); + color: var(--nlux-input-text-color); - border-width: var(--nluxc-border-width); + border-width: var(--nlux-border-width); border-style: solid; - border-color: var(--nluxc-input-border-color); + border-color: var(--nlux-input-border-color); - border-radius: var(--nluxc-border-radius-m); + border-radius: var(--nlux-border-radius-m); outline: none; - background-color: var(--nluxc-input-background-color); + background-color: var(--nlux-input-background-color); &:disabled, &:disabled:hover { cursor: auto; - color: var(--nluxc-input-disabled-text-color); - border-color: var(--nluxc-input-disabled-border-color); - background-color: var(--nluxc-input-disabled-background-color); + color: var(--nlux-input-disabled-text-color); + border-color: var(--nlux-input-disabled-border-color); + background-color: var(--nlux-input-disabled-background-color); } &:focus, &:active, &:hover { - color: var(--nluxc-input-active-text-color); - border-color: var(--nluxc-input-active-border-color); - background-color: var(--nluxc-input-active-background-color); - box-shadow: var(--nluxc-box-shadow); + color: var(--nlux-input-active-text-color); + border-color: var(--nlux-input-active-border-color); + background-color: var(--nlux-input-active-background-color); + box-shadow: var(--nlux-box-shadow); } &::placeholder { - color: var(--nluxc-input-placeholder-color); + color: var(--nlux-input-placeholder-color); &:focus, &:active, &:hover { - color: var(--nluxc-input-active-placeholder-color); + color: var(--nlux-input-active-placeholder-color); } &:disabled { - color: var(--nluxc-input-disabled-placeholder-color); + color: var(--nlux-input-disabled-placeholder-color); } } } diff --git a/packages/css/themes/src/luna/common/animation.css b/packages/css/themes/src/luna/common/animation.css new file mode 100644 index 00000000..6d9da9a4 --- /dev/null +++ b/packages/css/themes/src/luna/common/animation.css @@ -0,0 +1,47 @@ +@keyframes nlux-loader-spin { + 0%, + 100% { + box-shadow: .2em 0px 0 0px currentcolor; + } + 12% { + box-shadow: .2em .2em 0 0 currentcolor; + } + 25% { + box-shadow: 0 .2em 0 0px currentcolor; + } + 37% { + box-shadow: -.2em .2em 0 0 currentcolor; + } + 50% { + box-shadow: -.2em 0 0 0 currentcolor; + } + 62% { + box-shadow: -.2em -.2em 0 0 currentcolor; + } + 75% { + box-shadow: 0px -.2em 0 0 currentcolor; + } + 87% { + box-shadow: .2em -.2em 0 0 currentcolor; + } +} + +@keyframes nlux-fadeInUp { + 0% { + margin-top: 20px; + opacity: 0; + } + 100% { + margin-top: 0; + opacity: 1; + } +} + +@keyframes nlux-fadeOutUp { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/packages/css/themes/src/luna/common/colors.css b/packages/css/themes/src/luna/common/colors.css new file mode 100644 index 00000000..61dbd32b --- /dev/null +++ b/packages/css/themes/src/luna/common/colors.css @@ -0,0 +1,60 @@ +.nlux-AiChat-root.nlux-theme-luna { + --nlux-text-color: #1d1e1bff; + + --nlux-foreground-color: #333333; + --nlux-border-color: #cccccc; + + --nlux-input-background-color: #ffffff; + --nlux-input-border-color: #c5d7c9; + --nlux-input-text-color: #18210c; + --nlux-input-placeholder-color: #c5d7c9; + + --nlux-input-active-background-color: #ffffff; + --nlux-input-active-border-color: #c5d7c9; + --nlux-input-active-text-color: #18210c; + --nlux-input-active-placeholder-color: #c5d7c9; + + --nlux-input-disabled-background-color: #ffffff; + --nlux-input-disabled-border-color: #c5d7c9; + --nlux-input-disabled-text-color: #959993; + --nlux-input-disabled-placeholder-color: #c5d7c9; + + --nlux-button-background-color: #5fe095; + --nlux-button-border-color: #5fe095; + --nlux-button-text-color: #ffffff; + + --nlux-button-active-background-color: #72e7a1ff; + --nlux-button-active-border-color: #72e7a1ff; + --nlux-button-active-text-color: #ffffff; + + --nlux-button-disabled-background-color: #c5d7c9; + --nlux-button-disabled-border-color: #c5d7c9; + --nlux-button-disabled-text-color: #ffffff; + + --nlux-message-sent-background-color: #5fe095; + --nlux-message-sent-border-color: #6fc693; + --nlux-message-sent-text-color: #ffffff; + + --nlux-message-sent-active-background-color: #5fe095; + --nlux-message-sent-active-border-color: #6fc693; + --nlux-message-sent-active-text-color: #ffffff; + + --nlux-message-received-background-color: #f4f6f9; + --nlux-message-received-border-color: #dde5e2ff; + --nlux-message-received-text-color: #18210c; + + --nlux-code-block-background-color: #303530; + --nlux-code-block-hover-background-color: #404240; + --nlux-code-block-text-color: #dde5e2ff; + + --nlux-inline-code-background-color: #d5dad1; + --nlux-inline-code-text-color: #18210c; + + --nlux-message-received-active-background-color: #f4f6f9; + --nlux-message-received-active-border-color: #dde5e2ff; + --nlux-message-received-active-text-color: #18210c; + + --nlux-exceptions-box-error-background-color: #d05858; + --nlux-exceptions-box-error-border-color: #d05858; + --nlux-exceptions-box-error-text-color: #ffffff; +} diff --git a/packages/css/themes/src/luna/common/format.css b/packages/css/themes/src/luna/common/format.css new file mode 100644 index 00000000..85a4d8c2 --- /dev/null +++ b/packages/css/themes/src/luna/common/format.css @@ -0,0 +1,8 @@ +.nlux-AiChat-root { + font-family: var(--nlux-font-family); + font-size: var(--nlux-font-size); + font-weight: var(--nlux-font-weight); + line-height: var(--nlux-line-height); + + color: var(--nlux-text-color); +} diff --git a/packages/css/themes/src/luna/common/layout.css b/packages/css/themes/src/luna/common/layout.css new file mode 100644 index 00000000..2373b565 --- /dev/null +++ b/packages/css/themes/src/luna/common/layout.css @@ -0,0 +1,48 @@ +@import 'variables.css'; + +.nlux-AiChat-root { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + + -ms-user-select: none; + -webkit-user-select: none; /* Safari */ + user-select: none; + + :is(h1, h2, h3, h4, h5, h6) { + margin: 0; + padding: 0; + } + + > .nlux-comp-exp_box { + position: absolute; + z-index: 999999; + top: 0; + right: 0; + left: 0; + } + + > .nlux-chtRm-cntr { + display: flex; + flex-direction: column; + flex-grow: 1; + width: 100%; + height: 100%; + + > .nlux-chtRm-cnv-cntr { + overflow-x: hidden; + overflow-y: auto; + flex: 1; + margin-bottom: 0.5em; + padding: 0; + } + + > .nlux-chtRm-prmptBox-cntr { + flex: 0 0 auto; + padding: 0; + gap: var(--nlux-gap); + } + } +} diff --git a/packages/css/themes/src/luna/common/variables.css b/packages/css/themes/src/luna/common/variables.css new file mode 100644 index 00000000..7b1d02cc --- /dev/null +++ b/packages/css/themes/src/luna/common/variables.css @@ -0,0 +1,31 @@ +@import 'colors.css'; + +.nlux-AiChat-root.nlux-theme-luna { + --nlux-font-family: 'IBM Plex Sans', sans-serif; + --nlux-mono-font-family: monospace; + + --nlux-text-color: var(--nlux-text-color); + --nlux-font-weight: 400; + --nlux-line-height: 1.2; + + --nlux-font-size: 1rem; + --nlux-font-size-s: 0.8rem; + --nlux-font-size-l: 1.2rem; + + --nlux-padding-xs: 0.1rem; + --nlux-padding-s: 0.3rem; + --nlux-padding: 0.6rem; + --nlux-padding-m: 0.7rem; + --nlux-padding-l: 0.9rem; + + --nlux-gap: 0.5em; + + --nlux-border-radius-xs: 4px; + --nlux-border-radius-s: 6px; + --nlux-border-radius: 8px; + --nlux-border-radius-m: 10px; + --nlux-border-radius-l: 12px; + + --nlux-border-width: 1px; + --nlux-box-shadow: 0 0 2px rgb(0 0 0 / 30%); +} diff --git a/packages/css/themes/src/luna/components/AiChat.css b/packages/css/themes/src/luna/components/AiChat.css new file mode 100644 index 00000000..22c9f005 --- /dev/null +++ b/packages/css/themes/src/luna/components/AiChat.css @@ -0,0 +1,8 @@ +@import './Avatar.css'; +@import './ChatItem.css'; +@import './Loader.css'; +@import './Message.css'; +@import './PromptBox.css'; +@import './WelcomeMessage.css'; +@import './ExceptionsBox.css'; +@import './ChatSegment.css'; \ No newline at end of file diff --git a/packages/css/themes/src/luna/components/Avatar.css b/packages/css/themes/src/luna/components/Avatar.css new file mode 100644 index 00000000..9fc52491 --- /dev/null +++ b/packages/css/themes/src/luna/components/Avatar.css @@ -0,0 +1,40 @@ +.nlux-comp-avtr { + position: relative; + display: inline-block; + overflow: hidden; + align-items: stretch; + justify-content: stretch; + width: 50px; + color: var(--nlux-foreground-color); + border: 1px solid var(--nlux-border-color); + border-radius: 50%; + background-color: var(--nlux-background-color); + box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.1); + aspect-ratio: 1; + + > .avtr_ctn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + aspect-ratio: 1; + + > .avtr_ltr { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + > .avtr_img { + position: absolute; + top: 0; + left: 0; + display: block; + width: 100%; + height: 100%; + background-size: cover; + } + } +} diff --git a/packages/css/themes/src/luna/components/ChatItem.css b/packages/css/themes/src/luna/components/ChatItem.css new file mode 100644 index 00000000..e08fbb9b --- /dev/null +++ b/packages/css/themes/src/luna/components/ChatItem.css @@ -0,0 +1,46 @@ +.nlux-comp-cht_itm { + display: flex; + flex-direction: row; + margin-bottom: 0.5em; + gap: 0.5em; + + > .nlux-comp-msg { + align-items: flex-start; + flex: 1; + flex-direction: column; + flex-shrink: initial; + min-width: 80px; + min-height: 20px; + padding: var(--nlux-padding) var(--nlux-padding-m); + text-align: left; + border-width: var(--nlux-border-width); + border-style: solid; + border-radius: var(--nlux-border-radius-m); + outline: 0; + gap: var(--nlux-flex-gap); + } +} + +.nlux-comp-cht_itm.nlux_cht_itm_out { + flex-direction: row-reverse; + padding-left: 65px; + + > .nlux-comp-msg { + margin-right: var(--nlux-padding-s); + color: var(--nlux-message-sent-text-color); + border-color: var(--nlux-message-sent-border-color); + background-color: var(--nlux-message-sent-background-color); + } +} + +.nlux-comp-cht_itm.nlux_cht_itm_in { + flex-direction: row; + padding-right: 65px; + + > .nlux-comp-msg { + margin-left: var(--nlux-padding-s); + color: var(--nlux-message-received-text-color); + border-color: var(--nlux-message-received-border-color); + background-color: var(--nlux-message-received-background-color); + } +} diff --git a/packages/css/themes/src/luna/components/ChatSegment.css b/packages/css/themes/src/luna/components/ChatSegment.css new file mode 100644 index 00000000..470e8cba --- /dev/null +++ b/packages/css/themes/src/luna/components/ChatSegment.css @@ -0,0 +1,17 @@ +.nlux-chtSgm.nlux-chtSgm-cmpl { +} + +.nlux-chtSgm.nlux-chtSgm-err { + background-color: #f8d7da; +} + +.nlux-chtSgm.nlux-chtSgm-actv { +} + +.nlux-chtSgm > .nlux-chtSgm-ldr-cntr { + display: flex; + align-items: flex-start; + justify-content: flex-start; + + margin: 0.7rem 0.5rem 0.5rem 0.5rem; +} diff --git a/packages/css/themes/src/luna/components/ExceptionsBox.css b/packages/css/themes/src/luna/components/ExceptionsBox.css new file mode 100644 index 00000000..1fe496fa --- /dev/null +++ b/packages/css/themes/src/luna/components/ExceptionsBox.css @@ -0,0 +1,16 @@ +.nlux-comp-exp_box { + > .nlux-comp-exp_itm { + padding: var(--nlux-padding); + animation: nlux-fadeInUp 0.15s ease-out forwards; + opacity: 0; + + color: var(--nlux-exceptions-box-error-text-color); + border-color: var(--nlux-exceptions-box-error-border-color); + border-radius: var(--nlux-border-radius); + background-color: var(--nlux-exceptions-box-error-background-color); + } + + > .nlux-comp-exp_itm.nlux-comp-exp_itm_hide { + animation: nlux-fadeOutUp 0.1s ease-in forwards; + } +} diff --git a/packages/css/themes/src/luna/components/Loader.css b/packages/css/themes/src/luna/components/Loader.css new file mode 100644 index 00000000..3fc17b8d --- /dev/null +++ b/packages/css/themes/src/luna/components/Loader.css @@ -0,0 +1,38 @@ +.nlux_msg_ldr { + display: flex; + align-items: center; + justify-content: center; + + > .spn_ldr_ctn { + width: 17px; + + > .spn_ldr { + display: inline-block; + width: 15px; + height: 15px; + + transform: rotateZ(45deg); + border-radius: 50%; + perspective: 1000px; + + &:before, + &:after { + position: absolute; + top: 0; + left: 0; + display: block; + width: inherit; + height: inherit; + content: ''; + transform: rotateX(70deg); + animation: 1s nlux-loader-spin linear infinite; + border-radius: 50%; + } + + &:after { + transform: rotateY(70deg); + animation-delay: .4s; + } + } + } +} diff --git a/packages/css/themes/src/luna/components/Message.css b/packages/css/themes/src/luna/components/Message.css new file mode 100644 index 00000000..2bd7ea3d --- /dev/null +++ b/packages/css/themes/src/luna/components/Message.css @@ -0,0 +1,20 @@ +.nlux-comp-msg { + display: flex; + align-items: center; + justify-content: flex-start; + min-height: 1.5em; + margin: 0; + padding: 0.5em; + text-align: left; + color: var(--nlux-foreground-color); + border: 1px solid var(--nlux-border-color); + border-radius: 0.25em; + background-color: var(--nlux-background-color); + box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.1); +} + +.nlux-comp-msg.nlux_msg_loading { + display: flex; + flex: 0; + justify-content: center; +} diff --git a/packages/css/themes/src/luna/components/PromptBox.css b/packages/css/themes/src/luna/components/PromptBox.css new file mode 100644 index 00000000..d7e83e94 --- /dev/null +++ b/packages/css/themes/src/luna/components/PromptBox.css @@ -0,0 +1,86 @@ +.nlux-comp-prmptBox { + display: flex; + align-items: stretch; + flex-direction: row; + justify-content: center; + + border: none; + background-color: transparent; + gap: var(--nlux-gap); + + > textarea { + font-family: var(--nlux-font-family); + font-size: var(--nlux-font-size); + + font-weight: 400; + line-height: 1.5; + flex: 1; + padding: .375rem .75rem; + resize: none; + cursor: pointer; + transition: color .1s ease-in-out, background-color .1s ease-in-out, border-color .1s ease-in-out, box-shadow .1s ease-in-out; + color: var(--nlux-input-text-color); + border-width: var(--nlux-border-width); + border-style: solid; + border-color: var(--nlux-input-border-color); + border-radius: var(--nlux-border-radius-m); + outline: 0; + background-color: var(--nlux-input-background-color); + } + + > button { + font-family: var(--nlux-font-family); + font-size: var(--nlux-font-size); + font-weight: var(--nlux-font-weight); + line-height: var(--nlux-line-height); + display: flex; + align-items: center; + + justify-content: center; + width: 65px; + padding: var(--nlux-padding) var(--nlux-padding-l); + cursor: pointer; + color: var(--nlux-button-text-color); + border-width: var(--nlux-border-width); + border-style: solid; + border-color: var(--nlux-button-border-color); + border-radius: var(--nlux-border-radius-m); + background-color: var(--nlux-button-background-color); + + > .nlux_sndIcn { + width: 80%; + } + + > .nlux_msg_ldr { + display: none; + } + } + + > button:disabled { + cursor: not-allowed; + color: var(--nlux-button-disabled-text-color); + border-color: var(--nlux-button-disabled-border-color); + background-color: var(--nlux-button-disabled-background-color); + } +} + +.nlux-comp-prmptBox.nlux-prmpt-typing { + button > .nlux_sndIcn { + display: inline-block; + } + + button > .nlux_msg_ldr { + display: none; + } +} + +.nlux-comp-prmptBox.nlux-prmpt-submitting, +.nlux-comp-prmptBox.nlux-prmpt-waiting { + button > .nlux_sndIcn { + display: none; + } + + button > .nlux_msg_ldr { + display: inline-block; + } +} diff --git a/packages/css/themes/src/luna/components/WelcomeMessage.css b/packages/css/themes/src/luna/components/WelcomeMessage.css new file mode 100644 index 00000000..3f86bc3a --- /dev/null +++ b/packages/css/themes/src/luna/components/WelcomeMessage.css @@ -0,0 +1,2 @@ +.nlux-comp-wlc_msg { +} diff --git a/packages/css/themes/src/luna/theme.css b/packages/css/themes/src/luna/theme.css new file mode 100644 index 00000000..2996843f --- /dev/null +++ b/packages/css/themes/src/luna/theme.css @@ -0,0 +1,7 @@ +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap'); +@import './common/colors.css'; +@import './common/variables.css'; +@import './common/animation.css'; +@import './common/format.css'; +@import './common/layout.css'; +@import './components/AiChat.css'; diff --git a/packages/css/themes/src/nova/colors.css b/packages/css/themes/src/nova/colors.css index 33276c48..e5a93012 100644 --- a/packages/css/themes/src/nova/colors.css +++ b/packages/css/themes/src/nova/colors.css @@ -1,44 +1,44 @@ -.nluxc-root.nluxc-theme-nova { - --nluxc-text-color: #1d1e1bff; +.nlux-AiChat-root.nlux-theme-nova { + --nlux-text-color: #1d1e1bff; - --nluxc-input-background-color: #ffffff; - --nluxc-input-border-color: #c5d7c9; - --nluxc-input-text-color: #18210c; - --nluxc-input-placeholder-color: #c5d7c9; + --nlux-input-background-color: #ffffff; + --nlux-input-border-color: #c5d7c9; + --nlux-input-text-color: #18210c; + --nlux-input-placeholder-color: #c5d7c9; - --nluxc-input-active-background-color: #ffffff; - --nluxc-input-active-border-color: #c5d7c9; - --nluxc-input-active-text-color: #18210c; - --nluxc-input-active-placeholder-color: #c5d7c9; + --nlux-input-active-background-color: #ffffff; + --nlux-input-active-border-color: #c5d7c9; + --nlux-input-active-text-color: #18210c; + --nlux-input-active-placeholder-color: #c5d7c9; - --nluxc-input-disabled-background-color: #ffffff; - --nluxc-input-disabled-border-color: #c5d7c9; - --nluxc-input-disabled-text-color: #959993; - --nluxc-input-disabled-placeholder-color: #c5d7c9; + --nlux-input-disabled-background-color: #ffffff; + --nlux-input-disabled-border-color: #c5d7c9; + --nlux-input-disabled-text-color: #959993; + --nlux-input-disabled-placeholder-color: #c5d7c9; - --nluxc-button-background-color: #5fe095; - --nluxc-button-border-color: #5fe095; - --nluxc-button-text-color: #ffffff; + --nlux-button-background-color: #5fe095; + --nlux-button-border-color: #5fe095; + --nlux-button-text-color: #ffffff; - --nluxc-button-active-background-color: #72e7a1ff; - --nluxc-button-active-border-color: #72e7a1ff; - --nluxc-button-active-text-color: #ffffff; + --nlux-button-active-background-color: #72e7a1ff; + --nlux-button-active-border-color: #72e7a1ff; + --nlux-button-active-text-color: #ffffff; - --nluxc-button-disabled-background-color: #c5d7c9; - --nluxc-button-disabled-border-color: #c5d7c9; - --nluxc-button-disabled-text-color: #ffffff; + --nlux-button-disabled-background-color: #c5d7c9; + --nlux-button-disabled-border-color: #c5d7c9; + --nlux-button-disabled-text-color: #ffffff; - --nluxc-message-sent-background-color: #5fe095; - --nluxc-message-sent-border-color: #6fc693; - --nluxc-message-sent-text-color: #ffffff; + --nlux-message-sent-background-color: #5fe095; + --nlux-message-sent-border-color: #6fc693; + --nlux-message-sent-text-color: #ffffff; - --nluxc-message-sent-active-background-color: #5fe095; - --nluxc-message-sent-active-border-color: #6fc693; - --nluxc-message-sent-active-text-color: #ffffff; + --nlux-message-sent-active-background-color: #5fe095; + --nlux-message-sent-active-border-color: #6fc693; + --nlux-message-sent-active-text-color: #ffffff; - --nluxc-message-received-background-color: #f4f6f9; - --nluxc-message-received-border-color: #dde5e2ff; - --nluxc-message-received-text-color: #18210c; + --nlux-message-received-background-color: #f4f6f9; + --nlux-message-received-border-color: #dde5e2ff; + --nlux-message-received-text-color: #18210c; --nlux-code-block-background-color: #303530; --nlux-code-block-hover-background-color: #404240; @@ -47,11 +47,11 @@ --nlux-inline-code-background-color: #d5dad1; --nlux-inline-code-text-color: #18210c; - --nluxc-message-received-active-background-color: #f4f6f9; - --nluxc-message-received-active-border-color: #dde5e2ff; - --nluxc-message-received-active-text-color: #18210c; + --nlux-message-received-active-background-color: #f4f6f9; + --nlux-message-received-active-border-color: #dde5e2ff; + --nlux-message-received-active-text-color: #18210c; - --nluxc-exceptions-box-error-background-color: #d05858; - --nluxc-exceptions-box-error-border-color: #d05858; - --nluxc-exceptions-box-error-text-color: #ffffff; + --nlux-exceptions-box-error-background-color: #d05858; + --nlux-exceptions-box-error-border-color: #d05858; + --nlux-exceptions-box-error-text-color: #ffffff; } diff --git a/packages/css/themes/src/nova/variables.css b/packages/css/themes/src/nova/variables.css index 82a718b9..4fcaab62 100644 --- a/packages/css/themes/src/nova/variables.css +++ b/packages/css/themes/src/nova/variables.css @@ -1,29 +1,29 @@ -.nluxc-root.nluxc-theme-nova { - --nluxc-font-family: 'IBM Plex Sans', sans-serif; - --nluxc-mono-font-family: monospace; +.nlux-AiChat-root.nlux-theme-nova { + --nlux-font-family: 'IBM Plex Sans', sans-serif; + --nlux-mono-font-family: monospace; - --nluxc-text-color: var(--nluxc-text-color); - --nluxc-font-weight: 400; - --nluxc-line-height: 1.2; + --nlux-text-color: var(--nlux-text-color); + --nlux-font-weight: 400; + --nlux-line-height: 1.2; - --nluxc-font-size: 1rem; - --nluxc-font-size-s: 0.8rem; - --nluxc-font-size-l: 1.2rem; + --nlux-font-size: 1rem; + --nlux-font-size-s: 0.8rem; + --nlux-font-size-l: 1.2rem; - --nluxc-padding-xs: 0.1rem; - --nluxc-padding-s: 0.3rem; - --nluxc-padding: 0.6rem; - --nluxc-padding-m: 0.7rem; - --nluxc-padding-l: 0.9rem; + --nlux-padding-xs: 0.1rem; + --nlux-padding-s: 0.3rem; + --nlux-padding: 0.6rem; + --nlux-padding-m: 0.7rem; + --nlux-padding-l: 0.9rem; - --nluxc-flex-gap: 0.8rem; + --nlux-flex-gap: 0.8rem; - --nluxc-border-radius-xs: 4px; - --nluxc-border-radius-s: 6px; - --nluxc-border-radius: 8px; - --nluxc-border-radius-m: 10px; - --nluxc-border-radius-l: 12px; + --nlux-border-radius-xs: 4px; + --nlux-border-radius-s: 6px; + --nlux-border-radius: 8px; + --nlux-border-radius-m: 10px; + --nlux-border-radius-l: 12px; - --nluxc-border-width: 1px; - --nluxc-box-shadow: 0 0 2px rgb(0 0 0 / 30%); + --nlux-border-width: 1px; + --nlux-box-shadow: 0 0 2px rgb(0 0 0 / 30%); } diff --git a/packages/extra/highlighter/src/ext/highlightJsExtension.ts b/packages/extra/highlighter/src/ext/highlightJsExtension.ts index 05ae6898..0477eb87 100644 --- a/packages/extra/highlighter/src/ext/highlightJsExtension.ts +++ b/packages/extra/highlighter/src/ext/highlightJsExtension.ts @@ -1,5 +1,6 @@ -import {CreateHighlighterOptions, Highlighter, HighlighterColorMode, HighlighterExtension, warn} from '@nlux/core'; +import {CreateHighlighterOptions, Highlighter, HighlighterColorMode, HighlighterExtension} from '@nlux/core'; import hljs from 'highlight.js/lib/common'; +import {warn} from '../../../../shared/src/utils/warn'; import {languageAliases} from '../highlightJs/languages'; diff --git a/packages/extra/markdown/src/index.ts b/packages/extra/markdown/src/index.ts index 481eebe2..20b7fd23 100644 --- a/packages/extra/markdown/src/index.ts +++ b/packages/extra/markdown/src/index.ts @@ -1,4 +1,5 @@ -import {createMdStreamRenderer, HighlighterExtension} from '../../../js/core/src'; +import {HighlighterExtension} from '../../../js/core/src'; +import {createMdStreamRenderer} from '../../../shared/src/markdown/streamParser'; export type MarkdownStreamParser = { next(value: string): void; @@ -21,7 +22,7 @@ export const createMarkdownStreamParser = ( domElement, options?.syntaxHighlighter, { - openLinksInNewWindow: options?.openLinksInNewWindow || false, + openLinksInNewWindow: options?.openLinksInNewWindow || false, skipAnimation: options?.skipAnimation, streamingAnimationSpeed: options?.streamingAnimationSpeed, skipCopyToClipboardButton: true, diff --git a/packages/js/core/src/components/chat/chat-room/actions/submitPrompt.ts b/packages/js/core/src/components/chat/chat-room/actions/submitPrompt.ts deleted file mode 100644 index 5e358599..00000000 --- a/packages/js/core/src/components/chat/chat-room/actions/submitPrompt.ts +++ /dev/null @@ -1,198 +0,0 @@ -import {Observable} from '../../../../core/bus/observable'; -import {ExceptionId} from '../../../../exceptions/exceptions'; -import {ChatAdapterExtras} from '../../../../types/adapters/chat/chatAdapterExtras'; -import {DataTransferMode} from '../../../../types/adapters/chat/chatAdapter'; -import {ControllerContext} from '../../../../types/controllerContext'; -import {warn} from '../../../../x/warn'; -import {CompConversation} from '../../conversation/conversation.model'; -import {MessageContentType} from '../../message/message.types'; -import {CompPromptBox} from '../../prompt-box/prompt-box.model'; - -export const submitPromptFactory = ({ - context, - promptBoxInstance, - conversation, - messageToSend, - resetPromptBox, - dataTransferMode, -}: { - dataTransferMode?: DataTransferMode; - context: ControllerContext; - promptBoxInstance: CompPromptBox; - conversation: CompConversation; - messageToSend: string; - resetPromptBox: (resetTextInput?: boolean) => void; -}) => { - return () => { - const outMessageId = conversation.addMessage( - 'out', 'static', new Date(), messageToSend, - ); - - try { - // - // Disable prompt while sending message - // - promptBoxInstance.enableTextInput(false); - promptBoxInstance.setSendButtonStatus('loading'); - - // - // Important: This is where we send the message via the adapter. - // When both 'stream' and 'fetch' data transfer modes are supported by the adapter, we use the 'stream'. - // If only one of the two is supported, we use it. - // If none of the two is supported, we throw an error. - // - const adapter = context.adapter; - let observable: Observable | undefined; - let sentResponse: Promise | undefined; - let messageContentType: MessageContentType; - const supportedDataTransferModes: DataTransferMode[] = []; - if (typeof adapter.fetchText === 'function') { - supportedDataTransferModes.push('fetch'); - } - - if (typeof adapter.streamText === 'function') { - supportedDataTransferModes.push('stream'); - } - - if (supportedDataTransferModes.length === 0) { - throw new Error( - 'ChatAdapter does not support any data transfer mode! The provided adapter must implement either ' - + '`fetchText()` or `streamText()` methods.', - ); - } - - if (dataTransferMode && !supportedDataTransferModes.includes(dataTransferMode)) { - throw new Error( - `ChatAdapter does not support the requested data transfer mode: ${dataTransferMode}. ` + - `The supported data transfer modes for the provided adapter are: ` - + `${supportedDataTransferModes.join(', ')}`, - ); - } - - // Set the default data transfer mode based on the adapter's capabilities - const defaultDataTransferMode = supportedDataTransferModes.length === 1 ? - supportedDataTransferModes[0] : 'stream'; - - const dataTransferModeToUse = dataTransferMode ?? defaultDataTransferMode; - const extras: ChatAdapterExtras = { - aiChatProps: context.aiChatProps, - conversationHistory: conversation.getConversationContentForAdapter( - context.aiChatProps?.conversationOptions?.historyPayloadSize, - ), - }; - - if (dataTransferModeToUse === 'stream') { - if (!context.adapter.streamText) { - throw new Error('Streaming mode requested but adapter does not implement streamText'); - } - - observable = new Observable(); - context.adapter.streamText(messageToSend, observable, extras); - messageContentType = 'stream'; - } else { - if (!context.adapter.fetchText) { - throw new Error('Fetch mode requested but adapter does not implement fetchText'); - } - - observable = undefined; - sentResponse = context.adapter.fetchText(messageToSend, extras); - messageContentType = 'promise'; - } - - // We add the receiving message to the conversation + we track its loading status. - const inMessageId = conversation.addMessage( - 'in', - messageContentType, - new Date(), - ); - - const message = conversation.getMessageById(inMessageId); - if (!message) { - throw new Error(`Message with id ${inMessageId} not found`); - } - - // - // Emit matching events - // - context.emit('messageSent', messageToSend); - - // - // Handles messages sent as promises: - // Use case: Fetch adapters - // - if (messageContentType === 'promise' && sentResponse) { - sentResponse.then((promiseContent) => { - message.setContent(promiseContent); - resetPromptBox(true); - - // Only add user message to conversation content (used for history, and not displayed) if the - // message was sent successfully and a response was received. - conversation.updateConversationContent({role: 'user', message: messageToSend}); - conversation.updateConversationContent({role: 'ai', message: promiseContent}); - context.emit('messageReceived', promiseContent); - }).catch((error) => { - message.setErrored(); - conversation.removeMessage(outMessageId); - conversation.removeMessage(message.id); - resetPromptBox(false); - - const exceptionId: ExceptionId = error?.exceptionId ?? 'NX-AD-001'; - context.exception(exceptionId); - - context.emit('error', { - errorId: exceptionId, - message: error.message || 'An error occurred while sending message via Promise.', - }); - }); - } else { - // - // Handles messages sent as observables: - // Use case: Websocket adapters - // - if (messageContentType === 'stream' && observable) { - observable.subscribe({ - next: (streamContent) => { - if (typeof streamContent === 'string') { - message.appendContent(streamContent); - } - }, - error: (error: any) => { - message.setErrored(); - conversation.removeMessage(outMessageId); - conversation.removeMessage(message.id); - resetPromptBox(false); - - const exceptionId: ExceptionId = error?.exceptionId ?? 'NX-AD-001'; - context.exception(exceptionId); - - context.emit('error', { - errorId: exceptionId, - message: error.message || 'An error occurred while sending message via Observable.', - }); - }, - complete: () => { - message.commitContent(); - resetPromptBox(true); - - if (message.content) { - // Only add user message to conversation content (used for history, and not displayed) - // if the message was sent successfully and a response was received. - conversation.updateConversationContent({role: 'user', message: messageToSend}); - conversation.updateConversationContent({role: 'ai', message: message.content}); - context.emit('messageReceived', message.content); - } - }, - }); - } else { - // Not supposed to happen - throw new Error( - `Message sent with unknown type or data source - Message type: ${messageContentType}`, - ); - } - } - } catch (error) { - warn(error); - resetPromptBox(false); - } - }; -}; diff --git a/packages/js/core/src/components/chat/chat-room/chat-room.render.ts b/packages/js/core/src/components/chat/chat-room/chat-room.render.ts deleted file mode 100644 index 52aac9f5..00000000 --- a/packages/js/core/src/components/chat/chat-room/chat-room.render.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {NluxRenderingError} from '../../../core/error'; -import {CompRenderer} from '../../../types/comp'; -import {getElement} from '../../../utils/dom/getElement'; -import {listenToElement} from '../../../utils/dom/listenToElement'; -import {render} from '../../../x/render'; -import {source} from '../../../x/source'; -import {CompChatRoomActions, CompChatRoomElements, CompChatRoomEvents, CompChatRoomProps} from './chat-room.types'; - -const __ = (styleName: string) => `nluxc-chat-room-${styleName}`; - -const html = () => `` + - `
` + - `
` + - ``; - -export const renderChatRoom: CompRenderer< - CompChatRoomProps, CompChatRoomElements, CompChatRoomEvents, CompChatRoomActions -> = ({ - appendToRoot, - compEvent, - props, -}) => { - const dom = render(html()); - if (!dom) { - throw new NluxRenderingError({ - source: source('chat-room', 'render'), - message: 'Chat room could not be rendered', - }); - } - - const visibleProp = props.visible ?? true; - const chatRoomElement = document.createElement('div'); - - chatRoomElement.className = __('container'); - chatRoomElement.append(dom); - chatRoomElement.style.display = visibleProp ? '' : 'none'; - - if (typeof props.containerMaxHeight === 'number') { - chatRoomElement.style.maxHeight = `${props.containerMaxHeight}px`; - } else { - if (typeof props.containerMaxHeight === 'string') { - chatRoomElement.style.maxHeight = props.containerMaxHeight; - } - } - - if (typeof props.containerHeight === 'number') { - chatRoomElement.style.height = `${props.containerHeight}px`; - } else { - if (typeof props.containerHeight === 'string') { - chatRoomElement.style.height = props.containerHeight; - } - } - - if (typeof props.containerMaxWidth === 'number') { - chatRoomElement.style.maxWidth = `${props.containerMaxWidth}px`; - } else { - if (typeof props.containerMaxWidth === 'string') { - chatRoomElement.style.maxWidth = props.containerMaxWidth; - } - } - - if (typeof props.containerWidth === 'number') { - chatRoomElement.style.width = `${props.containerWidth}px`; - } else { - if (typeof props.containerWidth === 'string') { - chatRoomElement.style.width = props.containerWidth; - } - } - - const [conversationElement, removeMessagesContainerListeners] = listenToElement(chatRoomElement, - `:scope > .${__('conversation-container')}`, - ).on('click', compEvent('messages-container-clicked')) - .get(); - - const promptBoxElement = getElement(chatRoomElement, `:scope > .${__('prompt-box-container')}`); - appendToRoot(chatRoomElement); - compEvent('chat-room-ready')(); - - return { - elements: { - chatRoomContainer: chatRoomElement, - promptBoxContainer: promptBoxElement, - conversationContainer: conversationElement, - }, - onDestroy: () => { - removeMessagesContainerListeners(); - }, - }; -}; diff --git a/packages/js/core/src/components/chat/chat-room/chat-room.types.ts b/packages/js/core/src/components/chat/chat-room/chat-room.types.ts deleted file mode 100644 index 1b999615..00000000 --- a/packages/js/core/src/components/chat/chat-room/chat-room.types.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {BotPersona, UserPersona} from '../../../core/aiChat/options/personaOptions'; -import {ConversationItem} from '../../../types/conversation'; - -export type CompChatRoomEvents = 'chat-room-ready' - | 'messages-container-clicked'; - -export type CompChatRoomProps = { - visible?: boolean; - botPersona?: BotPersona, - userPersona?: UserPersona, - initialConversationContent?: readonly ConversationItem[]; - scrollWhenGenerating?: boolean; - streamingAnimationSpeed?: number | null; - containerMaxHeight?: number | string; - containerMaxWidth?: number | string; - containerHeight?: number | string; - containerWidth?: number | string; - promptBox?: { - placeholder?: string; - autoFocus?: boolean; - }, -}; - -export type CompChatRoomElements = { - chatRoomContainer: HTMLElement; - promptBoxContainer: HTMLElement; - conversationContainer: HTMLElement; -}; - -export type CompChatRoomActions = {}; diff --git a/packages/js/core/src/components/chat/chat-room/chat-room.update.ts b/packages/js/core/src/components/chat/chat-room/chat-room.update.ts deleted file mode 100644 index 38a5af27..00000000 --- a/packages/js/core/src/components/chat/chat-room/chat-room.update.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {CompUpdater} from '../../../types/comp'; -import {CompChatRoomActions, CompChatRoomElements, CompChatRoomProps} from './chat-room.types'; - -export const updateChatRoom: CompUpdater = ({ - propName, - newValue, - dom: {elements, actions}, -}) => { - if (propName === 'containerMaxHeight' && elements?.chatRoomContainer) { - elements.chatRoomContainer.style.maxHeight = typeof newValue === 'number' - ? `${newValue}px` - : (typeof newValue === 'string' ? newValue : ''); - return; - } - - if (propName === 'containerHeight' && elements?.chatRoomContainer) { - elements.chatRoomContainer.style.height = typeof newValue === 'number' - ? `${newValue}px` - : (typeof newValue === 'string' ? newValue : ''); - return; - } - - if (propName === 'containerMaxWidth' && elements?.chatRoomContainer) { - elements.chatRoomContainer.style.maxWidth = typeof newValue === 'number' - ? `${newValue}px` - : (typeof newValue === 'string' ? newValue : ''); - return; - } - - if (propName === 'containerWidth' && elements?.chatRoomContainer) { - elements.chatRoomContainer.style.width = typeof newValue === 'number' - ? `${newValue}px` - : (typeof newValue === 'string' ? newValue : ''); - return; - } -}; diff --git a/packages/js/core/src/components/chat/prompt-box/prompt-box.model.ts b/packages/js/core/src/components/chat/prompt-box/prompt-box.model.ts deleted file mode 100644 index 64b8a9e3..00000000 --- a/packages/js/core/src/components/chat/prompt-box/prompt-box.model.ts +++ /dev/null @@ -1,105 +0,0 @@ -import {BaseComp} from '../../../core/aiChat/comp/base'; -import {CompEventListener, Model} from '../../../core/aiChat/comp/decorators'; -import {ControllerContext} from '../../../types/controllerContext'; -import {renderChatbox} from './prompt-box.render'; -import { - CompPromptBoxActions, - CompPromptBoxButtonStatus, - CompPromptBoxElements, - CompPromptBoxEventListeners, - CompPromptBoxEvents, - CompPromptBoxProps, -} from './prompt-box.types'; -import {updateChatbox} from './prompt-box.update'; - -@Model('prompt-box', renderChatbox, updateChatbox) -export class CompPromptBox extends BaseComp { - - private userEventListeners?: CompPromptBoxEventListeners; - - constructor(context: ControllerContext, {props, eventListeners}: { - props: CompPromptBoxProps, - eventListeners?: CompPromptBoxEventListeners - }) { - super(context, props); - this.userEventListeners = eventListeners; - } - - public enableTextInput(enable = true) { - this.setProp('enableTextInput', enable); - - if (enable) { - this.focusTextInput(); - } - } - - public focusTextInput() { - this.executeDomAction('focusTextInput'); - } - - @CompEventListener('enter-key-pressed') - handleEnterKeyPressed() { - this.handleSendButtonClick(); - } - - @CompEventListener('escape-key-pressed') - handleEscapeKeyPressed() { - this.handleTextChange(''); - } - - @CompEventListener('send-message-clicked') - handleSendButtonClick() { - if (!this.getProp('textInputValue')) { - return; - } - - const callback = this.userEventListeners?.onSubmit; - if (callback) { - callback(); - } - } - - handleTextChange(newValue: string) { - const callback = this.userEventListeners?.onTextUpdated; - if (callback) { - callback(newValue); - } - - const oldValue = this.getProp('textInputValue') as string | undefined; - if (newValue !== oldValue) { - this.setProp('textInputValue', newValue); - } - - if (newValue === '') { - this.setProp('sendButtonStatus', 'disabled'); - } else { - this.setProp('sendButtonStatus', 'enabled'); - } - } - - @CompEventListener('text-updated') - handleTextInputUpdated(event: Event) { - const target = event.target; - if (!(target instanceof HTMLTextAreaElement)) { - return; - } - - this.handleTextChange(target.value); - } - - public resetSendButtonStatus() { - if (this.getProp('textInputValue') === '') { - this.setProp('sendButtonStatus', 'disabled'); - } else { - this.setProp('sendButtonStatus', 'enabled'); - } - } - - public resetTextInput() { - this.handleTextChange(''); - } - - public setSendButtonStatus(newStatus: CompPromptBoxButtonStatus) { - this.setProp('sendButtonStatus', newStatus); - } -} diff --git a/packages/js/core/src/components/chat/prompt-box/prompt-box.render.ts b/packages/js/core/src/components/chat/prompt-box/prompt-box.render.ts deleted file mode 100644 index 7aa24d6d..00000000 --- a/packages/js/core/src/components/chat/prompt-box/prompt-box.render.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {NluxRenderingError} from '../../../core/error'; -import {CompRenderer} from '../../../types/comp'; -import {listenToElement} from '../../../utils/dom/listenToElement'; -import {domOp} from '../../../x/domOp'; -import {render} from '../../../x/render'; -import {source} from '../../../x/source'; -import {CompPromptBoxActions, CompPromptBoxElements, CompPromptBoxEvents, CompPromptBoxProps} from './prompt-box.types'; - -const __ = (styleName: string) => `nluxc-prompt-box-${styleName}`; - -const html = (props: CompPromptBoxProps) => { - const buttonClass = `bt-primary-filled` + ` ` - + __('send-button') + ` ` - + (props.sendButtonStatus === 'loading' ? __('send-button-loading') : '') + ` ` - + (props.sendButtonStatus === 'disabled' ? __('send-button-disabled') : ''); - - return `` + - `${props.textInputValue ?? ''}` + - `` + - `` - + - `` - + - `` - + - `` + - `` + - ``; -}; - -export const renderChatbox: CompRenderer< - CompPromptBoxProps, CompPromptBoxElements, CompPromptBoxEvents, CompPromptBoxActions -> = ({ - appendToRoot, - props, - compEvent, -}) => { - const dom = render(html(props)); - if (!dom) { - throw new NluxRenderingError({ - source: source('prompt-box', 'render'), - message: 'Prompt box could not be rendered', - }); - } - - const promptBoxRoot = document.createElement('div'); - promptBoxRoot.append(dom); - promptBoxRoot.className = __('container'); - - appendToRoot(promptBoxRoot); - - const [textBoxElement, removeTextBoxListeners] = listenToElement(promptBoxRoot, ':scope > textarea') - .on('input', compEvent('text-updated')) - .on('keydown', (event: KeyboardEvent) => { - if (!event.shiftKey && event.key === 'Enter') { - compEvent('enter-key-pressed')(); - event.preventDefault(); - return; - } - - if (event.key === 'Escape') { - compEvent('escape-key-pressed')(); - event.preventDefault(); - return; - } - }).get(); - - const [sendButtonElement, removeSendButtonListeners] = listenToElement(promptBoxRoot, ':scope > button') - .on('click', compEvent('send-message-clicked')).get(); - - if (!(sendButtonElement instanceof HTMLButtonElement)) { - throw new Error('Expected a button element'); - } - - if (!(textBoxElement instanceof HTMLTextAreaElement)) { - throw new NluxRenderingError({ - source: source('prompt-box', 'render'), - message: 'Expected a textarea element', - }); - } - - const focusTextInput = () => domOp(() => { - textBoxElement.focus(); - textBoxElement.setSelectionRange(textBoxElement.value.length, textBoxElement.value.length); - }); - - return { - elements: { - textInput: textBoxElement, - sendButton: sendButtonElement, - }, - actions: { - focusTextInput, - updateButtonStatus: (status) => { - sendButtonElement.disabled = status === 'disabled' || status === 'loading'; - sendButtonElement.classList.toggle(__('send-button-loading'), status === 'loading'); - sendButtonElement.classList.toggle(__('send-button-disabled'), status === 'disabled'); - }, - }, - onDestroy: () => { - removeTextBoxListeners(); - removeSendButtonListeners(); - }, - }; -}; diff --git a/packages/js/core/src/components/chat/prompt-box/prompt-box.update.ts b/packages/js/core/src/components/chat/prompt-box/prompt-box.update.ts deleted file mode 100644 index 05ddbf84..00000000 --- a/packages/js/core/src/components/chat/prompt-box/prompt-box.update.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {CompUpdater} from '../../../types/comp'; -import { - CompPromptBoxActions, - CompPromptBoxButtonStatus, - CompPromptBoxElements, - CompPromptBoxProps, -} from './prompt-box.types'; - -export const updateChatbox: CompUpdater = ({ - propName, - newValue, - dom: {elements, actions}, -}) => { - switch (propName) { - case 'enableTextInput': - if (elements?.textInput instanceof HTMLTextAreaElement) { - elements.textInput.disabled = !newValue; - } - - if (elements?.sendButton instanceof HTMLButtonElement) { - elements.sendButton.disabled = !newValue; - } - break; - case 'sendButtonStatus': - if (['enabled', 'disabled', 'loading'].includes(newValue as any)) { - actions?.updateButtonStatus(newValue as CompPromptBoxButtonStatus); - } - break; - case 'textInputValue': - if (typeof newValue === 'string' && elements?.textInput instanceof HTMLTextAreaElement) { - elements.textInput.value = newValue; - } - break; - } -}; diff --git a/packages/js/core/src/components/miscellaneous/exceptions-box/model.ts b/packages/js/core/src/components/miscellaneous/exceptions-box/model.ts deleted file mode 100644 index 3216b3c7..00000000 --- a/packages/js/core/src/components/miscellaneous/exceptions-box/model.ts +++ /dev/null @@ -1,94 +0,0 @@ -import {BaseComp} from '../../../core/aiChat/comp/base'; -import {Model} from '../../../core/aiChat/comp/decorators'; -import {ControllerContext} from '../../../types/controllerContext'; -import {Exception, ExceptionType} from '../../../types/exception'; -import {renderExceptionsBox} from './render'; -import { - CompExceptionsBoxActions, - CompExceptionsBoxElements, - CompExceptionsBoxEvents, - CompExceptionsBoxProps, -} from './types'; -import {updateExceptionsBox} from './update'; - -const alertTimeout = 3000; - -@Model('exceptions-box', renderExceptionsBox, updateExceptionsBox) -export class CompExceptionsBox extends BaseComp< - CompExceptionsBoxProps, - CompExceptionsBoxElements, - CompExceptionsBoxEvents, - CompExceptionsBoxActions -> { - private alertExpiryTimer?: ReturnType | null; - private alertShowing = false; - private alertsQueue: Exception[] = []; - - constructor( - context: ControllerContext, - props: CompExceptionsBoxProps, - ) { - super(context, props); - } - - public destroy() { - super.destroy(); - if (this.alertExpiryTimer) { - clearTimeout(this.alertExpiryTimer); - } - - this.alertsQueue = []; - this.alertShowing = false; - this.alertExpiryTimer = null; - } - - public removeAllAlerts() { - if (this.alertShowing) { - this.setProp('visible', false); - this.alertShowing = false; - } - - if (this.alertExpiryTimer) { - clearTimeout(this.alertExpiryTimer); - this.alertExpiryTimer = null; - } - - this.alertsQueue = []; - } - - public showAlert(type: ExceptionType, message: string) { - if (this.alertShowing) { - this.alertsQueue.push({type, message}); - return; - } - - this.alertShowing = true; - - this.setProp('type', type); - this.setProp('message', message); - this.setProp('visible', true); - - this.alertExpiryTimer = setTimeout(() => { - this.setProp('visible', false); - this.alertShowing = false; - - // Hide the alert after a short delay before showing the next alert - setTimeout(() => { - this.checkAlertQueue(); - }, 200); - }, alertTimeout); - } - - private checkAlertQueue() { - if (this.alertsQueue.length === 0) { - return; - } - - const alert = this.alertsQueue.shift(); - if (!alert) { - return; - } - - this.showAlert(alert.type, alert.message); - } -} diff --git a/packages/js/core/src/components/miscellaneous/exceptions-box/render.ts b/packages/js/core/src/components/miscellaneous/exceptions-box/render.ts deleted file mode 100644 index d47aa235..00000000 --- a/packages/js/core/src/components/miscellaneous/exceptions-box/render.ts +++ /dev/null @@ -1,101 +0,0 @@ -import {NluxRenderingError} from '../../../core/error'; -import {CompRenderer} from '../../../types/comp'; -import {ExceptionType} from '../../../types/exception'; -import {render} from '../../../x/render'; -import {source} from '../../../x/source'; -import { - CompExceptionsBoxActions, - CompExceptionsBoxElements, - CompExceptionsBoxEvents, - CompExceptionsBoxProps, -} from './types'; - -const __ = (styleName: string) => `nluxc-exceptions-box-${styleName}`; - -const html = (props: CompExceptionsBoxProps) => `` + - `
` + - `` + - `
`; - -export const renderExceptionsBox: CompRenderer< - CompExceptionsBoxProps, - CompExceptionsBoxElements, - CompExceptionsBoxEvents, - CompExceptionsBoxActions -> = ({ - props, - compEvent, - appendToRoot, -}) => { - const exceptionsBoxRoot = render(html(props)); - if (!(exceptionsBoxRoot instanceof HTMLElement)) { - throw new NluxRenderingError({ - source: source('exceptions-box', 'render'), - message: 'Exception alert could not be rendered', - }); - } - - const exceptionContainerSelector = ':scope > .' + __('exception-container'); - const exceptionContainer = exceptionsBoxRoot.querySelector(exceptionContainerSelector); - if (!(exceptionContainer instanceof HTMLElement)) { - throw new NluxRenderingError({ - source: source('exceptions-box', 'render'), - message: 'Exception container element could not be found with selector ' + exceptionContainerSelector, - }); - } - - const messageSelector = ':scope > .' + __('exception-container') + ' > .' + __('message'); - const messageElement = exceptionsBoxRoot.querySelector(messageSelector); - if (!(messageElement instanceof HTMLElement)) { - throw new NluxRenderingError({ - source: source('exceptions-box', 'render'), - message: 'Exception message element could not be found with selector ' + messageSelector, - }); - } - - if (typeof props.containerMaxWidth === 'number') { - exceptionsBoxRoot.style.maxWidth = `${props.containerMaxWidth}px`; - } else { - if (typeof props.containerMaxWidth === 'string') { - exceptionsBoxRoot.style.maxWidth = props.containerMaxWidth; - } - } - - appendToRoot(exceptionsBoxRoot); - - return { - elements: {}, - actions: { - hide: () => { - exceptionContainer.style.display = 'none'; - messageElement.innerHTML = ''; - }, - show: () => { - exceptionContainer.style.display = ''; - }, - setMessage: (message: string) => { - messageElement.append(document.createTextNode(message)); - }, - setMessageType: (type: ExceptionType) => { - exceptionsBoxRoot.classList.remove('error-exception', 'warning-exception'); - exceptionsBoxRoot.classList.add(`${type}-exception`); - }, - updateContainerMaxWidth: (maxWidth: number | string | undefined) => { - if (typeof maxWidth === 'number') { - exceptionsBoxRoot.style.maxWidth = `${maxWidth}px`; - } else { - if (typeof maxWidth === 'string') { - exceptionsBoxRoot.style.maxWidth = maxWidth; - } else { - exceptionsBoxRoot.style.maxWidth = ''; - } - } - }, - }, - onDestroy: () => { - exceptionsBoxRoot.remove(); - }, - }; -}; diff --git a/packages/js/core/src/components/miscellaneous/exceptions-box/types.ts b/packages/js/core/src/components/miscellaneous/exceptions-box/types.ts deleted file mode 100644 index 7b76ecdf..00000000 --- a/packages/js/core/src/components/miscellaneous/exceptions-box/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {ExceptionType} from '../../../types/exception'; - -export type CompExceptionsBoxEvents = null; - -export type CompExceptionsBoxProps = Readonly<{ - type: ExceptionType; - message: string | undefined; - visible: boolean; - containerMaxWidth?: number | string; -}>; - -export type CompExceptionsBoxEventListeners = Partial<{}>; - -export type CompExceptionsBoxElements = Readonly<{}>; - -export type CompExceptionsBoxActions = Readonly<{ - show: () => void; - hide: () => void; - setMessage: (message: string) => void; - setMessageType: (type: ExceptionType) => void; - updateContainerMaxWidth: (maxWidth: number | string | undefined) => void; -}>; diff --git a/packages/js/core/src/components/miscellaneous/exceptions-box/update.ts b/packages/js/core/src/components/miscellaneous/exceptions-box/update.ts deleted file mode 100644 index 948d3d56..00000000 --- a/packages/js/core/src/components/miscellaneous/exceptions-box/update.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {CompUpdater} from '../../../types/comp'; -import {ExceptionType} from '../../../types/exception'; -import {CompExceptionsBoxActions, CompExceptionsBoxElements, CompExceptionsBoxProps} from './types'; - -export const updateExceptionsBox: CompUpdater< - CompExceptionsBoxProps, CompExceptionsBoxElements, CompExceptionsBoxActions -> = ({ - propName, - newValue, - dom: {elements, actions}, -}) => { - if (propName === 'visible' && typeof newValue === 'boolean' && actions) { - if (newValue) { - actions.show(); - } else { - actions.hide(); - } - - return; - } - - if (propName === 'message' && typeof newValue === 'string' && actions) { - actions.setMessage(newValue as string); - return; - } - - if ((propName === 'type') && ['error', 'warning'].includes(newValue as string) && actions) { - actions.setMessageType(newValue as ExceptionType); - return; - } - - if ((propName === 'containerMaxWidth') && actions) { - actions.updateContainerMaxWidth(newValue as number | string | undefined); - return; - } -}; diff --git a/packages/js/core/src/core/aiChat/options/layoutOptions.ts b/packages/js/core/src/core/aiChat/options/layoutOptions.ts deleted file mode 100644 index 690a0a1a..00000000 --- a/packages/js/core/src/core/aiChat/options/layoutOptions.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type LayoutOptions = { - /** - * The maximum height of the chat component. This can be a number or a string with a unit (e.g. '100px'). - * @default '400px' - */ - maxHeight?: number | string; - /** - * The maximum width of the chat component. This can be a number or a string with a unit (e.g. '100px'). - * @default '400px' - */ - maxWidth?: number | string; - /** - * The height of the chat component. This can be a number or a string with a unit (e.g. '100px'). - */ - height?: number | string; - /** - * The width of the chat component. This can be a number or a string with a unit (e.g. '100px'). - */ - width?: number | string; -}; diff --git a/packages/js/core/src/core/aiChat/options/promptBoxOptions.ts b/packages/js/core/src/core/aiChat/options/promptBoxOptions.ts deleted file mode 100644 index ba42f6cb..00000000 --- a/packages/js/core/src/core/aiChat/options/promptBoxOptions.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface PromptBoxOptions { - /** - * Indicates whether the prompt input field should be focused when the prompt is shown. - * @default false - */ - autoFocus?: boolean; - - /** - * The placeholder text to show in the prompt input field when it's empty. - * @default '' - */ - placeholder?: string; -} diff --git a/packages/js/core/src/core/aiChat/aiChat.ts b/packages/js/core/src/exports/aiChat/aiChat.ts similarity index 89% rename from packages/js/core/src/core/aiChat/aiChat.ts rename to packages/js/core/src/exports/aiChat/aiChat.ts index 257454bd..a494e6e1 100644 --- a/packages/js/core/src/core/aiChat/aiChat.ts +++ b/packages/js/core/src/exports/aiChat/aiChat.ts @@ -1,13 +1,13 @@ -import {registerAllComponents} from '../../components/components'; -import {ChatAdapter} from '../../types/adapters/chat/chatAdapter'; -import {ChatAdapterBuilder} from '../../types/adapters/chat/chatAdapterBuilder'; -import {StandardChatAdapter} from '../../types/adapters/chat/standardChatAdapter'; +import {ChatAdapter} from '../../../../../shared/src/types/adapters/chat/chatAdapter'; +import {ChatAdapterBuilder} from '../../../../../shared/src/types/adapters/chat/chatAdapterBuilder'; +import {StandardChatAdapter} from '../../../../../shared/src/types/adapters/chat/standardChatAdapter'; +import {ChatItem} from '../../../../../shared/src/types/conversation'; +import {NluxRenderingError, NluxUsageError, NluxValidationError} from '../../../../../shared/src/types/error'; +import {debug} from '../../../../../shared/src/utils/debug'; +import {registerAllComponents} from '../../logic/components'; import {IAiChat} from '../../types/aiChat/aiChat'; import {AiChatProps} from '../../types/aiChat/props'; -import {ConversationItem} from '../../types/conversation'; import {EventCallback, EventName, EventsMap} from '../../types/event'; -import {debug} from '../../x/debug'; -import {NluxRenderingError, NluxUsageError, NluxValidationError} from '../error'; import {NluxController} from './controller/controller'; import {HighlighterExtension} from './highlighter/highlighter'; import {ConversationOptions} from './options/conversationOptions'; @@ -15,20 +15,20 @@ import {LayoutOptions} from './options/layoutOptions'; import {PersonaOptions} from './options/personaOptions'; import {PromptBoxOptions} from './options/promptBoxOptions'; -export class AiChat implements IAiChat { - protected theAdapter: ChatAdapter | null = null; - protected theAdapterBuilder: StandardChatAdapter | null = null; +export class AiChat implements IAiChat { + protected theAdapter: ChatAdapter | null = null; + protected theAdapterBuilder: StandardChatAdapter | null = null; protected theAdapterType: 'builder' | 'instance' | null = null; protected theClassName: string | null = null; protected theConversationOptions: ConversationOptions | null = null; - protected theInitialConversation: ConversationItem[] | null = null; + protected theInitialConversation: ChatItem[] | null = null; protected theLayoutOptions: LayoutOptions | null = null; protected thePersonasOptions: PersonaOptions | null = null; protected thePromptBoxOptions: PromptBoxOptions | null = null; protected theSyntaxHighlighter: HighlighterExtension | null = null; protected theThemeId: string | null = null; - private controller: NluxController | null = null; - private unregisteredEventListeners: Map> = new Map(); + private controller: NluxController | null = null; + private unregisteredEventListeners: Map>> = new Map(); public get mounted(): boolean { return this.controller?.mounted ?? false; @@ -54,7 +54,7 @@ export class AiChat implements IAiChat { }); } - const adapterToUser: ChatAdapter | StandardChatAdapter | null = + const adapterToUser: ChatAdapter | StandardChatAdapter | null = this.theAdapter && this.theAdapterType === 'instance' ? this.theAdapter : (this.theAdapterType === 'builder' && this.theAdapterBuilder) ? this.theAdapterBuilder @@ -70,9 +70,11 @@ export class AiChat implements IAiChat { registerAllComponents(); - rootElement.innerHTML = ''; + const aiChatRoot = document.createElement('div'); + rootElement.appendChild(aiChatRoot); + const controller = new NluxController( - rootElement, + aiChatRoot, { themeId: this.theThemeId ?? undefined, adapter: adapterToUser, @@ -106,7 +108,7 @@ export class AiChat implements IAiChat { } }; - on(event: EventName, callback: EventsMap[EventName]) { + on(event: EventName, callback: EventsMap[EventName]) { if (this.controller) { this.controller.on(event, callback); @@ -135,7 +137,7 @@ export class AiChat implements IAiChat { this.unregisteredEventListeners.get(event)?.clear(); } - removeEventListener(event: EventName, callback: EventCallback) { + removeEventListener(event: EventName, callback: EventCallback) { this.controller?.removeEventListener(event, callback); this.unregisteredEventListeners.get(event)?.delete(callback); } @@ -171,7 +173,7 @@ export class AiChat implements IAiChat { this.unregisteredEventListeners.clear(); } - public updateProps(props: Partial) { + public updateProps(props: Partial>) { if (!this.controller) { throw new NluxRenderingError({ source: this.constructor.name, @@ -182,7 +184,7 @@ export class AiChat implements IAiChat { this.controller.updateProps(props); } - public withAdapter(adapter: ChatAdapter | ChatAdapterBuilder) { + public withAdapter(adapter: ChatAdapter | ChatAdapterBuilder) { if (this.mounted) { throw new NluxUsageError({ source: this.constructor.name, @@ -267,7 +269,7 @@ export class AiChat implements IAiChat { return this; } - public withInitialConversation(initialConversation: ConversationItem[]) { + public withInitialConversation(initialConversation: ChatItem[]) { if (this.mounted) { throw new NluxUsageError({ source: this.constructor.name, @@ -362,7 +364,7 @@ export class AiChat implements IAiChat { return this; } - withTheme(themeId: string): IAiChat { + withThemeId(themeId: string) { if (this.mounted) { throw new NluxUsageError({ source: this.constructor.name, diff --git a/packages/js/core/src/core/aiChat/comp/base.ts b/packages/js/core/src/exports/aiChat/comp/base.ts similarity index 84% rename from packages/js/core/src/core/aiChat/comp/base.ts rename to packages/js/core/src/exports/aiChat/comp/base.ts index 8427b053..0a69475c 100644 --- a/packages/js/core/src/core/aiChat/comp/base.ts +++ b/packages/js/core/src/exports/aiChat/comp/base.ts @@ -1,50 +1,47 @@ +import {NluxError, NluxUsageError} from '../../../../../../shared/src/types/error'; +import {domOp} from '../../../../../../shared/src/utils/dom/domOp'; +import {uid} from '../../../../../../shared/src/utils/uid'; +import {warn} from '../../../../../../shared/src/utils/warn'; import {CompDef, CompDom, CompRenderer, CompUpdater} from '../../../types/comp'; import {ControllerContext} from '../../../types/controllerContext'; -import {domOp} from '../../../x/domOp'; -import {uid} from '../../../x/uid'; -import {warn} from '../../../x/warn'; -import {NluxError, NluxUsageError} from '../../error'; import {CompRegistry} from './registry'; -export abstract class BaseComp { +export type CompStatus = 'unmounted' | 'rendered' | 'active' | 'destroyed'; + +export abstract class BaseComp { static __compEventListeners: Map | null = null; static __compId: string | null = null; static __renderer: CompRenderer | null = null; static __updater: CompUpdater | null = null; + /** * A reference to the component definition, as retrieved from the registry. - * @protected */ protected readonly def: CompDef | null; /** * Props that are used to render the component and update the DOM tree. * This map is constructed from the props provided by the user, but it can be modified by * the component using the setProp() method. - * @protected */ - protected elementProps: Map; + protected elementProps: Map; /** * Props that are provided by the component user. - * @protected */ - protected props?: Readonly; + protected props: Readonly; /** * A reference to the DOM tree of the current component, and a callback that is called when the * component is destroyed. - * @protected */ protected renderedDom: CompDom | null; /** * Internally used event listeners that are registerer by the renderer. * Those events are mounted on the DOM tree of the component by the renderer. * This map can be used by components that extend the BaseComp to register listeners. - * @protected */ protected rendererEventListeners: Map; /** * Props that are passed to the renderer. * This map is constructed from the props provided by the user. - * @private */ protected rendererProps: PropsType; /** @@ -52,27 +49,41 @@ export abstract class BaseComp * This could be an HTML element (for most of the cases when the component is rendered in the DOM tree) * or it could be a document fragment (for cases when the component is rendered in a virtual DOM tree). * This property is set to null when the component is not rendered. - * - * @protected */ protected renderingRoot: HTMLElement | DocumentFragment | null; /** * Element IDs of the sub-components that are mounted in the DOM tree of the current component. * The key is the ID of the sub-component and the value is the ID of the element in the DOM tree. - * @protected */ protected subComponentElementIds: Map = new Map(); /** * Sub-components that are mounted in the DOM tree of the current component. * This list should be filled by user by calling addPart() method in constructor of the component. - * - * @private */ - protected subComponents: Map> = new Map(); - private __context: Readonly | null = null; + protected subComponents: Map> = new Map(); + /** + * The context of the current chat component. + */ + private __context: Readonly> | null = null; + /** + * A flag that indicates if the current component is destroyed. + * This will prevent the component from being rendered, updated, or used in any way. + */ private __destroyed: boolean = false; + /** + * A unique identifier of the current component instance. + */ private readonly __instanceId: string; + /** + * The status of the current component. + */ + private __status: CompStatus = 'unmounted'; + /** + * A queue of actions that should be executed on the DOM tree when the component is rendered. + * This queue is used to store actions that are called before the component is rendered. + */ private actionsOnDomReady: Function[] = []; + private compEventGetter = (eventName: EventsType) => { const callback = this.rendererEventListeners.get(eventName as any); if (!callback) { @@ -87,7 +98,7 @@ export abstract class BaseComp return callback; }; - protected constructor(context: ControllerContext, props: PropsType) { + protected constructor(context: ControllerContext, props: PropsType) { const compId = Object.getPrototypeOf(this).constructor.__compId; if (!compId) { throw new NluxUsageError({ @@ -159,7 +170,11 @@ export abstract class BaseComp return this.renderingRoot; } - protected get context(): Readonly { + public get status(): CompStatus { + return this.__status; + } + + protected get context(): Readonly> { if (!this.__context) { throw new NluxUsageError({ source: this.constructor.name, @@ -178,6 +193,11 @@ export abstract class BaseComp this.destroyComponent(true); } + public getProp(name: keyof PropsType): PropsType[keyof PropsType] | null { + this.throwIfDestroyed(); + return this.elementProps.get(name) ?? null; + } + /** * Renders the current component in the DOM tree. * This method should be called by the component user to render the component. It should only be called once. @@ -187,8 +207,11 @@ export abstract class BaseComp * You can use destroyed property to check if the component is already destroyed before calling render(). * * @param root The root element where the component should be rendered. + * @param insertBeforeElement The element before which the component should be inserted. If not provided, the + * component will be appended to the root element. If provided, the component will be inserted before the + * provided element if it exists in the root element. */ - public render(root: HTMLElement) { + public render(root: HTMLElement, insertBeforeElement?: HTMLElement) { if (!this.def) { return; } @@ -230,10 +253,17 @@ export abstract class BaseComp // We append the virtual root element to the actual root element domOp(() => { - if (!this.destroyed) { + if (this.destroyed) { + return; + } + + if (insertBeforeElement) { + root.insertBefore(virtualRoot, insertBeforeElement); + } else { root.append(virtualRoot); - this.renderingRoot = root; } + + this.renderingRoot = root; }); } @@ -248,7 +278,7 @@ export abstract class BaseComp protected addSubComponent( id: string, - subComponent: BaseComp, + subComponent: BaseComp, rendererElementId?: keyof ElementsType, ) { this.throwIfDestroyed(); @@ -344,33 +374,18 @@ export abstract class BaseComp return result; } - protected getProp(name: keyof PropsType): PropsType[keyof PropsType] | null { - this.throwIfDestroyed(); - return this.elementProps.get(name) ?? null; - } - - protected getSubComponent(id: string): BaseComp | undefined { - return this.subComponents.get(id); - } - - protected hasSubComponent(id: string): boolean { - return this.subComponents.has(id); - } - protected removeSubComponent(id: string) { this.throwIfDestroyed(); - const subComp = this.subComponents.get(id); - if (!subComp) { - return; - } - - if (!subComp.destroyed) { - subComp.destroy(); - } - - this.subComponents.delete(id); - this.subComponentElementIds.delete(id); + domOp(() => { + const subComp = this.subComponents.get(id); + if (subComp) { + // Change sub-component root before deleting it to avoid deleting the root element + subComp.renderingRoot = null; + subComp.destroy(); + this.subComponents.delete(id); + } + }); } protected runDomActionsQueue() { @@ -392,20 +407,25 @@ export abstract class BaseComp * @param value * @protected */ - protected setProp(name: keyof PropsType, value: PropsType[keyof PropsType] | undefined | null) { + protected setProp(name: keyof PropsType, value: PropsType[keyof PropsType]) { if (this.destroyed) { warn(`Unable to set prop "${String(name)}" because component "${this.constructor.name}" is destroyed`); return; } - if (value === null) { - this.elementProps.delete(name); - } else { - this.elementProps.set(name, value); + if (!this.elementProps.has(name)) { + warn(`Unable to set prop "${String(name)}" because it does not exist in the component props`); + return; } - this.schedulePropUpdate(name); + this.schedulePropUpdate( + name, + this.elementProps.get(name)!, + value, + ); + this.props = Object.freeze(Object.fromEntries(this.elementProps)) as Readonly; + this.elementProps.set(name, value); } protected throwIfDestroyed() { @@ -477,8 +497,9 @@ export abstract class BaseComp this.__destroyed = true; this.__context = null; - this.props = undefined; + this.props = undefined as any; + this.elementProps.clear(); this.rendererEventListeners.clear(); this.subComponents.clear(); } @@ -500,12 +521,15 @@ export abstract class BaseComp subComp?.render(portal); } - private schedulePropUpdate(propName: keyof PropsType) { + private schedulePropUpdate( + propName: keyof PropsType, + currentValue: PropsType[keyof PropsType], + newValue: PropsType[keyof PropsType], + ) { if (!this.renderedDom || !this.def?.update) { return; } - const newValue = this.elementProps.get(propName); const renderedDom = this.renderedDom; const renderingRoot = this.renderingRoot; const updater = this.def.update; @@ -517,6 +541,7 @@ export abstract class BaseComp domOp(() => { updater({ propName, + currentValue, newValue, dom: { root: renderingRoot, diff --git a/packages/js/core/src/core/aiChat/comp/comp.ts b/packages/js/core/src/exports/aiChat/comp/comp.ts similarity index 95% rename from packages/js/core/src/core/aiChat/comp/comp.ts rename to packages/js/core/src/exports/aiChat/comp/comp.ts index 4d92baed..40ad08c3 100644 --- a/packages/js/core/src/core/aiChat/comp/comp.ts +++ b/packages/js/core/src/exports/aiChat/comp/comp.ts @@ -20,7 +20,7 @@ export const comp = any>( // IMPORTANT ✨ The lines below are responsible for creating all instances of all components. return { - withContext: (newContext: ControllerContext) => { + withContext: (newContext: ControllerContext) => { return { create: (): InstanceType => { return new CompClass(newContext, {}); diff --git a/packages/js/core/src/core/aiChat/comp/decorators.ts b/packages/js/core/src/exports/aiChat/comp/decorators.ts similarity index 95% rename from packages/js/core/src/core/aiChat/comp/decorators.ts rename to packages/js/core/src/exports/aiChat/comp/decorators.ts index fc6b41e2..1586914a 100644 --- a/packages/js/core/src/core/aiChat/comp/decorators.ts +++ b/packages/js/core/src/exports/aiChat/comp/decorators.ts @@ -1,5 +1,5 @@ +import {NluxUsageError} from '../../../../../../shared/src/types/error'; import {CompRenderer, CompUpdater} from '../../../types/comp'; -import {NluxUsageError} from '../../error'; export const Model = ( compId: string, diff --git a/packages/js/core/src/core/aiChat/comp/registry.ts b/packages/js/core/src/exports/aiChat/comp/registry.ts similarity index 90% rename from packages/js/core/src/core/aiChat/comp/registry.ts rename to packages/js/core/src/exports/aiChat/comp/registry.ts index bd15df74..3e802eb3 100644 --- a/packages/js/core/src/core/aiChat/comp/registry.ts +++ b/packages/js/core/src/exports/aiChat/comp/registry.ts @@ -1,14 +1,11 @@ +import {debug} from '../../../../../../shared/src/utils/debug'; +import {warn} from '../../../../../../shared/src/utils/warn'; import {CompDef} from '../../../types/comp'; -import {debug} from '../../../x/debug'; -import {warn} from '../../../x/warn'; import {BaseComp} from './base'; export class CompRegistry { public static componentDefs: Map> = new Map(); - private constructor() { - } - public static register(compClass: typeof BaseComp) { const compId = compClass.__compId; if (!compId) { diff --git a/packages/js/core/src/core/aiChat/controller/context.ts b/packages/js/core/src/exports/aiChat/controller/context.ts similarity index 60% rename from packages/js/core/src/core/aiChat/controller/context.ts rename to packages/js/core/src/exports/aiChat/controller/context.ts index 1e5c9f51..be205ba3 100644 --- a/packages/js/core/src/core/aiChat/controller/context.ts +++ b/packages/js/core/src/exports/aiChat/controller/context.ts @@ -2,22 +2,22 @@ import {AiChatProps} from '../../../types/aiChat/props'; import {ControllerContext, ControllerContextProps} from '../../../types/controllerContext'; import {EventName, EventsMap} from '../../../types/event'; -export const createControllerContext = ( - props: ControllerContextProps, - getAiChatProps: () => Readonly, +export const createControllerContext = ( + props: ControllerContextProps, + getAiChatProps: () => Readonly>, emitEvent: ( event: EventToEmit, - ...params: Parameters + ...params: Parameters[EventToEmit]> ) => void, -): ControllerContext => { - const context: ControllerContext = { +): ControllerContext => { + const context: ControllerContext = { ...props, - update: (newProps: Partial) => { + update: (newProps: Partial>) => { Object.assign(context, newProps); }, emit: ( eventName: EventToEmit, - ...params: Parameters + ...params: Parameters[EventToEmit]> ) => { emitEvent(eventName, ...params); }, diff --git a/packages/js/core/src/core/aiChat/controller/controller.ts b/packages/js/core/src/exports/aiChat/controller/controller.ts similarity index 84% rename from packages/js/core/src/core/aiChat/controller/controller.ts rename to packages/js/core/src/exports/aiChat/controller/controller.ts index 14cdb109..31429b31 100644 --- a/packages/js/core/src/core/aiChat/controller/controller.ts +++ b/packages/js/core/src/exports/aiChat/controller/controller.ts @@ -1,18 +1,18 @@ -import {ExceptionId, NluxExceptions} from '../../../exceptions/exceptions'; +import {ExceptionId, NluxExceptions} from '../../../../../../shared/src/types/exceptions'; +import {uid} from '../../../../../../shared/src/utils/uid'; +import {warn} from '../../../../../../shared/src/utils/warn'; import {AiChatInternalProps, AiChatProps} from '../../../types/aiChat/props'; import {ControllerContext} from '../../../types/controllerContext'; import {EventCallback, EventName} from '../../../types/event'; -import {uid} from '../../../x/uid'; -import {warn} from '../../../x/warn'; import {EventManager} from '../events/eventManager'; import {NluxRenderer} from '../renderer/renderer'; import {createControllerContext} from './context'; -export class NluxController { +export class NluxController { - private readonly eventManager = new EventManager(); + private readonly eventManager = new EventManager(); private readonly nluxInstanceId = uid(); - private props: AiChatInternalProps; + private props: AiChatInternalProps; private renderException = (exceptionId: string) => { if (!this.mounted || !this.renderer) { @@ -28,13 +28,13 @@ export class NluxController { this.renderer.renderEx(exception.type, exception.message); }; - private renderer: NluxRenderer | null = null; + private renderer: NluxRenderer | null = null; private readonly rootCompId: string; private readonly rootElement: HTMLElement; constructor( rootElement: HTMLElement, - props: AiChatInternalProps, + props: AiChatInternalProps, ) { this.rootCompId = 'chat-room'; this.rootElement = rootElement; @@ -58,7 +58,7 @@ export class NluxController { return; } - const newContext: ControllerContext = createControllerContext({ + const newContext: ControllerContext = createControllerContext({ instanceId: this.nluxInstanceId, exception: this.renderException, adapter: this.props.adapter, @@ -98,7 +98,7 @@ export class NluxController { this.renderer.mount(); } - public on(event: EventName, callback: EventCallback) { + public on(event: EventName, callback: EventCallback) { this.eventManager.on(event, callback); } @@ -110,7 +110,7 @@ export class NluxController { this.eventManager.removeAllEventListenersForAllEvent(); } - removeEventListener(event: EventName, callback: EventCallback) { + removeEventListener(event: EventName, callback: EventCallback) { this.eventManager.removeEventListener(event, callback); } @@ -131,7 +131,7 @@ export class NluxController { this.renderer = null; } - public updateProps(props: Partial) { + public updateProps(props: Partial>) { this.renderer?.updateProps(props); this.props = { ...this.props, diff --git a/packages/js/core/src/core/aiChat/events/eventManager.ts b/packages/js/core/src/exports/aiChat/events/eventManager.ts similarity index 67% rename from packages/js/core/src/core/aiChat/events/eventManager.ts rename to packages/js/core/src/exports/aiChat/events/eventManager.ts index c5071c9f..a5dc71a5 100644 --- a/packages/js/core/src/core/aiChat/events/eventManager.ts +++ b/packages/js/core/src/exports/aiChat/events/eventManager.ts @@ -1,13 +1,16 @@ import {EventName, EventsMap} from '../../../types/event'; -export class EventManager { +export class EventManager { - public emit = (event: EventToEmit, ...params: Parameters) => { + public emit = ( + event: EventToEmit, + ...params: Parameters[EventToEmit]> + ) => { if (!this.eventListeners.has(event)) { return; } - this.eventListeners.get(event)?.forEach((callback: EventsMap[EventName]) => { + this.eventListeners.get(event)?.forEach((callback: EventsMap[EventName]) => { if (typeof callback !== 'function') { return; } @@ -16,7 +19,10 @@ export class EventManager { }); }; - public on = (event: EventToAdd, callback: EventsMap[EventToAdd]) => { + public on = ( + event: EventToAdd, + callback: EventsMap[EventToAdd], + ) => { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, new Set()); } @@ -28,7 +34,10 @@ export class EventManager { public removeAllEventListenersForAllEvent = () => this.eventListeners.clear(); - public removeEventListener = (event: EventToUpdate, callback: EventsMap[EventToUpdate]) => { + public removeEventListener = ( + event: EventToUpdate, + callback: EventsMap[EventToUpdate], + ) => { if (!this.eventListeners.has(event)) { return; } @@ -39,20 +48,25 @@ export class EventManager { } }; - public updateEventListeners = (events: Partial) => { + public updateEventListeners = ( + events: Partial>, + ) => { // // Replace all listeners for events present in the new events object // This overwrites any existing listeners for these events! But it will not remove // listeners for events that are not present in the `events: Partial` object. // - const eventKeys = Object.keys(events) as Array; + const eventKeys = Object.keys(events) as Array>; for (const eventName of eventKeys) { this.eventListeners.set( eventName, - new Set([events[eventName] as EventsMap[EventName]]), + new Set([events[eventName] as EventsMap[EventName]]), ); } }; - private readonly eventListeners: Map> = new Map(); + private readonly eventListeners: Map< + EventName, + Set[EventName]> + > = new Map(); } diff --git a/packages/js/core/src/core/aiChat/highlighter/highlighter.ts b/packages/js/core/src/exports/aiChat/highlighter/highlighter.ts similarity index 100% rename from packages/js/core/src/core/aiChat/highlighter/highlighter.ts rename to packages/js/core/src/exports/aiChat/highlighter/highlighter.ts diff --git a/packages/js/core/src/core/aiChat/options/conversationOptions.ts b/packages/js/core/src/exports/aiChat/options/conversationOptions.ts similarity index 100% rename from packages/js/core/src/core/aiChat/options/conversationOptions.ts rename to packages/js/core/src/exports/aiChat/options/conversationOptions.ts diff --git a/packages/js/core/src/exports/aiChat/options/layoutOptions.ts b/packages/js/core/src/exports/aiChat/options/layoutOptions.ts new file mode 100644 index 00000000..a9dda0de --- /dev/null +++ b/packages/js/core/src/exports/aiChat/options/layoutOptions.ts @@ -0,0 +1,10 @@ +export type LayoutOptions = { + /** + * The height of the chat component. This can be a number or a string with a unit (e.g. '100px'). + */ + height?: number | string; + /** + * The width of the chat component. This can be a number or a string with a unit (e.g. '100px'). + */ + width?: number | string; +}; diff --git a/packages/js/core/src/core/aiChat/options/personaOptions.ts b/packages/js/core/src/exports/aiChat/options/personaOptions.ts similarity index 100% rename from packages/js/core/src/core/aiChat/options/personaOptions.ts rename to packages/js/core/src/exports/aiChat/options/personaOptions.ts diff --git a/packages/js/core/src/exports/aiChat/options/promptBoxOptions.ts b/packages/js/core/src/exports/aiChat/options/promptBoxOptions.ts new file mode 100644 index 00000000..45cdbf81 --- /dev/null +++ b/packages/js/core/src/exports/aiChat/options/promptBoxOptions.ts @@ -0,0 +1,33 @@ +export interface PromptBoxOptions { + /** + * Indicates whether the prompt input field should be focused when the prompt is shown. + * @default false + */ + autoFocus?: boolean; + + /** + * This will override the disabled state of the submit button when the prompt box is in 'typing' status. + * It will not have any impact in the prompt box 'submitting' and 'waiting' statuses, as the submit button + * is always disabled in these statuses. + * + * Default: Submit button is only enabled when the message is not empty. + */ + disableSubmitButton?: boolean; + + /** + * The placeholder message to be displayed in the prompt input field when empty. + */ + placeholder?: string; + + /** + * The shortcut to submit the prompt message. + * + * - `Enter`: The user can submit the prompt message by pressing the `Enter` key. In order to add a new line, the + * user can press `Shift + Enter`. + * - `CommandEnter`: When this is used, the user can submit the prompt message by pressing `Ctrl + Enter` on + * Windows/Linux or `Cmd + Enter` on macOS. In order to add a new line, the user can press `Enter`. + * + * @default 'Enter' + */ + submitShortcut?: 'Enter' | 'CommandEnter'; +} diff --git a/packages/js/core/src/core/aiChat/renderer/renderer.ts b/packages/js/core/src/exports/aiChat/renderer/renderer.ts similarity index 61% rename from packages/js/core/src/core/aiChat/renderer/renderer.ts rename to packages/js/core/src/exports/aiChat/renderer/renderer.ts index 2338cc6a..2f0da13e 100644 --- a/packages/js/core/src/core/aiChat/renderer/renderer.ts +++ b/packages/js/core/src/exports/aiChat/renderer/renderer.ts @@ -1,13 +1,13 @@ -import {CompChatRoom} from '../../../components/chat/chat-room/chat-room.model'; -import {CompChatRoomProps} from '../../../components/chat/chat-room/chat-room.types'; -import {CompExceptionsBox} from '../../../components/miscellaneous/exceptions-box/model'; -import {CompExceptionsBoxProps} from '../../../components/miscellaneous/exceptions-box/types'; +import {ChatItem} from '../../../../../../shared/src/types/conversation'; +import {NluxRenderingError} from '../../../../../../shared/src/types/error'; +import {ExceptionType} from '../../../../../../shared/src/types/exception'; +import {getRootClassNames} from '../../../../../../shared/src/utils/dom/getRootClassNames'; +import {warn} from '../../../../../../shared/src/utils/warn'; +import {CompChatRoom} from '../../../logic/chat/chat-room/chat-room.model'; +import {CompChatRoomProps} from '../../../logic/chat/chat-room/chat-room.types'; +import {CompExceptionsBox} from '../../../logic/miscellaneous/exceptions-box/model'; import {AiChatInternalProps, AiChatProps} from '../../../types/aiChat/props'; import {ControllerContext} from '../../../types/controllerContext'; -import {ConversationItem} from '../../../types/conversation'; -import {ExceptionType} from '../../../types/exception'; -import {warn} from '../../../x/warn'; -import {NluxRenderingError} from '../../error'; import {comp} from '../comp/comp'; import {CompRegistry} from '../comp/registry'; import {ConversationOptions} from '../options/conversationOptions'; @@ -15,32 +15,32 @@ import {LayoutOptions} from '../options/layoutOptions'; import {PersonaOptions} from '../options/personaOptions'; import {PromptBoxOptions} from '../options/promptBoxOptions'; -export class NluxRenderer { - private static readonly defaultThemeId = 'nova'; +export class NluxRenderer { + private static readonly defaultThemeId = 'luna'; - private readonly __context: ControllerContext; + private readonly __context: ControllerContext; - private chatRoom: CompChatRoom | null = null; - private exceptionsBox: CompExceptionsBox | null = null; + private chatRoom: CompChatRoom | null = null; + private exceptionsBox: CompExceptionsBox | null = null; private isDestroyed: boolean = false; private isMounted: boolean = false; - private readonly rootClassName: string = 'nluxc-root'; + private readonly rootClassName: string = 'nlux-AiChat-root'; private rootCompId: string | null = null; private rootElement: HTMLElement | null = null; private rootElementInitialClassName: string | null; private theClassName: string | null = null; private theConversationOptions: Readonly = {}; - private theInitialConversationContent: Readonly | null = null; + private theInitialConversationContent: Readonly[]> | null = null; private theLayoutOptions: Readonly = {}; private thePersonasOptions: Readonly = {}; private thePromptBoxOptions: Readonly = {}; private theThemeId: string; constructor( - context: ControllerContext, + context: ControllerContext, rootCompId: string, rootElement: HTMLElement, - props: AiChatInternalProps | null = null, + props: AiChatInternalProps | null = null, ) { if (!rootCompId) { throw new NluxRenderingError({ @@ -109,7 +109,6 @@ export class NluxRenderer { } this.chatRoom?.hide(); - this.exceptionsBox?.removeAllAlerts(); } mount() { @@ -127,8 +126,8 @@ export class NluxRenderer { }); } - let rootComp: CompChatRoom | null = null; - let exceptionAlert: CompExceptionsBox | null = null; + let rootComp: CompChatRoom | null = null; + let exceptionAlert: CompExceptionsBox | null = null; try { // Root component can only be a chat room component. @@ -142,36 +141,30 @@ export class NluxRenderer { // // IMPORTANT ✨ This is where the CompChatRoom is instantiated! // - rootComp = comp(CompChatRoom).withContext(this.context).withProps({ - visible: true, - botPersona: this.thePersonasOptions?.bot ?? undefined, - userPersona: this.thePersonasOptions?.user ?? undefined, - initialConversationContent: this.theInitialConversationContent ?? undefined, - scrollWhenGenerating: this.theConversationOptions?.scrollWhenGenerating, - streamingAnimationSpeed: this.theConversationOptions?.streamingAnimationSpeed, - containerMaxHeight: this.theLayoutOptions?.maxHeight || undefined, - containerHeight: this.theLayoutOptions?.height || undefined, - containerMaxWidth: this.theLayoutOptions?.maxWidth || undefined, - containerWidth: this.theLayoutOptions?.width || undefined, - promptBox: { - placeholder: this.thePromptBoxOptions?.placeholder ?? undefined, - autoFocus: this.thePromptBoxOptions?.autoFocus ?? undefined, - }, - }).create(); + rootComp = comp(CompChatRoom) + .withContext(this.context) + .withProps>({ + visible: true, + botPersona: this.thePersonasOptions?.bot ?? undefined, + userPersona: this.thePersonasOptions?.user ?? undefined, + initialConversationContent: this.theInitialConversationContent ?? undefined, + scrollWhenGenerating: this.theConversationOptions?.scrollWhenGenerating, + streamingAnimationSpeed: this.theConversationOptions?.streamingAnimationSpeed, + promptBox: { + placeholder: this.thePromptBoxOptions?.placeholder ?? undefined, + autoFocus: this.thePromptBoxOptions?.autoFocus ?? undefined, + disableSubmitButton: this.thePromptBoxOptions?.disableSubmitButton ?? undefined, + submitShortcut: this.thePromptBoxOptions?.submitShortcut ?? undefined, + }, + }).create(); const CompExceptionsBoxConstructor: typeof CompExceptionsBox | undefined = CompRegistry .retrieve('exceptions-box')?.model as any; if (CompExceptionsBoxConstructor) { - exceptionAlert = comp(CompExceptionsBox) + exceptionAlert = comp(CompExceptionsBox) .withContext(this.context) - .withProps({ - containerMaxWidth: this.theLayoutOptions?.maxWidth - || undefined, - message: undefined, - visible: false, - type: 'error', - }).create(); + .create(); } else { warn('Exception alert component is not registered! No exceptions will be shown.'); } @@ -184,6 +177,7 @@ export class NluxRenderer { } this.setRootElementClassNames(); + this.setRoomElementDimensions(this.theLayoutOptions); if (exceptionAlert) { exceptionAlert.render(this.rootElement); @@ -273,7 +267,7 @@ export class NluxRenderer { this.isMounted = false; } - public updateProps(props: Partial) { + public updateProps(props: Partial>) { if (props.hasOwnProperty('className')) { const newClassName = props.className || undefined; if (newClassName) { @@ -291,6 +285,14 @@ export class NluxRenderer { } } + if (props.hasOwnProperty('themeId')) { + const newThemeId = props.themeId ?? NluxRenderer.defaultThemeId; + if (newThemeId !== this.theThemeId) { + this.theThemeId = newThemeId; + this.setRootElementClassNames(); + } + } + if (props.hasOwnProperty('adapter') && props.adapter) { this.context.update({ adapter: props.adapter, @@ -298,29 +300,23 @@ export class NluxRenderer { } if (props.hasOwnProperty('layoutOptions')) { - const propsToUpdate: Partial = {}; - if (props.layoutOptions?.hasOwnProperty('maxHeight')) { - propsToUpdate.containerMaxHeight = props.layoutOptions.maxHeight; - } - + const newLayoutOptions: Partial = {}; if (props.layoutOptions?.hasOwnProperty('height')) { - propsToUpdate.containerHeight = props.layoutOptions.height; - } - - if (props.layoutOptions?.hasOwnProperty('maxWidth')) { - propsToUpdate.containerMaxWidth = props.layoutOptions.maxWidth; + newLayoutOptions.height = props.layoutOptions.height; } if (props.layoutOptions?.hasOwnProperty('width')) { - propsToUpdate.containerWidth = props.layoutOptions.width; + newLayoutOptions.width = props.layoutOptions.width; } - this.theLayoutOptions = { - ...this.theLayoutOptions, - ...props.layoutOptions, - }; + if (Object.keys(newLayoutOptions).length > 0) { + this.theLayoutOptions = { + ...this.theLayoutOptions, + ...newLayoutOptions, + }; - this.chatRoom?.setProps(propsToUpdate); + this.setRoomElementDimensions(newLayoutOptions); + } } if (props.hasOwnProperty('conversationOptions')) { @@ -332,7 +328,46 @@ export class NluxRenderer { } if (props.hasOwnProperty('promptBoxOptions')) { - // TODO - Apply new prompt box options + const changedPromptBoxOptions: Partial = {}; + + if ( + props.promptBoxOptions?.hasOwnProperty('placeholder') + && props.promptBoxOptions.placeholder !== this.thePromptBoxOptions.placeholder + ) { + changedPromptBoxOptions.placeholder = props.promptBoxOptions.placeholder; + } + + if ( + props.promptBoxOptions?.hasOwnProperty('autoFocus') + && props.promptBoxOptions.autoFocus !== this.thePromptBoxOptions.autoFocus + ) { + changedPromptBoxOptions.autoFocus = props.promptBoxOptions.autoFocus; + } + + if ( + props.promptBoxOptions?.hasOwnProperty('disableSubmitButton') + && props.promptBoxOptions.disableSubmitButton !== this.thePromptBoxOptions.disableSubmitButton + ) { + changedPromptBoxOptions.disableSubmitButton = props.promptBoxOptions.disableSubmitButton; + } + + if ( + props.promptBoxOptions?.hasOwnProperty('submitShortcut') + && props.promptBoxOptions.submitShortcut !== this.thePromptBoxOptions.submitShortcut + ) { + changedPromptBoxOptions.submitShortcut = props.promptBoxOptions.submitShortcut; + } + + if (Object.keys(changedPromptBoxOptions).length > 0) { + this.thePromptBoxOptions = { + ...this.thePromptBoxOptions, + ...changedPromptBoxOptions, + }; + + this.chatRoom?.setProps({ + promptBox: this.thePromptBoxOptions, + }); + } } if (props.hasOwnProperty('syntaxHighlighter')) { @@ -340,7 +375,7 @@ export class NluxRenderer { } if (props.hasOwnProperty('personaOptions')) { - const changedPersonaProps: Partial = {}; + const changedPersonaProps: Partial> = {}; if ( props.personaOptions?.hasOwnProperty('bot') && props.personaOptions.bot !== this.thePersonasOptions?.bot @@ -364,18 +399,45 @@ export class NluxRenderer { } } + private setRoomElementDimensions(newDimensions: { + width?: number | string; + height?: number | string; + }) { + if (!this.rootElement) { + return; + } + + this.rootElement.style.minWidth = '280px'; + this.rootElement.style.minHeight = '280px'; + + if (newDimensions.hasOwnProperty('width')) { + this.rootElement.style.width = (typeof newDimensions.width === 'number') ? `${newDimensions.width}px` : ( + typeof newDimensions.width === 'string' ? newDimensions.width : '' + ); + } + + if (newDimensions.hasOwnProperty('height')) { + this.rootElement.style.height = (typeof newDimensions.height === 'number') ? `${newDimensions.height}px` : ( + typeof newDimensions.height === 'string' ? newDimensions.height : '' + ); + } + } + private setRootElementClassNames() { if (!this.rootElement) { return; } - this.rootElement.classList.add(this.rootClassName); + const rootClassNames = getRootClassNames({ + themeId: this.themeId, + rootClassName: this.rootClassName, + }); if (this.theClassName) { - this.rootElement.classList.add(this.theClassName); + rootClassNames.push(this.theClassName); } - const themeCssClass = `nluxc-theme-${this.themeId}`; - this.rootElement.classList.add(themeCssClass); + this.rootElement.className = ''; + this.rootElement.classList.add(...rootClassNames); } } diff --git a/packages/js/core/src/core/aiContext/aiContext.ts b/packages/js/core/src/exports/aiContext/aiContext.ts similarity index 95% rename from packages/js/core/src/core/aiContext/aiContext.ts rename to packages/js/core/src/exports/aiContext/aiContext.ts index 1f8c6792..5b089604 100644 --- a/packages/js/core/src/core/aiContext/aiContext.ts +++ b/packages/js/core/src/exports/aiContext/aiContext.ts @@ -1,18 +1,18 @@ -import {ContextAdapter} from '../../types/adapters/context/contextAdapter'; -import {ContextAdapterBuilder} from '../../types/adapters/context/contextAdapterBuilder'; -import {ContextTasksAdapter} from '../../types/adapters/context/contextTasksAdapter'; -import {AiContext, AiContextItemStatus, AiContextStatus} from '../../types/aiContext/aiContext'; -import {ContextItemHandler, ContextTaskHandler} from '../../types/aiContext/contextObservers'; +import {ContextAdapter} from '../../../../../shared/src/types/adapters/context/contextAdapter'; +import {ContextAdapterBuilder} from '../../../../../shared/src/types/adapters/context/contextAdapterBuilder'; +import {ContextTasksAdapter} from '../../../../../shared/src/types/adapters/context/contextTasksAdapter'; +import {AiContext, AiContextItemStatus, AiContextStatus} from '../../../../../shared/src/types/aiContext/aiContext'; +import {ContextItemHandler, ContextTaskHandler} from '../../../../../shared/src/types/aiContext/contextObservers'; import { ContextActionResult, DestroyContextResult, FlushContextResult, InitializeContextResult, RunTaskResult, -} from '../../types/aiContext/contextResults'; -import {ContextItemDataType, ContextItems} from '../../types/aiContext/data'; +} from '../../../../../shared/src/types/aiContext/contextResults'; +import {ContextItemDataType, ContextItems} from '../../../../../shared/src/types/aiContext/data'; +import {warn} from '../../../../../shared/src/utils/warn'; import {isContextTasksAdapter} from '../../utils/adapters/isContextTasksAdapter'; -import {warn} from '../../x/warn'; import {DataSyncService} from './dataSyncService'; import {DataSyncOptions} from './options/dataSyncOptions'; import {TasksService} from './tasksService'; @@ -321,7 +321,7 @@ class AiContextImpl implements AiContext { status = 'set'; } }) - .catch((error) => { + .catch(() => { warn( `${this.constructor.name}.registerTask() failed to register task \'${taskId}\'!\n` + `The task will be marked as deleted and will not be updated anymore.`, @@ -353,7 +353,7 @@ class AiContextImpl implements AiContext { status = 'set'; } }) - .catch((error) => { + .catch(() => { if (status === 'updating') { status = 'set'; } @@ -372,7 +372,7 @@ class AiContextImpl implements AiContext { status = 'set'; } }) - .catch((error) => { + .catch(() => { if (status === 'updating') { status = 'set'; } @@ -393,7 +393,7 @@ class AiContextImpl implements AiContext { status = 'set'; } }) - .catch((error) => { + .catch(() => { if (status === 'updating') { status = 'set'; } @@ -496,7 +496,7 @@ class AiContextImpl implements AiContext { warn( `${this.constructor.name}.unregisterTask() called on a state that has not been initialized! ` + `Use ${this.constructor.name}.initialize() to initialize the context before attempting any task ` + - `unregistration.`, + `unregister.`, ); return Promise.resolve({ diff --git a/packages/js/core/src/core/aiContext/dataSyncService.ts b/packages/js/core/src/exports/aiContext/dataSyncService.ts similarity index 96% rename from packages/js/core/src/core/aiContext/dataSyncService.ts rename to packages/js/core/src/exports/aiContext/dataSyncService.ts index 282081fd..a5417dfc 100644 --- a/packages/js/core/src/core/aiContext/dataSyncService.ts +++ b/packages/js/core/src/exports/aiContext/dataSyncService.ts @@ -1,8 +1,8 @@ -import {ContextAdapter} from '../../types/adapters/context/contextAdapter'; -import {ContextDataAdapter} from '../../types/adapters/context/contextDataAdapter'; -import {DestroyContextResult, InitializeContextResult} from '../../types/aiContext/contextResults'; -import {ContextItemDataType, ContextItems} from '../../types/aiContext/data'; -import {warn} from '../../x/warn'; +import {ContextAdapter} from '../../../../../shared/src/types/adapters/context/contextAdapter'; +import {ContextDataAdapter} from '../../../../../shared/src/types/adapters/context/contextDataAdapter'; +import {DestroyContextResult, InitializeContextResult} from '../../../../../shared/src/types/aiContext/contextResults'; +import {ContextItemDataType, ContextItems} from '../../../../../shared/src/types/aiContext/data'; +import {warn} from '../../../../../shared/src/utils/warn'; type UpdateQueueItem = { operation: 'set'; diff --git a/packages/js/core/src/core/aiContext/options/dataSyncOptions.ts b/packages/js/core/src/exports/aiContext/options/dataSyncOptions.ts similarity index 100% rename from packages/js/core/src/core/aiContext/options/dataSyncOptions.ts rename to packages/js/core/src/exports/aiContext/options/dataSyncOptions.ts diff --git a/packages/js/core/src/core/aiContext/tasksService.ts b/packages/js/core/src/exports/aiContext/tasksService.ts similarity index 97% rename from packages/js/core/src/core/aiContext/tasksService.ts rename to packages/js/core/src/exports/aiContext/tasksService.ts index 8538a736..05a2a92b 100644 --- a/packages/js/core/src/core/aiContext/tasksService.ts +++ b/packages/js/core/src/exports/aiContext/tasksService.ts @@ -1,7 +1,11 @@ -import {ContextTasksAdapter} from '../../types/adapters/context/contextTasksAdapter'; -import {ContextActionResult, DestroyContextResult, RunTaskResult} from '../../types/aiContext/contextResults'; -import {ContextTasks} from '../../types/aiContext/data'; -import {warn} from '../../x/warn'; +import {ContextTasksAdapter} from '../../../../../shared/src/types/adapters/context/contextTasksAdapter'; +import { + ContextActionResult, + DestroyContextResult, + RunTaskResult, +} from '../../../../../shared/src/types/aiContext/contextResults'; +import {ContextTasks} from '../../../../../shared/src/types/aiContext/data'; +import {warn} from '../../../../../shared/src/utils/warn'; type UpdateQueueItem = { operation: 'set'; diff --git a/packages/js/core/src/core/bus/observable.ts b/packages/js/core/src/exports/bus/observable.ts similarity index 100% rename from packages/js/core/src/core/bus/observable.ts rename to packages/js/core/src/exports/bus/observable.ts diff --git a/packages/js/core/src/core/bus/observer.ts b/packages/js/core/src/exports/bus/observer.ts similarity index 100% rename from packages/js/core/src/core/bus/observer.ts rename to packages/js/core/src/exports/bus/observer.ts diff --git a/packages/js/core/src/core/bus/source.ts b/packages/js/core/src/exports/bus/source.ts similarity index 100% rename from packages/js/core/src/core/bus/source.ts rename to packages/js/core/src/exports/bus/source.ts diff --git a/packages/js/core/src/core/global.ts b/packages/js/core/src/exports/global.ts similarity index 90% rename from packages/js/core/src/core/global.ts rename to packages/js/core/src/exports/global.ts index 78f5de43..2e478ca3 100644 --- a/packages/js/core/src/core/global.ts +++ b/packages/js/core/src/exports/global.ts @@ -1,4 +1,4 @@ -import {debug} from '../x/debug'; +import {debug} from '../../../../shared/src/utils/debug'; const globalNlux: { version: string; diff --git a/packages/js/core/src/index.ts b/packages/js/core/src/index.ts index 2e2a3993..e28025d1 100644 --- a/packages/js/core/src/index.ts +++ b/packages/js/core/src/index.ts @@ -1,32 +1,26 @@ // CHAT _____________________ -import {AiChat} from './core/aiChat/aiChat'; +import {AiChat} from './exports/aiChat/aiChat'; -export {AiChat} from './core/aiChat/aiChat'; +export {AiChat} from './exports/aiChat/aiChat'; -export const createAiChat = (): AiChat => new AiChat(); +export const createAiChat = (): AiChat => new AiChat(); -export {Observable} from './core/bus/observable'; -export {NluxError, NluxUsageError, NluxValidationError, NluxRenderingError, NluxConfigError} from './core/error'; -export {createMdStreamRenderer} from './core/aiChat/markdown/streamParser'; -export {debug} from './x/debug'; -export {warn, warnOnce} from './x/warn'; -export {uid} from './x/uid'; +export {Observable} from './exports/bus/observable'; -export type {ExceptionId} from './exceptions/exceptions'; -export type {ConversationOptions} from './core/aiChat/options/conversationOptions'; -export type {PromptBoxOptions} from './core/aiChat/options/promptBoxOptions'; -export type {LayoutOptions} from './core/aiChat/options/layoutOptions'; +export type {ConversationOptions} from './exports/aiChat/options/conversationOptions'; +export type {PromptBoxOptions} from './exports/aiChat/options/promptBoxOptions'; +export type {LayoutOptions} from './exports/aiChat/options/layoutOptions'; export type { PersonaOptions, BotPersona, UserPersona, -} from './core/aiChat/options/personaOptions'; +} from './exports/aiChat/options/personaOptions'; export type { IObserver, -} from './core/bus/observer'; +} from './exports/bus/observer'; export type { AiChatInternalProps, @@ -48,7 +42,7 @@ export type { export type { StandardChatAdapter, -} from './types/adapters/chat/standardChatAdapter'; +} from '../../../shared/src/types/adapters/chat/standardChatAdapter'; export type { StandardAdapterInfo, @@ -56,7 +50,7 @@ export type { AdapterDecodeFunction, InputFormat, OutputFormat, -} from './types/adapters/chat/standardAdapterConfig'; +} from '../../../shared/src/types/adapters/chat/standardAdapterConfig'; export type { AiChatProps, @@ -66,30 +60,30 @@ export type { ChatAdapter, StreamingAdapterObserver, DataTransferMode, -} from './types/adapters/chat/chatAdapter'; +} from '../../../shared/src/types/adapters/chat/chatAdapter'; export type { ChatAdapterExtras, -} from './types/adapters/chat/chatAdapterExtras'; +} from '../../../shared/src/types/adapters/chat/chatAdapterExtras'; export type { ChatAdapterBuilder, -} from './types/adapters/chat/chatAdapterBuilder'; +} from '../../../shared/src/types/adapters/chat/chatAdapterBuilder'; export type { AssistResult, AssistAdapter, -} from './types/adapters/assist/assistAdapter'; +} from '../../../shared/src/types/adapters/assist/assistAdapter'; export type { AssistAdapterBuilder, -} from './types/adapters/assist/assistAdapterBuilder'; +} from '../../../shared/src/types/adapters/assist/assistAdapterBuilder'; export type { StreamParser, StandardStreamParser, StandardStreamParserOutput, -} from './types/markdown/streamParser'; +} from '../../../shared/src/types/markdown/streamParser'; // CONTEXT __________________ @@ -100,38 +94,38 @@ export type { ContextTasks, ContextItem, ContextTask, -} from './types/aiContext/data'; +} from '../../../shared/src/types/aiContext/data'; export type { ContextAdapter, -} from './types/adapters/context/contextAdapter'; +} from '../../../shared/src/types/adapters/context/contextAdapter'; export type { ContextAdapterExtras, -} from './types/adapters/context/contextAdapterExtras'; +} from '../../../shared/src/types/adapters/context/contextAdapterExtras'; export type { ContextTasksAdapter, -} from './types/adapters/context/contextTasksAdapter'; +} from '../../../shared/src/types/adapters/context/contextTasksAdapter'; export type { ContextDataAdapter, -} from './types/adapters/context/contextDataAdapter'; +} from '../../../shared/src/types/adapters/context/contextDataAdapter'; export type { ContextAdapterBuilder, -} from './types/adapters/context/contextAdapterBuilder'; +} from '../../../shared/src/types/adapters/context/contextAdapterBuilder'; export type { AiContext, AiContextStatus, -} from './types/aiContext/aiContext'; +} from '../../../shared/src/types/aiContext/aiContext'; export type { ContextItemHandler, ContextTaskHandler, ContextDomElementHandler, -} from './types/aiContext/contextObservers'; +} from '../../../shared/src/types/aiContext/contextObservers'; export type { InitializeContextResult, @@ -140,19 +134,19 @@ export type { RunTaskResult, ContextActionResult, SetContextResult, -} from './types/aiContext/contextResults'; +} from '../../../shared/src/types/aiContext/contextResults'; export type { DataSyncOptions, -} from './core/aiContext/options/dataSyncOptions'; +} from './exports/aiContext/options/dataSyncOptions'; export { createAiContext, -} from './core/aiContext/aiContext'; +} from './exports/aiContext/aiContext'; export { predefinedContextSize, -} from './core/aiContext/options/dataSyncOptions'; +} from './exports/aiContext/options/dataSyncOptions'; // HIGHLIGHTER ______________ @@ -162,13 +156,13 @@ export type { HighlighterExtension, HighlighterColorMode, CreateHighlighterOptions, -} from './core/aiChat/highlighter/highlighter'; +} from './exports/aiChat/highlighter/highlighter'; -// OTHER ____________________ +// CONVERSATION _____________ export type { - ConversationItem, -} from './types/conversation'; + ChatItem, +} from '../../../shared/src/types/conversation'; export type { ParticipantRole, diff --git a/packages/js/core/src/logic/chat/chat-room/actions/submitPrompt.ts b/packages/js/core/src/logic/chat/chat-room/actions/submitPrompt.ts new file mode 100644 index 00000000..3bc86a88 --- /dev/null +++ b/packages/js/core/src/logic/chat/chat-room/actions/submitPrompt.ts @@ -0,0 +1,97 @@ +import {submitPrompt} from '../../../../../../../shared/src/services/submitPrompt/submitPromptImpl'; +import {ChatAdapterExtras} from '../../../../../../../shared/src/types/adapters/chat/chatAdapterExtras'; +import {warn} from '../../../../../../../shared/src/utils/warn'; +import {ControllerContext} from '../../../../types/controllerContext'; +import {CompConversation} from '../../conversation/conversation.model'; +import {CompPromptBox} from '../../prompt-box/prompt-box.model'; + +export const submitPromptFactory = ({ + context, + promptBoxInstance, + conversation, + messageToSend, + resetPromptBox, +}: { + context: ControllerContext; + promptBoxInstance: CompPromptBox; + conversation: CompConversation; + messageToSend: string; + resetPromptBox: (resetTextInput?: boolean) => void; +}) => { + return () => { + const segmentId = conversation.addChatSegment(); + + try { + // Disable prompt while sending message + const currentPromptBoxProps = promptBoxInstance.getProp('domCompProps'); + promptBoxInstance.setDomProps({...currentPromptBoxProps, status: 'submitting'}); + + // Build request and submit prompt + const extras: ChatAdapterExtras = { + aiChatProps: context.aiChatProps, + conversationHistory: conversation.getConversationContentForAdapter( + context.aiChatProps?.conversationOptions?.historyPayloadSize, + ), + }; + const result = submitPrompt(messageToSend, context.adapter, extras); + + // Listen to observable events + // Always listen to error event + result.observable.on('error', (error) => { + conversation.removeChatSegment(segmentId); + resetPromptBox(false); + + context.exception('NX-AD-001'); + context.emit('error', { + errorId: 'NX-AD-001', + message: 'An error occurred while submitting prompt', + }); + }); + + // When streaming or fet is complete, update conversation content and trigger messageReceived event + const messageContentCompleteHandler = () => { + // if (message.content) { + // // Only add user message to conversation content (used for history, and not displayed) + // // if the message was sent successfully and a response was received. + // conversation.updateConversationContent({role: 'user', message: messageToSend}); + // conversation.updateConversationContent( + // {role: 'ai', message: message.content as any}, + // ); + // + // context.emit('messageReceived', message.content as any); + // } + }; + + result.observable.on('userMessageReceived', (userMessage) => { + conversation.addChatItem(segmentId, userMessage); + }); + + if (result.dataTransferMode === 'fetch') { + // In fetch mode — Listen to aiMessageReceived event and complete event + result.observable.on('aiMessageReceived', (aiMessage) => { + conversation.addChatItem(segmentId, aiMessage); + conversation.completeChatSegment(segmentId); + messageContentCompleteHandler(); + resetPromptBox(true); + }); + } else { + result.observable.on('aiMessageStreamStarted', (aiMessageStream) => { + conversation.addChatItem(segmentId, aiMessageStream); + }); + + result.observable.on('aiChunkReceived', (aiMessageChunk, chatItemId) => { + conversation.addChunk(segmentId, chatItemId, aiMessageChunk); + }); + + result.observable.on('complete', () => { + conversation.completeChatSegment(segmentId); + messageContentCompleteHandler(); + resetPromptBox(true); + }); + } + } catch (error) { + warn(error); + resetPromptBox(false); + } + }; +}; diff --git a/packages/js/core/src/components/chat/chat-room/chat-room.model.ts b/packages/js/core/src/logic/chat/chat-room/chat-room.model.ts similarity index 55% rename from packages/js/core/src/components/chat/chat-room/chat-room.model.ts rename to packages/js/core/src/logic/chat/chat-room/chat-room.model.ts index cb5e9f6d..165d4ec0 100644 --- a/packages/js/core/src/components/chat/chat-room/chat-room.model.ts +++ b/packages/js/core/src/logic/chat/chat-room/chat-room.model.ts @@ -1,14 +1,15 @@ -import {BaseComp} from '../../../core/aiChat/comp/base'; -import {comp} from '../../../core/aiChat/comp/comp'; -import {CompEventListener, Model} from '../../../core/aiChat/comp/decorators'; -import {HistoryPayloadSize} from '../../../core/aiChat/options/conversationOptions'; -import {BotPersona, UserPersona} from '../../../core/aiChat/options/personaOptions'; -import {isStandardChatAdapter, StandardChatAdapter} from '../../../types/adapters/chat/standardChatAdapter'; +import {ChatItem} from '../../../../../../shared/src/types/conversation'; +import {PromptBoxProps} from '../../../../../../shared/src/ui/PromptBox/props'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {comp} from '../../../exports/aiChat/comp/comp'; +import {CompEventListener, Model} from '../../../exports/aiChat/comp/decorators'; +import {HistoryPayloadSize} from '../../../exports/aiChat/options/conversationOptions'; +import {BotPersona, UserPersona} from '../../../exports/aiChat/options/personaOptions'; import {ControllerContext} from '../../../types/controllerContext'; -import {ConversationItem} from '../../../types/conversation'; import {CompConversation} from '../conversation/conversation.model'; import {CompConversationProps} from '../conversation/conversation.types'; import {CompPromptBox} from '../prompt-box/prompt-box.model'; +import {CompPromptBoxProps} from '../prompt-box/prompt-box.types'; import {submitPromptFactory} from './actions/submitPrompt'; import {renderChatRoom} from './chat-room.render'; import {CompChatRoomActions, CompChatRoomElements, CompChatRoomEvents, CompChatRoomProps} from './chat-room.types'; @@ -16,39 +17,33 @@ import {updateChatRoom} from './chat-room.update'; import {getStreamingAnimationSpeed} from './utils/streamingAnimationSpeed'; @Model('chat-room', renderChatRoom, updateChatRoom) -export class CompChatRoom extends BaseComp< - CompChatRoomProps, CompChatRoomElements, CompChatRoomEvents, CompChatRoomActions +export class CompChatRoom extends BaseComp< + AiMsg, CompChatRoomProps, CompChatRoomElements, CompChatRoomEvents, CompChatRoomActions > { // Set scroll when generating default value to true, when not specified static defaultScrollWhenGeneratingUserOption = true; - private conversation: CompConversation; - private promptBoxInstance: CompPromptBox; + private conversation: CompConversation; + private promptBoxInstance: CompPromptBox; private promptBoxText: string = ''; - constructor(context: ControllerContext, { - containerMaxHeight, - containerHeight, - containerMaxWidth, - containerWidth, - scrollWhenGenerating, - streamingAnimationSpeed, - visible = true, - promptBox, - botPersona, - userPersona, - initialConversationContent, - }: CompChatRoomProps) { + constructor(context: ControllerContext, { + scrollWhenGenerating, + streamingAnimationSpeed, + visible = true, + promptBox, + botPersona, + userPersona, + initialConversationContent, + }: CompChatRoomProps, + ) { super(context, { visible, - containerMaxHeight, - containerHeight, - containerMaxWidth, - containerWidth, scrollWhenGenerating, streamingAnimationSpeed, botPersona, userPersona, + promptBox, }); const scrollWhenGeneratingUserOption = scrollWhenGenerating @@ -62,7 +57,12 @@ export class CompChatRoom extends BaseComp< initialConversationContent, ); - this.addPromptBox(promptBox?.placeholder, promptBox?.autoFocus); + this.addPromptBox( + promptBox?.placeholder, + promptBox?.autoFocus, + promptBox?.disableSubmitButton, + promptBox?.submitShortcut, + ); // @ts-ignore if (!this.conversation || !this.promptBoxInstance) { @@ -78,35 +78,19 @@ export class CompChatRoom extends BaseComp< this.setProp('visible', false); } - @CompEventListener('messages-container-clicked') + @CompEventListener('segments-container-clicked') messagesContainerClicked() { this.promptBoxInstance?.focusTextInput(); } @CompEventListener('chat-room-ready') - onChatRoomReady(event: MouseEvent) { + onChatRoomReady() { this.context.emit('ready', { aiChatProps: this.context.aiChatProps, }); } - public setProps(props: Partial) { - if (props.hasOwnProperty('containerMaxHeight')) { - this.setProp('containerMaxHeight', props.containerMaxHeight ?? undefined); - } - - if (props.hasOwnProperty('containerHeight')) { - this.setProp('containerHeight', props.containerHeight ?? undefined); - } - - if (props.hasOwnProperty('containerMaxWidth')) { - this.setProp('containerMaxWidth', props.containerMaxWidth ?? undefined); - } - - if (props.hasOwnProperty('containerWidth')) { - this.setProp('containerWidth', props.containerWidth ?? undefined); - } - + public setProps(props: Partial>) { if (props.hasOwnProperty('scrollWhenGenerating')) { this.conversation?.toggleAutoScrollToStreamingMessage(props.scrollWhenGenerating ?? true); } @@ -124,6 +108,18 @@ export class CompChatRoom extends BaseComp< if (props.hasOwnProperty('userPersona')) { this.conversation?.setUserPersona(props.userPersona ?? undefined); } + + if (props.hasOwnProperty('promptBox')) { + if (this.promptBoxInstance) { + const currentDomProps = this.promptBoxInstance.getProp('domCompProps')!; + const newProps: PromptBoxProps = { + ...currentDomProps, + ...props.promptBox, + }; + + this.promptBoxInstance.setDomProps(newProps); + } + } } public show() { @@ -135,11 +131,11 @@ export class CompChatRoom extends BaseComp< streamingAnimationSpeed: number, botPersona?: BotPersona, userPersona?: UserPersona, - initialConversationContent?: readonly ConversationItem[], + initialConversationContent?: readonly ChatItem[], ) { - this.conversation = comp(CompConversation) + this.conversation = comp(CompConversation) .withContext(this.context) - .withProps({ + .withProps>({ scrollWhenGenerating, streamingAnimationSpeed, botPersona, @@ -158,15 +154,19 @@ export class CompChatRoom extends BaseComp< private addPromptBox( placeholder?: string, autoFocus?: boolean, + disableSubmitButton?: boolean, + submitShortcut?: 'Enter' | 'CommandEnter', ) { - this.promptBoxInstance = comp(CompPromptBox).withContext(this.context).withProps({ + this.promptBoxInstance = comp(CompPromptBox).withContext(this.context).withProps({ props: { - enableTextInput: true, - sendButtonStatus: 'disabled', - textInputValue: '', - placeholder, - autoFocus, - }, + domCompProps: { + status: 'typing', + placeholder, + autoFocus, + disableSubmitButton, + submitShortcut, + }, + } satisfies CompPromptBoxProps, eventListeners: { onTextUpdated: (newValue: string) => this.handlePromptBoxTextChange(newValue), onSubmit: () => this.handlePromptBoxSubmit(), @@ -177,17 +177,20 @@ export class CompChatRoom extends BaseComp< } private handlePromptBoxSubmit() { - const {adapter} = this.context; - const adapterAsStandardAdapter: StandardChatAdapter | undefined = isStandardChatAdapter(adapter as any) ? - adapter as any : undefined; - + const promptBoxProps: Partial | undefined = this.props.promptBox; submitPromptFactory({ - dataTransferMode: adapterAsStandardAdapter ? adapterAsStandardAdapter.dataTransferMode : undefined, context: this.context, promptBoxInstance: this.promptBoxInstance, conversation: this.conversation, messageToSend: this.promptBoxText, - resetPromptBox: (resetTextInput?: boolean) => this.resetPromptBox(resetTextInput), + resetPromptBox: (resetTextInput?: boolean) => { + // Check to handle edge case when reset is called after the component is destroyed! + // Example: When the user submits a message and the component is destroyed before the response is + // received. + if (!this.destroyed) { + this.resetPromptBox(resetTextInput, promptBoxProps?.autoFocus); + } + }, })(); } @@ -195,17 +198,25 @@ export class CompChatRoom extends BaseComp< this.promptBoxText = newValue; } - private resetPromptBox(resetTextInput: boolean = false) { + private resetPromptBox(resetTextInput: boolean = false, focusOnReset: boolean = false) { if (!this.promptBoxInstance) { return; } - this.promptBoxInstance.enableTextInput(true); + const currentCompProps = this.promptBoxInstance.getProp('domCompProps') as PromptBoxProps; + const newProps: PromptBoxProps = { + ...currentCompProps, + status: 'typing', + }; + if (resetTextInput) { - this.promptBoxInstance.resetTextInput(); + newProps.message = ''; } - this.promptBoxInstance.resetSendButtonStatus(); - this.promptBoxInstance.focusTextInput(); + this.promptBoxInstance.setDomProps(newProps); + + if (focusOnReset) { + this.promptBoxInstance.focusTextInput(); + } } } diff --git a/packages/js/core/src/logic/chat/chat-room/chat-room.render.ts b/packages/js/core/src/logic/chat/chat-room/chat-room.render.ts new file mode 100644 index 00000000..c9fc92ae --- /dev/null +++ b/packages/js/core/src/logic/chat/chat-room/chat-room.render.ts @@ -0,0 +1,58 @@ +import {AnyAiMsg} from '../../../../../../shared/src/types/anyAiMsg'; +import {NluxRenderingError} from '../../../../../../shared/src/types/error'; +import {render} from '../../../../../../shared/src/utils/dom/render'; +import {CompRenderer} from '../../../types/comp'; +import {getElement} from '../../../utils/dom/getElement'; +import {listenToElement} from '../../../utils/dom/listenToElement'; +import {source} from '../../../utils/source'; +import {CompChatRoomActions, CompChatRoomElements, CompChatRoomEvents, CompChatRoomProps} from './chat-room.types'; + +const __ = (styleName: string) => `nlux-chtRm-${styleName}`; + +const html = () => `` + + `
` + + `
` + + ``; + +export const renderChatRoom: CompRenderer< + CompChatRoomProps, CompChatRoomElements, CompChatRoomEvents, CompChatRoomActions +> = ({ + appendToRoot, + compEvent, + props, +}) => { + const dom = render(html()); + if (!dom) { + throw new NluxRenderingError({ + source: source('chat-room', 'render'), + message: 'Chat room could not be rendered', + }); + } + + const visibleProp = props.visible ?? true; + const chatRoomElement = document.createElement('div'); + + chatRoomElement.className = __('cntr'); + chatRoomElement.append(dom); + chatRoomElement.style.display = visibleProp ? '' : 'none'; + + const [conversationElement, removeMessagesContainerListeners] = listenToElement(chatRoomElement, + `:scope > .${__('cnv-cntr')}`, + ).on('click', compEvent('segments-container-clicked')) + .get(); + + const promptBoxElement = getElement(chatRoomElement, `:scope > .${__('prmptBox-cntr')}`); + appendToRoot(chatRoomElement); + compEvent('chat-room-ready')(); + + return { + elements: { + chatRoomContainer: chatRoomElement, + promptBoxContainer: promptBoxElement, + conversationContainer: conversationElement, + }, + onDestroy: () => { + removeMessagesContainerListeners(); + }, + }; +}; diff --git a/packages/js/core/src/logic/chat/chat-room/chat-room.types.ts b/packages/js/core/src/logic/chat/chat-room/chat-room.types.ts new file mode 100644 index 00000000..c0eca5f0 --- /dev/null +++ b/packages/js/core/src/logic/chat/chat-room/chat-room.types.ts @@ -0,0 +1,24 @@ +import {ChatItem} from '../../../../../../shared/src/types/conversation'; +import {PromptBoxProps} from '../../../../../../shared/src/ui/PromptBox/props'; +import {BotPersona, UserPersona} from '../../../exports/aiChat/options/personaOptions'; + +export type CompChatRoomEvents = 'chat-room-ready' + | 'segments-container-clicked'; + +export type CompChatRoomProps = { + visible?: boolean; + botPersona?: BotPersona, + userPersona?: UserPersona, + initialConversationContent?: readonly ChatItem[]; + scrollWhenGenerating?: boolean; + streamingAnimationSpeed?: number | null; + promptBox: Partial; +}; + +export type CompChatRoomElements = { + chatRoomContainer: HTMLElement; + promptBoxContainer: HTMLElement; + conversationContainer: HTMLElement; +}; + +export type CompChatRoomActions = {}; diff --git a/packages/js/core/src/logic/chat/chat-room/chat-room.update.ts b/packages/js/core/src/logic/chat/chat-room/chat-room.update.ts new file mode 100644 index 00000000..d3039063 --- /dev/null +++ b/packages/js/core/src/logic/chat/chat-room/chat-room.update.ts @@ -0,0 +1,10 @@ +import {AnyAiMsg} from '../../../../../../shared/src/types/anyAiMsg'; +import {CompUpdater} from '../../../types/comp'; +import {CompChatRoomActions, CompChatRoomElements, CompChatRoomProps} from './chat-room.types'; + +export const updateChatRoom: CompUpdater, CompChatRoomElements, CompChatRoomActions> = ({ + propName, + newValue, + dom: {elements}, +}) => { +}; diff --git a/packages/js/core/src/components/chat/chat-room/utils/streamingAnimationSpeed.ts b/packages/js/core/src/logic/chat/chat-room/utils/streamingAnimationSpeed.ts similarity index 90% rename from packages/js/core/src/components/chat/chat-room/utils/streamingAnimationSpeed.ts rename to packages/js/core/src/logic/chat/chat-room/utils/streamingAnimationSpeed.ts index 133370e1..9443137d 100644 --- a/packages/js/core/src/components/chat/chat-room/utils/streamingAnimationSpeed.ts +++ b/packages/js/core/src/logic/chat/chat-room/utils/streamingAnimationSpeed.ts @@ -1,4 +1,4 @@ -import {markdownDefaultStreamingAnimationSpeed} from '../../../../core/aiChat/markdown/streamParser'; +import {markdownDefaultStreamingAnimationSpeed} from '../../../../../../../shared/src/markdown/streamParser'; export const getStreamingAnimationSpeed = (streamingAnimationSpeed?: number | null) => { // undefined => default animation speed diff --git a/packages/js/core/src/components/chat/chat-room/utils/textMessage.ts b/packages/js/core/src/logic/chat/chat-room/utils/textMessage.ts similarity index 67% rename from packages/js/core/src/components/chat/chat-room/utils/textMessage.ts rename to packages/js/core/src/logic/chat/chat-room/utils/textMessage.ts index 0ba07221..794b2c2a 100644 --- a/packages/js/core/src/components/chat/chat-room/utils/textMessage.ts +++ b/packages/js/core/src/logic/chat/chat-room/utils/textMessage.ts @@ -1,20 +1,20 @@ -import {comp} from '../../../../core/aiChat/comp/comp'; -import {BotPersona, UserPersona} from '../../../../core/aiChat/options/personaOptions'; +import {comp} from '../../../../exports/aiChat/comp/comp'; +import {BotPersona, UserPersona} from '../../../../exports/aiChat/options/personaOptions'; import {ControllerContext} from '../../../../types/controllerContext'; import {CompMessage} from '../../message/message.model'; import {CommonMessageProps, CompMessageProps, MessageContentType} from '../../message/message.types'; -export const textMessage = ( - context: ControllerContext, +export const textMessage = ( + context: ControllerContext, direction: 'in' | 'out', trackResizeAndDomChange: boolean, streamingAnimationSpeed: number, contentType: MessageContentType, - content?: string, + content?: AiMsg | string, createdAt?: Date, botPersona?: BotPersona, userPersona?: UserPersona, -): CompMessage => { +): CompMessage => { const defaultContentStatus = contentType === 'stream' ? 'connecting' : contentType === 'promise' ? 'loading' : 'loaded'; @@ -23,23 +23,27 @@ export const textMessage = ( loadingStatus: defaultContentStatus, streamingAnimationSpeed, contentType, - content, createdAt: createdAt ?? new Date(), trackResize: trackResizeAndDomChange, trackDomChange: trackResizeAndDomChange, }; - const props: CompMessageProps = direction === 'in' ? { + const props: CompMessageProps = direction === 'in' ? { ...commonProps, direction, botPersona: botPersona, + content: content as AiMsg, } : { ...commonProps, direction, userPersona: userPersona, + content: content as string, }; - return comp(CompMessage).withContext(context).withProps(props).create(); + return comp(CompMessage) + .withContext(context) + .withProps>(props) + .create(); }; export const messageInList = 'message-container-in-list'; diff --git a/packages/js/core/src/logic/chat/chatItem/chatItem.model.ts b/packages/js/core/src/logic/chat/chatItem/chatItem.model.ts new file mode 100644 index 00000000..965b922d --- /dev/null +++ b/packages/js/core/src/logic/chat/chatItem/chatItem.model.ts @@ -0,0 +1,28 @@ +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {Model} from '../../../exports/aiChat/comp/decorators'; +import {ControllerContext} from '../../../types/controllerContext'; +import {renderChatItem} from './chatItem.render'; +import {CompChatItemActions, CompChatItemElements, CompChatItemEvents, CompChatItemProps} from './chatItem.types'; +import {updateChatItem} from './chatItem.update'; + +@Model('chatItem', renderChatItem, updateChatItem) +export class CompChatItem extends BaseComp< + AiMsg, CompChatItemProps, CompChatItemElements, CompChatItemEvents, CompChatItemActions +> { + constructor( + context: ControllerContext, + props: CompChatItemProps, + ) { + super(context, props); + } + + public addChunk(chunk: string) { + this.throwIfDestroyed(); + this.executeDomAction('processStreamedChunk', chunk); + } + + public commitChunks() { + this.throwIfDestroyed(); + // TODO - implement chunking + } +} diff --git a/packages/js/core/src/logic/chat/chatItem/chatItem.render.ts b/packages/js/core/src/logic/chat/chatItem/chatItem.render.ts new file mode 100644 index 00000000..e12432fe --- /dev/null +++ b/packages/js/core/src/logic/chat/chatItem/chatItem.render.ts @@ -0,0 +1,38 @@ +import {createChatItemDom} from '../../../../../../shared/src/ui/ChatItem/create'; +import {CompRenderer} from '../../../types/comp'; +import {getElement} from '../../../utils/dom/getElement'; +import {CompChatItemActions, CompChatItemElements, CompChatItemEvents, CompChatItemProps} from './chatItem.types'; + +export const renderChatItem: CompRenderer< + CompChatItemProps, CompChatItemElements, CompChatItemEvents, CompChatItemActions +> = ({ + props, + appendToRoot, +}) => { + + const root = createChatItemDom(props.domProps); + appendToRoot(root); + + const messageContainer = getElement(root, '.nlux-comp-msg'); + + return { + elements: { + chatItemContainer: root, + }, + actions: { + focus: () => { + root.focus(); + }, + processStreamedChunk: (chunk: string) => { + if (messageContainer) { + messageContainer.append( + document.createTextNode(chunk), + ); + } + }, + }, + onDestroy: () => { + root.remove(); + }, + }; +}; diff --git a/packages/js/core/src/logic/chat/chatItem/chatItem.types.ts b/packages/js/core/src/logic/chat/chatItem/chatItem.types.ts new file mode 100644 index 00000000..c6a93a96 --- /dev/null +++ b/packages/js/core/src/logic/chat/chatItem/chatItem.types.ts @@ -0,0 +1,17 @@ +import {ChatItemProps} from '../../../../../../shared/src/ui/ChatItem/props'; + +export type CompChatItemProps = { + uid: string; + domProps: ChatItemProps; +}; + +export type CompChatItemElements = Readonly<{ + chatItemContainer: HTMLElement; +}>; + +export type CompChatItemActions = Readonly<{ + focus: () => void; + processStreamedChunk: (chunk: string) => void; +}>; + +export type CompChatItemEvents = Readonly<{}>; diff --git a/packages/js/core/src/logic/chat/chatItem/chatItem.update.ts b/packages/js/core/src/logic/chat/chatItem/chatItem.update.ts new file mode 100644 index 00000000..7daa3356 --- /dev/null +++ b/packages/js/core/src/logic/chat/chatItem/chatItem.update.ts @@ -0,0 +1,7 @@ +import {CompUpdater} from '../../../types/comp'; +import {CompChatItemActions, CompChatItemElements, CompChatItemProps} from './chatItem.types'; + +export const updateChatItem: CompUpdater< + CompChatItemProps, CompChatItemElements, CompChatItemActions +> = () => { +}; diff --git a/packages/js/core/src/logic/chat/chatSegment/chatSegment.model.ts b/packages/js/core/src/logic/chat/chatSegment/chatSegment.model.ts new file mode 100644 index 00000000..8913f3f3 --- /dev/null +++ b/packages/js/core/src/logic/chat/chatSegment/chatSegment.model.ts @@ -0,0 +1,128 @@ +import {ChatSegmentItem} from '../../../../../../shared/src/types/chatSegment/chatSegment'; +import {ChatItemProps} from '../../../../../../shared/src/ui/ChatItem/props'; +import {domOp} from '../../../../../../shared/src/utils/dom/domOp'; +import {warnOnce} from '../../../../../../shared/src/utils/warn'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {comp} from '../../../exports/aiChat/comp/comp'; +import {CompEventListener, Model} from '../../../exports/aiChat/comp/decorators'; +import {ControllerContext} from '../../../types/controllerContext'; +import {CompChatItem} from '../chatItem/chatItem.model'; +import {CompChatItemProps} from '../chatItem/chatItem.types'; +import {renderChatSegment} from './chatSegment.render'; +import { + CompChatSegmentActions, + CompChatSegmentElements, + CompChatSegmentEvents, + CompChatSegmentProps, +} from './chatSegment.types'; +import {updateChatSegment} from './chatSegment.update'; +import {getChatItemPropsFromSegmentItem} from './utils/getChatItemProps'; + +@Model('chatSegment', renderChatSegment, updateChatSegment) +export class CompChatSegment extends BaseComp< + AiMsg, CompChatSegmentProps, CompChatSegmentElements, CompChatSegmentEvents, CompChatSegmentActions +> { + private chatItems: Map> = new Map(); + + constructor( + context: ControllerContext, + props: CompChatSegmentProps, + ) { + super(context, props); + } + + public addChatItem(item: ChatSegmentItem) { + this.throwIfDestroyed(); + if (this.chatItems.has(item.uid)) { + throw new Error(`CompChatSegment: chat item with id "${item.uid}" already exists`); + } + + let compChatItemProps: ChatItemProps | undefined = getChatItemPropsFromSegmentItem(item); + // TODO - Add additional props + // loader / name / picture + + if (!compChatItemProps) { + throw new Error(`CompChatSegment: chat item with id "${item.uid}" has invalid props`); + } + + const newChatItemComp = comp(CompChatItem) + .withContext(this.context) + .withProps({ + uid: item.uid, + domProps: compChatItemProps, + } satisfies CompChatItemProps, + ) + .create(); + + this.chatItems.set(item.uid, newChatItemComp); + + if (!this.rendered) { + // If the chat segment is not rendered, we don't need to render the chat item yet! + return; + } + + if (!this.renderedDom?.elements?.chatSegmentContainer) { + warnOnce('CompChatSegment: chatSegmentContainer is not available'); + return; + } + + newChatItemComp.render( + this.renderedDom.elements.chatSegmentContainer, + this.renderedDom.elements.loaderContainer, + ); + } + + public addChunk(chatItemId: string, chunk: string) { + domOp(() => { + this.throwIfDestroyed(); + const chatItem = this.chatItems.get(chatItemId); + if (!chatItem) { + throw new Error(`CompChatSegment: chat item with id "${chatItemId}" not found`); + } + + chatItem.addChunk(chunk); + }); + } + + public complete() { + this.throwIfDestroyed(); + this.chatItems.forEach((comp) => comp.commitChunks()); + this.setProp('status', 'complete'); + } + + destroy() { + this.chatItems.forEach((comp) => comp.destroy()); + this.chatItems.clear(); + super.destroy(); + } + + @CompEventListener('chat-segment-ready') + private onChatSegmentReady() { + domOp(() => { + if (!this.renderedDom?.elements?.chatSegmentContainer) { + return; + } + + const chatSegmentContainer = this.renderedDom?.elements?.chatSegmentContainer; + this.chatItems.forEach((comp) => { + if (!comp.rendered) { + comp.render(chatSegmentContainer); + } + }); + }); + } + + @CompEventListener('loader-hidden') + private onLoaderHidden() { + if (this.renderedDom?.elements) { + this.renderedDom.elements.loaderContainer = undefined; + } + } + + @CompEventListener('loader-shown') + private onLoaderShown(loader: HTMLElement) { + if (this.renderedDom?.elements) { + this.renderedDom.elements.loaderContainer = loader; + } + } +} diff --git a/packages/js/core/src/logic/chat/chatSegment/chatSegment.render.ts b/packages/js/core/src/logic/chat/chatSegment/chatSegment.render.ts new file mode 100644 index 00000000..dd1d61b5 --- /dev/null +++ b/packages/js/core/src/logic/chat/chatSegment/chatSegment.render.ts @@ -0,0 +1,64 @@ +import {createLoaderDom} from '../../../../../../shared/src/ui/Loader/create'; +import {getChatSegmentClassName} from '../../../../../../shared/src/utils/dom/getChatSegmentClassName'; +import {CompRenderer} from '../../../types/comp'; +import { + CompChatSegmentActions, + CompChatSegmentElements, + CompChatSegmentEvents, + CompChatSegmentProps, +} from './chatSegment.types'; + +export const renderChatSegment: CompRenderer< + CompChatSegmentProps, CompChatSegmentElements, CompChatSegmentEvents, CompChatSegmentActions +> = ({ + props, + compEvent, + appendToRoot, +}) => { + + // Create container + let loaderContainer: HTMLElement | undefined; + const container = document.createElement('div'); + container.className = getChatSegmentClassName(props.status); + + const showLoader = () => { + if (!loaderContainer) { + loaderContainer = document.createElement('div'); + loaderContainer.className = 'nlux-chtSgm-ldr-cntr'; + + const loaderDom = createLoaderDom(); + loaderContainer.appendChild(loaderDom); + container.appendChild(loaderContainer); + compEvent('loader-shown')(loaderContainer); + } + }; + + const hideLoader = () => { + if (loaderContainer) { + loaderContainer.remove(); + loaderContainer = undefined; + compEvent('loader-hidden')(); + } + }; + + if (props.status === 'active') { + showLoader(); + } + + appendToRoot(container); + compEvent('chat-segment-ready')(); + + return { + elements: { + chatSegmentContainer: container, + loaderContainer: loaderContainer, + }, + actions: { + showLoader, + hideLoader, + }, + onDestroy: () => { + container.remove(); + }, + }; +}; diff --git a/packages/js/core/src/logic/chat/chatSegment/chatSegment.types.ts b/packages/js/core/src/logic/chat/chatSegment/chatSegment.types.ts new file mode 100644 index 00000000..54547d31 --- /dev/null +++ b/packages/js/core/src/logic/chat/chatSegment/chatSegment.types.ts @@ -0,0 +1,18 @@ +import {ChatSegmentStatus} from '../../../../../../shared/src/types/chatSegment/chatSegment'; + +export type CompChatSegmentProps = Readonly<{ + uid: string; + status: ChatSegmentStatus; +}>; + +export type CompChatSegmentElements = { + chatSegmentContainer: HTMLElement; + loaderContainer?: HTMLElement; +}; + +export type CompChatSegmentActions = Readonly<{ + showLoader: () => void; + hideLoader: () => void; +}>; + +export type CompChatSegmentEvents = 'chat-segment-ready' | 'loader-shown' | 'loader-hidden'; diff --git a/packages/js/core/src/logic/chat/chatSegment/chatSegment.update.ts b/packages/js/core/src/logic/chat/chatSegment/chatSegment.update.ts new file mode 100644 index 00000000..bb17292b --- /dev/null +++ b/packages/js/core/src/logic/chat/chatSegment/chatSegment.update.ts @@ -0,0 +1,29 @@ +import {ChatSegmentStatus} from '../../../../../../shared/src/types/chatSegment/chatSegment'; +import {debug} from '../../../../../../shared/src/utils/debug'; +import {getChatSegmentClassName} from '../../../../../../shared/src/utils/dom/getChatSegmentClassName'; +import {CompUpdater} from '../../../types/comp'; +import {CompChatSegmentActions, CompChatSegmentElements, CompChatSegmentProps} from './chatSegment.types'; + +export const updateChatSegment: CompUpdater< + CompChatSegmentProps, CompChatSegmentElements, CompChatSegmentActions +> = ({propName, newValue, dom}) => { + if (propName === 'status') { + const rootContainer: HTMLElement | undefined = dom.elements?.chatSegmentContainer; + if (!rootContainer) { + return; + } + + const newStatus = newValue as ChatSegmentStatus; + rootContainer.className = getChatSegmentClassName(newStatus); + if (newStatus === 'active') { + dom.actions?.showLoader(); + } else { + dom.actions?.hideLoader(); + } + } + + if (propName === 'uid') { + debug('updateChatSegment — uid is not updatable'); + // Do nothing + } +}; diff --git a/packages/js/core/src/logic/chat/chatSegment/utils/getChatItemProps.ts b/packages/js/core/src/logic/chat/chatSegment/utils/getChatItemProps.ts new file mode 100644 index 00000000..e816f5d3 --- /dev/null +++ b/packages/js/core/src/logic/chat/chatSegment/utils/getChatItemProps.ts @@ -0,0 +1,39 @@ +import {ChatSegmentItem} from '../../../../../../../shared/src/types/chatSegment/chatSegment'; +import {ChatItemProps} from '../../../../../../../shared/src/ui/ChatItem/props'; +import {stringifyRandomResponse} from '../../../../../../../shared/src/utils/stringifyRandomResponse'; + +export const getChatItemPropsFromSegmentItem = (segmentItem: ChatSegmentItem): ChatItemProps | undefined => { + if (segmentItem.participantRole === 'ai') { + const status = segmentItem.status === 'complete' ? 'rendered' : ( + segmentItem.status === 'streaming' ? 'streaming' : 'error' + ); + + if (segmentItem.dataTransferMode === 'stream') { + return {status, direction: 'incoming'}; + } + + if (segmentItem.status === 'complete') { + return { + status, + direction: 'incoming', + message: stringifyRandomResponse(segmentItem.content), + }; + } + + if (segmentItem.status === 'loading') { + return {status, direction: 'incoming'}; + } + + if (segmentItem.status === 'error') { + return {status, direction: 'incoming'}; + } + + return; + } + + return { + status: 'rendered', + direction: 'outgoing', + message: segmentItem.content, + } satisfies ChatItemProps; +}; diff --git a/packages/js/core/src/components/chat/conversation/conversation.model.ts b/packages/js/core/src/logic/chat/conversation/conversation.model.ts similarity index 63% rename from packages/js/core/src/components/chat/conversation/conversation.model.ts rename to packages/js/core/src/logic/chat/conversation/conversation.model.ts index 4c257387..4a42f17a 100644 --- a/packages/js/core/src/components/chat/conversation/conversation.model.ts +++ b/packages/js/core/src/logic/chat/conversation/conversation.model.ts @@ -1,13 +1,18 @@ -import {BaseComp} from '../../../core/aiChat/comp/base'; -import {comp} from '../../../core/aiChat/comp/comp'; -import {CompEventListener, Model} from '../../../core/aiChat/comp/decorators'; -import {HistoryPayloadSize} from '../../../core/aiChat/options/conversationOptions'; -import {BotPersona, UserPersona} from '../../../core/aiChat/options/personaOptions'; +import {ChatSegmentItem} from '../../../../../../shared/src/types/chatSegment/chatSegment'; +import {ChatItem} from '../../../../../../shared/src/types/conversation'; +import {debug} from '../../../../../../shared/src/utils/debug'; +import {uid} from '../../../../../../shared/src/utils/uid'; +import {warnOnce} from '../../../../../../shared/src/utils/warn'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {comp} from '../../../exports/aiChat/comp/comp'; +import {CompEventListener, Model} from '../../../exports/aiChat/comp/decorators'; +import {HistoryPayloadSize} from '../../../exports/aiChat/options/conversationOptions'; +import {BotPersona, UserPersona} from '../../../exports/aiChat/options/personaOptions'; import {ControllerContext} from '../../../types/controllerContext'; -import {ConversationItem} from '../../../types/conversation'; -import {warnOnce} from '../../../x/warn'; import {CompList} from '../../miscellaneous/list/model'; import {messageInList, textMessage} from '../chat-room/utils/textMessage'; +import {CompChatSegment} from '../chatSegment/chatSegment.model'; +import {CompChatSegmentProps} from '../chatSegment/chatSegment.types'; import {CompMessage} from '../message/message.model'; import type {MessageContentType} from '../message/message.types'; import {renderConversation} from './conversation.render'; @@ -21,33 +26,75 @@ import { import {updateConversation} from './conversation.update'; @Model('conversation', renderConversation, updateConversation) -export class CompConversation extends BaseComp< - CompConversationProps, CompConversationElements, CompConversationEvents, CompConversationActions +export class CompConversation extends BaseComp< + AiMsg, CompConversationProps, CompConversationElements, CompConversationEvents, CompConversationActions > { - private conversationContent: ConversationItem[] = []; + private readonly chatSegmentsById: Map> = new Map(); + private readonly conversationContent: ChatItem[] = []; private lastMessageId?: string; private lastMessageResizedListener?: Function; private messagesContainerRendered: boolean = false; - private messagesList: CompList | undefined; + private messagesList: CompList> | undefined; private scrollWhenGeneratingUserOption: boolean; private scrollingStickToConversationEnd: boolean = true; - constructor(context: ControllerContext, props: CompConversationProps) { + constructor(context: ControllerContext, props: CompConversationProps) { super(context, props); this.addConversation(); this.scrollWhenGeneratingUserOption = props.scrollWhenGenerating ?? true; this.conversationContent = props.messages?.map((message) => ({...message})) ?? []; } + public addChatItem(segmentId: string, item: ChatSegmentItem) { + const chatSegment = this.chatSegmentsById.get(segmentId); + if (!chatSegment) { + throw new Error(`CompConversation: chat segment with id "${segmentId}" not found`); + } + + if (chatSegment.destroyed) { + // This could happen when streaming messages are received after the chat segment is destroyed + warnOnce(`CompConversation: chat segment with id "${segmentId}" is destroyed and cannot be used`); + return; + } + + chatSegment.addChatItem(item); + } + + public addChatSegment() { + this.throwIfDestroyed(); + + const segmentId = uid(); + const newChatSegmentComp = comp(CompChatSegment) + .withContext(this.context) + .withProps({ + uid: segmentId, + status: 'active', + } satisfies CompChatSegmentProps) + .create(); + + this.chatSegmentsById.set(segmentId, newChatSegmentComp); + this.addSubComponent(segmentId, newChatSegmentComp, 'messagesContainer'); + return segmentId; + }; + + public addChunk(segmentId: string, chatItemId: string, chunk: string) { + const chatSegment = this.chatSegmentsById.get(segmentId); + if (!chatSegment) { + throw new Error(`CompConversation: chat segment with id "${segmentId}" not found`); + } + + chatSegment.addChunk(chatItemId, chunk); + } + public addMessage( direction: 'in' | 'out', contentType: MessageContentType, createdAt: Date, - content?: string, + content?: AiMsg | string, ): string { if (!this.messagesList || !this.props) { - throw new Error(`CompConversation: messagesList is not initialized! Make sure you call` + - `addConversation() before calling addMessage()!`); + throw new Error(`CompConversation: messagesList is not initialized! Make sure you call ` + + `addConversation() before calling addMessage()`); } // @@ -112,9 +159,24 @@ export class CompConversation extends BaseComp< return message.id; } + public completeChatSegment(segmentId: string) { + const chatSegment = this.chatSegmentsById.get(segmentId); + if (!chatSegment) { + throw new Error(`CompConversation: chat segment with id "${segmentId}" not found`); + } + + if (chatSegment.destroyed) { + // This could happen when streaming messages are received after the chat segment is destroyed + debug(`CompConversation: chat segment with id "${segmentId}" is destroyed and cannot be used`); + return; + } + + chatSegment.complete(); + } + public getConversationContentForAdapter( historyPayloadSize: HistoryPayloadSize = 'max', - ): Readonly | undefined { + ): ChatItem[] | undefined { if (typeof historyPayloadSize === 'number' && historyPayloadSize <= 0) { warnOnce( `Invalid value provided for 'historyPayloadSize' : "${historyPayloadSize}"! ` + @@ -136,10 +198,20 @@ export class CompConversation extends BaseComp< return this.conversationContent.slice(-historyPayloadSize); } - public getMessageById(messageId: string): CompMessage | undefined { + public getMessageById(messageId: string): CompMessage | undefined { return this.messagesList?.getComponentById(messageId); } + public removeChatSegment(segmentId: string) { + const chatSegment = this.chatSegmentsById.get(segmentId); + if (!chatSegment) { + return; + } + + this.removeSubComponent(segmentId); + this.chatSegmentsById.delete(segmentId); + } + public removeMessage(messageId: string) { this.messagesList?.removeComponentById(messageId); if (!this.messagesList || this.messagesList.size === 0) { @@ -179,13 +251,15 @@ export class CompConversation extends BaseComp< this.scrollWhenGeneratingUserOption = autoScrollToStreamingMessage; } - public updateConversationContent(newItem: ConversationItem) { + public updateConversationContent(newItem: ChatItem) { this.conversationContent.push(newItem); } private addConversation() { - this.messagesList = comp(CompList).withContext(this.context).create(); - this.addSubComponent(this.messagesList.id, this.messagesList, 'messagesContainer'); + this.messagesList = comp(CompList>).withContext(this.context).create(); + this.addSubComponent( + this.messagesList.id, this.messagesList, 'messagesContainer', + ); const initialMessages = this.props?.messages!; if (initialMessages) { diff --git a/packages/js/core/src/components/chat/conversation/conversation.render.ts b/packages/js/core/src/logic/chat/conversation/conversation.render.ts similarity index 87% rename from packages/js/core/src/components/chat/conversation/conversation.render.ts rename to packages/js/core/src/logic/chat/conversation/conversation.render.ts index bb6285a4..2290c221 100644 --- a/packages/js/core/src/components/chat/conversation/conversation.render.ts +++ b/packages/js/core/src/logic/chat/conversation/conversation.render.ts @@ -1,10 +1,11 @@ -import {BotPersona, UserPersona} from '../../../core/aiChat/options/personaOptions'; -import {NluxRenderingError} from '../../../core/error'; +import {AnyAiMsg} from '../../../../../../shared/src/types/anyAiMsg'; +import {NluxRenderingError} from '../../../../../../shared/src/types/error'; +import {render} from '../../../../../../shared/src/utils/dom/render'; +import {BotPersona, UserPersona} from '../../../exports/aiChat/options/personaOptions'; import {CompRenderer} from '../../../types/comp'; import {listenToElement} from '../../../utils/dom/listenToElement'; -import {render} from '../../../x/render'; -import {source} from '../../../x/source'; -import {throttle} from '../../../x/throttle'; +import {source} from '../../../utils/source'; +import {throttle} from '../../../utils/throttle'; import { CompConversationActions, CompConversationElements, @@ -14,14 +15,14 @@ import { import {createEmptyWelcomeMessage, createWelcomeMessage} from './utils/createWelcomeMessage'; import {messagesScrollHandlerFactory} from './utils/messagesScrollHandler'; -export const __ = (styleName: string) => `nluxc-conversation-${styleName}`; +export const __ = (styleName: string) => `nlux-chtRm-cnv-${styleName}`; const html = () => `` + - `
` + + `
` + ``; export const renderConversation: CompRenderer< - CompConversationProps, CompConversationElements, CompConversationEvents, CompConversationActions + CompConversationProps, CompConversationElements, CompConversationEvents, CompConversationActions > = ({ appendToRoot, compEvent, @@ -76,7 +77,7 @@ export const renderConversation: CompRenderer< return { elements: { - messagesContainer, + messagesContainer: messagesContainer, }, actions: { scrollToBottom: () => { @@ -129,7 +130,7 @@ export const renderConversation: CompRenderer< } } }, - updateUserPersona: (newValue: UserPersona | undefined) => { + updateUserPersona: (_newValue: UserPersona | undefined) => { // TODO - Update messages where user persona is used }, }, diff --git a/packages/js/core/src/components/chat/conversation/conversation.types.ts b/packages/js/core/src/logic/chat/conversation/conversation.types.ts similarity index 75% rename from packages/js/core/src/components/chat/conversation/conversation.types.ts rename to packages/js/core/src/logic/chat/conversation/conversation.types.ts index 9796ceb7..d99e92b5 100644 --- a/packages/js/core/src/components/chat/conversation/conversation.types.ts +++ b/packages/js/core/src/logic/chat/conversation/conversation.types.ts @@ -1,5 +1,5 @@ -import {BotPersona, UserPersona} from '../../../core/aiChat/options/personaOptions'; -import {ConversationItem} from '../../../types/conversation'; +import {ChatItem} from '../../../../../../shared/src/types/conversation'; +import {BotPersona, UserPersona} from '../../../exports/aiChat/options/personaOptions'; export type CompConversationEvents = 'user-scrolled'; @@ -10,8 +10,8 @@ export type CompConversationScrollParams = Readonly<{ export type CompConversationScrollCallback = (params: CompConversationScrollParams) => void; -export type CompConversationProps = Readonly<{ - messages?: readonly ConversationItem[]; +export type CompConversationProps = Readonly<{ + messages?: readonly ChatItem[]; scrollWhenGenerating: boolean; streamingAnimationSpeed: number; botPersona?: BotPersona; diff --git a/packages/js/core/src/components/chat/conversation/conversation.update.ts b/packages/js/core/src/logic/chat/conversation/conversation.update.ts similarity index 63% rename from packages/js/core/src/components/chat/conversation/conversation.update.ts rename to packages/js/core/src/logic/chat/conversation/conversation.update.ts index ce7705d9..223c2ee3 100644 --- a/packages/js/core/src/components/chat/conversation/conversation.update.ts +++ b/packages/js/core/src/logic/chat/conversation/conversation.update.ts @@ -1,13 +1,14 @@ -import {BotPersona} from '../../../core/aiChat/options/personaOptions'; +import {AnyAiMsg} from '../../../../../../shared/src/types/anyAiMsg'; +import {BotPersona} from '../../../exports/aiChat/options/personaOptions'; import {CompUpdater} from '../../../types/comp'; import {CompConversationActions, CompConversationElements, CompConversationProps} from './conversation.types'; export const updateConversation: CompUpdater< - CompConversationProps, CompConversationElements, CompConversationActions + CompConversationProps, CompConversationElements, CompConversationActions > = ({ propName, newValue, - dom: {elements, actions}, + dom: {actions}, }) => { if (propName === 'botPersona') { if (!actions) { diff --git a/packages/js/core/src/components/chat/conversation/utils/createWelcomeMessage.ts b/packages/js/core/src/logic/chat/conversation/utils/createWelcomeMessage.ts similarity index 95% rename from packages/js/core/src/components/chat/conversation/utils/createWelcomeMessage.ts rename to packages/js/core/src/logic/chat/conversation/utils/createWelcomeMessage.ts index d0e33643..6f05d8bd 100644 --- a/packages/js/core/src/components/chat/conversation/utils/createWelcomeMessage.ts +++ b/packages/js/core/src/logic/chat/conversation/utils/createWelcomeMessage.ts @@ -1,5 +1,5 @@ -import {render} from '../../../../x/render'; -import {warn} from '../../../../x/warn'; +import {render} from '../../../../../../../shared/src/utils/dom/render'; +import {warn} from '../../../../../../../shared/src/utils/warn'; import {__} from '../conversation.render'; const welcomeMessageHtml = () => `` + diff --git a/packages/js/core/src/components/chat/conversation/utils/messagesScrollHandler.ts b/packages/js/core/src/logic/chat/conversation/utils/messagesScrollHandler.ts similarity index 100% rename from packages/js/core/src/components/chat/conversation/utils/messagesScrollHandler.ts rename to packages/js/core/src/logic/chat/conversation/utils/messagesScrollHandler.ts diff --git a/packages/js/core/src/components/chat/message/message.model.ts b/packages/js/core/src/logic/chat/message/message.model.ts similarity index 84% rename from packages/js/core/src/components/chat/message/message.model.ts rename to packages/js/core/src/logic/chat/message/message.model.ts index ddd0a4d3..a0ac1a47 100644 --- a/packages/js/core/src/components/chat/message/message.model.ts +++ b/packages/js/core/src/logic/chat/message/message.model.ts @@ -1,9 +1,9 @@ -import {BaseComp} from '../../../core/aiChat/comp/base'; -import {CompEventListener, Model} from '../../../core/aiChat/comp/decorators'; -import {BotPersona} from '../../../core/aiChat/options/personaOptions'; +import {debug} from '../../../../../../shared/src/utils/debug'; +import {warn} from '../../../../../../shared/src/utils/warn'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {CompEventListener, Model} from '../../../exports/aiChat/comp/decorators'; +import {BotPersona} from '../../../exports/aiChat/options/personaOptions'; import {ControllerContext} from '../../../types/controllerContext'; -import {debug} from '../../../x/debug'; -import {warn} from '../../../x/warn'; import {renderMessage} from './message.render'; import { CompMessageActions, @@ -18,17 +18,20 @@ import {updateMessage} from './message.update'; export type MessageContentStatusChangeListener = (status: MessageContentLoadingStatus) => void; @Model('message', renderMessage, updateMessage) -export class CompMessage extends BaseComp< - CompMessageProps, CompMessageElements, CompMessageEvents, CompMessageActions +export class CompMessage extends BaseComp< + AiMsg, CompMessageProps, CompMessageElements, CompMessageEvents, CompMessageActions > { - private __content?: string; + private __content?: AiMsg | string; private contentStatus: MessageContentLoadingStatus; private contentStatusChangeListeners: Set = new Set(); private readonly contentType: MessageContentType; private domChangeListeners: Set = new Set(); private resizeListeners: Set = new Set(); - constructor(context: ControllerContext, props: CompMessageProps) { + constructor( + context: ControllerContext, + props: CompMessageProps, + ) { super(context, props); this.__content = props.content; @@ -68,7 +71,7 @@ export class CompMessage extends BaseComp< } /** - * Called to commit the content and close the stream when the content is provided via stream. + * Called to complete the content and close the stream when the content is provided via stream. * Once this is called, the content cannot be appended anymore. * It should be called when the stream is closed. */ @@ -122,7 +125,7 @@ export class CompMessage extends BaseComp< * It should be called when the promise is resolved. * @param {string} content */ - public setContent(content: string) { + public setContent(content: AiMsg) { if (this.contentType !== 'promise') { throw new Error(`CompMessage: content can only be set when contentType is 'promise'!`); } @@ -160,11 +163,14 @@ export class CompMessage extends BaseComp< private handleCompCopyToClipboardTriggered(event: ClipboardEvent) { event.preventDefault(); if (this.__content) { - debug(`Copying selected message to clipboard!`); - navigator.clipboard.writeText(this.__content).catch(error => { - warn('Failed to copy selected message to clipboard!'); - debug(error); - }); + if (typeof this.__content === 'string') { + navigator.clipboard.writeText(this.__content).catch(error => { + warn('Failed to copy selected message to clipboard!'); + debug(error); + }); + } else { + warn('Cannot copy message to clipboard because the content is not a string!'); + } } } diff --git a/packages/js/core/src/components/chat/message/message.render.ts b/packages/js/core/src/logic/chat/message/message.render.ts similarity index 90% rename from packages/js/core/src/components/chat/message/message.render.ts rename to packages/js/core/src/logic/chat/message/message.render.ts index 161bc0bd..16110ac7 100644 --- a/packages/js/core/src/components/chat/message/message.render.ts +++ b/packages/js/core/src/logic/chat/message/message.render.ts @@ -1,12 +1,12 @@ -import {createMdStreamRenderer} from '../../../core/aiChat/markdown/streamParser'; -import {BotPersona} from '../../../core/aiChat/options/personaOptions'; -import {NluxRenderingError} from '../../../core/error'; +import {createMdStreamRenderer} from '../../../../../../shared/src/markdown/streamParser'; +import {NluxRenderingError} from '../../../../../../shared/src/types/error'; +import {StandardStreamParserOutput} from '../../../../../../shared/src/types/markdown/streamParser'; +import {render} from '../../../../../../shared/src/utils/dom/render'; +import {BotPersona} from '../../../exports/aiChat/options/personaOptions'; import {CompRenderer} from '../../../types/comp'; -import {StandardStreamParserOutput} from '../../../types/markdown/streamParser'; import {listenToElement} from '../../../utils/dom/listenToElement'; -import {textToHtml} from '../../../x/parseTextMessage'; -import {render} from '../../../x/render'; -import {source} from '../../../x/source'; +import {textToHtml} from '../../../utils/parseTextMessage'; +import {source} from '../../../utils/source'; import { CompMessageActions, CompMessageElements, @@ -16,9 +16,9 @@ import { } from './message.types'; import {createPersonaDom} from './utils/createPersonaDom'; -export const __ = (styleName: string) => `nluxc-text-message-${styleName}`; +export const __ = (styleName: string) => `nlux-text-message-${styleName}`; -const html = ({content}: CompMessageProps) => `` + +const html = ({content}: CompMessageProps) => `` + `
${content ? textToHtml(content) : ''}
` + ``; @@ -29,7 +29,7 @@ const loaderHtml = () => `` + + ``; export const renderMessage: CompRenderer< - CompMessageProps, CompMessageElements, CompMessageEvents, CompMessageActions + CompMessageProps, CompMessageElements, CompMessageEvents, CompMessageActions > = ({appendToRoot, props, context, compEvent}) => { if (props.format !== 'text') { throw new NluxRenderingError({ diff --git a/packages/js/core/src/components/chat/message/message.types.ts b/packages/js/core/src/logic/chat/message/message.types.ts similarity index 89% rename from packages/js/core/src/components/chat/message/message.types.ts rename to packages/js/core/src/logic/chat/message/message.types.ts index f9e1af12..1fb50806 100644 --- a/packages/js/core/src/components/chat/message/message.types.ts +++ b/packages/js/core/src/logic/chat/message/message.types.ts @@ -1,5 +1,5 @@ // Possible values for 'static' content: Loading, Loaded. -import {BotPersona} from '../../../core/aiChat/options/personaOptions'; +import {BotPersona} from '../../../exports/aiChat/options/personaOptions'; export type MessageStaticContentStatus = 'loading' | 'loaded'; @@ -26,15 +26,15 @@ export type CommonMessageProps = Readonly<{ format: 'text' | 'markdown' | 'html'; loadingStatus: MessageContentLoadingStatus; streamingAnimationSpeed: number; - content?: string; contentType: MessageContentType; createdAt: Date; trackResize: boolean; trackDomChange: boolean; }>; -export type InMessageProps = CommonMessageProps & Readonly<{ +export type InMessageProps = CommonMessageProps & Readonly<{ direction: 'in'; + content?: AiMsg; botPersona?: { name: string; picture: string | HTMLElement; @@ -44,13 +44,14 @@ export type InMessageProps = CommonMessageProps & Readonly<{ export type OutMessageProps = CommonMessageProps & Readonly<{ direction: 'out'; + content?: string; userPersona?: { name: string; picture: string | HTMLElement; }; }>; -export type CompMessageProps = InMessageProps | OutMessageProps; +export type CompMessageProps = InMessageProps | OutMessageProps; export type CompMessageElements = Readonly<{ container: HTMLElement; diff --git a/packages/js/core/src/components/chat/message/message.update.ts b/packages/js/core/src/logic/chat/message/message.update.ts similarity index 83% rename from packages/js/core/src/components/chat/message/message.update.ts rename to packages/js/core/src/logic/chat/message/message.update.ts index b5b45f0f..1c21e973 100644 --- a/packages/js/core/src/components/chat/message/message.update.ts +++ b/packages/js/core/src/logic/chat/message/message.update.ts @@ -2,7 +2,7 @@ import {CompUpdater} from '../../../types/comp'; import {CompMessageActions, CompMessageElements, CompMessageProps} from './message.types'; export const updateMessage: CompUpdater< - CompMessageProps, CompMessageElements, CompMessageActions + CompMessageProps, CompMessageElements, CompMessageActions > = ({propName, newValue, dom}) => { if (propName === 'loadingStatus' && newValue) { dom.actions?.setContentStatus(newValue as any); diff --git a/packages/js/core/src/components/chat/message/utils/createPersonaDom.ts b/packages/js/core/src/logic/chat/message/utils/createPersonaDom.ts similarity index 90% rename from packages/js/core/src/components/chat/message/utils/createPersonaDom.ts rename to packages/js/core/src/logic/chat/message/utils/createPersonaDom.ts index 8a79fdce..f0e24b5a 100644 --- a/packages/js/core/src/components/chat/message/utils/createPersonaDom.ts +++ b/packages/js/core/src/logic/chat/message/utils/createPersonaDom.ts @@ -1,6 +1,6 @@ -import {escapeHtml} from '../../../../x/dom/escapeHtml'; -import {render} from '../../../../x/render'; -import {warn} from '../../../../x/warn'; +import {escapeHtml} from '../../../../../../../shared/src/utils/dom/escapeHtml'; +import {render} from '../../../../../../../shared/src/utils/dom/render'; +import {warn} from '../../../../../../../shared/src/utils/warn'; import {__} from '../message.render'; const personaHtml = () => `` + diff --git a/packages/js/core/src/logic/chat/prompt-box/prompt-box.model.ts b/packages/js/core/src/logic/chat/prompt-box/prompt-box.model.ts new file mode 100644 index 00000000..156306c6 --- /dev/null +++ b/packages/js/core/src/logic/chat/prompt-box/prompt-box.model.ts @@ -0,0 +1,96 @@ +import {PromptBoxProps} from '../../../../../../shared/src/ui/PromptBox/props'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {CompEventListener, Model} from '../../../exports/aiChat/comp/decorators'; +import {ControllerContext} from '../../../types/controllerContext'; +import {renderChatbox} from './prompt-box.render'; +import { + CompPromptBoxActions, + CompPromptBoxElements, + CompPromptBoxEventListeners, + CompPromptBoxEvents, + CompPromptBoxProps, +} from './prompt-box.types'; +import {updateChatbox} from './prompt-box.update'; + +@Model('prompt-box', renderChatbox, updateChatbox) +export class CompPromptBox extends BaseComp< + AiMsg, CompPromptBoxProps, CompPromptBoxElements, CompPromptBoxEvents, CompPromptBoxActions +> { + + private userEventListeners?: CompPromptBoxEventListeners; + + constructor(context: ControllerContext, {props, eventListeners}: { + props: CompPromptBoxProps, + eventListeners?: CompPromptBoxEventListeners + }) { + super(context, props); + this.userEventListeners = eventListeners; + } + + public focusTextInput() { + this.executeDomAction('focusTextInput'); + } + + @CompEventListener('command-enter-key-pressed') + handleCommandEnterKeyPressed(event?: KeyboardEvent) { + const submitShortcut = this.getProp('domCompProps')?.submitShortcut; + if (submitShortcut === 'CommandEnter') { + this.handleSendButtonClick(); + event?.preventDefault(); + } + } + + @CompEventListener('enter-key-pressed') + handleEnterKeyPressed(event?: KeyboardEvent) { + const submitShortcut = this.getProp('domCompProps')?.submitShortcut; + if (!submitShortcut || submitShortcut === 'Enter') { + this.handleSendButtonClick(); + event?.preventDefault(); + } + } + + @CompEventListener('send-message-clicked') + handleSendButtonClick() { + const domCompProps = this.getProp('domCompProps'); + if (domCompProps?.disableSubmitButton) { + return; + } + + const message = domCompProps?.message; + if (!message) { + return; + } + + const callback = this.userEventListeners?.onSubmit; + if (callback) { + callback(); + } + } + + handleTextChange(newValue: string) { + const callback = this.userEventListeners?.onTextUpdated; + if (callback) { + callback(newValue); + } + + const currentCompProps = this.getProp('domCompProps') as PromptBoxProps; + this.setDomProps({ + ...currentCompProps, + message: newValue, + }); + } + + @CompEventListener('text-updated') + handleTextInputUpdated(event: Event) { + const target = event.target; + if (!(target instanceof HTMLTextAreaElement)) { + return; + } + + this.handleTextChange(target.value); + } + + public setDomProps(props: PromptBoxProps) { + this.setProp('domCompProps', props); + } +} diff --git a/packages/js/core/src/logic/chat/prompt-box/prompt-box.render.ts b/packages/js/core/src/logic/chat/prompt-box/prompt-box.render.ts new file mode 100644 index 00000000..bf61b441 --- /dev/null +++ b/packages/js/core/src/logic/chat/prompt-box/prompt-box.render.ts @@ -0,0 +1,71 @@ +import {NluxRenderingError} from '../../../../../../shared/src/types/error'; +import {createPromptBoxDom} from '../../../../../../shared/src/ui/PromptBox/create'; +import {domOp} from '../../../../../../shared/src/utils/dom/domOp'; +import {CompRenderer} from '../../../types/comp'; +import {listenToElement} from '../../../utils/dom/listenToElement'; +import {source} from '../../../utils/source'; +import {CompPromptBoxActions, CompPromptBoxElements, CompPromptBoxEvents, CompPromptBoxProps} from './prompt-box.types'; + +export const renderChatbox: CompRenderer< + CompPromptBoxProps, CompPromptBoxElements, CompPromptBoxEvents, CompPromptBoxActions +> = ({ + appendToRoot, + props, + compEvent, +}) => { + const promptBoxRoot = createPromptBoxDom(props.domCompProps); + + appendToRoot(promptBoxRoot); + + const [textBoxElement, removeTextBoxListeners] = listenToElement(promptBoxRoot, ':scope > textarea') + .on('input', compEvent('text-updated')) + .on('keydown', (event: KeyboardEvent) => { + const isEnter = event.key === 'Enter'; + const aModifierKeyIsPressed = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; + if (isEnter && !aModifierKeyIsPressed) { + compEvent('enter-key-pressed')(event); + return; + } + + const isCommandEnter = event.getModifierState('Meta') && event.key === 'Enter'; + const isControlEnter = event.getModifierState('Control') && event.key === 'Enter'; + if (isCommandEnter || isControlEnter) { + compEvent('command-enter-key-pressed')(event); + return; + } + }).get(); + + const [sendButtonElement, removeSendButtonListeners] = listenToElement(promptBoxRoot, ':scope > button') + .on('click', compEvent('send-message-clicked')).get(); + + if (!(sendButtonElement instanceof HTMLButtonElement)) { + throw new Error('Expected a button element'); + } + + if (!(textBoxElement instanceof HTMLTextAreaElement)) { + throw new NluxRenderingError({ + source: source('prompt-box', 'render'), + message: 'Expected a textarea element', + }); + } + + const focusTextInput = () => domOp(() => { + textBoxElement.focus(); + textBoxElement.setSelectionRange(textBoxElement.value.length, textBoxElement.value.length); + }); + + return { + elements: { + root: promptBoxRoot, + textInput: textBoxElement, + sendButton: sendButtonElement, + }, + actions: { + focusTextInput, + }, + onDestroy: () => { + removeTextBoxListeners(); + removeSendButtonListeners(); + }, + }; +}; diff --git a/packages/js/core/src/components/chat/prompt-box/prompt-box.types.ts b/packages/js/core/src/logic/chat/prompt-box/prompt-box.types.ts similarity index 61% rename from packages/js/core/src/components/chat/prompt-box/prompt-box.types.ts rename to packages/js/core/src/logic/chat/prompt-box/prompt-box.types.ts index 17e24470..a3ac5cc7 100644 --- a/packages/js/core/src/components/chat/prompt-box/prompt-box.types.ts +++ b/packages/js/core/src/logic/chat/prompt-box/prompt-box.types.ts @@ -1,16 +1,13 @@ +import {PromptBoxProps} from '../../../../../../shared/src/ui/PromptBox/props'; + export type CompPromptBoxEvents = 'text-updated' | 'send-message-clicked' | 'escape-key-pressed' - | 'enter-key-pressed'; - -export type CompPromptBoxButtonStatus = 'enabled' | 'disabled' | 'loading'; + | 'enter-key-pressed' + | 'command-enter-key-pressed'; export type CompPromptBoxProps = Readonly<{ - sendButtonStatus: CompPromptBoxButtonStatus; - enableTextInput: boolean; - textInputValue: string; - placeholder?: string; - autoFocus?: boolean; + domCompProps: PromptBoxProps; }>; export type CompPromptBoxEventListeners = Partial<{ @@ -20,11 +17,11 @@ export type CompPromptBoxEventListeners = Partial<{ }>; export type CompPromptBoxElements = Readonly<{ + root: HTMLElement; textInput: HTMLTextAreaElement; sendButton: HTMLButtonElement; }>; export type CompPromptBoxActions = Readonly<{ focusTextInput: () => void; - updateButtonStatus: (status: CompPromptBoxButtonStatus) => void; }>; diff --git a/packages/js/core/src/logic/chat/prompt-box/prompt-box.update.ts b/packages/js/core/src/logic/chat/prompt-box/prompt-box.update.ts new file mode 100644 index 00000000..7c479eae --- /dev/null +++ b/packages/js/core/src/logic/chat/prompt-box/prompt-box.update.ts @@ -0,0 +1,14 @@ +import {updatePromptBoxDom} from '../../../../../../shared/src/ui/PromptBox/update'; +import {CompUpdater} from '../../../types/comp'; +import {CompPromptBoxActions, CompPromptBoxElements, CompPromptBoxProps} from './prompt-box.types'; + +export const updateChatbox: CompUpdater = ({ + propName, + currentValue, + newValue, + dom, +}) => { + if (propName === 'domCompProps' && dom.elements?.root) { + updatePromptBoxDom(dom.elements.root, currentValue, newValue); + } +}; diff --git a/packages/js/core/src/components/components.ts b/packages/js/core/src/logic/components.ts similarity index 73% rename from packages/js/core/src/components/components.ts rename to packages/js/core/src/logic/components.ts index ac059a87..c50d105a 100644 --- a/packages/js/core/src/components/components.ts +++ b/packages/js/core/src/logic/components.ts @@ -1,6 +1,8 @@ -import {CompRegistry} from '../core/aiChat/comp/registry'; -import {getGlobalNlux} from '../core/global'; +import {CompRegistry} from '../exports/aiChat/comp/registry'; +import {getGlobalNlux} from '../exports/global'; import {CompChatRoom} from './chat/chat-room/chat-room.model'; +import {CompChatItem} from './chat/chatItem/chatItem.model'; +import {CompChatSegment} from './chat/chatSegment/chatSegment.model'; import {CompConversation} from './chat/conversation/conversation.model'; import {CompMessage} from './chat/message/message.model'; import {CompPromptBox} from './chat/prompt-box/prompt-box.model'; @@ -14,6 +16,8 @@ const componentsById = () => ({ 'conversation': CompConversation, 'prompt-box': CompPromptBox, 'message': CompMessage, + 'chatSegment': CompChatSegment, + 'chatItem': CompChatItem, }); export const registerAllComponents = () => { @@ -23,7 +27,7 @@ export const registerAllComponents = () => { return; } - Object.entries(componentsById()).forEach(([id, comp]) => { + Object.entries(componentsById()).forEach(([, comp]) => { CompRegistry.register(comp as any); }); diff --git a/packages/js/core/src/logic/miscellaneous/exceptions-box/model.ts b/packages/js/core/src/logic/miscellaneous/exceptions-box/model.ts new file mode 100644 index 00000000..1287614a --- /dev/null +++ b/packages/js/core/src/logic/miscellaneous/exceptions-box/model.ts @@ -0,0 +1,36 @@ +import {ExceptionType} from '../../../../../../shared/src/types/exception'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {Model} from '../../../exports/aiChat/comp/decorators'; +import {ControllerContext} from '../../../types/controllerContext'; +import {renderExceptionsBox} from './render'; +import { + CompExceptionsBoxActions, + CompExceptionsBoxElements, + CompExceptionsBoxEvents, + CompExceptionsBoxProps, +} from './types'; +import {updateExceptionsBox} from './update'; + +@Model('exceptions-box', renderExceptionsBox, updateExceptionsBox) +export class CompExceptionsBox extends BaseComp< + AiMsg, + CompExceptionsBoxProps, + CompExceptionsBoxElements, + CompExceptionsBoxEvents, + CompExceptionsBoxActions +> { + constructor( + context: ControllerContext, + props: CompExceptionsBoxProps, + ) { + super(context, props); + } + + public destroy() { + super.destroy(); + } + + public showAlert(type: ExceptionType, message: string) { + this.executeDomAction('displayException', message); + } +} diff --git a/packages/js/core/src/logic/miscellaneous/exceptions-box/render.ts b/packages/js/core/src/logic/miscellaneous/exceptions-box/render.ts new file mode 100644 index 00000000..cb778243 --- /dev/null +++ b/packages/js/core/src/logic/miscellaneous/exceptions-box/render.ts @@ -0,0 +1,43 @@ +import { + createExceptionsBoxController, + ExceptionsBoxController, +} from '../../../../../../shared/src/ui/ExceptionsBox/control'; +import {createExceptionsBoxDom} from '../../../../../../shared/src/ui/ExceptionsBox/create'; +import {CompRenderer} from '../../../types/comp'; +import { + CompExceptionsBoxActions, + CompExceptionsBoxElements, + CompExceptionsBoxEvents, + CompExceptionsBoxProps, +} from './types'; + +export const renderExceptionsBox: CompRenderer< + CompExceptionsBoxProps, + CompExceptionsBoxElements, + CompExceptionsBoxEvents, + CompExceptionsBoxActions +> = ({ + props, + appendToRoot, +}) => { + const exceptionsBoxRoot = createExceptionsBoxDom(); + appendToRoot(exceptionsBoxRoot); + + let controller: ExceptionsBoxController | undefined = createExceptionsBoxController(exceptionsBoxRoot); + + return { + elements: { + root: exceptionsBoxRoot, + }, + actions: { + displayException: (message: string) => { + controller?.displayException(message); + }, + }, + onDestroy: () => { + controller?.destroy(); + exceptionsBoxRoot.remove(); + controller = undefined; + }, + }; +}; diff --git a/packages/js/core/src/logic/miscellaneous/exceptions-box/types.ts b/packages/js/core/src/logic/miscellaneous/exceptions-box/types.ts new file mode 100644 index 00000000..32ff32b3 --- /dev/null +++ b/packages/js/core/src/logic/miscellaneous/exceptions-box/types.ts @@ -0,0 +1,11 @@ +export type CompExceptionsBoxEvents = null; + +export type CompExceptionsBoxProps = Readonly; + +export type CompExceptionsBoxElements = Readonly<{ + root: HTMLElement; +}>; + +export type CompExceptionsBoxActions = Readonly<{ + displayException: (message: string) => void; +}>; diff --git a/packages/js/core/src/logic/miscellaneous/exceptions-box/update.ts b/packages/js/core/src/logic/miscellaneous/exceptions-box/update.ts new file mode 100644 index 00000000..785074b7 --- /dev/null +++ b/packages/js/core/src/logic/miscellaneous/exceptions-box/update.ts @@ -0,0 +1,7 @@ +import {CompUpdater} from '../../../types/comp'; +import {CompExceptionsBoxActions, CompExceptionsBoxElements, CompExceptionsBoxProps} from './types'; + +export const updateExceptionsBox: CompUpdater< + CompExceptionsBoxProps, CompExceptionsBoxElements, CompExceptionsBoxActions +> = () => { +}; diff --git a/packages/js/core/src/components/miscellaneous/list/model.ts b/packages/js/core/src/logic/miscellaneous/list/model.ts similarity index 86% rename from packages/js/core/src/components/miscellaneous/list/model.ts rename to packages/js/core/src/logic/miscellaneous/list/model.ts index 8475db51..074d0d6f 100644 --- a/packages/js/core/src/components/miscellaneous/list/model.ts +++ b/packages/js/core/src/logic/miscellaneous/list/model.ts @@ -1,8 +1,8 @@ -import {BaseComp} from '../../../core/aiChat/comp/base'; -import {Model} from '../../../core/aiChat/comp/decorators'; -import {NluxError} from '../../../core/error'; +import {NluxError} from '../../../../../../shared/src/types/error'; +import {domOp} from '../../../../../../shared/src/utils/dom/domOp'; +import {BaseComp} from '../../../exports/aiChat/comp/base'; +import {Model} from '../../../exports/aiChat/comp/decorators'; import {ControllerContext} from '../../../types/controllerContext'; -import {domOp} from '../../../x/domOp'; import {renderList} from './render'; import {CompListElements, CompListEvents, CompListProps} from './types'; import {updateList} from './update'; @@ -13,14 +13,14 @@ type ItemToRender = { } @Model('list', renderList, updateList) -export class CompList> - extends BaseComp { +export class CompList> + extends BaseComp { private componentsById: Map = new Map(); private componentsToRender: Array> = []; private renderedComponents: Array = []; - constructor(context: ControllerContext, props: CompListProps) { + constructor(context: ControllerContext, props: CompListProps) { super(context, props); } @@ -99,14 +99,6 @@ export class CompList> this.renderedComponents.forEach(callback); } - public getComponentAt(index: number): CompType | undefined { - if (index < 0 || index >= this.renderedComponents.length) { - return; - } - - return this.renderedComponents[index]; - } - public getComponentById(id: string): CompType | undefined { for (const component of this.renderedComponents) { if (component.id === id) { @@ -115,15 +107,6 @@ export class CompList> } } - public removeComponent(component: BaseComp) { - for (let i = 0; i < this.renderedComponents.length; i++) { - if (this.renderedComponents[i] === component) { - this.removeComponentAt(i); - return; - } - } - } - public removeComponentAt(index: number) { if (index < 0 || index >= this.renderedComponents.length) { return; diff --git a/packages/js/core/src/components/miscellaneous/list/render.ts b/packages/js/core/src/logic/miscellaneous/list/render.ts similarity index 100% rename from packages/js/core/src/components/miscellaneous/list/render.ts rename to packages/js/core/src/logic/miscellaneous/list/render.ts diff --git a/packages/js/core/src/components/miscellaneous/list/types.ts b/packages/js/core/src/logic/miscellaneous/list/types.ts similarity index 100% rename from packages/js/core/src/components/miscellaneous/list/types.ts rename to packages/js/core/src/logic/miscellaneous/list/types.ts diff --git a/packages/js/core/src/components/miscellaneous/list/update.ts b/packages/js/core/src/logic/miscellaneous/list/update.ts similarity index 100% rename from packages/js/core/src/components/miscellaneous/list/update.ts rename to packages/js/core/src/logic/miscellaneous/list/update.ts diff --git a/packages/js/core/src/types/aiChat/aiChat.ts b/packages/js/core/src/types/aiChat/aiChat.ts index 9666deab..31f8980b 100644 --- a/packages/js/core/src/types/aiChat/aiChat.ts +++ b/packages/js/core/src/types/aiChat/aiChat.ts @@ -1,10 +1,10 @@ -import {HighlighterExtension} from '../../core/aiChat/highlighter/highlighter'; -import {ConversationOptions} from '../../core/aiChat/options/conversationOptions'; -import {LayoutOptions} from '../../core/aiChat/options/layoutOptions'; -import {PersonaOptions} from '../../core/aiChat/options/personaOptions'; -import {PromptBoxOptions} from '../../core/aiChat/options/promptBoxOptions'; -import {ChatAdapterBuilder} from '../adapters/chat/chatAdapterBuilder'; -import {ConversationItem} from '../conversation'; +import {ChatAdapterBuilder} from '../../../../../shared/src/types/adapters/chat/chatAdapterBuilder'; +import {ChatItem} from '../../../../../shared/src/types/conversation'; +import {HighlighterExtension} from '../../exports/aiChat/highlighter/highlighter'; +import {ConversationOptions} from '../../exports/aiChat/options/conversationOptions'; +import {LayoutOptions} from '../../exports/aiChat/options/layoutOptions'; +import {PersonaOptions} from '../../exports/aiChat/options/personaOptions'; +import {PromptBoxOptions} from '../../exports/aiChat/options/promptBoxOptions'; import {EventCallback, EventName, EventsMap} from '../event'; import {AiChatProps} from './props'; @@ -12,7 +12,7 @@ import {AiChatProps} from './props'; * The main interface representing AiChat component. * It provides methods to instantiate, mount, and unmount the component, and listen to its events. */ -export interface IAiChat { +export interface IAiChat { /** * Hides the chat component. * This does not unmount the component. It will only hide the chat component from the view. @@ -39,7 +39,7 @@ export interface IAiChat { * @param {EventsMap[EventName]} callback The callback to be called, that should match the event type. * @returns {IAiChat} */ - on(event: EventName, callback: EventsMap[EventName]): IAiChat; + on(event: EventName, callback: EventsMap[EventName]): IAiChat; /** * Removes all event listeners from the chat component. @@ -56,7 +56,7 @@ export interface IAiChat { * @param {EventName} event The name of the event to remove the listener from. * @param {EventsMap[EventName]} callback The callback to be removed. */ - removeEventListener(event: EventName, callback: EventCallback): void; + removeEventListener(event: EventName, callback: EventCallback): void; /** * Shows the chat component. @@ -77,7 +77,7 @@ export interface IAiChat { * * @param {Partial} props The properties to be updated. */ - updateProps(props: Partial): void; + updateProps(props: Partial>): void; /** * Enabled providing an adapter to the chat component. @@ -86,7 +86,7 @@ export interface IAiChat { * * @param {ChatAdapterBuilder} adapterBuilder The builder for the chat adapter. **/ - withAdapter(adapterBuilder: ChatAdapterBuilder): IAiChat; + withAdapter(adapterBuilder: ChatAdapterBuilder): IAiChat; /** * Enables providing a class name to the chat component. @@ -95,7 +95,7 @@ export interface IAiChat { * * @param {string} className The class name to be added to the chat component. */ - withClassName(className: string): IAiChat; + withClassName(className: string): IAiChat; /** * Enables providing conversation options to the chat component. @@ -104,17 +104,17 @@ export interface IAiChat { * * @param {ConversationOptions} conversationOptions The conversation options to be used. */ - withConversationOptions(conversationOptions: ConversationOptions): IAiChat; + withConversationOptions(conversationOptions: ConversationOptions): IAiChat; /** * Enables providing an initial conversation to the chat component. * The initial conversation will be used to populate the chat component with a conversation history. * This method can be called before mounting the chat component, and it can be called only once. * - * @param {ConversationItem[]} initialConversation + * @param {ChatItem[]} initialConversation * @returns {IAiChat} */ - withInitialConversation(initialConversation: ConversationItem[]): IAiChat; + withInitialConversation(initialConversation: ChatItem[]): IAiChat; /** * Enables providing layout options to the chat component. The layout options will be used to configure the @@ -123,7 +123,7 @@ export interface IAiChat { * * @param {LayoutOptions} layoutOptions The layout options to be used. */ - withLayoutOptions(layoutOptions: LayoutOptions): IAiChat; + withLayoutOptions(layoutOptions: LayoutOptions): IAiChat; /** * Enables providing persona options to the chat component. The persona options will be used to configure @@ -132,7 +132,7 @@ export interface IAiChat { * * @param {PersonaOptions} personaOptions The persona options to be used. */ - withPersonaOptions(personaOptions: PersonaOptions): IAiChat; + withPersonaOptions(personaOptions: PersonaOptions): IAiChat; /** * Enables providing prompt box options to the chat component. @@ -140,7 +140,7 @@ export interface IAiChat { * * @param {PromptBoxOptions} promptBoxOptions The prompt box options to be used. */ - withPromptBoxOptions(promptBoxOptions: PromptBoxOptions): IAiChat; + withPromptBoxOptions(promptBoxOptions: PromptBoxOptions): IAiChat; /** * Enables providing a syntax highlighter to the chat component. @@ -148,7 +148,7 @@ export interface IAiChat { * * @param {HighlighterExtension} syntaxHighlighter The syntax highlighter to be used. */ - withSyntaxHighlighter(syntaxHighlighter: HighlighterExtension): IAiChat; + withSyntaxHighlighter(syntaxHighlighter: HighlighterExtension): IAiChat; /** * Enables providing a theme to the chat component. @@ -156,5 +156,5 @@ export interface IAiChat { * * @param {string} themeId The id of the theme to be used. */ - withTheme(themeId: string): IAiChat; + withThemeId(themeId: string): IAiChat; } diff --git a/packages/js/core/src/types/aiChat/props.ts b/packages/js/core/src/types/aiChat/props.ts index 51259704..fd64c222 100644 --- a/packages/js/core/src/types/aiChat/props.ts +++ b/packages/js/core/src/types/aiChat/props.ts @@ -1,22 +1,22 @@ -import {HighlighterExtension} from '../../core/aiChat/highlighter/highlighter'; -import {ConversationOptions} from '../../core/aiChat/options/conversationOptions'; -import {LayoutOptions} from '../../core/aiChat/options/layoutOptions'; -import {PersonaOptions} from '../../core/aiChat/options/personaOptions'; -import {PromptBoxOptions} from '../../core/aiChat/options/promptBoxOptions'; -import {ChatAdapter} from '../adapters/chat/chatAdapter'; -import {StandardChatAdapter} from '../adapters/chat/standardChatAdapter'; -import {ConversationItem} from '../conversation'; +import {ChatAdapter} from '../../../../../shared/src/types/adapters/chat/chatAdapter'; +import {StandardChatAdapter} from '../../../../../shared/src/types/adapters/chat/standardChatAdapter'; +import {ChatItem} from '../../../../../shared/src/types/conversation'; +import {HighlighterExtension} from '../../exports/aiChat/highlighter/highlighter'; +import {ConversationOptions} from '../../exports/aiChat/options/conversationOptions'; +import {LayoutOptions} from '../../exports/aiChat/options/layoutOptions'; +import {PersonaOptions} from '../../exports/aiChat/options/personaOptions'; +import {PromptBoxOptions} from '../../exports/aiChat/options/promptBoxOptions'; import {EventsMap} from '../event'; /** * These are the props that are used internally by the AiChat component. */ -export type AiChatInternalProps = { - adapter: ChatAdapter | StandardChatAdapter; - events?: Partial; +export type AiChatInternalProps = { + adapter: ChatAdapter | StandardChatAdapter; + events?: Partial>; themeId?: string; className?: string; - initialConversation?: ConversationItem[]; + initialConversation?: ChatItem[]; promptBoxOptions: PromptBoxOptions; conversationOptions: ConversationOptions; personaOptions: PersonaOptions; @@ -31,9 +31,9 @@ export type AiChatInternalProps = { * * It excludes properties that are used for initialization such as `initialConversation`. */ -export type AiChatProps = Readonly<{ - adapter: ChatAdapter | StandardChatAdapter; - events?: Partial; +export type AiChatProps = Readonly<{ + adapter: ChatAdapter | StandardChatAdapter; + events?: Partial>; themeId?: string; className?: string; promptBoxOptions?: Readonly; diff --git a/packages/js/core/src/types/comp.ts b/packages/js/core/src/types/comp.ts index 4dd80d16..daa63fb8 100644 --- a/packages/js/core/src/types/comp.ts +++ b/packages/js/core/src/types/comp.ts @@ -1,4 +1,4 @@ -import {BaseComp} from '../core/aiChat/comp/base'; +import {BaseComp} from '../exports/aiChat/comp/base'; import {ControllerContext} from './controllerContext'; /** @@ -28,7 +28,7 @@ export type CompRenderer void, compEvent: (eventName: EventsType) => Function, props: Readonly, - context: ControllerContext, + context: ControllerContext, }) => CompDom; /** @@ -39,7 +39,8 @@ export type CompRenderer = (params: { propName: keyof PropsType, - newValue: PropsType[keyof PropsType] | undefined | null, + currentValue: PropsType[keyof PropsType], + newValue: PropsType[keyof PropsType], dom: { root: HTMLElement | DocumentFragment, elements?: ElementsType, diff --git a/packages/js/core/src/types/controllerContext.ts b/packages/js/core/src/types/controllerContext.ts index 9f76d4d7..1d0b15fb 100644 --- a/packages/js/core/src/types/controllerContext.ts +++ b/packages/js/core/src/types/controllerContext.ts @@ -1,22 +1,27 @@ -import {HighlighterExtension} from '../core/aiChat/highlighter/highlighter'; -import {ExceptionId} from '../exceptions/exceptions'; -import {ChatAdapter} from './adapters/chat/chatAdapter'; -import {StandardChatAdapter} from './adapters/chat/standardChatAdapter'; +import {ChatAdapter} from '../../../../shared/src/types/adapters/chat/chatAdapter'; +import {StandardChatAdapter} from '../../../../shared/src/types/adapters/chat/standardChatAdapter'; +import {ExceptionId} from '../../../../shared/src/types/exceptions'; +import {HighlighterExtension} from '../exports/aiChat/highlighter/highlighter'; import {AiChatProps} from './aiChat/props'; import {EventName, EventsMap} from './event'; -export type ControllerContextProps = Readonly<{ +export type ControllerContextProps = Readonly<{ instanceId: string; exception: (exceptionId: ExceptionId) => void; - adapter: ChatAdapter | StandardChatAdapter; + adapter: ChatAdapter | StandardChatAdapter; syntaxHighlighter?: HighlighterExtension; }>; /** * Internal context specific to the controller. */ -export type ControllerContext = ControllerContextProps & { - update: (props: Partial) => void; - emit: (eventName: EventToEmit, ...params: Parameters) => void; - get aiChatProps(): Readonly; +export type ControllerContext = ControllerContextProps & { + update: ( + props: Partial>, + ) => void; + emit: ( + eventName: EventToEmit, + ...params: Parameters[EventToEmit]> + ) => void; + get aiChatProps(): Readonly>; }; diff --git a/packages/js/core/src/types/conversation.ts b/packages/js/core/src/types/conversation.ts deleted file mode 100644 index f34a7f1c..00000000 --- a/packages/js/core/src/types/conversation.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {ParticipantRole} from './participant'; - -export type ConversationItem = { - role: ParticipantRole; - message: string; -} diff --git a/packages/js/core/src/types/event.ts b/packages/js/core/src/types/event.ts index b6882382..0ada5511 100644 --- a/packages/js/core/src/types/event.ts +++ b/packages/js/core/src/types/event.ts @@ -1,19 +1,20 @@ -import {ExceptionId} from '../exceptions/exceptions'; +import {AnyAiMsg} from '../../../../shared/src/types/anyAiMsg'; +import {ChatItem} from '../../../../shared/src/types/conversation'; +import {ExceptionId} from '../../../../shared/src/types/exceptions'; import {AiChatProps} from './aiChat/props'; -import {ConversationItem} from './conversation'; export type ErrorEventDetails = { errorId: ExceptionId; message: string; }; -export type ReadyEventDetails = { - aiChatProps: AiChatProps; +export type ReadyEventDetails = { + aiChatProps: AiChatProps; } -export type PreDestroyEventDetails = { - aiChatProps: AiChatProps; - conversationHistory: Readonly; +export type PreDestroyEventDetails = { + aiChatProps: AiChatProps; + conversationHistory: Readonly[]>; } /** @@ -29,7 +30,7 @@ export type ErrorCallback = (errorDetails: ErrorEventDetails) => void; * * @param message The message that was received. */ -export type MessageReceivedCallback = (message: string) => void; +export type MessageReceivedCallback = (message: AiMsg) => void; /** * The callback for when a message is sent. @@ -45,7 +46,7 @@ export type MessageSentCallback = (message: string) => void; * * @param readyDetails The details of the ready event such as the AiChatProps used to initialize the chat component. */ -export type ReadyCallback = (readyDetails: ReadyEventDetails) => void; +export type ReadyCallback = (readyDetails: ReadyEventDetails) => void; /** * The callback for when the chat component is about to be destroyed. @@ -54,16 +55,16 @@ export type ReadyCallback = (readyDetails: ReadyEventDetails) => void; * @param preDestroyDetails The details of the pre-destroy event such as the AiChatProps used to initialize the chat * component and the conversation history. */ -export type PreDestroyCallback = (preDestroyDetails: PreDestroyEventDetails) => void; +export type PreDestroyCallback = (preDestroyDetails: PreDestroyEventDetails) => void; -export type EventsMap = { - ready: ReadyCallback; - preDestroy: PreDestroyCallback; +export type EventsMap = { + ready: ReadyCallback; + preDestroy: PreDestroyCallback; messageSent: MessageSentCallback; - messageReceived: MessageReceivedCallback; + messageReceived: MessageReceivedCallback; error: ErrorCallback; }; -export type EventName = keyof EventsMap; +export type EventName = keyof EventsMap; -export type EventCallback = EventsMap[EventName]; +export type EventCallback = EventsMap[EventName]; diff --git a/packages/js/core/src/types/exception.ts b/packages/js/core/src/types/exception.ts deleted file mode 100644 index 7ee2ade7..00000000 --- a/packages/js/core/src/types/exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type ExceptionType = 'error' | 'warning'; - -export type Exception = { - type: ExceptionType; - message: string; -}; diff --git a/packages/js/core/src/utils/adapters/isContextTasksAdapter.ts b/packages/js/core/src/utils/adapters/isContextTasksAdapter.ts index 178b74d1..df21733d 100644 --- a/packages/js/core/src/utils/adapters/isContextTasksAdapter.ts +++ b/packages/js/core/src/utils/adapters/isContextTasksAdapter.ts @@ -1,4 +1,4 @@ -import {ContextTasksAdapter} from '../../types/adapters/context/contextTasksAdapter'; +import {ContextTasksAdapter} from '../../../../../shared/src/types/adapters/context/contextTasksAdapter'; export const isContextTasksAdapter = (adapter: any): ContextTasksAdapter | false => { if ( diff --git a/packages/js/core/src/utils/dom/getElement.ts b/packages/js/core/src/utils/dom/getElement.ts index 4e520543..57090440 100644 --- a/packages/js/core/src/utils/dom/getElement.ts +++ b/packages/js/core/src/utils/dom/getElement.ts @@ -1,4 +1,4 @@ -import {NluxRenderingError, NluxUsageError} from '../../core/error'; +import {NluxRenderingError, NluxUsageError} from '../../../../../shared/src/types/error'; const source = 'dom/getElement'; diff --git a/packages/js/core/src/utils/dom/listenToElement.ts b/packages/js/core/src/utils/dom/listenToElement.ts index 8a80e55f..2fa4ac59 100644 --- a/packages/js/core/src/utils/dom/listenToElement.ts +++ b/packages/js/core/src/utils/dom/listenToElement.ts @@ -1,4 +1,4 @@ -import {NluxRenderingError} from '../../core/error'; +import {NluxRenderingError} from '../../../../../shared/src/types/error'; const source = 'dom/listenTo'; diff --git a/packages/js/core/src/x/parseTextMessage.ts b/packages/js/core/src/utils/parseTextMessage.ts similarity index 100% rename from packages/js/core/src/x/parseTextMessage.ts rename to packages/js/core/src/utils/parseTextMessage.ts diff --git a/packages/js/core/src/x/source.ts b/packages/js/core/src/utils/source.ts similarity index 100% rename from packages/js/core/src/x/source.ts rename to packages/js/core/src/utils/source.ts diff --git a/packages/js/core/src/x/throttle.ts b/packages/js/core/src/utils/throttle.ts similarity index 90% rename from packages/js/core/src/x/throttle.ts rename to packages/js/core/src/utils/throttle.ts index 2fcd739b..135bf972 100644 --- a/packages/js/core/src/x/throttle.ts +++ b/packages/js/core/src/utils/throttle.ts @@ -1,4 +1,4 @@ -import {NluxError} from '../core/error'; +import {NluxError} from '../../../../shared/src/types/error'; export const throttle = (callback: CallbackType, limitInMilliseconds: number) => { let waiting = false; diff --git a/packages/js/core/src/x/character/isWhitespace.ts b/packages/js/core/src/x/character/isWhitespace.ts deleted file mode 100644 index cc6e9a17..00000000 --- a/packages/js/core/src/x/character/isWhitespace.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const isWhitespace = (character: string): boolean => { - return /^[ \t]{1}$/.test(character); -}; diff --git a/packages/js/core/src/x/formatShortDateTime.ts b/packages/js/core/src/x/formatShortDateTime.ts deleted file mode 100644 index ce1a6b52..00000000 --- a/packages/js/core/src/x/formatShortDateTime.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const formatShortDateTime = (date: Date) => { - const now = new Date(); - const nowYear = now.getFullYear(); - const nowMonth = now.getMonth(); - const nowDate = now.getDate(); - const nowHours = now.getHours(); - const nowMinutes = now.getMinutes(); - const nowSeconds = now.getSeconds(); - const year = date.getFullYear(); - const month = date.getMonth(); - const day = date.getDate(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - const seconds = date.getSeconds(); - if (year === nowYear && month === nowMonth && day === nowDate) { - if (hours === nowHours && minutes === nowMinutes && seconds === nowSeconds) { - return 'now'; - } else { - if (hours === nowHours && minutes === nowMinutes) { - return `${seconds - nowSeconds}s ago`; - } else { - if (hours === nowHours) { - return `${minutes - nowMinutes}m ago`; - } else { - return `${hours - nowHours}h ago`; - } - } - } - } else { - return `${year}-${month}-${day}`; - } -}; diff --git a/packages/js/core/src/x/toKebabCase.ts b/packages/js/core/src/x/toKebabCase.ts deleted file mode 100644 index f0e1182a..00000000 --- a/packages/js/core/src/x/toKebabCase.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const toKebabCase = (str: string): string => { - return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); -}; diff --git a/packages/js/hf/src/createChatAdapter.ts b/packages/js/hf/src/createChatAdapter.ts index 2565740b..428eb1c6 100644 --- a/packages/js/hf/src/createChatAdapter.ts +++ b/packages/js/hf/src/createChatAdapter.ts @@ -1,4 +1,4 @@ import {ChatAdapterBuilder} from './hf/builder/builder'; import {ChatAdapterBuilderImpl} from './hf/builder/builderImpl'; -export const createChatAdapter = (): ChatAdapterBuilder => new ChatAdapterBuilderImpl(); +export const createChatAdapter = (): ChatAdapterBuilder => new ChatAdapterBuilderImpl(); diff --git a/packages/js/hf/src/hf/adapter/chatAdapter.ts b/packages/js/hf/src/hf/adapter/chatAdapter.ts index 29aab608..d6222b9f 100644 --- a/packages/js/hf/src/hf/adapter/chatAdapter.ts +++ b/packages/js/hf/src/hf/adapter/chatAdapter.ts @@ -1,27 +1,21 @@ -import {HfInference, TextGenerationOutput, TextGenerationStreamOutput} from '@huggingface/inference'; -import { - DataTransferMode, - NluxError, - NluxValidationError, - StandardAdapterInfo, - StandardChatAdapter, - StreamingAdapterObserver, - uid, - warn, -} from '@nlux/core'; -import {adapterErrorToExceptionId} from '../../x/adapterErrorToExceptionId'; +import {HfInference, TextGenerationStreamOutput} from '@huggingface/inference'; +import {DataTransferMode, StandardAdapterInfo, StandardChatAdapter, StreamingAdapterObserver} from '@nlux/core'; +import {NluxError, NluxValidationError} from '../../../../../shared/src/types/error'; +import {uid} from '../../../../../shared/src/utils/uid'; +import {warn} from '../../../../../shared/src/utils/warn'; +import {adapterErrorToExceptionId} from '../../utils/adapterErrorToExceptionId'; import {ChatAdapterOptions} from '../types/chatAdapterOptions'; -export class HfChatAdapterImpl implements StandardChatAdapter { +export class HfChatAdapterImpl implements StandardChatAdapter { static defaultDataTransferMode: DataTransferMode = 'fetch'; static defaultMaxNewTokens = 500; private readonly __instanceId: string; private inference: HfInference; - private readonly options: ChatAdapterOptions; + private readonly options: ChatAdapterOptions; - constructor(options: ChatAdapterOptions) { + constructor(options: ChatAdapterOptions) { if (!options.model && !options.endpoint) { throw new NluxValidationError({ source: this.constructor.name, @@ -56,7 +50,7 @@ export class HfChatAdapterImpl implements StandardChatAdapter { }; } - async fetchText(message: string): Promise { + async fetchText(message: string): Promise { if (!this.options.model && !this.options.endpoint) { throw new NluxValidationError({ source: this.constructor.name, @@ -72,7 +66,7 @@ export class HfChatAdapterImpl implements StandardChatAdapter { }, }; - let output: TextGenerationOutput | undefined = undefined; + let output: any = undefined; try { if (this.options.endpoint) { @@ -95,8 +89,8 @@ export class HfChatAdapterImpl implements StandardChatAdapter { return await this.decode(output); } - send(message: string, observer?: StreamingAdapterObserver): void | Promise { - const promise = new Promise(async (resolve, reject) => { + send(message: string, observer?: StreamingAdapterObserver): void | Promise { + const promise = new Promise(async (resolve, reject) => { if (!message) { throw new NluxValidationError({ source: this.constructor.name, @@ -153,7 +147,7 @@ export class HfChatAdapterImpl implements StandardChatAdapter { }, }; - let output: AsyncGenerator | undefined = undefined; + let output: AsyncGenerator | undefined = undefined; try { if (this.options.endpoint) { @@ -173,7 +167,9 @@ export class HfChatAdapterImpl implements StandardChatAdapter { break; } - observer.next(await this.decode(value.token)); + observer.next( + await this.decode(value.token) as any, + ); } observer.complete(); @@ -187,7 +183,7 @@ export class HfChatAdapterImpl implements StandardChatAdapter { }); } - private async decode(payload: any): Promise { + private async decode(payload: any): Promise { const output = (() => { if (typeof payload === 'string') { return payload; diff --git a/packages/js/hf/src/hf/builder/builder.ts b/packages/js/hf/src/hf/builder/builder.ts index 7c6ccd7b..85c76d25 100644 --- a/packages/js/hf/src/hf/builder/builder.ts +++ b/packages/js/hf/src/hf/builder/builder.ts @@ -2,7 +2,7 @@ import {ChatAdapterBuilder as CoreChatAdapterBuilder, DataTransferMode, Standard import {HfInputPreProcessor} from '../types/inputPreProcessor'; import {HfOutputPreProcessor} from '../types/outputPreProcessor'; -export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { +export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { /** * Create a new Hugging Face Inference API adapter. * Adapter users don't need to call this method directly. It will be called by nlux when the adapter is expected @@ -10,7 +10,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * * @returns {StandardChatAdapter} */ - create(): StandardChatAdapter; + create(): StandardChatAdapter; /** * The authorization token to use for Hugging Face Inference API. @@ -24,7 +24,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} authToken * @returns {ChatAdapterBuilder} */ - withAuthToken(authToken: string): ChatAdapterBuilder; + withAuthToken(authToken: string): ChatAdapterBuilder; /** * Instruct the adapter to connect to API and load data either in streaming mode or in fetch mode. @@ -36,7 +36,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @default 'stream' * @returns {ChatAdapterBuilder} */ - withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder; + withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder; /** * The endpoint to use for Hugging Face Inference API. @@ -48,7 +48,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} endpoint * @returns {ChatAdapterBuilder} */ - withEndpoint(endpoint: string): ChatAdapterBuilder; + withEndpoint(endpoint: string): ChatAdapterBuilder; /** * This function will be called before sending the input to the Hugging Face Inference API. @@ -59,7 +59,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {(input: any) => any} inputPreProcessor * @returns {ChatAdapterBuilder} */ - withInputPreProcessor(inputPreProcessor: HfInputPreProcessor): ChatAdapterBuilder; + withInputPreProcessor(inputPreProcessor: HfInputPreProcessor): ChatAdapterBuilder; /** * The maximum number of new tokens that can be generated by the Hugging Face Inference API. @@ -70,7 +70,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {number} maxNewTokens * @returns {ChatAdapterBuilder} */ - withMaxNewTokens(maxNewTokens: number): ChatAdapterBuilder; + withMaxNewTokens(maxNewTokens: number): ChatAdapterBuilder; /** * The model or the endpoint to use for Hugging Face Inference API. @@ -81,7 +81,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} model * @returns {ChatAdapterBuilder} */ - withModel(model: string): ChatAdapterBuilder; + withModel(model: string): ChatAdapterBuilder; /** * This function will be called after receiving the output from the Hugging Face Inference API, and before @@ -92,7 +92,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {HfOutputPreProcessor} outputPreProcessor * @returns {ChatAdapterBuilder} */ - withOutputPreProcessor(outputPreProcessor: HfOutputPreProcessor): ChatAdapterBuilder; + withOutputPreProcessor(outputPreProcessor: HfOutputPreProcessor): ChatAdapterBuilder; /** * The initial system to send to the Hugging Face Inference API. @@ -102,5 +102,5 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} message * @returns {ChatAdapterBuilder} */ - withSystemMessage(message: string): ChatAdapterBuilder; + withSystemMessage(message: string): ChatAdapterBuilder; } diff --git a/packages/js/hf/src/hf/builder/builderImpl.ts b/packages/js/hf/src/hf/builder/builderImpl.ts index bd3cf263..f1ef9948 100644 --- a/packages/js/hf/src/hf/builder/builderImpl.ts +++ b/packages/js/hf/src/hf/builder/builderImpl.ts @@ -1,22 +1,23 @@ -import {DataTransferMode, NluxUsageError, NluxValidationError} from '@nlux/core'; +import {DataTransferMode} from '@nlux/core'; +import {NluxUsageError, NluxValidationError} from '../../../../../shared/src/types/error'; import {HfChatAdapterImpl} from '../adapter/chatAdapter'; import {HfInputPreProcessor} from '../types/inputPreProcessor'; import {HfOutputPreProcessor} from '../types/outputPreProcessor'; import {ChatAdapterBuilder} from './builder'; -export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { +export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { private theAuthToken: string | null = null; private theDataTransferMode: DataTransferMode = 'stream'; private theEndpoint: string | null = null; - private theInputPreProcessor: HfInputPreProcessor | null = null; + private theInputPreProcessor: HfInputPreProcessor | null = null; private theMaxNewTokens: number | null = null; private theModel: string | null = null; - private theOutputPreProcessor: HfOutputPreProcessor | null = null; + private theOutputPreProcessor: HfOutputPreProcessor | null = null; private theSystemMessage: string | null = null; private withDataTransferModeCalled = false; - create(): HfChatAdapterImpl { + create(): HfChatAdapterImpl { if (!this.theModel && !this.theEndpoint) { throw new NluxValidationError({ source: this.constructor.name, @@ -39,7 +40,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { }); } - withAuthToken(authToken: string): ChatAdapterBuilder { + withAuthToken(authToken: string): ChatAdapterBuilder { if (this.theAuthToken !== null) { throw new NluxUsageError({ source: this.constructor.name, @@ -51,7 +52,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder { + withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder { if (this.withDataTransferModeCalled) { throw new NluxUsageError({ source: this.constructor.name, @@ -64,7 +65,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withEndpoint(endpoint: string): ChatAdapterBuilder { + withEndpoint(endpoint: string): ChatAdapterBuilder { if (this.theEndpoint !== null) { throw new NluxUsageError({ source: this.constructor.name, @@ -76,7 +77,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withInputPreProcessor(inputPreProcessor: HfInputPreProcessor): ChatAdapterBuilder { + withInputPreProcessor(inputPreProcessor: HfInputPreProcessor): ChatAdapterBuilder { if (this.theInputPreProcessor !== null) { throw new NluxUsageError({ source: this.constructor.name, @@ -88,7 +89,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withMaxNewTokens(maxNewTokens: number): ChatAdapterBuilder { + withMaxNewTokens(maxNewTokens: number): ChatAdapterBuilder { if (this.theMaxNewTokens !== null) { throw new NluxUsageError({ source: this.constructor.name, @@ -112,7 +113,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withOutputPreProcessor(outputPreProcessor: HfOutputPreProcessor): ChatAdapterBuilder { + withOutputPreProcessor(outputPreProcessor: HfOutputPreProcessor): ChatAdapterBuilder { if (this.theOutputPreProcessor !== null) { throw new NluxUsageError({ source: this.constructor.name, @@ -124,7 +125,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withSystemMessage(message: string): ChatAdapterBuilder { + withSystemMessage(message: string): ChatAdapterBuilder { if (this.theSystemMessage !== null) { throw new NluxUsageError({ source: this.constructor.name, @@ -135,5 +136,4 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { this.theSystemMessage = message; return this; } -}; - +} diff --git a/packages/js/hf/src/hf/preProcessors/llama2.ts b/packages/js/hf/src/hf/preProcessors/llama2.ts index 24cb7312..acac96fc 100644 --- a/packages/js/hf/src/hf/preProcessors/llama2.ts +++ b/packages/js/hf/src/hf/preProcessors/llama2.ts @@ -1,7 +1,7 @@ import {HfInputPreProcessor} from '../types/inputPreProcessor'; import {HfOutputPreProcessor} from '../types/outputPreProcessor'; -export const llama2InputPreProcessor: HfInputPreProcessor = ( +export const llama2InputPreProcessor: HfInputPreProcessor = ( input, adapterOptions, ) => { @@ -14,7 +14,7 @@ export const llama2InputPreProcessor: HfInputPreProcessor = ( ); }; -export const llama2OutputPreProcessor: HfOutputPreProcessor = ( +export const llama2OutputPreProcessor: HfOutputPreProcessor = ( output, ) => { // Strip any HTML-like tags from the output diff --git a/packages/js/hf/src/hf/types/chatAdapterOptions.ts b/packages/js/hf/src/hf/types/chatAdapterOptions.ts index 9801b990..6237c9e4 100644 --- a/packages/js/hf/src/hf/types/chatAdapterOptions.ts +++ b/packages/js/hf/src/hf/types/chatAdapterOptions.ts @@ -2,14 +2,14 @@ import {DataTransferMode} from '@nlux/core'; import {HfInputPreProcessor} from './inputPreProcessor'; import {HfOutputPreProcessor} from './outputPreProcessor'; -export type ChatAdapterOptions = { +export type ChatAdapterOptions = { dataTransferMode?: DataTransferMode; model?: string; endpoint?: string; authToken?: string; preProcessors?: { - input?: HfInputPreProcessor; - output?: HfOutputPreProcessor; + input?: HfInputPreProcessor; + output?: HfOutputPreProcessor; }; maxNewTokens?: number; systemMessage?: string; diff --git a/packages/js/hf/src/hf/types/inputPreProcessor.ts b/packages/js/hf/src/hf/types/inputPreProcessor.ts index 11d7fbfc..757d3bfe 100644 --- a/packages/js/hf/src/hf/types/inputPreProcessor.ts +++ b/packages/js/hf/src/hf/types/inputPreProcessor.ts @@ -1,6 +1,6 @@ import {ChatAdapterOptions} from './chatAdapterOptions'; -export type HfInputPreProcessor = ( +export type HfInputPreProcessor = ( input: string, - adapterOptions: Readonly, + adapterOptions: Readonly>, ) => string; diff --git a/packages/js/hf/src/hf/types/outputPreProcessor.ts b/packages/js/hf/src/hf/types/outputPreProcessor.ts index b885097d..1c95e016 100644 --- a/packages/js/hf/src/hf/types/outputPreProcessor.ts +++ b/packages/js/hf/src/hf/types/outputPreProcessor.ts @@ -1 +1 @@ -export type HfOutputPreProcessor = (output: string) => string; +export type HfOutputPreProcessor = (output: string) => AiMsg; diff --git a/packages/js/hf/src/index.ts b/packages/js/hf/src/index.ts index 275b66e1..975c56be 100644 --- a/packages/js/hf/src/index.ts +++ b/packages/js/hf/src/index.ts @@ -5,10 +5,6 @@ export type { DataTransferMode, } from '@nlux/core'; -export { - debug, -} from '@nlux/core'; - export type { ChatAdapterOptions, } from './hf/types/chatAdapterOptions'; diff --git a/packages/js/hf/src/x/adapterErrorToExceptionId.ts b/packages/js/hf/src/utils/adapterErrorToExceptionId.ts similarity index 84% rename from packages/js/hf/src/x/adapterErrorToExceptionId.ts rename to packages/js/hf/src/utils/adapterErrorToExceptionId.ts index e0191029..3e9e511f 100644 --- a/packages/js/hf/src/x/adapterErrorToExceptionId.ts +++ b/packages/js/hf/src/utils/adapterErrorToExceptionId.ts @@ -1,4 +1,4 @@ -import {ExceptionId} from '@nlux/core'; +import {ExceptionId} from '../../../../shared/src/types/exceptions'; export const adapterErrorToExceptionId = (error: any): ExceptionId | null => { if (typeof error === 'object' && error !== null) { diff --git a/packages/js/hf/src/x/serverResponseToExceptionId.ts b/packages/js/hf/src/x/serverResponseToExceptionId.ts deleted file mode 100644 index 69f82e89..00000000 --- a/packages/js/hf/src/x/serverResponseToExceptionId.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {ExceptionId} from '@nlux/core'; - -export const serverResponseToExceptionId = (response: Response): ExceptionId | null => { - if (response.ok) { - return null; - } - - if (response.status >= 500 && response.status < 600) { - return 'NX-NT-003'; - } - - if (response.status >= 400 && response.status < 500) { - return 'NX-NT-004'; - } - - return 'NX-NT-001'; -}; diff --git a/packages/js/langchain/src/index.ts b/packages/js/langchain/src/index.ts index ab7806f8..5fcf8d93 100644 --- a/packages/js/langchain/src/index.ts +++ b/packages/js/langchain/src/index.ts @@ -5,8 +5,6 @@ export type { DataTransferMode, } from '@nlux/core'; -export {debug} from '@nlux/core'; - export type {ChatAdapterBuilder} from './langserve/builder/builder'; export type { diff --git a/packages/js/langchain/src/langserve/adapter/adapter.ts b/packages/js/langchain/src/langserve/adapter/adapter.ts index b10ecda8..f5b6ebda 100644 --- a/packages/js/langchain/src/langserve/adapter/adapter.ts +++ b/packages/js/langchain/src/langserve/adapter/adapter.ts @@ -1,29 +1,29 @@ import { ChatAdapterExtras, - ConversationItem, + ChatItem, DataTransferMode, StandardAdapterInfo, StandardChatAdapter, StreamingAdapterObserver, - uid, - warn, } from '@nlux/core'; +import {uid} from '../../../../../shared/src/utils/uid'; +import {warn} from '../../../../../shared/src/utils/warn'; import {ChatAdapterOptions} from '../types/adapterOptions'; -import {LangServeHeaders} from '../types/langServe'; import {LangServeInputPreProcessor} from '../types/inputPreProcessor'; +import {LangServeHeaders} from '../types/langServe'; import {LangServeOutputPreProcessor} from '../types/outputPreProcessor'; import {getDataTransferModeToUse} from '../utils/getDataTransferModeToUse'; import {getEndpointUrlToUse} from '../utils/getEndpointUrlToUse'; +import {getHeadersToUse} from '../utils/getHeadersToUse'; import {getRunnableNameToUse} from '../utils/getRunnableNameToUse'; import {getSchemaUrlToUse} from '../utils/getSchemaUrlToUse'; import {transformInputBasedOnSchema} from '../utils/transformInputBasedOnSchema'; -import { getHeadersToUse } from '../utils/getHeadersToUse'; -export abstract class LangServeAbstractAdapter implements StandardChatAdapter { +export abstract class LangServeAbstractAdapter implements StandardChatAdapter { static defaultDataTransferMode: DataTransferMode = 'stream'; private readonly __instanceId: string; - private readonly __options: ChatAdapterOptions; + private readonly __options: ChatAdapterOptions; private readonly theDataTransferModeToUse: DataTransferMode; private readonly theEndpointUrlToUse: string; @@ -33,7 +33,7 @@ export abstract class LangServeAbstractAdapter implements StandardChatAdapter { private readonly theRunnableNameToUse: string; private readonly theUseInputSchemaOptionToUse: boolean; - constructor(options: ChatAdapterOptions) { + protected constructor(options: ChatAdapterOptions) { this.__instanceId = `${this.info.id}-${uid()}`; this.__options = {...options}; @@ -78,7 +78,7 @@ export abstract class LangServeAbstractAdapter implements StandardChatAdapter { }; } - get inputPreProcessor(): LangServeInputPreProcessor | undefined { + get inputPreProcessor(): LangServeInputPreProcessor | undefined { return this.__options.inputPreProcessor; } @@ -86,7 +86,7 @@ export abstract class LangServeAbstractAdapter implements StandardChatAdapter { return this.theInputSchemaToUse; } - get outputPreProcessor(): LangServeOutputPreProcessor | undefined { + get outputPreProcessor(): LangServeOutputPreProcessor | undefined { return this.__options.outputPreProcessor; } @@ -118,7 +118,7 @@ export abstract class LangServeAbstractAdapter implements StandardChatAdapter { } } - abstract fetchText(message: string, extras: ChatAdapterExtras): Promise; + abstract fetchText(message: string, extras: ChatAdapterExtras): Promise; init() { if (!this.inputPreProcessor && this.useInputSchema) { @@ -128,33 +128,17 @@ export abstract class LangServeAbstractAdapter implements StandardChatAdapter { } } - abstract streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void; + abstract streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void; - protected getDisplayableMessageFromAiOutput(aiMessage: object | string): string | undefined { + protected getDisplayableMessageFromAiOutput(aiMessage: object | string): AiMsg { if (this.outputPreProcessor) { return this.outputPreProcessor(aiMessage); } - if (typeof aiMessage === 'string') { - return aiMessage; - } - - const messageWithContent = aiMessage as any; - if (typeof messageWithContent === 'object' && messageWithContent && typeof messageWithContent.content - === 'string') { - return messageWithContent.content; - } - - warn( - `LangServe adapter is unable to process output returned from the endpoint:\n ${JSON.stringify( - aiMessage, - )}`, - ); - - return undefined; + return aiMessage as AiMsg; } - protected getRequestBody(message: string, conversationHistory?: readonly ConversationItem[]): string { + protected getRequestBody(message: string, conversationHistory?: readonly ChatItem[]): string { if (this.inputPreProcessor) { const preProcessedInput = this.inputPreProcessor(message, conversationHistory); return JSON.stringify({ diff --git a/packages/js/langchain/src/langserve/adapter/fetch.ts b/packages/js/langchain/src/langserve/adapter/fetch.ts index 47a23751..9d5de777 100644 --- a/packages/js/langchain/src/langserve/adapter/fetch.ts +++ b/packages/js/langchain/src/langserve/adapter/fetch.ts @@ -1,18 +1,19 @@ -import {ChatAdapterExtras, NluxUsageError, StreamingAdapterObserver} from '@nlux/core'; +import {ChatAdapterExtras, StreamingAdapterObserver} from '@nlux/core'; +import {NluxUsageError} from '../../../../../shared/src/types/error'; import {LangServeAbstractAdapter} from './adapter'; -export class LangServeFetchAdapter extends LangServeAbstractAdapter { +export class LangServeFetchAdapter extends LangServeAbstractAdapter { constructor(options: any) { super(options); } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText(message: string, extras: ChatAdapterExtras): Promise { const body = this.getRequestBody(message, extras.conversationHistory); const response = await fetch(this.endpointUrl, { method: 'POST', headers: { ...this.headers, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, body, }); @@ -30,10 +31,10 @@ export class LangServeFetchAdapter extends LangServeAbstractAdapter { } const output = (typeof result === 'object' && result) ? result.output : undefined; - return this.getDisplayableMessageFromAiOutput(output) ?? ''; + return this.getDisplayableMessageFromAiOutput(output); } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot stream text from the fetch adapter!', diff --git a/packages/js/langchain/src/langserve/adapter/stream.ts b/packages/js/langchain/src/langserve/adapter/stream.ts index 6f3aa252..b4a343bb 100644 --- a/packages/js/langchain/src/langserve/adapter/stream.ts +++ b/packages/js/langchain/src/langserve/adapter/stream.ts @@ -1,27 +1,29 @@ -import {ChatAdapterExtras, NluxError, NluxUsageError, StreamingAdapterObserver, warn} from '@nlux/core'; +import {ChatAdapterExtras, StreamingAdapterObserver} from '@nlux/core'; +import {NluxError, NluxUsageError} from '../../../../../shared/src/types/error'; +import {warn} from '../../../../../shared/src/utils/warn'; import {parseChunk} from '../parser/parseChunk'; import {adapterErrorToExceptionId} from '../utils/adapterErrorToExceptionId'; import {LangServeAbstractAdapter} from './adapter'; -export class LangServeStreamAdapter extends LangServeAbstractAdapter { +export class LangServeStreamAdapter extends LangServeAbstractAdapter { constructor(options: any) { super(options); } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText(message: string, extras: ChatAdapterExtras): Promise { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot fetch text using the stream adapter!', }); } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { const body = this.getRequestBody(message, extras.conversationHistory); fetch(this.endpointUrl, { method: 'POST', headers: { ...this.headers, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, body, }) diff --git a/packages/js/langchain/src/langserve/builder/builder.ts b/packages/js/langchain/src/langserve/builder/builder.ts index 216bbde5..f651b186 100644 --- a/packages/js/langchain/src/langserve/builder/builder.ts +++ b/packages/js/langchain/src/langserve/builder/builder.ts @@ -1,14 +1,14 @@ import {ChatAdapterBuilder as CoreChatAdapterBuilder, DataTransferMode, StandardChatAdapter} from '@nlux/core'; -import {LangServeHeaders} from '../types/langServe'; import {LangServeInputPreProcessor} from '../types/inputPreProcessor'; +import {LangServeHeaders} from '../types/langServe'; import {LangServeOutputPreProcessor} from '../types/outputPreProcessor'; -export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { - create(): StandardChatAdapter; - withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder; - withHeaders(headers: LangServeHeaders): ChatAdapterBuilder; - withInputPreProcessor(inputPreProcessor: LangServeInputPreProcessor): ChatAdapterBuilder; - withInputSchema(useInputSchema: boolean): ChatAdapterBuilder; - withOutputPreProcessor(outputPreProcessor: LangServeOutputPreProcessor): ChatAdapterBuilder; - withUrl(runnableUrl: string): ChatAdapterBuilder; +export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { + create(): StandardChatAdapter; + withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder; + withHeaders(headers: LangServeHeaders): ChatAdapterBuilder; + withInputPreProcessor(inputPreProcessor: LangServeInputPreProcessor): ChatAdapterBuilder; + withInputSchema(useInputSchema: boolean): ChatAdapterBuilder; + withOutputPreProcessor(outputPreProcessor: LangServeOutputPreProcessor): ChatAdapterBuilder; + withUrl(runnableUrl: string): ChatAdapterBuilder; } diff --git a/packages/js/langchain/src/langserve/builder/builderImpl.ts b/packages/js/langchain/src/langserve/builder/builderImpl.ts index b363537f..e82b9b7a 100644 --- a/packages/js/langchain/src/langserve/builder/builderImpl.ts +++ b/packages/js/langchain/src/langserve/builder/builderImpl.ts @@ -1,23 +1,24 @@ -import {DataTransferMode, NluxUsageError} from '@nlux/core'; +import {DataTransferMode} from '@nlux/core'; +import {NluxUsageError} from '../../../../../shared/src/types/error'; import {LangServeAbstractAdapter} from '../adapter/adapter'; import {LangServeFetchAdapter} from '../adapter/fetch'; import {LangServeStreamAdapter} from '../adapter/stream'; import {ChatAdapterOptions} from '../types/adapterOptions'; -import {LangServeHeaders} from '../types/langServe'; import {LangServeInputPreProcessor} from '../types/inputPreProcessor'; +import {LangServeHeaders} from '../types/langServe'; import {LangServeOutputPreProcessor} from '../types/outputPreProcessor'; import {getDataTransferModeToUse} from '../utils/getDataTransferModeToUse'; import {ChatAdapterBuilder} from './builder'; -export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { +export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { private theDataTransferMode?: DataTransferMode; private theHeaders?: LangServeHeaders; - private theInputPreProcessor?: LangServeInputPreProcessor; - private theOutputPreProcessor?: LangServeOutputPreProcessor; + private theInputPreProcessor?: LangServeInputPreProcessor; + private theOutputPreProcessor?: LangServeOutputPreProcessor; private theUrl?: string; private theUseInputSchema?: boolean; - constructor(cloneFrom?: LangServeAdapterBuilderImpl) { + constructor(cloneFrom?: LangServeAdapterBuilderImpl) { if (cloneFrom) { this.theDataTransferMode = cloneFrom.theDataTransferMode; this.theHeaders = cloneFrom.theHeaders; @@ -27,7 +28,7 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { } } - create(): LangServeAbstractAdapter { + create(): LangServeAbstractAdapter { if (!this.theUrl) { throw new NluxUsageError({ source: this.constructor.name, @@ -36,7 +37,7 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { }); } - const options: ChatAdapterOptions = { + const options: ChatAdapterOptions = { url: this.theUrl, dataTransferMode: this.theDataTransferMode, headers: this.theHeaders, @@ -47,13 +48,13 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { const dataTransferModeToUse = getDataTransferModeToUse(options); if (dataTransferModeToUse === 'stream') { - return new LangServeStreamAdapter(options); + return new LangServeStreamAdapter(options); } return new LangServeFetchAdapter(options); } - withDataTransferMode(mode: DataTransferMode): LangServeAdapterBuilderImpl { + withDataTransferMode(mode: DataTransferMode): LangServeAdapterBuilderImpl { if (this.theDataTransferMode !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -64,8 +65,8 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { this.theDataTransferMode = mode; return this; } - - withHeaders(headers: LangServeHeaders): ChatAdapterBuilder { + + withHeaders(headers: LangServeHeaders): ChatAdapterBuilder { if (this.theHeaders !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -77,7 +78,7 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withInputPreProcessor(inputPreProcessor: LangServeInputPreProcessor): ChatAdapterBuilder { + withInputPreProcessor(inputPreProcessor: LangServeInputPreProcessor): ChatAdapterBuilder { if (this.theInputPreProcessor !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -89,7 +90,7 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withInputSchema(useInputSchema: boolean): ChatAdapterBuilder { + withInputSchema(useInputSchema: boolean): ChatAdapterBuilder { if (this.theUseInputSchema !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -101,7 +102,7 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withOutputPreProcessor(outputPreProcessor: LangServeOutputPreProcessor): ChatAdapterBuilder { + withOutputPreProcessor(outputPreProcessor: LangServeOutputPreProcessor): ChatAdapterBuilder { if (this.theOutputPreProcessor !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -113,7 +114,7 @@ export class LangServeAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withUrl(runnableUrl: string): ChatAdapterBuilder { + withUrl(runnableUrl: string): ChatAdapterBuilder { if (this.theUrl !== undefined) { throw new NluxUsageError({ source: this.constructor.name, diff --git a/packages/js/langchain/src/langserve/builder/createChatAdapter.ts b/packages/js/langchain/src/langserve/builder/createChatAdapter.ts index ca781ea9..b844bdd7 100644 --- a/packages/js/langchain/src/langserve/builder/createChatAdapter.ts +++ b/packages/js/langchain/src/langserve/builder/createChatAdapter.ts @@ -1,4 +1,6 @@ import {ChatAdapterBuilder} from './builder'; import {LangServeAdapterBuilderImpl} from './builderImpl'; -export const createChatAdapter = (): ChatAdapterBuilder => new LangServeAdapterBuilderImpl(); +export const createChatAdapter = function (): ChatAdapterBuilder { + return new LangServeAdapterBuilderImpl(); +}; diff --git a/packages/js/langchain/src/langserve/parser/parseStreamedEvent.ts b/packages/js/langchain/src/langserve/parser/parseStreamedEvent.ts index af0308d3..0e52b493 100644 --- a/packages/js/langchain/src/langserve/parser/parseStreamedEvent.ts +++ b/packages/js/langchain/src/langserve/parser/parseStreamedEvent.ts @@ -1,4 +1,5 @@ -import {debug, warn} from '@nlux/core'; +import {debug} from '../../../../../shared/src/utils/debug'; +import {warn} from '../../../../../shared/src/utils/warn'; export const parseStreamedEvent = (event: string): { event: 'data' | 'end'; diff --git a/packages/js/langchain/src/langserve/types/adapterOptions.ts b/packages/js/langchain/src/langserve/types/adapterOptions.ts index defc3c97..ef942632 100644 --- a/packages/js/langchain/src/langserve/types/adapterOptions.ts +++ b/packages/js/langchain/src/langserve/types/adapterOptions.ts @@ -1,9 +1,9 @@ import {DataTransferMode} from '@nlux/core'; -import {LangServeHeaders} from './langServe'; import {LangServeInputPreProcessor} from './inputPreProcessor'; +import {LangServeHeaders} from './langServe'; import {LangServeOutputPreProcessor} from './outputPreProcessor'; -export type ChatAdapterOptions = { +export type ChatAdapterOptions = { /** * The URL of the LangServe runnable. * @@ -42,7 +42,7 @@ export type ChatAdapterOptions = { * no attribute can be matched to the user message), the adapter will send the user message * as a string. */ - inputPreProcessor?: LangServeInputPreProcessor; + inputPreProcessor?: LangServeInputPreProcessor; /** * When no `inputPreProcessor` is provided, the adapter will attempt to call `input_schema` @@ -62,5 +62,5 @@ export type ChatAdapterOptions = { * no attribute can be matched to expected output), the adapter will return the LangServe runnable * output as a string. */ - outputPreProcessor?: LangServeOutputPreProcessor; + outputPreProcessor?: LangServeOutputPreProcessor; }; diff --git a/packages/js/langchain/src/langserve/types/inputPreProcessor.ts b/packages/js/langchain/src/langserve/types/inputPreProcessor.ts index c16b04bd..0879432f 100644 --- a/packages/js/langchain/src/langserve/types/inputPreProcessor.ts +++ b/packages/js/langchain/src/langserve/types/inputPreProcessor.ts @@ -1,4 +1,4 @@ -import {ConversationItem} from '@nlux/core'; +import {ChatItem} from '@nlux/core'; /** * A function that can be used to pre-process the input before sending it to the runnable. @@ -23,7 +23,7 @@ import {ConversationItem} from '@nlux/core'; * } * ``` */ -export type LangServeInputPreProcessor = ( +export type LangServeInputPreProcessor = ( input: string, - conversationHistory?: readonly ConversationItem[], + conversationHistory?: readonly ChatItem[], ) => any; diff --git a/packages/js/langchain/src/langserve/types/io.ts b/packages/js/langchain/src/langserve/types/io.ts deleted file mode 100644 index f27a0051..00000000 --- a/packages/js/langchain/src/langserve/types/io.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type LangServeInboundPayload = string; -export type LangServeOutboundPayload = string | undefined; diff --git a/packages/js/langchain/src/langserve/types/outputPreProcessor.ts b/packages/js/langchain/src/langserve/types/outputPreProcessor.ts index 56aba387..1aefd4ad 100644 --- a/packages/js/langchain/src/langserve/types/outputPreProcessor.ts +++ b/packages/js/langchain/src/langserve/types/outputPreProcessor.ts @@ -10,4 +10,4 @@ * You check your runnable's documentation to see what it returns before you write this function. * This function should return a string that will be displayed to the user. */ -export type LangServeOutputPreProcessor = (output: any) => string; +export type LangServeOutputPreProcessor = (output: any) => AiMsg; diff --git a/packages/js/langchain/src/langserve/utils/adapterErrorToExceptionId.ts b/packages/js/langchain/src/langserve/utils/adapterErrorToExceptionId.ts index 41fa8392..337a26d6 100644 --- a/packages/js/langchain/src/langserve/utils/adapterErrorToExceptionId.ts +++ b/packages/js/langchain/src/langserve/utils/adapterErrorToExceptionId.ts @@ -1,4 +1,4 @@ -import {ExceptionId} from '@nlux/core'; +import {ExceptionId} from '../../../../../shared/src/types/exceptions'; export const adapterErrorToExceptionId = (error: any): ExceptionId | null => { if (typeof error === 'object' && error !== null) { diff --git a/packages/js/langchain/src/langserve/utils/getBaseUrlFromUrlOption.ts b/packages/js/langchain/src/langserve/utils/getBaseUrlFromUrlOption.ts index 9a569d41..5bd2bba4 100644 --- a/packages/js/langchain/src/langserve/utils/getBaseUrlFromUrlOption.ts +++ b/packages/js/langchain/src/langserve/utils/getBaseUrlFromUrlOption.ts @@ -1,10 +1,11 @@ +import {AnyAiMsg} from '../../../../../shared/src/types/anyAiMsg'; import {ChatAdapterOptions} from '../types/adapterOptions'; import {isUrlWithSupportedEndpoint} from './isUrlWithSupportedEndpoint'; /** * When the URL provided by the user does not end with /invoke or /stream we assume that the user has provided * the base URL. When the URL provided does end with /invoke or /stream, we assume that the user has provided the - * endpoint URL and we strip the endpoint type from it. + * endpoint URL, and we strip the endpoint type from it. * * Examples: * @@ -15,7 +16,7 @@ import {isUrlWithSupportedEndpoint} from './isUrlWithSupportedEndpoint'; * The base URL is also: https://pynlux.api.nlux.ai/einbot * Since it does not end with /invoke or /stream. */ -export const getBaseUrlFromUrlOption = (adapterOptions: ChatAdapterOptions): string => { +export const getBaseUrlFromUrlOption = (adapterOptions: ChatAdapterOptions): string => { const urlOption = adapterOptions.url; if (!isUrlWithSupportedEndpoint(urlOption)) { return urlOption; diff --git a/packages/js/langchain/src/langserve/utils/getDataTransferModeToUse.ts b/packages/js/langchain/src/langserve/utils/getDataTransferModeToUse.ts index 8a564c3a..654cab8e 100644 --- a/packages/js/langchain/src/langserve/utils/getDataTransferModeToUse.ts +++ b/packages/js/langchain/src/langserve/utils/getDataTransferModeToUse.ts @@ -1,10 +1,11 @@ -import {DataTransferMode, warnOnce} from '@nlux/core'; +import {DataTransferMode} from '@nlux/core'; +import {warnOnce} from '../../../../../shared/src/utils/warn'; import {LangServeAbstractAdapter} from '../adapter/adapter'; import {ChatAdapterOptions} from '../types/adapterOptions'; import {getDataTransferModeFromEndpointType} from './getDataTransferModeFromEndpointType'; import {getEndpointTypeFromUrl} from './getEndpointTypeFromUrl'; -export const getDataTransferModeToUse = (adapterOptions: ChatAdapterOptions): DataTransferMode => { +export const getDataTransferModeToUse = (adapterOptions: ChatAdapterOptions): DataTransferMode => { const runnableEndpointAction = getEndpointTypeFromUrl( adapterOptions.url, ); @@ -31,4 +32,4 @@ export const getDataTransferModeToUse = (adapterOptions: ChatAdapterOptions): Da } return dataTransferMode; -}; \ No newline at end of file +}; diff --git a/packages/js/langchain/src/langserve/utils/getEndpointTypeToUse.ts b/packages/js/langchain/src/langserve/utils/getEndpointTypeToUse.ts index 0662c323..4d49f8ff 100644 --- a/packages/js/langchain/src/langserve/utils/getEndpointTypeToUse.ts +++ b/packages/js/langchain/src/langserve/utils/getEndpointTypeToUse.ts @@ -3,7 +3,9 @@ import {LangServeEndpointType} from '../types/langServe'; import {getDataTransferModeToUse} from './getDataTransferModeToUse'; import {getEndpointTypeFromUrl} from './getEndpointTypeFromUrl'; -export const getEndpointTypeToUse = (adapterOptions: ChatAdapterOptions): LangServeEndpointType => { +export const getEndpointTypeToUse = ( + adapterOptions: ChatAdapterOptions, +): LangServeEndpointType => { const urlFromOptions = adapterOptions.url; const actionFromUrl = getEndpointTypeFromUrl(urlFromOptions); if (actionFromUrl) { diff --git a/packages/js/langchain/src/langserve/utils/getEndpointUrlToUse.ts b/packages/js/langchain/src/langserve/utils/getEndpointUrlToUse.ts index b6f1be07..b6a113d4 100644 --- a/packages/js/langchain/src/langserve/utils/getEndpointUrlToUse.ts +++ b/packages/js/langchain/src/langserve/utils/getEndpointUrlToUse.ts @@ -1,8 +1,11 @@ +import {AnyAiMsg} from '../../../../../shared/src/types/anyAiMsg'; import {ChatAdapterOptions} from '../types/adapterOptions'; import {getBaseUrlFromUrlOption} from './getBaseUrlFromUrlOption'; import {getEndpointTypeToUse} from './getEndpointTypeToUse'; -export const getEndpointUrlToUse = (adapterOptions: ChatAdapterOptions): string => { +export const getEndpointUrlToUse = ( + adapterOptions: ChatAdapterOptions, +): string => { const baseUrl = getBaseUrlFromUrlOption(adapterOptions).replace(/\/$/, ''); const endpointType = getEndpointTypeToUse(adapterOptions); return `${baseUrl}/${endpointType}`; diff --git a/packages/js/langchain/src/langserve/utils/getHeadersToUse.ts b/packages/js/langchain/src/langserve/utils/getHeadersToUse.ts index 37c59012..000fad32 100644 --- a/packages/js/langchain/src/langserve/utils/getHeadersToUse.ts +++ b/packages/js/langchain/src/langserve/utils/getHeadersToUse.ts @@ -1,8 +1,9 @@ +import {AnyAiMsg} from '../../../../../shared/src/types/anyAiMsg'; import {ChatAdapterOptions} from '../types/adapterOptions'; import {LangServeHeaders} from '../types/langServe'; export const getHeadersToUse = ( - adapterOptions: ChatAdapterOptions + adapterOptions: ChatAdapterOptions, ): LangServeHeaders => { return adapterOptions.headers || {}; }; \ No newline at end of file diff --git a/packages/js/langchain/src/langserve/utils/getRunnableNameToUse.ts b/packages/js/langchain/src/langserve/utils/getRunnableNameToUse.ts index 044b9021..fa541469 100644 --- a/packages/js/langchain/src/langserve/utils/getRunnableNameToUse.ts +++ b/packages/js/langchain/src/langserve/utils/getRunnableNameToUse.ts @@ -1,7 +1,8 @@ +import {AnyAiMsg} from '../../../../../shared/src/types/anyAiMsg'; import {ChatAdapterOptions} from '../types/adapterOptions'; import {getBaseUrlFromUrlOption} from './getBaseUrlFromUrlOption'; -export const getRunnableNameToUse = (adapterOptions: ChatAdapterOptions): string => { +export const getRunnableNameToUse = (adapterOptions: ChatAdapterOptions): string => { const baseUrl = getBaseUrlFromUrlOption(adapterOptions).replace(/\/$/, ''); const runnableName = baseUrl.split('/').pop(); return runnableName || 'langserve-runnable'; diff --git a/packages/js/langchain/src/langserve/utils/getSchemaUrlToUse.ts b/packages/js/langchain/src/langserve/utils/getSchemaUrlToUse.ts index beab3d69..46365679 100644 --- a/packages/js/langchain/src/langserve/utils/getSchemaUrlToUse.ts +++ b/packages/js/langchain/src/langserve/utils/getSchemaUrlToUse.ts @@ -1,7 +1,8 @@ +import {AnyAiMsg} from '../../../../../shared/src/types/anyAiMsg'; import {ChatAdapterOptions} from '../types/adapterOptions'; import {getBaseUrlFromUrlOption} from './getBaseUrlFromUrlOption'; -export const getSchemaUrlToUse = (adapterOptions: ChatAdapterOptions, type: 'input' | 'output'): string => { +export const getSchemaUrlToUse = (adapterOptions: ChatAdapterOptions, type: 'input' | 'output'): string => { const baseUrl = getBaseUrlFromUrlOption(adapterOptions).replace(/\/$/, ''); if (type === 'input') { return `${baseUrl}/input_schema`; diff --git a/packages/js/langchain/src/langserve/utils/transformInputBasedOnSchema.ts b/packages/js/langchain/src/langserve/utils/transformInputBasedOnSchema.ts index fd6b3731..1c0e77a5 100644 --- a/packages/js/langchain/src/langserve/utils/transformInputBasedOnSchema.ts +++ b/packages/js/langchain/src/langserve/utils/transformInputBasedOnSchema.ts @@ -1,8 +1,9 @@ -import {ConversationItem, warn} from '@nlux/core'; +import {ChatItem} from '@nlux/core'; +import {warn} from '../../../../../shared/src/utils/warn'; -export const transformInputBasedOnSchema = ( +export const transformInputBasedOnSchema = ( message: string, - conversationHistory: readonly ConversationItem[] | undefined, + conversationHistory: readonly ChatItem[] | undefined, schema: any, runnableName: string, ): any | undefined => { diff --git a/packages/js/nlbridge/src/index.ts b/packages/js/nlbridge/src/index.ts index 2d39e56b..b415733c 100644 --- a/packages/js/nlbridge/src/index.ts +++ b/packages/js/nlbridge/src/index.ts @@ -6,10 +6,6 @@ export type { DataTransferMode, } from '@nlux/core'; -export { - debug, -} from '@nlux/core'; - export type { ChatAdapterUsageMode, ChatAdapterOptions, diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts index e4310bc9..7c1b4a48 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts @@ -5,11 +5,11 @@ import { StandardAdapterInfo, StandardChatAdapter, StreamingAdapterObserver, - uid, } from '@nlux/core'; +import {uid} from '../../../../../shared/src/utils/uid'; import {ChatAdapterOptions, ChatAdapterUsageMode} from '../types/chatAdapterOptions'; -export abstract class NLBridgeAbstractAdapter implements StandardChatAdapter { +export abstract class NLBridgeAbstractAdapter implements StandardChatAdapter { static defaultDataTransferMode: DataTransferMode = 'stream'; private readonly __instanceId: string; @@ -61,12 +61,12 @@ export abstract class NLBridgeAbstractAdapter implements StandardChatAdapter { abstract fetchText( message: string, - extras: ChatAdapterExtras, - ): Promise; + extras: ChatAdapterExtras, + ): Promise; abstract streamText( message: string, observer: StreamingAdapterObserver, - extras: ChatAdapterExtras, + extras: ChatAdapterExtras, ): void; } diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builder.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builder.ts index a8032060..c7087509 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builder.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builder.ts @@ -5,9 +5,9 @@ import { } from '@nlux/core'; import {ChatAdapterUsageMode} from '../../types/chatAdapterOptions'; -export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { - create(): StandardChatAdapter; - withContext(context: CoreAiContext): ChatAdapterBuilder; - withMode(mode: ChatAdapterUsageMode): ChatAdapterBuilder; - withUrl(endpointUrl: string): ChatAdapterBuilder; +export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { + create(): StandardChatAdapter; + withContext(context: CoreAiContext): ChatAdapterBuilder; + withMode(mode: ChatAdapterUsageMode): ChatAdapterBuilder; + withUrl(endpointUrl: string): ChatAdapterBuilder; } diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builderImpl.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builderImpl.ts index 3f84dbbe..caae7ddc 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builderImpl.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/builderImpl.ts @@ -1,16 +1,17 @@ -import {AiContext as CoreAiContext, NluxUsageError, StandardChatAdapter} from '@nlux/core'; +import {AiContext as CoreAiContext, StandardChatAdapter} from '@nlux/core'; +import {NluxUsageError} from '../../../../../../shared/src/types/error'; import {ChatAdapterOptions, ChatAdapterUsageMode} from '../../types/chatAdapterOptions'; import {NLBridgeAbstractAdapter} from '../adapter'; import {NLBridgeFetchAdapter} from '../fetch'; import {NLBridgeStreamAdapter} from '../stream'; import {ChatAdapterBuilder} from './builder'; -export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { +export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { private theContext?: CoreAiContext | undefined; private theMode?: ChatAdapterUsageMode; private theUrl?: string; - constructor(cloneFrom?: ChatAdapterBuilderImpl) { + constructor(cloneFrom?: ChatAdapterBuilderImpl) { if (cloneFrom) { this.theUrl = cloneFrom.theUrl; this.theMode = cloneFrom.theMode; @@ -18,7 +19,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { } } - create(): StandardChatAdapter { + create(): StandardChatAdapter { if (!this.theUrl) { throw new NluxUsageError({ source: this.constructor.name, @@ -37,13 +38,13 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { ?? NLBridgeAbstractAdapter.defaultDataTransferMode; if (dataTransferModeToUse === 'stream') { - return new NLBridgeStreamAdapter(options); + return new NLBridgeStreamAdapter(options); } - return new NLBridgeFetchAdapter(options); + return new NLBridgeFetchAdapter(options); } - withContext(context: CoreAiContext): ChatAdapterBuilderImpl { + withContext(context: CoreAiContext): ChatAdapterBuilderImpl { if (this.theContext !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -55,7 +56,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withMode(mode: ChatAdapterUsageMode): ChatAdapterBuilderImpl { + withMode(mode: ChatAdapterUsageMode): ChatAdapterBuilderImpl { if (this.theMode !== undefined) { throw new NluxUsageError({ source: this.constructor.name, @@ -67,7 +68,7 @@ export class ChatAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withUrl(endpointUrl: string): ChatAdapterBuilderImpl { + withUrl(endpointUrl: string): ChatAdapterBuilderImpl { if (this.theUrl !== undefined) { throw new NluxUsageError({ source: this.constructor.name, diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts index cd4b0a87..470b2814 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts @@ -1,4 +1,4 @@ import {ChatAdapterBuilder} from './builder'; import {ChatAdapterBuilderImpl} from './builderImpl'; -export const createChatAdapter = (): ChatAdapterBuilder => new ChatAdapterBuilderImpl(); +export const createChatAdapter = (): ChatAdapterBuilder => new ChatAdapterBuilderImpl(); diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts index b021a62a..8a10535c 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts @@ -1,12 +1,13 @@ -import {ChatAdapterExtras, NluxError, NluxUsageError, StreamingAdapterObserver} from '@nlux/core'; +import {ChatAdapterExtras, StreamingAdapterObserver} from '@nlux/core'; +import {NluxError, NluxUsageError} from '../../../../../shared/src/types/error'; import {NLBridgeAbstractAdapter} from './adapter'; -export class NLBridgeFetchAdapter extends NLBridgeAbstractAdapter { +export class NLBridgeFetchAdapter extends NLBridgeAbstractAdapter { constructor(options: any) { super(options); } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText(message: string, extras: ChatAdapterExtras): Promise { if (this.context && this.context.contextId) { await this.context.flush(); } @@ -61,7 +62,7 @@ export class NLBridgeFetchAdapter extends NLBridgeAbstractAdapter { } } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot stream text from the fetch adapter!', diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts index a70b96b7..ee67774f 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts @@ -1,19 +1,21 @@ -import {ChatAdapterExtras, NluxUsageError, StreamingAdapterObserver, warn} from '@nlux/core'; +import {ChatAdapterExtras, StreamingAdapterObserver} from '@nlux/core'; +import {NluxUsageError} from '../../../../../shared/src/types/error'; +import {warn} from '../../../../../shared/src/utils/warn'; import {NLBridgeAbstractAdapter} from './adapter'; -export class NLBridgeStreamAdapter extends NLBridgeAbstractAdapter { +export class NLBridgeStreamAdapter extends NLBridgeAbstractAdapter { constructor(options: any) { super(options); } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText(message: string, extras: ChatAdapterExtras): Promise { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot fetch text using the stream adapter!', }); } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { const submitPrompt = () => fetch(this.endpointUrl, { method: 'POST', headers: { diff --git a/packages/js/nlbridge/src/nlbridge/contextAdapter/assistAdapter.ts b/packages/js/nlbridge/src/nlbridge/contextAdapter/assistAdapter.ts index 901960ce..50d85163 100644 --- a/packages/js/nlbridge/src/nlbridge/contextAdapter/assistAdapter.ts +++ b/packages/js/nlbridge/src/nlbridge/contextAdapter/assistAdapter.ts @@ -7,7 +7,7 @@ export class NLBridgeAssistAdapter implements AssistAdapter { this.url = url; } - async assist(message: string, extras: ChatAdapterExtras): Promise { + async assist(message: string, extras: ChatAdapterExtras): Promise { if (!extras.contextId) { return { success: false, diff --git a/packages/js/openai/src/createUnsafeChatAdapter.ts b/packages/js/openai/src/createUnsafeChatAdapter.ts index 6ee7ec58..6869d48c 100644 --- a/packages/js/openai/src/createUnsafeChatAdapter.ts +++ b/packages/js/openai/src/createUnsafeChatAdapter.ts @@ -1,12 +1,12 @@ -import {warnOnce} from '@nlux/core'; +import {warnOnce} from '../../../shared/src/utils/warn'; import {ChatAdapterBuilder} from './openai/gpt/builders/builder'; import {OpenAiAdapterBuilderImpl} from './openai/gpt/builders/builderImpl'; -export const createUnsafeChatAdapter = (): ChatAdapterBuilder => { +export const createUnsafeChatAdapter = (): ChatAdapterBuilder => { warnOnce('You just have created an OpenAI adapter that connects to the API directly from the browser. ' + 'This is not recommended for production use. We recommend that you implement a server-side proxy and configure ' + 'a customized adapter for it. To learn more about how to create custom adapters for nlux, visit:\n' + 'https://nlux.dev/learn/adapters/custom-adapters'); - return new OpenAiAdapterBuilderImpl(); + return new OpenAiAdapterBuilderImpl(); }; diff --git a/packages/js/openai/src/index.ts b/packages/js/openai/src/index.ts index 9cb2c8bf..5dd23795 100644 --- a/packages/js/openai/src/index.ts +++ b/packages/js/openai/src/index.ts @@ -5,10 +5,6 @@ export type { DataTransferMode, } from '@nlux/core'; -export { - debug, -} from '@nlux/core'; - export type { ChatAdapterOptions, } from './openai/gpt/types/chatAdapterOptions'; diff --git a/packages/js/openai/src/openai/gpt/adapters/adapter.ts b/packages/js/openai/src/openai/gpt/adapters/adapter.ts index 05f331b3..79cb06ec 100644 --- a/packages/js/openai/src/openai/gpt/adapters/adapter.ts +++ b/packages/js/openai/src/openai/gpt/adapters/adapter.ts @@ -4,16 +4,16 @@ import { StandardAdapterInfo, StandardChatAdapter, StreamingAdapterObserver, - uid, - warn, } from '@nlux/core'; import OpenAI from 'openai'; +import {uid} from '../../../../../../shared/src/utils/uid'; +import {warn} from '../../../../../../shared/src/utils/warn'; import {gptAdapterInfo} from '../config'; import {ChatAdapterOptions} from '../types/chatAdapterOptions'; import {OpenAiModel} from '../types/model'; import {defaultChatGptModel, defaultDataTransferMode} from './config'; -export abstract class OpenAiAbstractAdapter implements StandardChatAdapter { +export abstract class OpenAiAbstractAdapter implements StandardChatAdapter { protected readonly model: OpenAiModel; protected readonly openai: OpenAI; @@ -64,7 +64,7 @@ export abstract class OpenAiAbstractAdapter implements StandardChatAdapter { return gptAdapterInfo; } - abstract fetchText(message: string, extras: ChatAdapterExtras): Promise; + abstract fetchText(message: string, extras: ChatAdapterExtras): Promise; - abstract streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void; + abstract streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void; } diff --git a/packages/js/openai/src/openai/gpt/adapters/fetch.ts b/packages/js/openai/src/openai/gpt/adapters/fetch.ts index f1949f85..75878a44 100644 --- a/packages/js/openai/src/openai/gpt/adapters/fetch.ts +++ b/packages/js/openai/src/openai/gpt/adapters/fetch.ts @@ -1,12 +1,14 @@ -import {ChatAdapterExtras, NluxUsageError, StreamingAdapterObserver, warn} from '@nlux/core'; +import {ChatAdapterExtras, StreamingAdapterObserver} from '@nlux/core'; import OpenAI from 'openai'; +import {NluxUsageError} from '../../../../../../shared/src/types/error'; +import {warn} from '../../../../../../shared/src/utils/warn'; import {adapterErrorToExceptionId} from '../../../utils/adapterErrorToExceptionId'; import {conversationHistoryToMessagesList} from '../../../utils/conversationHistoryToMessagesList'; import {decodePayload} from '../../../utils/decodePayload'; import {ChatAdapterOptions} from '../types/chatAdapterOptions'; import {OpenAiAbstractAdapter} from './adapter'; -export class OpenAiFetchAdapter extends OpenAiAbstractAdapter { +export class OpenAiFetchAdapter extends OpenAiAbstractAdapter { constructor({ apiKey, model, @@ -24,7 +26,7 @@ export class OpenAiFetchAdapter extends OpenAiAbstractAdapter { } } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText(message: string, extras: ChatAdapterExtras): Promise { const messagesToSend: Array< OpenAI.Chat.Completions.ChatCompletionSystemMessageParam | OpenAI.Chat.Completions.ChatCompletionUserMessageParam | @@ -37,9 +39,8 @@ export class OpenAiFetchAdapter extends OpenAiAbstractAdapter { ] : []; if (extras.conversationHistory) { - messagesToSend.push( - ...conversationHistoryToMessagesList(extras.conversationHistory), - ); + const itemsInOpenAiFormat = conversationHistoryToMessagesList(extras.conversationHistory); + messagesToSend.push(...itemsInOpenAiFormat); } messagesToSend.push({ @@ -54,10 +55,13 @@ export class OpenAiFetchAdapter extends OpenAiAbstractAdapter { messages: messagesToSend, }); - const result = await decodePayload(response); + const result = await decodePayload(response); if (result === undefined) { warn('Undecodable message received from OpenAI'); - return ''; + throw new NluxUsageError({ + source: this.constructor.name, + message: 'Undecodable message received from OpenAI', + }); } else { return result; } @@ -72,7 +76,7 @@ export class OpenAiFetchAdapter extends OpenAiAbstractAdapter { } } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot stream text from the fetch adapter!', diff --git a/packages/js/openai/src/openai/gpt/adapters/stream.ts b/packages/js/openai/src/openai/gpt/adapters/stream.ts index d07eaa57..cf8c9166 100644 --- a/packages/js/openai/src/openai/gpt/adapters/stream.ts +++ b/packages/js/openai/src/openai/gpt/adapters/stream.ts @@ -1,12 +1,14 @@ -import {ChatAdapterExtras, NluxUsageError, StreamingAdapterObserver, warn} from '@nlux/core'; +import {ChatAdapterExtras, StreamingAdapterObserver} from '@nlux/core'; import OpenAI from 'openai'; +import {NluxUsageError} from '../../../../../../shared/src/types/error'; +import {warn} from '../../../../../../shared/src/utils/warn'; import {adapterErrorToExceptionId} from '../../../utils/adapterErrorToExceptionId'; import {conversationHistoryToMessagesList} from '../../../utils/conversationHistoryToMessagesList'; import {decodeChunk} from '../../../utils/decodeChunk'; import {ChatAdapterOptions} from '../types/chatAdapterOptions'; import {OpenAiAbstractAdapter} from './adapter'; -export class OpenAiStreamingAdapter extends OpenAiAbstractAdapter { +export class OpenAiStreamingAdapter extends OpenAiAbstractAdapter { constructor({ apiKey, model, @@ -24,14 +26,14 @@ export class OpenAiStreamingAdapter extends OpenAiAbstractAdapter { } } - fetchText(message: string): Promise { + fetchText(message: string): Promise { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot fetch text from the streaming adapter!', }); } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { const messagesToSend: Array< OpenAI.Chat.Completions.ChatCompletionSystemMessageParam | OpenAI.Chat.Completions.ChatCompletionUserMessageParam | @@ -44,9 +46,24 @@ export class OpenAiStreamingAdapter extends OpenAiAbstractAdapter { ] : []; if (extras.conversationHistory) { - messagesToSend.push( - ...conversationHistoryToMessagesList(extras.conversationHistory), + const newItems: ( + OpenAI.Chat.Completions.ChatCompletionUserMessageParam | + OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam | + OpenAI.Chat.Completions.ChatCompletionSystemMessageParam + )[] = conversationHistoryToMessagesList( + extras.conversationHistory).map( + (item) => { + const content: string = typeof item.content === 'string' + ? item.content : JSON.stringify(item.content); + + return { + content, + role: item.role, + }; + }, ); + + messagesToSend.push(...newItems); } messagesToSend.push({ diff --git a/packages/js/openai/src/openai/gpt/builders/builder.ts b/packages/js/openai/src/openai/gpt/builders/builder.ts index 799de83a..4a088b09 100644 --- a/packages/js/openai/src/openai/gpt/builders/builder.ts +++ b/packages/js/openai/src/openai/gpt/builders/builder.ts @@ -1,6 +1,6 @@ import {ChatAdapterBuilder as CoreChatAdapterBuilder, DataTransferMode, StandardChatAdapter} from '@nlux/core'; -export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { +export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { /** * Create a new ChatGPT API adapter. * Adapter users don't need to call this method directly. It will be called by nlux when the adapter is expected @@ -8,7 +8,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * * @returns {StandardChatAdapter} */ - create(): StandardChatAdapter; + create(): StandardChatAdapter; /** * The API key to use to connect to ChatGPT API. @@ -18,7 +18,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} apiKey * @returns {ChatAdapterBuilder} */ - withApiKey(apiKey: string): ChatAdapterBuilder; + withApiKey(apiKey: string): ChatAdapterBuilder; /** * Instruct the adapter to connect to API and load data either in streaming mode or in fetch mode. @@ -30,7 +30,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @default 'stream' * @returns {ChatAdapterBuilder} */ - withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder; + withDataTransferMode(mode: DataTransferMode): ChatAdapterBuilder; /** * The model or the endpoint to use for ChatGPT Inference API. @@ -41,7 +41,7 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} model * @returns {ChatAdapterBuilder} */ - withModel(model: string): ChatAdapterBuilder; + withModel(model: string): ChatAdapterBuilder; /** * The initial system to send to ChatGPT API. @@ -50,5 +50,5 @@ export interface ChatAdapterBuilder extends CoreChatAdapterBuilder { * @param {string} message * @returns {ChatAdapterBuilder} */ - withSystemMessage(message: string): ChatAdapterBuilder; + withSystemMessage(message: string): ChatAdapterBuilder; } diff --git a/packages/js/openai/src/openai/gpt/builders/builderImpl.ts b/packages/js/openai/src/openai/gpt/builders/builderImpl.ts index 3379bf49..aa96cf22 100644 --- a/packages/js/openai/src/openai/gpt/builders/builderImpl.ts +++ b/packages/js/openai/src/openai/gpt/builders/builderImpl.ts @@ -1,4 +1,5 @@ -import {DataTransferMode, NluxUsageError} from '@nlux/core'; +import {DataTransferMode} from '@nlux/core'; +import {NluxUsageError} from '../../../../../../shared/src/types/error'; import {defaultDataTransferMode} from '../adapters/config'; import {OpenAiFetchAdapter} from '../adapters/fetch'; import {OpenAiStreamingAdapter} from '../adapters/stream'; @@ -6,7 +7,7 @@ import {ChatAdapterOptions} from '../types/chatAdapterOptions'; import {OpenAiModel} from '../types/model'; import {ChatAdapterBuilder} from './builder'; -export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { +export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { protected apiKey: string | null = null; protected dataTransferMode: DataTransferMode = defaultDataTransferMode; protected model: OpenAiModel | null = null; @@ -16,7 +17,7 @@ export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { protected withModelCalled: boolean = false; protected withSystemMessageCalled: boolean = false; - constructor(cloneFrom?: OpenAiAdapterBuilderImpl) { + constructor(cloneFrom?: OpenAiAdapterBuilderImpl) { if (cloneFrom) { this.apiKey = cloneFrom.apiKey; this.dataTransferMode = cloneFrom.dataTransferMode; @@ -30,7 +31,7 @@ export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { } } - create(): OpenAiStreamingAdapter | OpenAiFetchAdapter { + create(): OpenAiStreamingAdapter | OpenAiFetchAdapter { if (!this.apiKey) { throw new NluxUsageError({ source: this.constructor.name, @@ -53,7 +54,7 @@ export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { return new OpenAiFetchAdapter(options); } - withApiKey(apiKey: string): OpenAiAdapterBuilderImpl { + withApiKey(apiKey: string): OpenAiAdapterBuilderImpl { if (this.withApiKeyCalled) { throw new NluxUsageError({ source: this.constructor.name, @@ -67,7 +68,7 @@ export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withDataTransferMode(mode: DataTransferMode): OpenAiAdapterBuilderImpl { + withDataTransferMode(mode: DataTransferMode): OpenAiAdapterBuilderImpl { if (this.withDataTransferModeCalled) { throw new NluxUsageError({ source: this.constructor.name, @@ -82,7 +83,7 @@ export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withModel(model: OpenAiModel): OpenAiAdapterBuilderImpl { + withModel(model: OpenAiModel): OpenAiAdapterBuilderImpl { if (this.withModelCalled) { throw new NluxUsageError({ source: this.constructor.name, @@ -96,7 +97,7 @@ export class OpenAiAdapterBuilderImpl implements ChatAdapterBuilder { return this; } - withSystemMessage(message: string): OpenAiAdapterBuilderImpl { + withSystemMessage(message: string): OpenAiAdapterBuilderImpl { if (this.withSystemMessageCalled) { throw new NluxUsageError({ source: this.constructor.name, diff --git a/packages/js/openai/src/utils/adapterErrorToExceptionId.ts b/packages/js/openai/src/utils/adapterErrorToExceptionId.ts index e0191029..3e9e511f 100644 --- a/packages/js/openai/src/utils/adapterErrorToExceptionId.ts +++ b/packages/js/openai/src/utils/adapterErrorToExceptionId.ts @@ -1,4 +1,4 @@ -import {ExceptionId} from '@nlux/core'; +import {ExceptionId} from '../../../../shared/src/types/exceptions'; export const adapterErrorToExceptionId = (error: any): ExceptionId | null => { if (typeof error === 'object' && error !== null) { diff --git a/packages/js/openai/src/utils/conversationHistoryToMessagesList.ts b/packages/js/openai/src/utils/conversationHistoryToMessagesList.ts index 80b3b8a1..24017726 100644 --- a/packages/js/openai/src/utils/conversationHistoryToMessagesList.ts +++ b/packages/js/openai/src/utils/conversationHistoryToMessagesList.ts @@ -1,9 +1,46 @@ -import {ConversationItem} from '@nlux/core'; +import {ChatItem} from '@nlux/core'; +import OpenAI from 'openai'; +import {warn} from '../../../../shared/src/utils/warn'; import {participantRoleToOpenAiRole} from './participantRoleToOpenAiRole'; -export const conversationHistoryToMessagesList = ( - conversationHistory: readonly ConversationItem[], -) => conversationHistory.map((item) => ({ - role: participantRoleToOpenAiRole(item.role), - content: item.message, -})); +export const conversationHistoryToMessagesList: ( + conversationHistory: readonly ChatItem[], +) => Array< + OpenAI.Chat.Completions.ChatCompletionSystemMessageParam | + OpenAI.Chat.Completions.ChatCompletionUserMessageParam | + OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam +> = ( + conversationHistory, +) => { + return conversationHistory.map((item) => { + let content: string | undefined; + + // We only want to send strings or numbers to OpenAI + // We convert objects to strings + if (typeof item.message === 'string' || item.message === 'number') { + content = `${item.message}`; + } else { + if (item.message === 'object') { + content = JSON.stringify(item.message); + } + } + + // We don't want to send empty messages to OpenAI + if (content === undefined) { + warn( + `Empty message or unsupported message format found in conversation history and will ` + + `not be included in the conversation history sent to OpenAI.`, + ); + return undefined; + } + + return { + role: participantRoleToOpenAiRole(item.role), + content, + }; + }).filter((item) => item !== undefined) as Array< + OpenAI.Chat.Completions.ChatCompletionSystemMessageParam | + OpenAI.Chat.Completions.ChatCompletionUserMessageParam | + OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam + >; +}; diff --git a/packages/js/openai/src/utils/decodePayload.ts b/packages/js/openai/src/utils/decodePayload.ts index e5a2f6f3..bf7374b9 100644 --- a/packages/js/openai/src/utils/decodePayload.ts +++ b/packages/js/openai/src/utils/decodePayload.ts @@ -3,9 +3,9 @@ import OpenAI from 'openai'; export const decodePayload: AdapterDecodeFunction< OpenAI.Chat.Completions.ChatCompletion -> = async ( +> = async ( payload: OpenAI.Chat.Completions.ChatCompletion, -): Promise => { +) => { if (!payload.choices || !payload.choices[0]) { throw Error('Invalid payload'); } @@ -15,5 +15,7 @@ export const decodePayload: AdapterDecodeFunction< return undefined; } - return content; + // TODO - Handle other types of messages + + return content as unknown as AiMsg; }; diff --git a/packages/react/core/rollup.config.ts b/packages/react/core/rollup.config.ts index 024a1ccd..f3edd71f 100644 --- a/packages/react/core/rollup.config.ts +++ b/packages/react/core/rollup.config.ts @@ -40,6 +40,7 @@ const packageConfig: () => Promise = async () => ([ ], external: [ 'react', + 'react-dom', ], output: generateOutputConfig(packageName, outputFile, isProduction), }, diff --git a/packages/react/core/src/components/AiChat/handleNewPropsReceived.ts b/packages/react/core/src/components/AiChat/handleNewPropsReceived.ts deleted file mode 100644 index 76080184..00000000 --- a/packages/react/core/src/components/AiChat/handleNewPropsReceived.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {AiChatProps, warn} from '@nlux/core'; -import {adapterParamToUsableAdapter} from '../../utils/adapterParamToUsableAdapter'; -import {optionsUpdater} from '../../utils/optionsUpdater'; -import {personaOptionsUpdater} from '../../utils/personasUpdater'; -import type {AiChatComponentProps} from './props'; - -export const handleNewPropsReceived = async ( - currentProps: AiChatComponentProps, - newProps: AiChatComponentProps, -): Promise | undefined> => { - const eventListeners = optionsUpdater( - currentProps.events, - newProps.events, - ); - - const layoutOptions = optionsUpdater( - currentProps.layoutOptions, - newProps.layoutOptions, - ); - - const conversationOptions = optionsUpdater( - currentProps.conversationOptions, - newProps.conversationOptions, - ); - - const promptBoxOptions = optionsUpdater( - currentProps.promptBoxOptions, - newProps.promptBoxOptions, - ); - - const personaOptions = await personaOptionsUpdater( - currentProps.personaOptions, - newProps.personaOptions, - ); - - type MutableAiChatProps = { - -readonly [P in keyof AiChatProps]: AiChatProps[P]; - }; - - const propsToUpdate: Partial = {}; - - if (eventListeners !== undefined) { - propsToUpdate.events = eventListeners ?? {}; - } - - if (layoutOptions !== undefined) { - propsToUpdate.layoutOptions = layoutOptions ?? {}; - } - - if (conversationOptions !== undefined) { - propsToUpdate.conversationOptions = conversationOptions ?? {}; - } - - if (promptBoxOptions !== undefined) { - propsToUpdate.promptBoxOptions = promptBoxOptions ?? {}; - } - - if (personaOptions !== undefined) { - propsToUpdate.personaOptions = personaOptions ?? {}; - } - - const newAdapterProp = currentProps.adapter !== newProps.adapter - ? newProps.adapter - : undefined; - - if (newAdapterProp !== undefined) { - const newAdapter = adapterParamToUsableAdapter(newAdapterProp); - if (!newAdapter) { - warn({ - message: 'Invalid new adapter property provided! The adapter must be an instance of ChatAdapter ' - + 'or ChatAdapterBuilder.', - type: 'invalid-adapter', - }); - } else { - propsToUpdate.adapter = newAdapter; - } - } - - if (currentProps.className !== newProps.className) { - propsToUpdate.className = newProps.className; - } - - if (currentProps.syntaxHighlighter !== newProps.syntaxHighlighter) { - propsToUpdate.syntaxHighlighter = newProps.syntaxHighlighter; - } - - const somethingChanged = Object.keys(propsToUpdate).length > 0; - if (!somethingChanged) { - return; - } - - return propsToUpdate; -}; diff --git a/packages/react/core/src/components/AiChat/index.tsx b/packages/react/core/src/components/AiChat/index.tsx deleted file mode 100644 index b3cf8a5f..00000000 --- a/packages/react/core/src/components/AiChat/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import {AiChat as AiChatType, createAiChat, warn} from '@nlux/core'; -import React, {useEffect, useRef, useState} from 'react'; -import {reactPersonasToCorePersonas} from '../../utils/reactPersonasToCorePersonas'; -import {handleNewPropsReceived} from './handleNewPropsReceived'; -import {AiChatComponentProps} from './props'; - -export const AiChat = (props: Readonly) => { - const rootElement = useRef(null); - const [currentProps, setCurrentProps] = useState | null>(null); - const aiChat = useRef(null); - - useEffect(() => { - if (!rootElement.current) { - throw new Error('Root element is not defined'); - } - - let shouldMount = true; - - const { - adapter, - className, - syntaxHighlighter, - layoutOptions, - conversationOptions, - promptBoxOptions, - personaOptions, - events, - initialConversation, - } = props; - - let newInstance = createAiChat().withAdapter(adapter); - - if (layoutOptions) { - newInstance = newInstance.withLayoutOptions(layoutOptions); - } - - if (promptBoxOptions) { - newInstance = newInstance.withPromptBoxOptions(promptBoxOptions); - } - - if (conversationOptions) { - newInstance = newInstance.withConversationOptions(conversationOptions); - } - - if (className) { - newInstance = newInstance.withClassName(className); - } - - if (syntaxHighlighter) { - newInstance = newInstance.withSyntaxHighlighter(syntaxHighlighter); - } - - if (initialConversation) { - newInstance = newInstance.withInitialConversation(initialConversation); - } - - if (events) { - const keys: (keyof typeof events)[] = Object.keys(events) as any; - for (const eventName of keys) { - const handler = events[eventName]; - if (handler) { - newInstance.on(eventName, handler); - } - } - } - - if (personaOptions) { - reactPersonasToCorePersonas(personaOptions).then((corePersonas) => { - newInstance = newInstance.withPersonaOptions(corePersonas); - if (!shouldMount) { - return; - } - - if (!rootElement.current) { - warn('Root element is not defined! AiChat cannot be mounted.'); - return; - } - - newInstance.mount(rootElement.current); - aiChat.current = newInstance; - }); - } else { - newInstance.mount(rootElement.current); - aiChat.current = newInstance; - } - - return () => { - shouldMount = false; - newInstance?.unmount(); - }; - }, []); - - useEffect(() => { - let isUseEffectCancelled = false; - if (!currentProps) { - setCurrentProps(props); - return; - } - - if (aiChat.current) { - handleNewPropsReceived(currentProps, props).then((newProps) => { - if (isUseEffectCancelled || !newProps) { - return; - } - - if (!aiChat.current) { - warn('AiChat is not defined! Cannot update.'); - return; - } - - aiChat.current.updateProps(newProps); - setCurrentProps(props); - }); - } - - return () => { - isUseEffectCancelled = true; - }; - }, [props, currentProps]); - - - return ( -
- ); -}; diff --git a/packages/react/core/src/components/AiChat/props.ts b/packages/react/core/src/components/AiChat/props.ts deleted file mode 100644 index 6fb4086a..00000000 --- a/packages/react/core/src/components/AiChat/props.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - ChatAdapter, - ChatAdapterBuilder, - ConversationItem, - ConversationOptions, - EventsMap, - HighlighterExtension, - LayoutOptions, - PromptBoxOptions, -} from '@nlux/core'; -import {PersonaOptions} from './personaOptions'; - -/** - * Properties for the AiChat React component. - */ -export type AiChatComponentProps = { - /** - * The adapter or adapter builder to use for the conversation. - * This can be obtained via useChatAdapter() hook for standard adapters or by creating your own custom adapter - * that implements `ChatAdapter` or `ChatAdapterBuilder` interfaces. - */ - adapter: ChatAdapter | ChatAdapterBuilder; - - /** - * Event listeners to be attached to chat room events. - */ - events?: Partial; - - /** - * CSS class name to be applied to the root element. - */ - className?: string; - - /** - * The initial conversation to display when the AiChat component is mounted. - * This property is not reactive, which means that its value is only read once when the - * component is mounted and any subsequent changes to it will be ignored. - */ - initialConversation?: ConversationItem[]; - - /** - * The syntax highlighter to use for any source code generated by the LLM - * and displayed in the conversation. - */ - syntaxHighlighter?: HighlighterExtension; - - /** - * The options to use for the conversation. - */ - conversationOptions?: ConversationOptions; - - /** - * The options to use for the layout. - */ - layoutOptions?: LayoutOptions; - - /** - * The options to use for the prompt box. - */ - promptBoxOptions?: PromptBoxOptions; - - /** - * The options to use for the personaOptions. - */ - personaOptions?: PersonaOptions -}; diff --git a/packages/react/core/src/exports/AiChat.tsx b/packages/react/core/src/exports/AiChat.tsx new file mode 100644 index 00000000..ab639a12 --- /dev/null +++ b/packages/react/core/src/exports/AiChat.tsx @@ -0,0 +1,106 @@ +import {ChatAdapterExtras} from '@nlux/core'; +import {forwardRef, ReactElement, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {ChatSegment} from '../../../../shared/src/types/chatSegment/chatSegment'; +import {createExceptionsBoxController} from '../../../../shared/src/ui/ExceptionsBox/control'; +import {className as compExceptionsBoxClassName} from '../../../../shared/src/ui/ExceptionsBox/create'; +import {PromptBoxStatus} from '../../../../shared/src/ui/PromptBox/props'; +import {getRootClassNames} from '../../../../shared/src/utils/dom/getRootClassNames'; +import {ConversationComp} from '../logic/Conversation/ConversationComp'; +import {ImperativeConversationCompProps} from '../logic/Conversation/props'; +import {PromptBoxComp} from '../ui/PromptBox/PromptBoxComp'; +import {adapterParamToUsableAdapter} from '../utils/adapterParamToUsableAdapter'; +import {chatItemsToChatSegment} from '../utils/chatItemsToChatSegment'; +import {reactPropsToCoreProps} from '../utils/reactPropsToCoreProps'; +import {useAiChatStyle} from './hooks/useAiChatStyle'; +import {useSubmitPromptHandler} from './hooks/useSubmitPromptHandler'; +import {AiChatComponentProps} from './props'; + +export const AiChat: ( + props: AiChatComponentProps, +) => ReactElement = function ( + props: AiChatComponentProps, +): ReactElement { + const conversationRef = useRef(null); + const exceptionBoxRef = useRef(null); + + const exceptionBoxController = useMemo(() => { + return exceptionBoxRef.current ? createExceptionsBoxController(exceptionBoxRef.current) : undefined; + }, [exceptionBoxRef.current]); + + const showException = useCallback((message: string) => { + exceptionBoxController?.displayException(message); + }, [exceptionBoxController]); + + const [prompt, setPrompt] = useState(''); + const [promptBoxStatus, setPromptBoxStatus] = useState('typing'); + const [initialSegment, setInitialSegment] = useState>(); + const [chatSegments, setChatSegments] = useState[]>([]); + + const adapterToUse = useMemo( + () => adapterParamToUsableAdapter(props.adapter), [props.adapter], + ); + + const adapterExtras: ChatAdapterExtras | undefined = useMemo(() => ( + adapterToUse ? {aiChatProps: reactPropsToCoreProps(props, adapterToUse)} : undefined + ), [props, adapterToUse]); + + const hasValidInput = useMemo(() => prompt.length > 0, [prompt]); + const handlePromptChange = useCallback((value: string) => setPrompt(value), [setPrompt]); + const handleSubmitPrompt = useSubmitPromptHandler({ + adapterToUse, + adapterExtras, + prompt, + promptBoxOptions: props.promptBoxOptions, + showException, + chatSegments, + setChatSegments, + setPromptBoxStatus, + conversationRef, + }); + + useEffect(() => setInitialSegment( + props.initialConversation ? chatItemsToChatSegment(props.initialConversation) : undefined, + ), [props.initialConversation]); + + const segments = useMemo(() => ( + initialSegment ? [initialSegment, ...chatSegments] : chatSegments + ), [initialSegment, chatSegments]); + + const rootStyle = useAiChatStyle(props.layoutOptions); + const rootClassNames = getRootClassNames({ + className: props.className, + themeId: props.themeId, + }).join(' '); + + const ForwardConversationComp = forwardRef(ConversationComp); + + return ( +
+
+
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/packages/react/core/src/exports/hooks/useAiChatStyle.ts b/packages/react/core/src/exports/hooks/useAiChatStyle.ts new file mode 100644 index 00000000..c29d4a58 --- /dev/null +++ b/packages/react/core/src/exports/hooks/useAiChatStyle.ts @@ -0,0 +1,21 @@ +import {LayoutOptions} from '@nlux/core'; +import {CSSProperties, useMemo} from 'react'; + +export const useAiChatStyle = (layoutOptions: LayoutOptions | undefined): CSSProperties => { + return useMemo(() => { + const result: CSSProperties = { + minWidth: '280px', + minHeight: '280px', + }; + + if (layoutOptions?.width) { + result.width = layoutOptions.width; + } + + if (layoutOptions?.height) { + result.height = layoutOptions.height; + } + + return result; + }, [layoutOptions?.width, layoutOptions?.height]); +}; diff --git a/packages/react/core/src/exports/hooks/useSubmitPromptHandler.ts b/packages/react/core/src/exports/hooks/useSubmitPromptHandler.ts new file mode 100644 index 00000000..43fad8dc --- /dev/null +++ b/packages/react/core/src/exports/hooks/useSubmitPromptHandler.ts @@ -0,0 +1,182 @@ +import {ChatAdapter, ChatAdapterExtras, PromptBoxOptions, StandardChatAdapter} from '@nlux/core'; +import {MutableRefObject, useCallback, useEffect, useMemo, useRef} from 'react'; +import {submitPrompt} from '../../../../../shared/src/services/submitPrompt/submitPromptImpl'; +import {ChatSegment} from '../../../../../shared/src/types/chatSegment/chatSegment'; +import {ChatSegmentAiMessage} from '../../../../../shared/src/types/chatSegment/chatSegmentAiMessage'; +import {ChatSegmentUserMessage} from '../../../../../shared/src/types/chatSegment/chatSegmentUserMessage'; +import {warn} from '../../../../../shared/src/utils/warn'; +import {ImperativeConversationCompProps} from '../../logic/Conversation/props'; + +type SubmitPromptHandlerProps = { + adapterToUse?: ChatAdapter | StandardChatAdapter; + adapterExtras?: ChatAdapterExtras; + prompt: string; + promptBoxOptions?: PromptBoxOptions; + chatSegments: ChatSegment[]; + showException: (message: string) => void; + setChatSegments: (segments: ChatSegment[]) => void; + setPromptBoxStatus: (status: 'typing' | 'submitting') => void; + conversationRef: MutableRefObject +}; + +export const useSubmitPromptHandler = (props: SubmitPromptHandlerProps) => { + const { + adapterToUse, + adapterExtras, + prompt, + promptBoxOptions, + showException, + chatSegments, + setChatSegments, + setPromptBoxStatus, + conversationRef, + } = props; + + const hasValidInput = useMemo(() => prompt.length > 0, [prompt]); + const domToReactRef = useRef({ + chatSegments, + setChatSegments, + setPromptBoxStatus, + showException, + }); + + useEffect(() => { + domToReactRef.current = { + chatSegments, + setChatSegments, + setPromptBoxStatus, + showException, + }; + }, [chatSegments, setChatSegments, setPromptBoxStatus, showException]); + + return useCallback( + () => { + if (!adapterToUse || !adapterExtras) { + warn('No valid adapter was provided to AiChat component'); + return; + } + + if (!hasValidInput) { + return; + } + + if (promptBoxOptions?.disableSubmitButton) { + return; + } + + setPromptBoxStatus('submitting'); + const { + segment: chatSegment, + observable: chatSegmentObservable, + } = submitPrompt( + prompt, + adapterToUse, + adapterExtras, + ); + + if (chatSegment.status === 'error') { + warn('Error occurred while submitting prompt'); + showException('Error occurred while submitting prompt'); + setPromptBoxStatus('typing'); + return; + } + + // THE FOLLOWING CODE IS USED TO TRIGGER AN UPDATE OF THE REACT STATE. + // The 'on' event listeners are implemented by @nlux/core non-React prompt handler. + // On 'complete' and 'update' events, the chat segment is updated, but in order + // to trigger a check and potentially re-render the React component, we need to change + // the reference of the parts array by creating a new array. + + const handleSegmentItemReceived = (item: ChatSegmentAiMessage | ChatSegmentUserMessage) => { + const currentChatSegments = domToReactRef.current.chatSegments; + const newChatSegments: ChatSegment[] = currentChatSegments.map( + (currentChatSegment) => { + if (currentChatSegment.uid !== chatSegmentObservable.segmentId) { + return currentChatSegment; + } + + return { + ...currentChatSegment, + items: [ + ...currentChatSegment.items, + {...item}, + ], + }; + }, + ); + + domToReactRef.current.setChatSegments(newChatSegments); + }; + + chatSegmentObservable.on('userMessageReceived', (userMessage) => { + handleSegmentItemReceived(userMessage); + }); + + chatSegmentObservable.on('aiMessageStreamStarted', (aiStreamedMessage) => { + handleSegmentItemReceived(aiStreamedMessage); + domToReactRef.current.setPromptBoxStatus('typing'); + }); + + chatSegmentObservable.on('aiMessageReceived', (aiMessage) => { + const currentChatSegments = domToReactRef.current.chatSegments; + const newChatSegments: ChatSegment[] = currentChatSegments.map( + (currentChatSegment) => { + if (currentChatSegment.uid !== chatSegmentObservable.segmentId) { + return currentChatSegment; + } + + return {...currentChatSegment, items: [...currentChatSegment.items, {...aiMessage}]}; + }, + ); + + domToReactRef.current.setChatSegments(newChatSegments); + }); + + chatSegmentObservable.on('complete', (completeChatSegment) => { + domToReactRef.current.setPromptBoxStatus('typing'); + const currentChatSegments = domToReactRef.current.chatSegments; + const newChatSegments: ChatSegment[] = currentChatSegments.map( + (currentChatSegment) => { + if (currentChatSegment.uid !== chatSegmentObservable.segmentId) { + return currentChatSegment; + } + + return {...completeChatSegment}; + }, + ); + + domToReactRef.current.setChatSegments(newChatSegments); + }); + + chatSegmentObservable.on('aiChunkReceived', (chunk: string, messageId: string) => { + requestAnimationFrame(() => { + // We need to wait a bit before streaming the chunk to the chat item + // because of the React lifecycle. The chat item might not be rendered yet. + conversationRef.current?.streamChunk(chatSegment.uid, messageId, chunk); + }); + }); + + chatSegmentObservable.on('error', (exception) => { + const parts = domToReactRef.current.chatSegments; + const newParts = parts.filter((part) => part.uid !== chatSegment.uid); + + domToReactRef.current.setChatSegments(newParts); + domToReactRef.current.setPromptBoxStatus('typing'); + domToReactRef.current.showException(exception); + }); + + domToReactRef.current.setChatSegments([ + ...domToReactRef.current.chatSegments, + chatSegment, + ]); + }, + [ + showException, + domToReactRef, + prompt, + adapterToUse, + adapterExtras, + promptBoxOptions?.disableSubmitButton, + ], + ); +}; diff --git a/packages/react/core/src/components/AiChat/personaOptions.ts b/packages/react/core/src/exports/personaOptions.tsx similarity index 100% rename from packages/react/core/src/components/AiChat/personaOptions.ts rename to packages/react/core/src/exports/personaOptions.tsx diff --git a/packages/react/core/src/exports/props.tsx b/packages/react/core/src/exports/props.tsx new file mode 100644 index 00000000..da2d2c60 --- /dev/null +++ b/packages/react/core/src/exports/props.tsx @@ -0,0 +1,74 @@ +import { + ChatAdapter, + ChatAdapterBuilder, + ChatItem, + ConversationOptions, + EventsMap, + HighlighterExtension, + LayoutOptions, + PromptBoxOptions, +} from '@nlux/core'; +import {FunctionComponent} from 'react'; +import {PersonaOptions} from './personaOptions'; + +/** + * Props for the AiChat component. + */ +export type AiChatComponentProps = { + /** + * The chat adapter to use. + * This can be an instance of a chat adapter, or a chat adapter builder. + */ + adapter: ChatAdapter | ChatAdapterBuilder; + + /** + * A map of event handlers. + */ + events?: Partial>; + + /** + * The class name to add to the root element of the component. + */ + className?: string; + + /** + * The theme ID to use. + * This should be the ID of a theme that has been loaded into the page. + */ + themeId?: string; + + /** + * The initial conversation to display. + */ + initialConversation?: ChatItem[]; + + /** + * The syntax highlighter to use. + */ + syntaxHighlighter?: HighlighterExtension; + + /** + * Options for the conversation. + */ + conversationOptions?: ConversationOptions; + + /** + * Custom AI message renderer. + */ + aiMessageComponent?: FunctionComponent<{message: AiMsg}>; + + /** + * Layout options. + */ + layoutOptions?: LayoutOptions; + + /** + * Options for the prompt box. + */ + promptBoxOptions?: PromptBoxOptions; + + /** + * Options for the persona. + */ + personaOptions?: PersonaOptions; +}; diff --git a/packages/react/core/src/hooks/useDeepCompareEffect/index.ts b/packages/react/core/src/hooks/useDeepCompareEffect/index.ts index cdbd4fe0..dee4cdf8 100644 --- a/packages/react/core/src/hooks/useDeepCompareEffect/index.ts +++ b/packages/react/core/src/hooks/useDeepCompareEffect/index.ts @@ -6,7 +6,7 @@ // import {useEffect, useMemo, useRef} from 'react'; -import {dequal as deepEqual} from '../dequal'; +import {dequal as deepEqual} from '../../../../../shared/src/utils/dequal'; type UseEffectParams = Parameters type EffectCallback = UseEffectParams[0] @@ -19,17 +19,17 @@ function checkDeps(deps: DependencyList) { if (!deps || !deps.length) { throw new Error( 'useDeepCompareEffect should not be used with no dependencies. Use React.useEffect instead.', - ) + ); } if (deps.every(isPrimitive)) { throw new Error( 'useDeepCompareEffect should not be used with dependencies that are all primitive values. Use React.useEffect instead.', - ) + ); } } function isPrimitive(val: unknown) { - return val == null || /^[sbn]/.test(typeof val) + return val == null || /^[sbn]/.test(typeof val); } /** @@ -37,16 +37,16 @@ function isPrimitive(val: unknown) { * @returns a memoized version of the value as long as it remains deeply equal */ export function useDeepCompareMemoize(value: T) { - const ref = useRef(value) - const signalRef = useRef(0) + const ref = useRef(value); + const signalRef = useRef(0); if (!deepEqual(value, ref.current)) { - ref.current = value - signalRef.current += 1 + ref.current = value; + signalRef.current += 1; } // eslint-disable-next-line react-hooks/exhaustive-deps - return useMemo(() => ref.current, [signalRef.current]) + return useMemo(() => ref.current, [signalRef.current]); } function useDeepCompareEffect( @@ -54,10 +54,10 @@ function useDeepCompareEffect( dependencies: DependencyList, ): UseEffectReturn { if (process.env.NODE_ENV !== 'production') { - checkDeps(dependencies) + checkDeps(dependencies); } // eslint-disable-next-line react-hooks/exhaustive-deps - return useEffect(callback, useDeepCompareMemoize(dependencies)) + return useEffect(callback, useDeepCompareMemoize(dependencies)); } export function useDeepCompareEffectNoCheck( @@ -65,7 +65,7 @@ export function useDeepCompareEffectNoCheck( dependencies: DependencyList, ): UseEffectReturn { // eslint-disable-next-line react-hooks/exhaustive-deps - return useEffect(callback, useDeepCompareMemoize(dependencies)) + return useEffect(callback, useDeepCompareMemoize(dependencies)); } export default useDeepCompareEffect; diff --git a/packages/react/core/src/index.tsx b/packages/react/core/src/index.tsx index 66d5d59c..7d06039b 100644 --- a/packages/react/core/src/index.tsx +++ b/packages/react/core/src/index.tsx @@ -11,7 +11,6 @@ export type { PromptBoxOptions, ConversationOptions, HighlighterExtension, - ExceptionId, } from '@nlux/core'; export type { @@ -32,22 +31,15 @@ export type { PersonaOptions, BotPersona, UserPersona, -} from './components/AiChat/personaOptions'; +} from './exports/personaOptions'; export type { AiChatComponentProps, -} from './components/AiChat/props'; +} from './exports/props'; export { - NluxError, - NluxUsageError, - NluxValidationError, - NluxRenderingError, - NluxConfigError, - debug, -} from '@nlux/core'; - -export {AiChat} from './components/AiChat'; + AiChat, +} from './exports/AiChat'; export type { UpdateContextItem, diff --git a/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx b/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx new file mode 100644 index 00000000..a39e4452 --- /dev/null +++ b/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx @@ -0,0 +1,209 @@ +import {createRef, forwardRef, ReactNode, Ref, RefObject, useEffect, useImperativeHandle, useMemo} from 'react'; +import {AiUnifiedMessage} from '../../../../../shared/src/types/chatSegment/chatSegmentAiMessage'; +import {getChatSegmentClassName} from '../../../../../shared/src/utils/dom/getChatSegmentClassName'; +import {warn} from '../../../../../shared/src/utils/warn'; +import {ChatItemComp} from '../../ui/ChatItem/ChatItemComp'; +import {ChatItemImperativeProps} from '../../ui/ChatItem/props'; +import {LoaderComp} from '../../ui/Loader/LoaderComp'; +import {ChatSegmentImperativeProps, ChatSegmentProps} from './props'; +import {isPrimitiveReactNodeType} from './utils/isPrimitiveReactNodeType'; +import {nameFromMessageAndPersona} from './utils/nameFromMessageAndPersona'; +import {pictureFromMessageAndPersona} from './utils/pictureFromMessageAndPersona'; + +export const ChatSegmentComp: ( + props: ChatSegmentProps, + ref: Ref>, +) => ReactNode = function ( + props: ChatSegmentProps, + ref: Ref>, +): ReactNode { + const {chatSegment} = props; + const chatItemsRef = useMemo( + () => new Map>(), [], + ); + + useEffect(() => { + if (chatSegment.items.length === 0) { + chatItemsRef.clear(); + return; + } + + const itemsInRefsMap = new Set(chatItemsRef.keys()); + const itemsInSegments = new Set(chatSegment.items.map((item) => item.uid)); + for (const itemInRefsMap of itemsInRefsMap) { + if (!itemsInSegments.has(itemInRefsMap)) { + chatItemsRef.delete(itemInRefsMap); + } + } + }, [chatSegment.items]); + + const loader = useMemo(() => { + if (chatSegment.status !== 'active') { + return null; + } + + return ( +
+ {props.loader ?? } +
+ ); + }, [chatSegment.status, props.loader]); + + const rootClassName = useMemo(() => getChatSegmentClassName(chatSegment.status), [chatSegment.status]); + + useImperativeHandle(ref, () => ({ + scrollToBottom: () => { + // TODO - Implement scroll to bottom + }, + streamChunk: (messageId: string, chunk: string) => { + const messageCompRef = chatItemsRef.get(messageId); + if (messageCompRef?.current) { + messageCompRef.current.streamChunk(chunk); + } + }, + }), []); + + const chatItems = chatSegment.items; + if (chatItems.length === 0) { + return null; + } + + return ( +
+ {chatItems.map((chatItem, index) => { + let ref: RefObject | undefined = chatItemsRef.get(chatItem.uid); + if (!ref) { + ref = createRef(); + chatItemsRef.set(chatItem.uid, ref); + } + + const ForwardRefChatItemComp = forwardRef( + ChatItemComp, + ); + + if (chatItem.participantRole === 'user') { + // + // User chat item — That should always be in complete state. + // + if (chatItem.status !== 'complete') { + warn( + `User chat item should be always be in complete state — ` + + `Current status: ${(chatItem as AiUnifiedMessage).status} — ` + + `Segment UID: ${chatSegment.uid}`, + ); + return null; + } + + if (!isPrimitiveReactNodeType(chatItem.content)) { + warn( + `User chat item should have primitive content (string, number, boolean, null) — ` + + `Current content: ${JSON.stringify(chatItem.content)} — ` + + `Segment UID: ${chatSegment.uid}`, + ); + + return null; + } + + return ( + + ); + } else { + // + // AI chat item — That can be in loading, streaming, or complete state. + // + if (chatItem.status === 'complete') { + // + // AI chat item in complete state. + // + if (chatItem.dataTransferMode === 'stream') { + // + // When the AI chat item is in complete state and the data transfer mode is stream, + // we do not render the message content — as it is streamed. + // We do not rely on custom renderer here since the content is streamed as string. + // + return ( + + ); + } else { + // + // When the AI chat item is in complete state and the data transfer mode is fetch, + // we render the message content. We also check if the content is primitive and if a custom + // renderer is provided. + // + if (!isPrimitiveReactNodeType(chatItem.content) && !props.customRenderer) { + warn( + `When the type of the AI chat content is not primitive (object or array), ` + + `a custom renderer must be provided — ` + + `Current content: ${JSON.stringify(chatItem.content)} — ` + + `Segment UID: ${chatSegment.uid}`, + ); + + return null; + } + + return ( + + ); + } + } else { + // + // AI chat item in loading or streaming state. + // No need for custom renderer here. + // + if (chatItem.status === 'loading' || chatItem.status === 'streaming') { + return ( + + ); + } + } + + return null; + } + })} + {loader} +
+ ); +}; diff --git a/packages/react/core/src/logic/ChatSegment/props.ts b/packages/react/core/src/logic/ChatSegment/props.ts new file mode 100644 index 00000000..60fe9f30 --- /dev/null +++ b/packages/react/core/src/logic/ChatSegment/props.ts @@ -0,0 +1,16 @@ +import {HighlighterExtension} from '@nlux/core'; +import {FunctionComponent, ReactElement} from 'react'; +import {ChatSegment} from '../../../../../shared/src/types/chatSegment/chatSegment'; +import {PersonaOptions} from '../../exports/personaOptions'; + +export type ChatSegmentProps = { + chatSegment: ChatSegment + customRenderer?: FunctionComponent<{message: AiMsg}>; + loader?: ReactElement; + personaOptions?: PersonaOptions; + syntaxHighlighter?: HighlighterExtension; +}; + +export type ChatSegmentImperativeProps = { + streamChunk: (messageId: string, chunk: string) => void; +}; diff --git a/packages/react/core/src/logic/ChatSegment/utils/isPrimitiveReactNodeType.ts b/packages/react/core/src/logic/ChatSegment/utils/isPrimitiveReactNodeType.ts new file mode 100644 index 00000000..32d157f1 --- /dev/null +++ b/packages/react/core/src/logic/ChatSegment/utils/isPrimitiveReactNodeType.ts @@ -0,0 +1,5 @@ +// Types that can be rendered in React +export const isPrimitiveReactNodeType = (value: any) => { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null + || value === undefined; +}; diff --git a/packages/react/core/src/logic/ChatSegment/utils/nameFromMessageAndPersona.ts b/packages/react/core/src/logic/ChatSegment/utils/nameFromMessageAndPersona.ts new file mode 100644 index 00000000..0f4880ea --- /dev/null +++ b/packages/react/core/src/logic/ChatSegment/utils/nameFromMessageAndPersona.ts @@ -0,0 +1,15 @@ +import {ParticipantRole} from '@nlux/core'; +import {AnyAiMsg} from '../../../../../../shared/src/types/anyAiMsg'; +import {ConversationCompProps} from '../../Conversation/props'; + +export const nameFromMessageAndPersona = (role: ParticipantRole, personaOptions: ConversationCompProps['personaOptions']) => { + if (role === 'ai') { + return personaOptions?.bot?.name; + } + + if (role === 'user') { + return personaOptions?.user?.name; + } + + return undefined; +}; diff --git a/packages/react/core/src/logic/ChatSegment/utils/pictureFromMessageAndPersona.ts b/packages/react/core/src/logic/ChatSegment/utils/pictureFromMessageAndPersona.ts new file mode 100644 index 00000000..226f2f90 --- /dev/null +++ b/packages/react/core/src/logic/ChatSegment/utils/pictureFromMessageAndPersona.ts @@ -0,0 +1,15 @@ +import {ParticipantRole} from '@nlux/core'; +import {AnyAiMsg} from '../../../../../../shared/src/types/anyAiMsg'; +import {ConversationCompProps} from '../../Conversation/props'; + +export const pictureFromMessageAndPersona = (role: ParticipantRole, personaOptions: ConversationCompProps['personaOptions']) => { + if (role === 'ai') { + return personaOptions?.bot?.picture; + } + + if (role === 'user') { + return personaOptions?.user?.picture; + } + + return undefined; +}; diff --git a/packages/react/core/src/logic/Conversation/ConversationComp.tsx b/packages/react/core/src/logic/Conversation/ConversationComp.tsx new file mode 100644 index 00000000..9d6a601d --- /dev/null +++ b/packages/react/core/src/logic/Conversation/ConversationComp.tsx @@ -0,0 +1,88 @@ +import {createRef, forwardRef, ReactNode, Ref, RefObject, useEffect, useImperativeHandle, useMemo} from 'react'; +import {WelcomeMessageComp} from '../../ui/WelcomeMessage/WelcomeMessageComp'; +import {ChatSegmentComp} from '../ChatSegment/ChatSegmentComp'; +import {ChatSegmentImperativeProps} from '../ChatSegment/props'; +import {ConversationCompProps, ImperativeConversationCompProps} from './props'; + +export type ConversationCompType = ( + props: ConversationCompProps, + ref: Ref, +) => ReactNode; + +export const ConversationComp: ConversationCompType = function ( + props: ConversationCompProps, + ref: Ref, +): ReactNode { + const {segments, personaOptions} = props; + const hasMessages = useMemo(() => segments.some((segment) => segment.items.length > 0), [segments]); + const hasAiPersona = personaOptions?.bot?.name && personaOptions.bot.picture; + const showWelcomeMessage = hasAiPersona && !hasMessages; + + const chatSegmentsRef = useMemo( + () => new Map>>(), [], + ); + + useEffect(() => { + if (props.segments.length === 0) { + chatSegmentsRef.clear(); + return; + } + + const itemsInRefsMap = new Set(chatSegmentsRef.keys()); + const itemsInSegments = new Set(props.segments.map((segment) => segment.uid)); + for (const itemInRefsMap of itemsInRefsMap) { + if (!itemsInSegments.has(itemInRefsMap)) { + chatSegmentsRef.delete(itemInRefsMap); + } + } + }, [props.segments]); + + useImperativeHandle(ref, () => ({ + scrollToBottom: () => { + // TODO - Implement scroll to bottom + }, + streamChunk: (segmentId: string, messageId: string, chunk: string) => { + const chatSegmentRef = chatSegmentsRef.get(segmentId); + if (chatSegmentRef?.current) { + chatSegmentRef.current.streamChunk(messageId, chunk); + } + }, + }), []); + + return ( + <> + {showWelcomeMessage && ( + + )} +
+ {segments.map((segment) => { + let ref: RefObject> | undefined = chatSegmentsRef.get(segment.uid); + if (!ref) { + ref = createRef(); + chatSegmentsRef.set(segment.uid, ref); + } + + const ForwardRefChatItemComp = forwardRef( + ChatSegmentComp, + ); + + return ( + + ); + })} +
+ + ); +}; diff --git a/packages/react/core/src/logic/Conversation/props.ts b/packages/react/core/src/logic/Conversation/props.ts new file mode 100644 index 00000000..4465ab15 --- /dev/null +++ b/packages/react/core/src/logic/Conversation/props.ts @@ -0,0 +1,18 @@ +import {ConversationOptions, HighlighterExtension} from '@nlux/core'; +import {FunctionComponent, ReactElement} from 'react'; +import {ChatSegment} from '../../../../../shared/src/types/chatSegment/chatSegment'; +import {PersonaOptions} from '../../exports/personaOptions'; + +export type ConversationCompProps = { + segments: ChatSegment[]; + conversationOptions?: ConversationOptions; + personaOptions?: PersonaOptions; + customRenderer?: FunctionComponent<{message: AiMsg}>; + syntaxHighlighter?: HighlighterExtension; + loader?: ReactElement; +}; + +export type ImperativeConversationCompProps = { + scrollToBottom: () => void; + streamChunk: (segmentId: string, messageId: string, chunk: string) => void; +}; diff --git a/packages/react/core/src/logic/StreamContainer/StreamContainerComp.tsx b/packages/react/core/src/logic/StreamContainer/StreamContainerComp.tsx new file mode 100644 index 00000000..a1c4a944 --- /dev/null +++ b/packages/react/core/src/logic/StreamContainer/StreamContainerComp.tsx @@ -0,0 +1,55 @@ +import {Ref, useEffect, useImperativeHandle, useRef} from 'react'; +import {className as compMessageClassName} from '../../../../../shared/src/ui/Message/create'; +import { + directionClassName as compMessageDirectionClassName, +} from '../../../../../shared/src/ui/Message/utils/applyNewDirectionClassName'; +import { + statusClassName as compMessageStatusClassName, +} from '../../../../../shared/src/ui/Message/utils/applyNewStatusClassName'; +import {StreamContainerImperativeProps, StreamContainerProps} from './props'; +import {streamingDomService} from './streamingDomServive'; + +export const StreamContainerComp = ( + props: StreamContainerProps, + ref: Ref, +) => { + const streamContainer = useRef(null); + + useEffect(() => { + if (streamContainer.current) { + streamContainer.current.appendChild(streamingDomService.getStreamingDomElement(props.uid)); + } else { + streamingDomService.deleteStreamingDomElement(props.uid); + } + }, [streamContainer.current]); + + useEffect(() => { + return () => { + streamingDomService.deleteStreamingDomElement(props.uid); + }; + }, []); + + useImperativeHandle(ref, () => ({ + streamChunk: (chunk: string) => { + const domById = streamingDomService.getStreamingDomElement(props.uid); + domById?.append( + // TODO — Handle proper streaming + document.createTextNode(chunk), + ); + }, + }), []); + + const compDirectionClassName = props.direction + ? compMessageDirectionClassName[props.direction] + : compMessageDirectionClassName['incoming']; + + const compStatusClassName = props.status + ? compMessageStatusClassName[props.status] + : compMessageStatusClassName['rendered']; + + const className = `${compMessageClassName} ${compStatusClassName} ${compDirectionClassName}`; + + return ( +
+ ); +}; diff --git a/packages/react/core/src/logic/StreamContainer/props.ts b/packages/react/core/src/logic/StreamContainer/props.ts new file mode 100644 index 00000000..cd388cee --- /dev/null +++ b/packages/react/core/src/logic/StreamContainer/props.ts @@ -0,0 +1,11 @@ +import {MessageDirection} from '../../../../../shared/src/ui/Message/props'; + +export type StreamContainerProps = { + uid: string, + direction: MessageDirection, + status: 'rendered' | 'streaming' | 'error'; +}; + +export type StreamContainerImperativeProps = { + streamChunk: (chunk: string) => void; +}; diff --git a/packages/react/core/src/logic/StreamContainer/streamingDomServive.ts b/packages/react/core/src/logic/StreamContainer/streamingDomServive.ts new file mode 100644 index 00000000..3ed670ec --- /dev/null +++ b/packages/react/core/src/logic/StreamContainer/streamingDomServive.ts @@ -0,0 +1,46 @@ +/** + * In order to implement an optimized streaming experience, we cannot rely on React to manage the DOM elements + * of the streaming messages. React UI is a function of state, and re-rendering the entire UI for every message + * chunk would be inefficient. Instead, we use a service to manage the DOM elements of the streaming messages. + * + * This service creates a DOM element for each message ID and caches it. When a message ID is no longer needed, + * the service deletes the DOM element. This way, we only have to update the DOM elements that are currently + * visible on the screen. + * + * The NLUX stream renderer uses this service to obtain the DOM element for each message ID and will update them + * in a very efficient way when new chunks are received. + */ + +export type StreamingDomService = { + getStreamingDomElement: (messageId: string) => HTMLDivElement; + deleteStreamingDomElement: (messageId: string) => void; +}; + +export const streamingDomService: StreamingDomService = (() => { + const streamingDomElementsByMessageId: Record = {}; + const victimMessageIds: Set = new Set(); + + return { + getStreamingDomElement: (messageId: string) => { + if (streamingDomElementsByMessageId[messageId] === undefined) { + streamingDomElementsByMessageId[messageId] = document.createElement('div'); + } + + victimMessageIds.delete(messageId); + return streamingDomElementsByMessageId[messageId]; + }, + deleteStreamingDomElement: (messageId: string) => { + victimMessageIds.add(messageId); + setTimeout(() => { + victimMessageIds.forEach((victimMessageId) => { + if (streamingDomElementsByMessageId[victimMessageId]) { + streamingDomElementsByMessageId[victimMessageId].remove(); + delete streamingDomElementsByMessageId[victimMessageId]; + } + }); + + victimMessageIds.clear(); + }, 100); + }, + }; +})(); diff --git a/packages/react/core/src/providers/createAiContext.tsx b/packages/react/core/src/providers/createAiContext.tsx index 27b9dcf7..7ca1a8f6 100644 --- a/packages/react/core/src/providers/createAiContext.tsx +++ b/packages/react/core/src/providers/createAiContext.tsx @@ -6,7 +6,7 @@ import { InitializeContextResult, predefinedContextSize, } from '@nlux/core'; -import React, {createContext, useEffect} from 'react'; +import {createContext, useEffect, useState} from 'react'; import {AiContext, AiContextProviderProps} from '../types/AiContext'; /** @@ -58,17 +58,17 @@ export const createAiContext = (adapter: ContextAdapter | ContextAdapterBuilder) const [ contextId, setContextId, - ] = React.useState(); + ] = useState(); const [ contextInitError, setContextInitError, - ] = React.useState(); + ] = useState(); const [ coreAiContext, setCoreAiContext, - ] = React.useState(); + ] = useState(); // // Initialize the AI context and get the contextId diff --git a/packages/react/core/src/types/AiContext.tsx b/packages/react/core/src/types/AiContext.ts similarity index 90% rename from packages/react/core/src/types/AiContext.tsx rename to packages/react/core/src/types/AiContext.ts index db600e7c..a58c2185 100644 --- a/packages/react/core/src/types/AiContext.tsx +++ b/packages/react/core/src/types/AiContext.ts @@ -13,7 +13,7 @@ export type AiContextProviderProps = { * This object is created as a result of calling createAiContext(). * * The Provider property is a React component that provides the AI context to the children. - * To be used as context aware app .. + * To be used as Context Aware App ... * * The ref property is a React context that can be used to access the React context value. * Do not use the ref property directly, the useAiContext() and useAiTask() hooks should be used instead. diff --git a/packages/react/core/src/ui/Avatar/AvatarComp.tsx b/packages/react/core/src/ui/Avatar/AvatarComp.tsx new file mode 100644 index 00000000..6ab16684 --- /dev/null +++ b/packages/react/core/src/ui/Avatar/AvatarComp.tsx @@ -0,0 +1,34 @@ +import {isValidElement} from 'react'; +import {className as compAvatarClassName} from '../../../../../shared/src/ui/Avatar/create'; +import { + renderedInitialsClassName as compAvatarInitialsClassName, + renderedPhotoClassName as compAvatarPhotoClassName, + renderedPhotoContainerClassName as compAvatarPhotoContainerClassName, +} from '../../../../../shared/src/ui/Avatar/utils/createPhotoContainerFromUrl'; +import {AvatarProps} from './props'; + +export const AvatarComp = (props: AvatarProps) => { + const isPictureUrl = typeof props.picture === 'string'; + const isPictureElement = !isPictureUrl && isValidElement(props.picture); + + return ( +
+ {isPictureElement && props.picture} + {!isPictureElement && isPictureUrl && ( +
+ + {props.name && props.name.length > 0 ? props.name[0].toUpperCase() : ''} + + {props.picture && ( +
+ )} +
+ )} +
+ ); +}; diff --git a/packages/react/core/src/ui/Avatar/props.ts b/packages/react/core/src/ui/Avatar/props.ts new file mode 100644 index 00000000..72f80eec --- /dev/null +++ b/packages/react/core/src/ui/Avatar/props.ts @@ -0,0 +1,6 @@ +import {ReactElement} from 'react'; + +export type AvatarProps = { + name?: string; + picture?: string | ReactElement; +}; diff --git a/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx b/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx new file mode 100644 index 00000000..1766aeb9 --- /dev/null +++ b/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx @@ -0,0 +1,89 @@ +import {FC, forwardRef, ReactElement, Ref, useImperativeHandle, useMemo, useRef} from 'react'; +import {className as compChatItemClassName} from '../../../../../shared/src/ui/ChatItem/create'; +import { + directionClassName as compChatItemDirectionClassName, +} from '../../../../../shared/src/ui/ChatItem/utils/applyNewDirectionClassName'; +import {StreamContainerImperativeProps} from '../../logic/StreamContainer/props'; +import {StreamContainerComp} from '../../logic/StreamContainer/StreamContainerComp'; +import {AvatarComp} from '../Avatar/AvatarComp'; +import {MessageComp} from '../Message/MessageComp'; +import {ChatItemImperativeProps, ChatItemProps} from './props'; + +export const ChatItemComp: ( + props: ChatItemProps, + ref: Ref, +) => ReactElement = function ( + props: ChatItemProps, + ref: Ref, +): ReactElement { + const picture = useMemo(() => { + if (props.picture === undefined && props.name === undefined) { + return null; + } + + return ; + }, [props.picture, props.name]); + + const streamContainer = useRef(null); + + useImperativeHandle(ref, () => ({ + streamChunk: (chunk: string) => { + if (streamContainer?.current) { + streamContainer.current.streamChunk(chunk); + } + }, + }), []); + + const isStreaming = useMemo( + () => props.status === 'streaming', + [props.status], + ); + + const compDirectionClassName = props.direction + ? compChatItemDirectionClassName[props.direction] + : compChatItemDirectionClassName['incoming']; + + const className = `${compChatItemClassName} ${compDirectionClassName}`; + const MessageRenderer: FC = useMemo(() => { + if (props.customRenderer) { + if (props.message === undefined) { + return () => null; + } + + return () => props.customRenderer ? props.customRenderer({ + message: props.message as AiMsg, + }) : null; + } + + // TODO - Markdown support + return () => <>{props.message !== undefined ? props.message : ''}; + }, [props.customRenderer, props.message]); + + const ForwardRefStreamContainerComp = forwardRef( + StreamContainerComp, + ); + + return ( +
+ {picture} + {isStreaming && ( + + )} + {!isStreaming && ( + + )} +
+ ); +}; diff --git a/packages/react/core/src/ui/ChatItem/props.ts b/packages/react/core/src/ui/ChatItem/props.ts new file mode 100644 index 00000000..5498b5a8 --- /dev/null +++ b/packages/react/core/src/ui/ChatItem/props.ts @@ -0,0 +1,17 @@ +import {FunctionComponent, ReactElement} from 'react'; +import {MessageDirection} from '../../../../../shared/src/ui/Message/props'; + +export type ChatItemProps = { + uid: string; + direction: MessageDirection; + status: 'rendered' | 'streaming' | 'loading' | 'error'; + loader?: ReactElement; + message?: AiMsg | string; + customRenderer?: FunctionComponent<{message: AiMsg}>; + name?: string; + picture?: string | ReactElement; +}; + +export type ChatItemImperativeProps = { + streamChunk: (chunk: string) => void; +}; diff --git a/packages/react/core/src/ui/ExceptionsBox/ExceptionsBoxComp.tsx b/packages/react/core/src/ui/ExceptionsBox/ExceptionsBoxComp.tsx new file mode 100644 index 00000000..09a52c44 --- /dev/null +++ b/packages/react/core/src/ui/ExceptionsBox/ExceptionsBoxComp.tsx @@ -0,0 +1,8 @@ +import {className as compExceptionsBoxClassName} from '../../../../../shared/src/ui/ExceptionsBox/create'; +import {ExceptionsBoxProps} from './props'; + +export const ExceptionsBoxComp = (props: ExceptionsBoxProps) => { + return ( +
{props.message}
+ ); +}; diff --git a/packages/react/core/src/ui/ExceptionsBox/props.ts b/packages/react/core/src/ui/ExceptionsBox/props.ts new file mode 100644 index 00000000..c98c4893 --- /dev/null +++ b/packages/react/core/src/ui/ExceptionsBox/props.ts @@ -0,0 +1,3 @@ +export type ExceptionsBoxProps = { + message: string; +}; diff --git a/packages/react/core/src/ui/Loader/LoaderComp.tsx b/packages/react/core/src/ui/Loader/LoaderComp.tsx new file mode 100644 index 00000000..94fc7b70 --- /dev/null +++ b/packages/react/core/src/ui/Loader/LoaderComp.tsx @@ -0,0 +1,9 @@ +import {className as compLoaderClassName} from '../../../../../shared/src/ui/Loader/create'; + +export const LoaderComp = () => { + return ( +
+
+
+ ); +}; diff --git a/packages/react/core/src/ui/Message/MessageComp.tsx b/packages/react/core/src/ui/Message/MessageComp.tsx new file mode 100644 index 00000000..00eab339 --- /dev/null +++ b/packages/react/core/src/ui/Message/MessageComp.tsx @@ -0,0 +1,52 @@ +import {className as compMessageClassName} from '../../../../../shared/src/ui/Message/create'; +import { + directionClassName as compMessageDirectionClassName, +} from '../../../../../shared/src/ui/Message/utils/applyNewDirectionClassName'; +import { + statusClassName as compMessageStatusClassName, +} from '../../../../../shared/src/ui/Message/utils/applyNewStatusClassName'; +import {LoaderComp} from '../Loader/LoaderComp'; +import {MessageProps} from './props'; + +export const MessageComp = function (props: MessageProps) { + const compStatusClassName = props.status + ? compMessageStatusClassName[props.status] + : compMessageStatusClassName['rendered']; + + const compDirectionClassName = props.direction + ? compMessageDirectionClassName[props.direction] + : compMessageDirectionClassName['incoming']; + + const className = `${compMessageClassName} ${compStatusClassName} ${compDirectionClassName}`; + + if (props.status === 'streaming') { + return ( +
+ ); + } + + if (props.status === 'loading') { + const loader = props.loader ?? ; + return ( +
+ {loader} +
+ ); + } + + if (props.status === 'error') { + return ( +
+ Error +
+ ); + } + + const message = typeof props.message === 'function' ? props.message() : props.message; + + return ( +
+ {message} +
+ ); +}; diff --git a/packages/react/core/src/ui/Message/props.ts b/packages/react/core/src/ui/Message/props.ts new file mode 100644 index 00000000..20010be2 --- /dev/null +++ b/packages/react/core/src/ui/Message/props.ts @@ -0,0 +1,10 @@ +import {FC, ReactElement, ReactNode} from 'react'; +import {MessageDirection} from '../../../../../shared/src/ui/Message/props'; + +export type MessageProps = { + uid: string; + direction: MessageDirection; + status: 'rendered' | 'streaming' | 'loading' | 'error'; + loader?: ReactElement; + message?: ReactNode | FC; +}; diff --git a/packages/react/core/src/ui/PromptBox/PromptBoxComp.tsx b/packages/react/core/src/ui/PromptBox/PromptBoxComp.tsx new file mode 100644 index 00000000..7918f44b --- /dev/null +++ b/packages/react/core/src/ui/PromptBox/PromptBoxComp.tsx @@ -0,0 +1,78 @@ +import {ChangeEvent, KeyboardEvent, useEffect, useMemo, useRef} from 'react'; +import {className as compPromptBoxClassName} from '../../../../../shared/src/ui/PromptBox/create'; +import { + statusClassName as compPromptBoxStatusClassName, +} from '../../../../../shared/src/ui/PromptBox/utils/applyNewStatusClassName'; +import {LoaderComp} from '../Loader/LoaderComp'; +import {SendIconComp} from '../SendIcon/SendIconComp'; +import {PromptBoxProps} from './props'; + +export const PromptBoxComp = (props: PromptBoxProps) => { + const compClassNameFromStats = compPromptBoxStatusClassName[props.status] || ''; + const className = `${compPromptBoxClassName} ${compClassNameFromStats}`; + + const disableTextarea = props.status === 'submitting'; + const disableButton = !props.hasValidInput || props.status === 'submitting' || props.status === 'waiting'; + const showSendIcon = props.status === 'typing'; + + const textareaRef = useRef(null); + useEffect(() => { + if (props.status === 'typing' && props.autoFocus && textareaRef.current) { + textareaRef.current.focus(); + } + }, [props.status, props.autoFocus, textareaRef.current]); + + const handleChange = useMemo(() => (e: ChangeEvent) => { + props.onChange?.(e.target.value); + }, [props.onChange]); + + const handleSubmit = useMemo(() => () => { + props.onSubmit?.(); + }, [props.onSubmit]); + + const handleKeyDown = useMemo(() => (e: KeyboardEvent) => { + if (!props.submitShortcut || props.submitShortcut === 'Enter') { + const isEnter = e.key === 'Enter'; + const aModifierKeyIsPressed = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; + if (isEnter && !aModifierKeyIsPressed) { + handleSubmit(); + e.preventDefault(); + } + + return; + } + + if (props.submitShortcut === 'CommandEnter') { + const isCommandEnter = e.key === 'Enter' && ( + e.getModifierState('Control') || e.getModifierState('Meta') + ); + + if (isCommandEnter) { + handleSubmit(); + e.preventDefault(); + } + } + }, [handleSubmit, props.submitShortcut]); + + return ( +
+ ')); + expect(html).toEqual( + expect.stringContaining('