├── .gitignore ├── LICENSE ├── README.md ├── js ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── README.md ├── docs │ ├── build.md │ └── entry.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── shopify.md ├── src │ ├── assets │ │ ├── immutable.css │ │ └── index.css │ ├── components │ │ ├── Bot.tsx │ │ ├── Button.tsx │ │ ├── ConversationContainer │ │ │ ├── AvatarSideContainer.tsx │ │ │ ├── ChatChunk.tsx │ │ │ ├── ConversationContainer.tsx │ │ │ ├── LoadingChunk.tsx │ │ │ ├── PopupBlockedToast.tsx │ │ │ └── index.ts │ │ ├── ErrorMessage.tsx │ │ ├── InputChatBlock.tsx │ │ ├── LiteBadge.tsx │ │ ├── SendButton.tsx │ │ ├── Spinner.tsx │ │ ├── StreamConversation │ │ │ ├── AvatarSideContainer.tsx │ │ │ ├── ChatChunk.tsx │ │ │ ├── LoadingChunk.tsx │ │ │ ├── PopupBlockedToast.tsx │ │ │ ├── StreamConversation.tsx │ │ │ ├── StreamInput.tsx │ │ │ └── index.ts │ │ ├── TypingBubble.tsx │ │ ├── avatars │ │ │ ├── Avatar.tsx │ │ │ └── DefaultAvatar.tsx │ │ ├── bubbles │ │ │ ├── GuestBubble.tsx │ │ │ ├── HostBubble.tsx │ │ │ ├── LoadingBubble.tsx │ │ │ └── StreamingBubble.tsx │ │ ├── icons │ │ │ ├── CheckIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── CloseIcon.tsx │ │ │ ├── ExternalLinkIcon.tsx │ │ │ ├── PdLogo.tsx │ │ │ ├── SendIcon.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── inputs │ │ │ ├── SearchInput.tsx │ │ │ ├── ShortTextInput.tsx │ │ │ ├── Textarea.tsx │ │ │ └── index.ts │ ├── constants.ts │ ├── env.d.ts │ ├── features │ │ ├── blocks │ │ │ ├── bubbles │ │ │ │ ├── audio │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AudioBubble.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── embed │ │ │ │ │ ├── components │ │ │ │ │ │ ├── EmbedBubble.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── image │ │ │ │ │ ├── components │ │ │ │ │ │ └── ImageBubble.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── textBubble │ │ │ │ │ ├── components │ │ │ │ │ │ ├── TextBubble.tsx │ │ │ │ │ │ └── plate │ │ │ │ │ │ │ └── PlateText.tsx │ │ │ │ │ ├── helpers │ │ │ │ │ │ ├── applyFilterRichText.ts │ │ │ │ │ │ ├── computeTypingDuration.ts │ │ │ │ │ │ └── convertRichTextToPlainText.ts │ │ │ │ │ └── index.ts │ │ │ │ └── video │ │ │ │ │ ├── components │ │ │ │ │ └── VideoBubble.tsx │ │ │ │ │ └── index.ts │ │ │ ├── inputs │ │ │ │ ├── buttons │ │ │ │ │ └── components │ │ │ │ │ │ ├── Buttons.tsx │ │ │ │ │ │ ├── Checkbox.tsx │ │ │ │ │ │ └── MultipleChoicesForm.tsx │ │ │ │ ├── date │ │ │ │ │ ├── components │ │ │ │ │ │ └── DateForm.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils │ │ │ │ │ │ └── parseReadableDate.ts │ │ │ │ ├── fileUpload │ │ │ │ │ ├── components │ │ │ │ │ │ └── FileUploadForm.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── payment │ │ │ │ │ ├── components │ │ │ │ │ │ ├── PaymentForm.tsx │ │ │ │ │ │ ├── StripePaymentForm.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── helpers │ │ │ │ │ │ └── paymentInProgressStorage.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── pictureChoice │ │ │ │ │ ├── MultiplePictureChoice.tsx │ │ │ │ │ └── SinglePictureChoice.tsx │ │ │ │ ├── rating │ │ │ │ │ ├── components │ │ │ │ │ │ └── RatingForm.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── textInput │ │ │ │ │ ├── components │ │ │ │ │ └── TextInput.tsx │ │ │ │ │ └── index.ts │ │ │ ├── integrations │ │ │ │ ├── chatwoot │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils │ │ │ │ │ │ ├── executeChatwoot.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── googleAnalytics │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils │ │ │ │ │ │ ├── executeGoogleAnalytics.ts │ │ │ │ │ │ └── index.ts │ │ │ │ └── pixel │ │ │ │ │ └── executePixel.ts │ │ │ └── logic │ │ │ │ ├── redirect │ │ │ │ ├── index.ts │ │ │ │ └── utils │ │ │ │ │ ├── executeRedirect.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── script │ │ │ │ └── executeScript.ts │ │ │ │ ├── setVariable │ │ │ │ └── executeSetVariable.ts │ │ │ │ └── wait │ │ │ │ └── utils │ │ │ │ └── executeWait.ts │ │ ├── bubble │ │ │ ├── components │ │ │ │ ├── Bubble.tsx │ │ │ │ ├── BubbleButton.tsx │ │ │ │ ├── PreviewMessage.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── button │ │ │ └── index.tsx │ │ ├── commands │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── close.ts │ │ │ │ ├── hidePreviewMessage.ts │ │ │ │ ├── index.ts │ │ │ │ ├── open.ts │ │ │ │ ├── setInputValue.ts │ │ │ │ ├── setPrefilledVariables.ts │ │ │ │ ├── showPreviewMessage.ts │ │ │ │ └── toggle.ts │ │ ├── popup │ │ │ ├── components │ │ │ │ ├── Popup.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── standard │ │ │ ├── components │ │ │ ├── Standard.tsx │ │ │ └── index.ts │ │ │ └── index.ts │ ├── global.d.ts │ ├── hooks │ │ └── useInitialActions.tsx │ ├── image.d.ts │ ├── index.ts │ ├── lib │ │ ├── gtag.ts │ │ ├── gtm.ts │ │ ├── hexToRgb.ts │ │ ├── phoneCountries.ts │ │ ├── pixel.ts │ │ ├── stripe.ts │ │ └── utils.ts │ ├── queries │ │ ├── getInitialChatReplyQuery.ts │ │ └── sendMessageQuery.ts │ ├── register.tsx │ ├── schemas │ │ ├── features │ │ │ ├── agent │ │ │ │ ├── agent.ts │ │ │ │ ├── index.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── theme │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schemas.ts │ │ │ │ └── variable.ts │ │ │ ├── blocks │ │ │ │ ├── baseSchemas.ts │ │ │ │ ├── bubbles │ │ │ │ │ ├── audio.ts │ │ │ │ │ ├── embed.ts │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── image.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── text.ts │ │ │ │ │ └── video │ │ │ │ │ │ ├── enums.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── schemas.ts │ │ │ │ ├── index.ts │ │ │ │ ├── inputs │ │ │ │ │ ├── choice.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── date.ts │ │ │ │ │ ├── email.ts │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── file.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── number.ts │ │ │ │ │ ├── payment │ │ │ │ │ │ ├── enums.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── schemas.ts │ │ │ │ │ ├── phone.ts │ │ │ │ │ ├── pictureChoice.ts │ │ │ │ │ ├── rating.ts │ │ │ │ │ ├── text.ts │ │ │ │ │ └── url.ts │ │ │ │ ├── integrations │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── googleAnalytics.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── pixel │ │ │ │ │ │ ├── constants.ts │ │ │ │ │ │ └── schemas.ts │ │ │ │ ├── logic │ │ │ │ │ ├── abTest.ts │ │ │ │ │ ├── agentLink.ts │ │ │ │ │ ├── condition.ts │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jump.ts │ │ │ │ │ ├── redirect.ts │ │ │ │ │ ├── script.ts │ │ │ │ │ ├── setVariable.ts │ │ │ │ │ └── wait.ts │ │ │ │ ├── schemas.ts │ │ │ │ └── start │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schemas.ts │ │ │ ├── chat.ts │ │ │ ├── items │ │ │ │ ├── baseSchemas.ts │ │ │ │ ├── enums.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schemas.ts │ │ │ │ └── types.ts │ │ │ ├── result.ts │ │ │ └── utils.ts │ │ └── index.ts │ ├── types.ts │ ├── utils │ │ ├── executeClientSideActions.ts │ │ ├── getApiEndPoint.ts │ │ ├── injectStartProps.ts │ │ ├── isMobileSignal.ts │ │ ├── setCssVariablesValue.ts │ │ ├── storage.ts │ │ ├── streamingMessageSignal.ts │ │ └── transformMessages.ts │ ├── web.ts │ └── window.ts ├── tailwind.config.cjs ├── tsconfig.json └── tsconfig │ ├── base.json │ ├── nextjs.json │ └── react-library.json ├── nextjs ├── .eslintrc.cjs ├── .npmignore ├── Development.md ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src │ └── index.ts ├── tsconfig.json └── tsconfig │ ├── base.json │ └── nextjs.json ├── package.json └── react ├── .eslintrc.cjs ├── .npmignore ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Bubble.tsx ├── Popup.tsx ├── Standard.tsx └── index.ts ├── tsconfig.json └── tsconfig ├── base.json └── react-library.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules folders 2 | **/node_modules/ 3 | 4 | # Ignore dist folders 5 | **/dist/ 6 | 7 | # Ignore .env files 8 | *.env* 9 | 10 | # Mac 11 | **/.DS_Store -------------------------------------------------------------------------------- /js/.eslintignore: -------------------------------------------------------------------------------- 1 | /src/react/** -------------------------------------------------------------------------------- /js/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['custom', 'plugin:solid/typescript'], 4 | plugins: ['solid'], 5 | rules: { 6 | '@next/next/no-img-element': 'off', 7 | '@next/next/no-html-link-for-pages': 'off', 8 | 'solid/no-innerhtml': 'off', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /js/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .eslintignore 3 | .eslintrc.cjs 4 | rollup.config.js 5 | tailwind.config.cjs 6 | tsconfig.json -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # Agent embed JS library 2 | 3 | Add an [OpenAI Chatbot](https://predictabledialogs.com) to your website using this library. 4 | 5 | ## Pre-requisite 6 | Create an agent on [https://predictabledialogs.com](https://predictabledialogs.com) 7 | 8 | ## Installation 9 | 10 | ### Using npm 11 | 12 | To install, run: 13 | 14 | ```bash 15 | npm install agent-embed 16 | ``` 17 | 18 | ## Standard Embed 19 | 20 | Here is a code example: 21 | 22 | ```html 23 | 30 | 31 | 32 | ``` 33 | 34 | This code is creating a container with a 100% width (will match parent width) and 600px height. 35 | 36 | ## Popup 37 | 38 | Here is an example: 39 | 40 | ```html 41 | 49 | ``` 50 | 51 | This code will automatically trigger the popup window after 3 seconds. 52 | 53 | ### Open or Close a popup 54 | 55 | You can use these commands: 56 | 57 | ```js 58 | Agent.open() 59 | ``` 60 | 61 | ```js 62 | Agent.close() 63 | ``` 64 | 65 | ```js 66 | Agent.toggle() 67 | ``` 68 | 69 | You can bind these commands on a button element, for example: 70 | 71 | ```html 72 | 73 | ``` 74 | 75 | ## Bubble 76 | 77 | Here is an example: 78 | 79 | ```html 80 | 97 | ``` 98 | 99 | This code will show the bubble and let a preview message appear after 5 seconds. 100 | 101 | ### Open or close the preview message 102 | 103 | You can use these commands: 104 | 105 | ```js 106 | Agent.showPreviewMessage() 107 | ``` 108 | 109 | ```js 110 | Agent.hidePreviewMessage() 111 | ``` 112 | 113 | ### Open or close the agent 114 | 115 | You can use these commands: 116 | 117 | ```js 118 | Agent.open() 119 | ``` 120 | 121 | ```js 122 | Agent.close() 123 | ``` 124 | 125 | ```js 126 | Agent.toggle() 127 | ``` 128 | 129 | You can bind these commands on a button element, for example: 130 | 131 | ```html 132 | 133 | ``` 134 | -------------------------------------------------------------------------------- /js/docs/build.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | The build creates an index.js & web.js 4 | 5 | ### web.js (CDN/Browser Bundle) 6 | This is the browser-specific bundle used for direct 29 | ``` 30 | 31 | Save the changes. 32 | Using Theme Customizer (for basic scripts): 33 | 34 | Some themes have sections where you can insert custom scripts without editing the code directly. Look for a section like Theme settings or Custom HTML. 35 | Add your JavaScript code in the provided area. 36 | 37 | Step 4: Test Your Script 38 | Preview your theme to see if the JavaScript works as intended. 39 | Check for any console errors and ensure the script doesn't break any existing functionality. 40 | 41 | 42 | ### Important Considerations: 43 | Backup Your Theme: Always backup your theme before making changes. 44 | 45 | Performance Impact: Be aware that adding too much JavaScript can slow down your site. 46 | 47 | Compatibility: Ensure your script is compatible with major browsers. 48 | 49 | Shopify Guidelines: Follow Shopify's guidelines and limitations for custom code. 50 | 51 | Using External JavaScript Files: 52 | If you prefer to use an external JS file: 53 | Upload the file to your Shopify store's Files section. 54 | Link to the file in your theme.liquid using the URL provided by Shopify after upload. 55 | 56 | Debugging: 57 | Use browser developer tools for debugging and testing your JavaScript. 58 | Advanced Customization: 59 | For more complex tasks, consider hiring a professional Shopify developer or using Shopify apps that can add the desired functionality without direct coding. 60 | 61 | Remember, the exact steps might slightly vary depending on your specific theme and its features. Always test thoroughly to ensure the custom JS doesn't interfere with your store's functionality. 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /js/src/assets/immutable.css: -------------------------------------------------------------------------------- 1 | #lite-badge { 2 | position: absolute !important; 3 | padding: 4px 8px !important; 4 | background-color: white !important; 5 | z-index: 50 !important; 6 | border-radius: 4px !important; 7 | color: rgb(17 24 39) !important; 8 | gap: 8px !important; 9 | font-size: 14px !important; 10 | line-height: 20px !important; 11 | font-weight: 600 !important; 12 | border-width: 1px !important; 13 | /* Make sure the badge is always displayed */ 14 | opacity: 1 !important; 15 | visibility: visible !important; 16 | display: flex !important; 17 | top: auto !important; 18 | right: auto !important; 19 | left: auto !important; 20 | bottom: 20px !important; 21 | transition: background-color 0.2s ease-in-out !important; 22 | } 23 | 24 | #lite-badge:hover { 25 | background-color: #f7f8ff !important; 26 | } 27 | -------------------------------------------------------------------------------- /js/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { children, JSX, Show, splitProps } from 'solid-js'; 2 | import { Spinner } from './Spinner'; 3 | 4 | type Props = { 5 | variant?: 'primary' | 'secondary'; 6 | children: JSX.Element; 7 | isDisabled?: boolean; 8 | isLoading?: boolean; 9 | } & JSX.ButtonHTMLAttributes; 10 | 11 | export const Button = (props: Props) => { 12 | const childrenReturn = children(() => props.children); 13 | const [local, buttonProps] = splitProps(props, ['disabled', 'class']); 14 | 15 | return ( 16 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /js/src/components/ConversationContainer/AvatarSideContainer.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup, onMount } from 'solid-js'; 2 | import { isMobile } from '@/utils/isMobileSignal'; 3 | import { Avatar } from '../avatars/Avatar'; 4 | 5 | type Props = { hostAvatarSrc?: string; hideAvatar?: boolean }; 6 | 7 | export const AvatarSideContainer = (props: Props) => { 8 | let avatarContainer: HTMLDivElement | undefined; 9 | const [top, setTop] = createSignal(0); 10 | 11 | const resizeObserver = new ResizeObserver((entries) => 12 | setTop(entries[0].target.clientHeight - (isMobile() ? 24 : 40)) 13 | ); 14 | 15 | onMount(() => { 16 | if (avatarContainer) { 17 | resizeObserver.observe(avatarContainer); 18 | } 19 | }); 20 | 21 | onCleanup(() => { 22 | if (avatarContainer) { 23 | resizeObserver.unobserve(avatarContainer); 24 | } 25 | }); 26 | 27 | return ( 28 |
35 |
46 | 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /js/src/components/ConversationContainer/LoadingChunk.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@/schemas'; 2 | import { Show } from 'solid-js'; 3 | import { LoadingBubble } from '../bubbles/LoadingBubble'; 4 | import { AvatarSideContainer } from './AvatarSideContainer'; 5 | 6 | type Props = { 7 | theme: Theme; 8 | }; 9 | 10 | export const LoadingChunk = (props: Props) => ( 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | ); 22 | 23 | export const ConnectingChunk = () => ( 24 |
25 |
26 |
27 |

28 | Connecting 29 | 33 | . 34 | 35 | 39 | . 40 | 41 | 45 | . 46 | 47 |

48 |
49 |
50 |
51 | ); 52 | -------------------------------------------------------------------------------- /js/src/components/ConversationContainer/PopupBlockedToast.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | url: string; 3 | onLinkClick: () => void; 4 | }; 5 | 6 | export const PopupBlockedToast = (props: Props) => { 7 | return ( 8 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /js/src/components/ConversationContainer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConversationContainer' 2 | -------------------------------------------------------------------------------- /js/src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | error: Error; 3 | }; 4 | export const ErrorMessage = (props: Props) => { 5 | return ( 6 |
7 |

{props.error.message}

8 |

{props.error.cause as string}

9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /js/src/components/InputChatBlock.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ChatReply, TextInputBlock, 3 | Theme 4 | } from '../schemas'; 5 | import { BotContext, InputSubmitContent } from '@/types'; 6 | import { TextInput } from '@/features/blocks/inputs/textInput'; 7 | import { createSignal, Switch, Match } from 'solid-js'; 8 | import { isNotDefined } from '@/lib/utils'; 9 | import { isMobile } from '@/utils/isMobileSignal'; 10 | 11 | type Props = { 12 | ref: HTMLDivElement | undefined; 13 | block: NonNullable; 14 | hasHostAvatar: boolean; 15 | guestAvatar?: Theme['chat']['guestAvatar']; 16 | inputIndex: number; 17 | activeInputId: number; 18 | context: BotContext; 19 | isInputPrefillEnabled: boolean; 20 | hasError: boolean; 21 | onSubmit: (answer: string) => void; 22 | onSkip: () => void; 23 | }; 24 | 25 | export const InputChatBlock = (props: Props) => { 26 | const [answer, setAnswer] = createSignal(); 27 | 28 | const handleSubmit = async ({ label, value }: InputSubmitContent) => { 29 | setAnswer(label ?? value); 30 | props.onSubmit(value ?? label); 31 | }; 32 | 33 | const handleSkip = (label: string) => { 34 | setAnswer(label); 35 | props.onSkip(); 36 | }; 37 | 38 | return ( 39 | 40 | 41 | {props.inputIndex === props.activeInputId && ( 42 |
47 | {props.hasHostAvatar && ( 48 |
51 | )} 52 | 60 |
61 | )} 62 | 63 | 64 | ); 65 | }; 66 | 67 | const Input = (props: { 68 | context: BotContext; 69 | block: NonNullable; 70 | inputIndex: number; 71 | isInputPrefillEnabled: boolean; 72 | onSubmit: (answer: InputSubmitContent) => void; 73 | onSkip: (label: string) => void; 74 | }) => { 75 | const onSubmit = (answer: InputSubmitContent) => props.onSubmit(answer); 76 | 77 | const getPrefilledValue = () => 78 | props.isInputPrefillEnabled ? props.block?.prefilledValue : undefined; 79 | 80 | return ( 81 | 87 | ); 88 | }; -------------------------------------------------------------------------------- /js/src/components/LiteBadge.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, onMount } from 'solid-js'; 2 | import { PredictableText } from './icons/PdLogo'; 3 | 4 | type Props = { 5 | botContainer: HTMLDivElement | undefined; 6 | }; 7 | 8 | export const LiteBadge = (props: Props) => { 9 | let liteBadge: HTMLAnchorElement | undefined; 10 | let observer: MutationObserver | undefined; 11 | 12 | const appendBadgeIfNecessary = (mutations: MutationRecord[]) => { 13 | mutations.forEach((mutation) => { 14 | mutation.removedNodes.forEach((removedNode) => { 15 | if ('id' in removedNode && liteBadge && removedNode.id == 'lite-badge') { 16 | console.log("Sorry, you can't remove the brand 😅"); 17 | props.botContainer?.append(liteBadge); 18 | } 19 | }); 20 | }); 21 | }; 22 | 23 | onMount(() => { 24 | if (!document || !props.botContainer) return; 25 | observer = new MutationObserver(appendBadgeIfNecessary); 26 | observer.observe(props.botContainer, { 27 | subtree: false, 28 | childList: true, 29 | }); 30 | }); 31 | 32 | onCleanup(() => { 33 | if (observer) observer.disconnect(); 34 | }); 35 | 36 | return ( 37 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /js/src/components/SendButton.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile } from '@/utils/isMobileSignal'; 2 | import { splitProps } from 'solid-js'; 3 | import { JSX } from 'solid-js/jsx-runtime'; 4 | import { SendIcon } from './icons'; 5 | import { Button } from './Button'; 6 | 7 | type SendButtonProps = { 8 | isDisabled?: boolean; 9 | isLoading?: boolean; 10 | disableIcon?: boolean; 11 | } & JSX.ButtonHTMLAttributes; 12 | 13 | export const SendButton = (props: SendButtonProps) => { 14 | const [local, others] = splitProps(props, ['disableIcon']); 15 | return ( 16 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /js/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export const Spinner = (props: JSX.SvgSVGAttributes) => ( 4 | 12 | 13 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /js/src/components/StreamConversation/AvatarSideContainer.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup, onMount } from 'solid-js'; 2 | import { isMobile } from '@/utils/isMobileSignal'; 3 | import { Avatar } from '../avatars/Avatar'; 4 | 5 | type Props = { hostAvatarSrc?: string; hideAvatar?: boolean, isPersisted?: boolean }; 6 | 7 | export const AvatarSideContainer = (props: Props) => { 8 | let avatarContainer: HTMLDivElement | undefined; 9 | const [top, setTop] = createSignal(0); 10 | 11 | const resizeObserver = new ResizeObserver((entries) => 12 | setTop(entries[0].target.clientHeight - (isMobile() ? 24 : 40)) 13 | ); 14 | 15 | onMount(() => { 16 | if (avatarContainer) { 17 | resizeObserver.observe(avatarContainer); 18 | } 19 | }); 20 | 21 | onCleanup(() => { 22 | if (avatarContainer) { 23 | resizeObserver.unobserve(avatarContainer); 24 | } 25 | }); 26 | 27 | return ( 28 |
35 |
46 | 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /js/src/components/StreamConversation/ChatChunk.tsx: -------------------------------------------------------------------------------- 1 | import { BotContext, ChatChunk as ChatChunkType } from '@/types'; 2 | import { isMobile } from '@/utils/isMobileSignal'; 3 | import type { ChatReply, Settings, Theme } from '@/schemas'; 4 | import { onMount, Show } from 'solid-js'; 5 | import { HostBubble } from '../bubbles/HostBubble'; 6 | import { AvatarSideContainer } from './AvatarSideContainer'; 7 | import { Match, Switch } from 'solid-js'; 8 | import { GuestBubble } from '../bubbles/GuestBubble'; 9 | import { StreamInput } from './StreamInput'; 10 | 11 | type Props = { 12 | message: any; 13 | input?: ChatReply['input']; 14 | theme: Theme; 15 | settings: Settings; 16 | displayIndex: string; 17 | onDisplayAssistantMessage: (bubbleOffsetTop?: number) => void; 18 | context: BotContext; 19 | hasError: boolean; 20 | hideAvatar: boolean; 21 | streamingMessageId: ChatChunkType['streamingMessageId']; 22 | onScrollToBottom: (top?: number) => void; 23 | filterResponse?: (response: string) => string; 24 | streamingHandlers?: { 25 | onInput?: (e: Event) => void; 26 | onSubmit: (e: Event) => void; 27 | }; 28 | isPersisted: boolean; 29 | }; 30 | 31 | export const ChatChunk = (props: Props) => { 32 | let inputRef: HTMLDivElement | undefined; 33 | 34 | return ( 35 |
36 |
37 | 38 | 39 | 42 | 47 | 48 | 49 |
59 | 66 |
67 |
68 | 69 |
70 | 76 |
77 |
78 |
79 |
80 | {props.input && (props.message.id === props.displayIndex) && ( 81 | 91 | )} 92 |
93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /js/src/components/StreamConversation/LoadingChunk.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@/schemas'; 2 | import { Show } from 'solid-js'; 3 | import { LoadingBubble } from '../bubbles/LoadingBubble'; 4 | import { AvatarSideContainer } from './AvatarSideContainer'; 5 | 6 | type Props = { 7 | theme: Theme; 8 | }; 9 | 10 | export const LoadingChunk = (props: Props) => { 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | )}; 23 | 24 | type ErrorProps = { 25 | theme: Theme; 26 | message: string | undefined; 27 | }; 28 | 29 | export const ErrorChunk = (props: ErrorProps) => { 30 | return ( 31 |
32 |
33 |
34 | 35 | 36 | 37 | {props.message} 38 |
39 |
40 |
41 | )}; 42 | 43 | 44 | export const ConnectingChunk = () => ( 45 |
46 |
47 |
48 |

49 | Connecting 50 | 54 | . 55 | 56 | 60 | . 61 | 62 | 66 | . 67 | 68 |

69 |
70 |
71 |
72 | ); 73 | -------------------------------------------------------------------------------- /js/src/components/StreamConversation/PopupBlockedToast.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | url: string; 3 | onLinkClick: () => void; 4 | }; 5 | 6 | export const PopupBlockedToast = (props: Props) => { 7 | return ( 8 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /js/src/components/StreamConversation/StreamInput.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ChatReply, TextInputBlock, 3 | Theme 4 | } from '../../schemas'; 5 | import { BotContext, InputSubmitContent } from '@/types'; 6 | import { TextInput } from '@/features/blocks/inputs/textInput'; 7 | import { createSignal, createEffect, Switch, Match } from 'solid-js'; 8 | import { isNotDefined } from '@/lib/utils'; 9 | import { isMobile } from '@/utils/isMobileSignal'; 10 | 11 | type Props = { 12 | ref: HTMLDivElement | undefined; 13 | block: NonNullable; 14 | hasHostAvatar: boolean; 15 | guestAvatar?: Theme['chat']['guestAvatar']; 16 | context: BotContext; 17 | isInputPrefillEnabled: boolean; 18 | hasError: boolean; 19 | streamingHandlers?: { 20 | onInput?: (e: Event) => void; 21 | onSubmit: (e: Event) => void; 22 | }; 23 | }; 24 | 25 | export const StreamInput = (props: Props) => { 26 | return ( 27 |
32 | {props.hasHostAvatar && ( 33 |
36 | )} 37 | 43 |
44 | ); 45 | }; 46 | 47 | const Input = (props: { 48 | context: BotContext; 49 | block: NonNullable; 50 | isInputPrefillEnabled: boolean; 51 | streamingHandlers?: { 52 | onInput?: (e: Event) => void; 53 | onSubmit: (e: Event) => void; 54 | }; 55 | }) => { 56 | 57 | const getPrefilledValue = () => 58 | props.isInputPrefillEnabled ? props.block?.prefilledValue : undefined; 59 | 60 | return ( 61 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /js/src/components/StreamConversation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StreamConversation' 2 | -------------------------------------------------------------------------------- /js/src/components/TypingBubble.tsx: -------------------------------------------------------------------------------- 1 | export const TypingBubble = () => ( 2 |
3 |
4 |
5 |
6 |
7 | ); 8 | -------------------------------------------------------------------------------- /js/src/components/avatars/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile } from '@/utils/isMobileSignal' 2 | import { createEffect, createSignal, Show } from 'solid-js' 3 | import { isNotEmpty } from '@/lib/utils' 4 | import { DefaultAvatar } from './DefaultAvatar' 5 | 6 | export const Avatar = (props: { initialAvatarSrc?: string, isPersisted?: boolean }) => { 7 | const [avatarSrc, setAvatarSrc] = createSignal(props.initialAvatarSrc) 8 | 9 | createEffect(() => { 10 | if ( 11 | avatarSrc()?.startsWith('{{') && 12 | props.initialAvatarSrc?.startsWith('http') 13 | ) 14 | setAvatarSrc(props.initialAvatarSrc) 15 | }) 16 | 17 | return ( 18 | }> 19 |
26 | Bot avatar 33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /js/src/components/avatars/DefaultAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile } from '@/utils/isMobileSignal' 2 | 3 | export const DefaultAvatar = () => { 4 | return ( 5 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /js/src/components/bubbles/GuestBubble.tsx: -------------------------------------------------------------------------------- 1 | import { Show } from 'solid-js' 2 | import { Avatar } from '../avatars/Avatar' 3 | 4 | type Props = { 5 | message: string 6 | showAvatar: boolean 7 | avatarSrc?: string 8 | isPersisted?: boolean 9 | } 10 | 11 | export const GuestBubble = (props: Props) => ( 12 |
16 | 22 | {props.message} 23 | 24 | 25 | 26 | 27 |
28 | ) 29 | -------------------------------------------------------------------------------- /js/src/components/bubbles/HostBubble.tsx: -------------------------------------------------------------------------------- 1 | import { TextBubble } from '@/features/blocks/bubbles/textBubble'; 2 | import type { 3 | TypingEmulation 4 | } from '@/schemas'; 5 | 6 | type Props = { 7 | message: { 8 | content: string; 9 | }; 10 | // messages: any; 11 | typingEmulation: TypingEmulation; 12 | onTransitionEnd: (offsetTop?: number) => void; 13 | filterResponse?: (response: string) => string; 14 | isPersisted?: boolean; 15 | }; 16 | 17 | export const HostBubble = (props: Props) => { 18 | const onTransitionEnd = (offsetTop?: number) => { 19 | props.onTransitionEnd(offsetTop); 20 | }; 21 | 22 | return ( 23 | {} : onTransitionEnd} 31 | filterResponse={props.filterResponse} 32 | isPersisted={props.isPersisted} 33 | /> 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /js/src/components/bubbles/LoadingBubble.tsx: -------------------------------------------------------------------------------- 1 | import { TypingBubble } from '@/components' 2 | 3 | export const LoadingBubble = () => ( 4 |
5 |
6 |
7 |
15 | 16 |
17 |

22 |

23 |
24 |
25 | ) 26 | -------------------------------------------------------------------------------- /js/src/components/bubbles/StreamingBubble.tsx: -------------------------------------------------------------------------------- 1 | import { streamingMessage } from '@/utils/streamingMessageSignal' 2 | import { createEffect, createSignal } from 'solid-js' 3 | 4 | type Props = { 5 | streamingMessageId: string 6 | } 7 | 8 | export const StreamingBubble = (props: Props) => { 9 | let ref: HTMLDivElement | undefined 10 | const [content, setContent] = createSignal('') 11 | 12 | createEffect(() => { 13 | if (streamingMessage()?.id === props.streamingMessageId) 14 | setContent(streamingMessage()?.content ?? '') 15 | }) 16 | 17 | return ( 18 |
19 |
20 |
21 |
29 |
34 | {content()} 35 |
36 |
37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /js/src/components/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js/jsx-runtime' 2 | 3 | export const CheckIcon = (props: JSX.SvgSVGAttributes) => ( 4 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /js/src/components/icons/ChevronDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js/jsx-runtime' 2 | 3 | export const ChevronDownIcon = (props: JSX.SvgSVGAttributes) => ( 4 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /js/src/components/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js/jsx-runtime' 2 | 3 | export const CloseIcon = (props: JSX.SvgSVGAttributes) => ( 4 | 14 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /js/src/components/icons/ExternalLinkIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js/jsx-runtime' 2 | 3 | export const ExternalLinkIcon = (props: JSX.SvgSVGAttributes) => ( 4 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /js/src/components/icons/SendIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js/jsx-runtime' 2 | 3 | export const SendIcon = (props: JSX.SvgSVGAttributes) => ( 4 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /js/src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SendIcon' 2 | -------------------------------------------------------------------------------- /js/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SendButton' 2 | export * from './TypingBubble' 3 | export * from './inputs' 4 | -------------------------------------------------------------------------------- /js/src/components/inputs/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Show, createSignal, splitProps } from 'solid-js' 2 | import { JSX } from 'solid-js/jsx-runtime' 3 | import { CloseIcon } from '../icons/CloseIcon' 4 | 5 | type Props = { 6 | ref: HTMLInputElement | undefined 7 | onInput: (value: string) => void 8 | onClear: () => void 9 | } & Omit, 'onInput'> 10 | 11 | export const SearchInput = (props: Props) => { 12 | const [value, setValue] = createSignal('') 13 | const [local, others] = splitProps(props, ['onInput', 'ref']) 14 | 15 | const changeValue = (value: string) => { 16 | setValue(value) 17 | local.onInput(value) 18 | } 19 | 20 | const clearValue = () => { 21 | setValue('') 22 | props.onClear() 23 | } 24 | 25 | return ( 26 |
27 | changeValue(e.currentTarget.value)} 34 | {...others} 35 | /> 36 | 0}> 37 | 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /js/src/components/inputs/ShortTextInput.tsx: -------------------------------------------------------------------------------- 1 | import { splitProps } from 'solid-js' 2 | import { JSX } from 'solid-js/jsx-runtime' 3 | 4 | type ShortTextInputProps = { 5 | ref: HTMLInputElement | undefined 6 | onInput: (e: Event) => void 7 | } & Omit, 'onInput'> 8 | 9 | export const ShortTextInput = (props: ShortTextInputProps) => { 10 | const [local, others] = splitProps(props, ['ref', 'onInput']) 11 | 12 | return ( 13 | local.onInput(e)} 19 | {...others} 20 | /> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /js/src/components/inputs/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile } from '@/utils/isMobileSignal' 2 | import { splitProps } from 'solid-js' 3 | import { JSX } from 'solid-js/jsx-runtime' 4 | 5 | type TextareaProps = { 6 | ref: HTMLTextAreaElement | undefined 7 | onInput: (e: Event) => void 8 | } & Omit, 'onInput'> 9 | 10 | export const Textarea = (props: TextareaProps) => { 11 | const [local, others] = splitProps(props, ['ref', 'onInput']) 12 | 13 | return ( 14 |