Compare commits

...

6 Commits

Author SHA1 Message Date
Henry ed2bcda91b add pdf qna 2023-04-24 23:20:26 +01:00
Henry 9c544bf12a Merge branch 'main' into feature/Embed 2023-04-24 19:00:54 +01:00
Henry 7def76a3ef add latest import 2023-04-24 18:49:09 +01:00
Henry 1db2cde22b add embed dialog 2023-04-24 17:59:51 +01:00
Henry e6f5173331 update embed package version 2023-04-24 16:12:11 +01:00
Henry 6258dc3981 add embed package 2023-04-24 14:44:57 +01:00
40 changed files with 1542 additions and 29 deletions

View File

@ -14,7 +14,7 @@ module.exports = {
}
},
parser: '@typescript-eslint/parser',
ignorePatterns: ['**/node_modules', '**/dist', '**/build', '**/package-lock.json'],
ignorePatterns: ['**/node_modules', '**/dist', '**/build', '**/package-lock.json', 'packages/embed/'],
plugins: ['unused-imports'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',

View File

@ -7,7 +7,8 @@
"packages/*",
"flowise",
"ui",
"components"
"components",
"embed"
],
"scripts": {
"build": "turbo run build",
@ -19,8 +20,8 @@
"clean": "npm exec -ws -- rimraf dist build",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"test": "turbo run test",
"lint": "eslint \"**/*.{js,jsx,ts,tsx,json,md}\"",
"lint-fix": "yarn lint --fix",
"lint": "eslint \"**/*.{js,jsx,ts,tsx,json,md}\" && cd packages/embed && yarn lint",
"lint-fix": "yarn lint --fix && cd packages/embed && yarn lint-fix",
"quick": "pretty-quick --staged",
"postinstall": "husky install"
},

View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:solid/typescript'],
plugins: ['@typescript-eslint', 'solid'],
parser: '@typescript-eslint/parser',
ignorePatterns: ['**/*.md'],
rules: {
'@next/next/no-img-element': 'off',
'@next/next/no-html-link-for-pages': 'off',
'solid/no-innerhtml': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}

2
packages/embed/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -0,0 +1,4 @@
src
rollup.config.js
tailwind.config.cjs
tsconfig.json

29
packages/embed/README.md Normal file
View File

@ -0,0 +1,29 @@
<!-- markdownlint-disable MD030 -->
# Flowise Embed
Javascript library to display flowise chatbot on your website
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Install:
```bash
npm i flowise-embed
```
## Embed in your HTML
```
<script type="module">
import Chatbot from 'https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js'
Chatbot.init({
chatflowid: '<chatflowid>',
apiHost: 'http://localhost:3000'
})
</script>
```
## License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).

21
packages/embed/base.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"downlevelIteration": true
},
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,45 @@
{
"name": "flowise-embed",
"version": "1.0.2",
"description": "Javascript library to display flowise chatbot on your website",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "rollup --watch --config rollup.config.js",
"build": "rollup --config rollup.config.js",
"lint": "eslint \"src/**/*.ts*\"",
"lint-fix": "eslint --fix \"src/**/*.ts*\""
},
"license": "MIT",
"dependencies": {
"solid-element": "1.7.0",
"solid-js": "1.7.1"
},
"devDependencies": {
"@babel/preset-typescript": "7.21.4",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-node-resolve": "15.0.1",
"@rollup/plugin-terser": "0.4.0",
"@rollup/plugin-typescript": "11.0.0",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"autoprefixer": "10.4.14",
"babel-preset-solid": "1.7.1",
"eslint": "^8.24.0",
"eslint-config-next": "13.2.4",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-solid": "0.12.0",
"postcss": "8.4.21",
"react": "18.2.0",
"rollup": "3.20.2",
"rollup-plugin-livereload": "2.0.5",
"rollup-plugin-postcss": "4.0.2",
"rollup-plugin-serve": "2.0.2",
"rollup-plugin-typescript-paths": "1.4.0",
"tailwindcss": "3.3.1",
"typescript": "5.0.3"
}
}

View File

@ -0,0 +1,57 @@
import resolve from '@rollup/plugin-node-resolve'
import terser from '@rollup/plugin-terser'
import { babel } from '@rollup/plugin-babel'
import postcss from 'rollup-plugin-postcss'
import autoprefixer from 'autoprefixer'
import tailwindcss from 'tailwindcss'
import typescript from '@rollup/plugin-typescript'
import { typescriptPaths } from 'rollup-plugin-typescript-paths'
// import serve from 'rollup-plugin-serve'
// import livereload from 'rollup-plugin-livereload'
const extensions = ['.ts', '.tsx']
const indexConfig = {
plugins: [
resolve({ extensions }),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: ['solid', '@babel/preset-typescript'],
extensions
}),
postcss({
plugins: [autoprefixer(), tailwindcss()],
extract: false,
modules: false,
autoModules: false,
minimize: true,
inject: false
}),
typescript(),
typescriptPaths({ preserveExtensions: true }),
terser({ output: { comments: false } })
/* If you want to see the live app
serve({
open: true,
verbose: true,
contentBase: ['dist'],
host: 'localhost',
port: 5678
}),
livereload({ watch: 'dist' })*/
]
}
const configs = [
{
...indexConfig,
input: './src/web.ts',
output: {
file: 'dist/web.js',
format: 'es'
}
}
]
export default configs

View File

@ -0,0 +1,239 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:host {
--chatbot-container-bg-image: none;
--chatbot-container-bg-color: transparent;
--chatbot-container-font-family: 'Open Sans';
--chatbot-button-bg-color: #673ab7;
--chatbot-button-color: #ffffff;
--chatbot-host-bubble-bg-color: #f7f8ff;
--chatbot-host-bubble-color: #303235;
--chatbot-guest-bubble-bg-color: #ff8e21;
--chatbot-guest-bubble-color: #ffffff;
--chatbot-input-bg-color: #ffffff;
--chatbot-input-color: #303235;
--chatbot-input-placeholder-color: #9095a0;
--chatbot-header-bg-color: #ffffff;
--chatbot-header-color: #303235;
--chatbot-border-radius: 6px;
/* Phone input */
--PhoneInputCountryFlag-borderColor: transparent;
--PhoneInput-color--focus: transparent;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollable-container::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollable-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.text-fade-in {
transition: opacity 400ms ease-in 200ms;
}
button,
input,
textarea {
font-weight: 300;
}
.text-input::-webkit-input-placeholder {
color: var(--chatbot-input-placeholder-color) !important;
opacity: 1 !important;
}
.text-input::-moz-placeholder {
color: var(--chatbot-input-placeholder-color) !important;
opacity: 1 !important;
}
.text-input::placeholder {
color: var(--chatbot-input-placeholder-color) !important;
opacity: 1 !important;
}
.chatbot-container {
background-image: var(--chatbot-container-bg-image);
background-color: var(--chatbot-container-bg-color);
font-family: var(--chatbot-container-font-family), -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
.chatbot-button {
color: var(--chatbot-button-color);
background-color: var(--chatbot-button-bg-color);
border: 1px solid var(--chatbot-button-bg-color);
border-radius: var(--chatbot-border-radius);
}
.chatbot-button.selectable {
color: var(--chatbot-host-bubble-color);
background-color: var(--chatbot-host-bubble-bg-color);
border: 1px solid var(--chatbot-button-bg-color);
}
.chatbot-input {
color: var(--chatbot-input-color);
background-color: var(--chatbot-input-bg-color);
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1);
border-radius: var(--chatbot-border-radius);
}
.chatbot-input-error-message {
color: var(--chatbot-input-color);
}
.chatbot-button > .send-icon {
fill: var(--chatbot-button-bg-color);
}
.chatbot-chat-view {
max-width: 800px;
}
.secondary-button {
background-color: var(--chatbot-host-bubble-bg-color);
color: var(--chatbot-host-bubble-color);
border-radius: var(--chatbot-border-radius);
}
.messagelist {
width: 100%;
height: 100%;
overflow-y: scroll;
border-radius: 0.5rem;
}
.messagelistloading {
display: flex;
width: 100%;
justify-content: center;
margin-top: 1rem;
}
.usermessage {
padding: 1rem 1.5rem 1rem 1.5rem;
}
.usermessagewaiting-light {
padding: 1rem 1.5rem 1rem 1.5rem;
background: linear-gradient(to left, #ede7f6, #e3f2fd, #ede7f6);
background-size: 200% 200%;
background-position: -100% 0;
animation: loading-gradient 2s ease-in-out infinite;
animation-direction: alternate;
animation-name: loading-gradient;
}
.usermessagewaiting-dark {
padding: 1rem 1.5rem 1rem 1.5rem;
color: #ececf1;
background: linear-gradient(to left, #2e2352, #1d3d60, #2e2352);
background-size: 200% 200%;
background-position: -100% 0;
animation: loading-gradient 2s ease-in-out infinite;
animation-direction: alternate;
animation-name: loading-gradient;
}
@keyframes loading-gradient {
0% {
background-position: -100% 0;
}
100% {
background-position: 100% 0;
}
}
.apimessage {
padding: 1rem 1.5rem 1rem 1.5rem;
animation: fadein 0.5s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.apimessage,
.usermessage,
.usermessagewaiting {
display: flex;
}
.markdownanswer {
line-height: 1.75;
}
.markdownanswer a:hover {
opacity: 0.8;
}
.markdownanswer a {
color: #16bed7;
font-weight: 500;
}
.markdownanswer code {
color: #15cb19;
font-weight: 500;
white-space: pre-wrap !important;
}
.markdownanswer ol,
.markdownanswer ul {
margin: 1rem;
}
.boticon,
.usericon {
margin-right: 1rem;
border-radius: 1rem;
}
.markdownanswer h1,
.markdownanswer h2,
.markdownanswer h3 {
font-size: inherit;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
flex-direction: column;
padding: 10px;
}
.cloud {
width: 400px;
height: calc(100% - 50px);
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
}
input {
background-color: transparent;
border: none;
padding: 10px 10px;
font-family: 'Poppins', sans-serif;
}

View File

@ -0,0 +1,155 @@
import { createSignal, createEffect, For } from 'solid-js'
import { sendMessageQuery } from '@/queries/sendMessageQuery'
import { TextInput } from './inputs/textInput'
type messageType = 'apiMessage' | 'userMessage' | 'usermessagewaiting'
export type MessageType = {
message: string
type: messageType
}
export type BotProps = {
chatflowid: string
apiHost?: string
}
export const Bot = (props: BotProps) => {
let ps: HTMLDivElement | undefined
const [userInput, setUserInput] = createSignal('')
const [loading, setLoading] = createSignal(false)
const [messages, setMessages] = createSignal<MessageType[]>([
{
message: 'Hi there! How can I help?',
type: 'apiMessage'
}
])
const scrollToBottom = () => {
setTimeout(() => {
ps?.scrollTo(0, ps.scrollHeight)
}, 50)
/*if (ps.current) {
const curr: any = ps.current
curr.scrollTo({ top: 1000000, behavior: 'smooth' })
}*/
}
// Handle errors
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }])
setLoading(false)
setUserInput('')
}
// Handle form submission
const handleSubmit = async (value: string) => {
setUserInput(value)
if (value.trim() === '') {
return
}
setLoading(true)
setMessages((prevMessages) => [...prevMessages, { message: value, type: 'userMessage' }])
// Send user question and history to API
const { data, error } = await sendMessageQuery({
chatflowid: props.chatflowid,
apiHost: props.apiHost,
body: {
question: value,
history: messages().filter((msg) => msg.message !== 'Hi there! How can I help?')
}
})
console.log(data)
if (data) {
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
setLoading(false)
setUserInput('')
setTimeout(() => {
scrollToBottom()
}, 100)
}
if (error) {
console.error(error)
const err: any = error
const errorData = err.response.data || `${err.response.status}: ${err.response.statusText}`
handleError(errorData)
return
}
}
// Auto scroll chat to bottom
createEffect(() => {
scrollToBottom()
})
createEffect(() => {
return () => {
setUserInput('')
setLoading(false)
setMessages([
{
message: 'Hi there! How can I help?',
type: 'apiMessage'
}
])
}
})
return (
<>
<div class='cloud'>
<div ref={ps} class='messagelist'>
<For each={messages()}>
{(message, index) => (
// The latest message sent by the user will be animated while waiting for a response
<div
style={{
display: 'flex',
'align-items': 'center',
background: message.type === 'apiMessage' ? '#f7f8ff' : ''
}}
class={
message.type === 'userMessage' && loading() && index() === messages().length - 1
? 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
alt='AI'
width='30'
height='30'
class='boticon'
/>
) : (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
alt='Me'
width='30'
height='30'
class='usericon'
/>
)}
<div class='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<span style={{ 'font-family': 'Poppins, sans-serif' }}>{message.message}</span>
</div>
</div>
)}
</For>
</div>
</div>
<TextInput defaultValue={userInput()} onSubmit={handleSubmit} />
</>
)
}

View File

@ -0,0 +1,46 @@
import { Show } from 'solid-js'
import { JSX } from 'solid-js/jsx-runtime'
import { SendIcon } from './icons'
type SendButtonProps = {
isDisabled?: boolean
isLoading?: boolean
disableIcon?: boolean
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
export const SendButton = (props: SendButtonProps) => {
return (
<button
type='submit'
disabled={props.isDisabled || props.isLoading}
{...props}
class={
'py-2 px-4 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 chatbot-button ' +
props.class
}
style={{ background: 'white', border: 'none' }}
>
<Show when={!props.isLoading} fallback={<Spinner class='text-white' />}>
<SendIcon class={'send-icon flex ' + (props.disableIcon ? 'hidden' : '')} />
</Show>
</button>
)
}
export const Spinner = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
<svg
{...props}
class={'animate-spin -ml-1 mr-3 h-5 w-5 ' + props.class}
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
data-testid='loading-spinner'
>
<circle class='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' stroke-width='4' />
<path
class='opacity-75'
fill='currentColor'
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
/>
</svg>
)

View File

@ -0,0 +1,7 @@
import { JSX } from 'solid-js/jsx-runtime'
export const SendIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' width='19px' color='white' {...props}>
<path d='M476.59 227.05l-.16-.07L49.35 49.84A23.56 23.56 0 0027.14 52 24.65 24.65 0 0016 72.59v113.29a24 24 0 0019.52 23.57l232.93 43.07a4 4 0 010 7.86L35.53 303.45A24 24 0 0016 327v113.31A23.57 23.57 0 0026.59 460a23.94 23.94 0 0013.22 4 24.55 24.55 0 009.52-1.93L476.4 285.94l.19-.09a32 32 0 000-58.8z' />
</svg>
)

View File

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

View File

@ -0,0 +1,22 @@
import { splitProps } from 'solid-js'
import { JSX } from 'solid-js/jsx-runtime'
type ShortTextInputProps = {
ref: HTMLInputElement | undefined
onInput: (value: string) => void
} & Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onInput'>
export const ShortTextInput = (props: ShortTextInputProps) => {
const [local, others] = splitProps(props, ['ref', 'onInput'])
return (
<input
ref={props.ref}
class='focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input'
type='text'
style={{ 'font-size': '16px' }}
onInput={(e) => local.onInput(e.currentTarget.value)}
{...others}
/>
)
}

View File

@ -0,0 +1,57 @@
import { ShortTextInput } from './ShortTextInput'
import { SendButton } from '@/components/SendButton'
import { isMobile } from '@/utils/isMobileSignal'
import { createSignal, onMount } from 'solid-js'
type Props = {
defaultValue?: string
onSubmit: (value: string) => void
}
export const TextInput = (props: Props) => {
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
const handleInput = (inputValue: string) => setInputValue(inputValue)
const checkIfInputIsValid = () => inputValue() !== '' && inputRef?.reportValidity()
const submit = () => {
if (checkIfInputIsValid()) props.onSubmit(inputValue())
setInputValue('')
}
const submitWhenEnter = (e: KeyboardEvent) => {
if (e.key === 'Enter') submit()
}
onMount(() => {
if (!isMobile() && inputRef) inputRef.focus()
})
return (
<div
class={'flex items-end justify-between pr-2 chatbot-input w-full'}
data-testid='input'
style={{
'border-top': '1px solid #eeeeee',
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
right: 0
}}
onKeyDown={submitWhenEnter}
>
<ShortTextInput
ref={inputRef as HTMLInputElement}
onInput={handleInput}
value={inputValue()}
placeholder='Type your question'
/>
<SendButton type='button' isDisabled={inputValue() === ''} class='my-2 ml-2' on:click={submit}>
<span style={{ 'font-family': 'Poppins, sans-serif' }}>Send</span>
</SendButton>
</div>
)
}

View File

@ -0,0 +1 @@
export { TextInput } from './components/TextInput'

View File

@ -0,0 +1,6 @@
import type { BubbleProps } from './features/bubble'
export const defaultBotProps: BubbleProps = {
chatflowid: '',
apiHost: undefined
}

10
packages/embed/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export {}
declare module 'solid-js' {
namespace JSX {
interface CustomEvents {
click: MouseEvent
pointerdown: PointerEvent
}
}
}

View File

@ -0,0 +1,55 @@
import { createSignal, Show, splitProps } from 'solid-js'
import styles from '../../../assets/index.css'
import { BubbleButton } from './BubbleButton'
import { BubbleParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
export type BubbleProps = BotProps & BubbleParams
export const Bubble = (props: BubbleProps) => {
const [bubbleProps] = splitProps(props, ['theme'])
const [isBotOpened, setIsBotOpened] = createSignal(false)
const [isBotStarted, setIsBotStarted] = createSignal(false)
const openBot = () => {
if (!isBotStarted()) setIsBotStarted(true)
setIsBotOpened(true)
}
const closeBot = () => {
setIsBotOpened(false)
}
const toggleBot = () => {
isBotOpened() ? closeBot() : openBot()
}
return (
<>
<style>{styles}</style>
<BubbleButton {...bubbleProps.theme?.button} toggleBot={toggleBot} isBotOpened={isBotOpened()} />
<div
part='bot'
style={{
height: 'calc(100% - 120px)',
transition: 'transform 200ms cubic-bezier(0, 1.2, 1, 1), opacity 150ms ease-out',
'transform-origin': 'bottom right',
transform: isBotOpened() ? 'scale3d(1, 1, 1)' : 'scale3d(0, 0, 1)',
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
'background-color': bubbleProps.theme?.chatWindow?.backgroundColor || '#ffffff',
'z-index': 42424242
}}
class={
'fixed sm:right-5 rounded-lg w-full sm:w-[400px] max-h-[704px]' +
(isBotOpened() ? ' opacity-1' : ' opacity-0 pointer-events-none') +
(props.theme?.button?.size === 'large' ? ' bottom-24' : ' bottom-20')
}
>
<Show when={isBotStarted()}>
<Bot chatflowid={props.chatflowid} apiHost={props.apiHost} />
</Show>
</div>
</>
)
}

View File

@ -0,0 +1,67 @@
import { Show } from 'solid-js'
import { isNotDefined } from '@/utils/index'
import { ButtonTheme } from '../types'
type Props = ButtonTheme & {
isBotOpened: boolean
toggleBot: () => void
}
const defaultButtonColor = '#673ab7'
const defaultIconColor = 'white'
export const BubbleButton = (props: Props) => {
return (
<button
part='button'
onClick={() => props.toggleBot()}
class={
'fixed bottom-5 right-5 shadow-md rounded-full hover:scale-110 active:scale-95 transition-transform duration-200 flex justify-center items-center animate-fade-in' +
(props.size === 'large' ? ' w-16 h-16' : ' w-12 h-12')
}
style={{
'background-color': props.backgroundColor ?? defaultButtonColor,
'z-index': 42424242
}}
>
<Show when={isNotDefined(props.customIconSrc)} keyed>
<svg
viewBox='0 0 24 24'
style={{
stroke: props.iconColor ?? defaultIconColor
}}
class={
`stroke-2 fill-transparent absolute duration-200 transition ` +
(props.isBotOpened ? 'scale-0 opacity-0' : 'scale-100 opacity-100') +
(props.size === 'large' ? ' w-9' : ' w-7')
}
>
<path d='M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' />
</svg>
</Show>
<Show when={props.customIconSrc}>
<img
src={props.customIconSrc}
class={'rounded-full object-cover' + (props.size === 'large' ? ' w-9 h-9' : ' w-7 h-7')}
alt='Bubble button icon'
/>
</Show>
<svg
viewBox='0 0 24 24'
style={{ fill: props.iconColor ?? 'white' }}
class={
`absolute duration-200 transition ` +
(props.isBotOpened ? 'scale-100 rotate-0 opacity-100' : 'scale-0 -rotate-180 opacity-0') +
(props.size === 'large' ? ' w-9' : ' w-7')
}
>
<path
fill-rule='evenodd'
clip-rule='evenodd'
d='M18.601 8.39897C18.269 8.06702 17.7309 8.06702 17.3989 8.39897L12 13.7979L6.60099 8.39897C6.26904 8.06702 5.73086 8.06702 5.39891 8.39897C5.06696 8.73091 5.06696 9.2691 5.39891 9.60105L11.3989 15.601C11.7309 15.933 12.269 15.933 12.601 15.601L18.601 9.60105C18.9329 9.2691 18.9329 8.73091 18.601 8.39897Z'
/>
</svg>
</button>
)
}

View File

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

View File

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

View File

@ -0,0 +1,19 @@
export type BubbleParams = {
theme?: BubbleTheme
}
export type BubbleTheme = {
chatWindow?: ChatWindowTheme
button?: ButtonTheme
}
export type ChatWindowTheme = {
backgroundColor?: string
}
export type ButtonTheme = {
size?: 'medium' | 'large'
backgroundColor?: string
iconColor?: string
customIconSrc?: string
}

1
packages/embed/src/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '*.css'

View File

@ -0,0 +1 @@
export type { BotProps } from './components/Bot'

View File

@ -0,0 +1,14 @@
import { sendRequest } from '@/utils/index'
export type MessageRequest = {
chatflowid: string
apiHost?: string
body: any
}
export const sendMessageQuery = ({ chatflowid, apiHost = 'http://localhost:3000', body }: MessageRequest) =>
sendRequest<any>({
method: 'POST',
url: `${apiHost}/api/v1/prediction/${chatflowid}`,
body
})

View File

@ -0,0 +1,8 @@
import { customElement } from 'solid-element'
import { defaultBotProps } from './constants'
import { Bubble } from './features/bubble'
export const registerWebComponents = () => {
if (typeof window === 'undefined') return
customElement('flowise-chatbot', defaultBotProps, Bubble)
}

View File

View File

@ -0,0 +1,38 @@
export const isNotDefined = <T>(value: T | undefined | null): value is undefined | null => value === undefined || value === null
export const isDefined = <T>(value: T | undefined | null): value is NonNullable<T> => value !== undefined && value !== null
export const isEmpty = (value: string | undefined | null): value is undefined => value === undefined || value === null || value === ''
export const isNotEmpty = (value: string | undefined | null): value is string => value !== undefined && value !== null && value !== ''
export const sendRequest = async <ResponseData>(
params:
| {
url: string
method: string
body?: Record<string, unknown> | FormData
}
| string
): Promise<{ data?: ResponseData; error?: Error }> => {
try {
const url = typeof params === 'string' ? params : params.url
const response = await fetch(url, {
method: typeof params === 'string' ? 'GET' : params.method,
mode: 'cors',
headers:
typeof params !== 'string' && isDefined(params.body)
? {
'Content-Type': 'application/json'
}
: undefined,
body: typeof params !== 'string' && isDefined(params.body) ? JSON.stringify(params.body) : undefined
})
const data = await response.json()
if (!response.ok) throw 'error' in data ? data.error : data
return { data }
} catch (e) {
console.error(e)
return { error: e as Error }
}
}

View File

@ -0,0 +1,3 @@
import { createSignal } from 'solid-js'
export const [isMobile, setIsMobile] = createSignal<boolean>()

10
packages/embed/src/web.ts Normal file
View File

@ -0,0 +1,10 @@
import { registerWebComponents } from './register'
import { parseChatbot, injectChatbotInWindow } from './window'
registerWebComponents()
const chatbot = parseChatbot()
injectChatbotInWindow(chatbot)
export default chatbot

View File

@ -0,0 +1,30 @@
/* eslint-disable solid/reactivity */
type BotProps = {
chatflowid: string
apiHost?: string
}
export const init = (props: BotProps) => {
const element = document.createElement('flowise-chatbot')
Object.assign(element, props)
document.body.appendChild(element)
}
type Chatbot = {
init: typeof init
}
declare const window:
| {
Chatbot: Chatbot | undefined
}
| undefined
export const parseChatbot = () => ({
init
})
export const injectChatbotInWindow = (bot: Chatbot) => {
if (typeof window === 'undefined') return
window.Chatbot = { ...bot }
}

View File

@ -0,0 +1,50 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const defaultTheme = require('tailwindcss/defaultTheme')
function rem2px(input, fontSize = 16) {
if (input == null) {
return input
}
switch (typeof input) {
case 'object':
if (Array.isArray(input)) {
return input.map((val) => rem2px(val, fontSize))
}
// eslint-disable-next-line no-case-declarations
const ret = {}
for (const key in input) {
ret[key] = rem2px(input[key], fontSize)
}
return ret
case 'string':
return input.replace(/(\d*\.?\d+)rem$/, (_, val) => `${parseFloat(val) * fontSize}px`)
case 'function':
return eval(input.toString().replace(/(\d*\.?\d+)rem/g, (_, val) => `${parseFloat(val) * fontSize}px`))
default:
return input
}
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
...rem2px(defaultTheme),
extend: {
keyframes: {
'fade-in': {
'0%': {
opacity: '0'
},
'100%': {
opacity: '1'
}
}
},
animation: {
'fade-in': 'fade-in 0.3s ease-out'
}
}
},
plugins: []
}

View File

@ -0,0 +1,19 @@
{
"extends": "./base.json",
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"jsx": "preserve",
"jsxImportSource": "solid-js",
"module": "ESNext",
"target": "ESNext",
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"emitDeclarationOnly": true
}
}

View File

@ -0,0 +1,456 @@
{
"description": "PDF file QnA using conversational retrieval QA chain",
"nodes": [
{
"width": 300,
"height": 376,
"id": "recursiveCharacterTextSplitter_0",
"position": {
"x": 417.887975676309,
"y": 199.63833359116117
},
"type": "customNode",
"data": {
"id": "recursiveCharacterTextSplitter_0",
"label": "Recursive Character Text Splitter",
"name": "recursiveCharacterTextSplitter",
"type": "RecursiveCharacterTextSplitter",
"baseClasses": ["RecursiveCharacterTextSplitter", "TextSplitter"],
"category": "Text Splitters",
"description": "Split documents recursively by different characters - starting with \"\n\n\", then \"\n\", then \" \"",
"inputParams": [
{
"label": "Chunk Size",
"name": "chunkSize",
"type": "number",
"default": 1000,
"optional": true
},
{
"label": "Chunk Overlap",
"name": "chunkOverlap",
"type": "number",
"optional": true
}
],
"inputAnchors": [],
"inputs": {
"chunkSize": 1000,
"chunkOverlap": ""
},
"outputAnchors": [
{
"id": "recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter",
"name": "recursiveCharacterTextSplitter",
"label": "RecursiveCharacterTextSplitter",
"type": "RecursiveCharacterTextSplitter | TextSplitter"
}
],
"selected": false
},
"selected": false,
"positionAbsolute": {
"x": 417.887975676309,
"y": 199.63833359116117
},
"dragging": false
},
{
"width": 300,
"height": 472,
"id": "openAI_0",
"position": {
"x": 1207.112878089014,
"y": 19.892224585997383
},
"type": "customNode",
"data": {
"id": "openAI_0",
"label": "OpenAI",
"name": "openAI",
"type": "OpenAI",
"baseClasses": ["OpenAI", "BaseLLM", "BaseLanguageModel"],
"category": "LLMs",
"description": "Wrapper around OpenAI large language models",
"inputParams": [
{
"label": "OpenAI Api Key",
"name": "openAIApiKey",
"type": "password"
},
{
"label": "Model Name",
"name": "modelName",
"type": "options",
"options": [
{
"label": "text-davinci-003",
"name": "text-davinci-003"
},
{
"label": "text-davinci-002",
"name": "text-davinci-002"
},
{
"label": "text-curie-001",
"name": "text-curie-001"
},
{
"label": "text-babbage-001",
"name": "text-babbage-001"
}
],
"default": "text-davinci-003",
"optional": true
},
{
"label": "Temperature",
"name": "temperature",
"type": "number",
"default": 0.7,
"optional": true
}
],
"inputAnchors": [],
"inputs": {
"modelName": "text-davinci-003",
"temperature": 0.7
},
"outputAnchors": [
{
"id": "openAI_0-output-openAI-OpenAI|BaseLLM|BaseLanguageModel",
"name": "openAI",
"label": "OpenAI",
"type": "OpenAI | BaseLLM | BaseLanguageModel"
}
],
"selected": false
},
"selected": false,
"positionAbsolute": {
"x": 1207.112878089014,
"y": 19.892224585997383
},
"dragging": false
},
{
"width": 300,
"height": 278,
"id": "openAIEmbeddings_0",
"position": {
"x": 790.9246671661176,
"y": 621.2742019819876
},
"type": "customNode",
"data": {
"id": "openAIEmbeddings_0",
"label": "OpenAI Embeddings",
"name": "openAIEmbeddings",
"type": "OpenAIEmbeddings",
"baseClasses": ["OpenAIEmbeddings", "Embeddings"],
"category": "Embeddings",
"description": "OpenAI API to generate embeddings for a given text",
"inputParams": [
{
"label": "OpenAI Api Key",
"name": "openAIApiKey",
"type": "password"
}
],
"inputAnchors": [],
"inputs": {},
"outputAnchors": [
{
"id": "openAIEmbeddings_0-output-openAIEmbeddings-OpenAIEmbeddings|Embeddings",
"name": "openAIEmbeddings",
"label": "OpenAIEmbeddings",
"type": "OpenAIEmbeddings | Embeddings"
}
],
"selected": false
},
"selected": false,
"positionAbsolute": {
"x": 790.9246671661176,
"y": 621.2742019819876
},
"dragging": false
},
{
"width": 300,
"height": 603,
"id": "pineconeUpsert_0",
"position": {
"x": 1207.9646568749058,
"y": 531.8684248168081
},
"type": "customNode",
"data": {
"id": "pineconeUpsert_0",
"label": "Pinecone Upsert Document",
"name": "pineconeUpsert",
"type": "Pinecone",
"baseClasses": ["Pinecone", "BaseRetriever"],
"category": "Vector Stores",
"description": "Upsert documents to Pinecone",
"inputParams": [
{
"label": "Pinecone Api Key",
"name": "pineconeApiKey",
"type": "password",
"id": "pineconeUpsert_0-input-pineconeApiKey-password"
},
{
"label": "Pinecone Environment",
"name": "pineconeEnv",
"type": "string",
"id": "pineconeUpsert_0-input-pineconeEnv-string"
},
{
"label": "Pinecone Index",
"name": "pineconeIndex",
"type": "string",
"id": "pineconeUpsert_0-input-pineconeIndex-string"
}
],
"inputAnchors": [
{
"label": "Document",
"name": "document",
"type": "Document",
"id": "pineconeUpsert_0-input-document-Document"
},
{
"label": "Embeddings",
"name": "embeddings",
"type": "Embeddings",
"id": "pineconeUpsert_0-input-embeddings-Embeddings"
}
],
"inputs": {
"document": "{{pdfFile_0.data.instance}}",
"embeddings": "{{openAIEmbeddings_0.data.instance}}",
"pineconeEnv": "us-west4-gcp",
"pineconeIndex": "test"
},
"outputAnchors": [
{
"name": "output",
"label": "Output",
"type": "options",
"options": [
{
"id": "pineconeUpsert_0-output-retriever-Pinecone|BaseRetriever",
"name": "retriever",
"label": "Pinecone Retriever",
"type": "Pinecone | BaseRetriever"
},
{
"id": "pineconeUpsert_0-output-vectorStore-Pinecone|VectorStore",
"name": "vectorStore",
"label": "Pinecone Vector Store",
"type": "Pinecone | VectorStore"
}
],
"default": "retriever"
}
],
"outputs": {
"output": "retriever"
},
"selected": false
},
"selected": false,
"positionAbsolute": {
"x": 1207.9646568749058,
"y": 531.8684248168081
},
"dragging": false
},
{
"width": 300,
"height": 455,
"id": "pdfFile_0",
"position": {
"x": 806.583408520452,
"y": 95.94591389235777
},
"type": "customNode",
"data": {
"id": "pdfFile_0",
"label": "Pdf File",
"name": "pdfFile",
"type": "Document",
"baseClasses": ["Document"],
"category": "Document Loaders",
"description": "Load data from PDF files",
"inputParams": [
{
"label": "Pdf File",
"name": "pdfFile",
"type": "file",
"fileType": ".pdf",
"id": "pdfFile_0-input-pdfFile-file"
},
{
"label": "Usage",
"name": "usage",
"type": "options",
"options": [
{
"label": "One document per page",
"name": "perPage"
},
{
"label": "One document per file",
"name": "perFile"
}
],
"default": "perPage",
"id": "pdfFile_0-input-usage-options"
}
],
"inputAnchors": [
{
"label": "Text Splitter",
"name": "textSplitter",
"type": "TextSplitter",
"optional": true,
"id": "pdfFile_0-input-textSplitter-TextSplitter"
}
],
"inputs": {
"textSplitter": "{{recursiveCharacterTextSplitter_0.data.instance}}",
"usage": "perPage"
},
"outputAnchors": [
{
"id": "pdfFile_0-output-pdfFile-Document",
"name": "pdfFile",
"label": "Document",
"type": "Document"
}
],
"outputs": {},
"selected": false
},
"selected": false,
"positionAbsolute": {
"x": 806.583408520452,
"y": 95.94591389235777
},
"dragging": false
},
{
"width": 300,
"height": 280,
"id": "conversationalRetrievalQAChain_0",
"position": {
"x": 1608.5085986864585,
"y": 415.26454165076757
},
"type": "customNode",
"data": {
"id": "conversationalRetrievalQAChain_0",
"label": "Conversational Retrieval QA Chain",
"name": "conversationalRetrievalQAChain",
"type": "ConversationalRetrievalQAChain",
"baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"],
"category": "Chains",
"description": "Document QA - built on RetrievalQAChain to provide a chat history component",
"inputParams": [],
"inputAnchors": [
{
"label": "LLM",
"name": "llm",
"type": "BaseLLM",
"id": "conversationalRetrievalQAChain_0-input-llm-BaseLLM"
},
{
"label": "Vector Store Retriever",
"name": "vectorStoreRetriever",
"type": "BaseRetriever",
"id": "conversationalRetrievalQAChain_0-input-vectorStoreRetriever-BaseRetriever"
}
],
"inputs": {
"llm": "{{openAI_0.data.instance}}",
"vectorStoreRetriever": "{{pineconeUpsert_0.data.instance}}"
},
"outputAnchors": [
{
"id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain",
"name": "conversationalRetrievalQAChain",
"label": "ConversationalRetrievalQAChain",
"type": "ConversationalRetrievalQAChain | BaseChain"
}
],
"outputs": {},
"selected": false
},
"selected": false,
"positionAbsolute": {
"x": 1608.5085986864585,
"y": 415.26454165076757
},
"dragging": false
}
],
"edges": [
{
"source": "openAIEmbeddings_0",
"sourceHandle": "openAIEmbeddings_0-output-openAIEmbeddings-OpenAIEmbeddings|Embeddings",
"target": "pineconeUpsert_0",
"targetHandle": "pineconeUpsert_0-input-embeddings-Embeddings",
"type": "buttonedge",
"id": "openAIEmbeddings_0-openAIEmbeddings_0-output-openAIEmbeddings-OpenAIEmbeddings|Embeddings-pineconeUpsert_0-pineconeUpsert_0-input-embeddings-Embeddings",
"data": {
"label": ""
}
},
{
"source": "recursiveCharacterTextSplitter_0",
"sourceHandle": "recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter",
"target": "pdfFile_0",
"targetHandle": "pdfFile_0-input-textSplitter-TextSplitter",
"type": "buttonedge",
"id": "recursiveCharacterTextSplitter_0-recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter-pdfFile_0-pdfFile_0-input-textSplitter-TextSplitter",
"data": {
"label": ""
}
},
{
"source": "pdfFile_0",
"sourceHandle": "pdfFile_0-output-pdfFile-Document",
"target": "pineconeUpsert_0",
"targetHandle": "pineconeUpsert_0-input-document-Document",
"type": "buttonedge",
"id": "pdfFile_0-pdfFile_0-output-pdfFile-Document-pineconeUpsert_0-pineconeUpsert_0-input-document-Document",
"data": {
"label": ""
}
},
{
"source": "openAI_0",
"sourceHandle": "openAI_0-output-openAI-OpenAI|BaseLLM|BaseLanguageModel",
"target": "conversationalRetrievalQAChain_0",
"targetHandle": "conversationalRetrievalQAChain_0-input-llm-BaseLLM",
"type": "buttonedge",
"id": "openAI_0-openAI_0-output-openAI-OpenAI|BaseLLM|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-llm-BaseLLM",
"data": {
"label": ""
}
},
{
"source": "pineconeUpsert_0",
"sourceHandle": "pineconeUpsert_0-output-retriever-Pinecone|BaseRetriever",
"target": "conversationalRetrievalQAChain_0",
"targetHandle": "conversationalRetrievalQAChain_0-input-vectorStoreRetriever-BaseRetriever",
"type": "buttonedge",
"id": "pineconeUpsert_0-pineconeUpsert_0-output-retriever-Pinecone|BaseRetriever-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-vectorStoreRetriever-BaseRetriever",
"data": {
"label": ""
}
}
]
}

View File

@ -55,7 +55,7 @@ export class App {
// Allow access from ui when yarn run dev
if (process.env.NODE_ENV !== 'production') {
this.app.use(cors({ credentials: true, origin: 'http://localhost:8080' }))
this.app.use(cors({ credentials: true, origin: ['http://localhost:8080', 'http://localhost:5678'] }))
}
// ----------------------------------------

View File

@ -0,0 +1 @@
<svg viewBox="0 0 512 512" focusable="false" class="chakra-icon css-lbf1w4" id="Capa_1" enable-background="new 0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m512 141.17v229.66c0 39.96-32.51 72.47-72.46 72.47h-367.08c-39.95 0-72.46-32.51-72.46-72.47v-229.66c0-39.96 32.51-72.47 72.46-72.47h367.08c39.95 0 72.46 32.51 72.46 72.47z" fill="#6aa9ff"></path></g><path d="m512 141.17v229.66c0 39.96-32.51 72.47-72.46 72.47h-183.54v-374.6h183.54c39.95 0 72.46 32.51 72.46 72.47z" fill="#4987ea"></path><g><path d="m146.16 349.223-78.4-78.4c-5.858-5.858-5.858-15.355 0-21.213l86.833-86.833c5.857-5.858 15.355-5.858 21.213 0s5.858 15.355 0 21.213l-76.226 76.226 67.793 67.794c5.858 5.858 5.858 15.355 0 21.213-5.857 5.858-15.355 5.859-21.213 0z" fill="#f0f7ff"></path></g><g><path d="m336.194 349.223c-5.858-5.858-5.858-15.355 0-21.213l76.226-76.227-67.793-67.794c-5.858-5.858-5.858-15.355 0-21.213 5.857-5.858 15.355-5.858 21.213 0l78.4 78.4c5.858 5.858 5.858 15.355 0 21.213l-86.833 86.833c-5.856 5.859-15.355 5.86-21.213.001z" fill="#dfe7f4"></path></g><g><path d="m309.54 148.7-53.54 151.6-25.78 72.99c-2.792 7.888-11.443 11.903-19.14 9.15-7.81-2.76-11.91-11.33-9.15-19.14l54.07-153.1 25.25-71.49c2.76-7.81 11.33-11.91 19.14-9.15s11.91 11.33 9.15 19.14z" fill="#f0f7ff"></path></g><path d="m309.54 148.7-53.54 151.6v-90.1l25.25-71.49c2.76-7.81 11.33-11.91 19.14-9.15s11.91 11.33 9.15 19.14z" fill="#dfe7f4"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -8,6 +8,7 @@ import { baseURL } from 'store/constant'
import pythonSVG from 'assets/images/python.svg'
import javascriptSVG from 'assets/images/javascript.svg'
import cURLSVG from 'assets/images/cURL.svg'
import EmbedSVG from 'assets/images/embed.svg'
function TabPanel(props) {
const { children, value, index, ...other } = props
@ -39,7 +40,7 @@ function a11yProps(index) {
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const codes = ['Python', 'JavaScript', 'cURL']
const codes = ['Embed', 'Python', 'JavaScript', 'cURL']
const [value, setValue] = useState(0)
const handleChange = (event, newValue) => {
@ -74,6 +75,15 @@ output = query({
const result = await response.json();
return result;
}
`
} else if (codeLang === 'Embed') {
return `<script type="module">
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed@latest/dist/web.js"
Chatbot.init({
chatflowid: "${dialogProps.chatflowid}",
apiHost: "${baseURL}",
})
</script>
`
} else if (codeLang === 'cURL') {
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
@ -86,7 +96,7 @@ output = query({
const getLang = (codeLang) => {
if (codeLang === 'Python') {
return 'python'
} else if (codeLang === 'JavaScript') {
} else if (codeLang === 'JavaScript' || codeLang === 'Embed') {
return 'javascript'
} else if (codeLang === 'cURL') {
return 'bash'
@ -99,6 +109,8 @@ output = query({
return pythonSVG
} else if (codeLang === 'JavaScript') {
return javascriptSVG
} else if (codeLang === 'Embed') {
return EmbedSVG
} else if (codeLang === 'cURL') {
return cURLSVG
}
@ -123,7 +135,7 @@ output = query({
<Tab
icon={
<img
style={{ objectFit: 'cover', height: 'auto', width: 'auto', marginLeft: 10 }}
style={{ objectFit: 'cover', height: 15, width: 'auto', marginLeft: 10 }}
src={getSVG(codeLang)}
alt='code'
/>
@ -138,6 +150,14 @@ output = query({
<div style={{ marginTop: 10 }}></div>
{codes.map((codeLang, index) => (
<TabPanel key={index} value={value} index={index}>
{codeLang === 'Embed' && (
<>
<span>
Paste this anywhere in the <code>{`<body>`}</code> tag of your html file
</span>
<div style={{ height: 10 }}></div>
</>
)}
<CopyBlock
theme={atomOneDark}
text={getCode(codeLang)}

View File

@ -81,7 +81,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const onAPIDialogClick = () => {
setAPIDialogProps({
title: 'Use this chatflow with API',
title: 'Embed in your application or use as API',
chatflowid: chatflow.id
})
setAPIDialogOpen(true)
@ -230,26 +230,28 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
)}
</Box>
<Box>
<ButtonBase title='API Endpoint' sx={{ borderRadius: '50%', mr: 2 }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.canvasHeader.deployLight,
color: theme.palette.canvasHeader.deployDark,
'&:hover': {
background: theme.palette.canvasHeader.deployDark,
color: theme.palette.canvasHeader.deployLight
}
}}
color='inherit'
onClick={onAPIDialogClick}
>
<IconWorldWww stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
{chatflow && chatflow.id && (
<ButtonBase title='Embed/API' sx={{ borderRadius: '50%', mr: 2 }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.canvasHeader.deployLight,
color: theme.palette.canvasHeader.deployDark,
'&:hover': {
background: theme.palette.canvasHeader.deployDark,
color: theme.palette.canvasHeader.deployLight
}
}}
color='inherit'
onClick={onAPIDialogClick}
>
<IconWorldWww stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
)}
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
<Avatar
variant='rounded'