Commit 1d7a65f6 authored by John Wang's avatar John Wang

Merge branch 'main' into feat/universal-chat

parents 5759122d dd1172b5
......@@ -2,6 +2,7 @@ import datetime
import logging
import random
import string
import time
import click
from flask import current_app
......@@ -13,7 +14,7 @@ from libs.helper import email as email_validate
from extensions.ext_database import db
from libs.rsa import generate_key_pair
from models.account import InvitationCode, Tenant
from models.dataset import Dataset
from models.dataset import Dataset, DatasetQuery, Document, DocumentSegment
from models.model import Account
import secrets
import base64
......@@ -172,7 +173,7 @@ def recreate_all_dataset_indexes():
page = 1
while True:
try:
datasets = db.session.query(Dataset).filter(Dataset.indexing_technique == 'high_quality')\
datasets = db.session.query(Dataset).filter(Dataset.indexing_technique == 'high_quality') \
.order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
except NotFound:
break
......@@ -188,12 +189,66 @@ def recreate_all_dataset_indexes():
else:
click.echo('passed.')
except Exception as e:
click.echo(click.style('Recreate dataset index error: {} {}'.format(e.__class__.__name__, str(e)), fg='red'))
click.echo(
click.style('Recreate dataset index error: {} {}'.format(e.__class__.__name__, str(e)), fg='red'))
continue
click.echo(click.style('Congratulations! Recreate {} dataset indexes.'.format(recreate_count), fg='green'))
@click.command('clean-unused-dataset-indexes', help='Clean unused dataset indexes.')
def clean_unused_dataset_indexes():
click.echo(click.style('Start clean unused dataset indexes.', fg='green'))
clean_days = int(current_app.config.get('CLEAN_DAY_SETTING'))
start_at = time.perf_counter()
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=clean_days)
page = 1
while True:
try:
datasets = db.session.query(Dataset).filter(Dataset.created_at < thirty_days_ago) \
.order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
except NotFound:
break
page += 1
for dataset in datasets:
dataset_query = db.session.query(DatasetQuery).filter(
DatasetQuery.created_at > thirty_days_ago,
DatasetQuery.dataset_id == dataset.id
).all()
if not dataset_query or len(dataset_query) == 0:
documents = db.session.query(Document).filter(
Document.dataset_id == dataset.id,
Document.indexing_status == 'completed',
Document.enabled == True,
Document.archived == False,
Document.updated_at > thirty_days_ago
).all()
if not documents or len(documents) == 0:
try:
# remove index
vector_index = IndexBuilder.get_index(dataset, 'high_quality')
kw_index = IndexBuilder.get_index(dataset, 'economy')
# delete from vector index
if vector_index:
vector_index.delete()
kw_index.delete()
# update document
update_params = {
Document.enabled: False
}
Document.query.filter_by(dataset_id=dataset.id).update(update_params)
db.session.commit()
click.echo(click.style('Cleaned unused dataset {} from db success!'.format(dataset.id),
fg='green'))
except Exception as e:
click.echo(
click.style('clean dataset index error: {} {}'.format(e.__class__.__name__, str(e)),
fg='red'))
end_at = time.perf_counter()
click.echo(click.style('Cleaned unused dataset from db success latency: {}'.format(end_at - start_at), fg='green'))
@click.command('sync-anthropic-hosted-providers', help='Sync anthropic hosted providers.')
def sync_anthropic_hosted_providers():
click.echo(click.style('Start sync anthropic hosted providers.', fg='green'))
......@@ -218,7 +273,9 @@ def sync_anthropic_hosted_providers():
)
count += 1
except Exception as e:
click.echo(click.style('Sync tenant anthropic hosted provider error: {} {}'.format(e.__class__.__name__, str(e)), fg='red'))
click.echo(click.style(
'Sync tenant anthropic hosted provider error: {} {}'.format(e.__class__.__name__, str(e)),
fg='red'))
continue
click.echo(click.style('Congratulations! Synced {} anthropic hosted providers.'.format(count), fg='green'))
......@@ -231,3 +288,4 @@ def register_commands(app):
app.cli.add_command(reset_encrypt_key_pair)
app.cli.add_command(recreate_all_dataset_indexes)
app.cli.add_command(sync_anthropic_hosted_providers)
app.cli.add_command(clean_unused_dataset_indexes)
......@@ -53,7 +53,8 @@ DEFAULTS = {
'DEFAULT_LLM_PROVIDER': 'openai',
'OPENAI_HOSTED_QUOTA_LIMIT': 200,
'ANTHROPIC_HOSTED_QUOTA_LIMIT': 1000,
'TENANT_DOCUMENT_COUNT': 100
'TENANT_DOCUMENT_COUNT': 100,
'CLEAN_DAY_SETTING': 30
}
......@@ -215,6 +216,7 @@ class Config:
self.NOTION_INTEGRATION_TOKEN = get_env('NOTION_INTEGRATION_TOKEN')
self.TENANT_DOCUMENT_COUNT = get_env('TENANT_DOCUMENT_COUNT')
self.CLEAN_DAY_SETTING = get_env('CLEAN_DAY_SETTING')
class CloudEditionConfig(Config):
......
......@@ -3,7 +3,6 @@ from flask import request
from flask_login import login_required, current_user
from flask_restful import Resource, reqparse, fields, marshal, marshal_with
from werkzeug.exceptions import NotFound, Forbidden
import services
from controllers.console import api
from controllers.console.datasets.error import DatasetNameDuplicateError
......
......@@ -2,6 +2,7 @@ import json
import logging
from typing import Optional, Union
import openai
import requests
from core.llm.provider.base import BaseProvider
......@@ -14,30 +15,37 @@ AZURE_OPENAI_API_VERSION = '2023-07-01-preview'
class AzureProvider(BaseProvider):
def get_models(self, model_id: Optional[str] = None, credentials: Optional[dict] = None) -> list[dict]:
credentials = self.get_credentials(model_id) if not credentials else credentials
url = "{}/openai/deployments?api-version={}".format(
str(credentials.get('openai_api_base')),
str(credentials.get('openai_api_version'))
)
headers = {
"api-key": str(credentials.get('openai_api_key')),
"content-type": "application/json; charset=utf-8"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
result = response.json()
return [{
'id': deployment['id'],
'name': '{} ({})'.format(deployment['id'], deployment['model'])
} for deployment in result['data'] if deployment['status'] == 'succeeded']
else:
if response.status_code == 401:
raise AzureAuthenticationError()
return []
def check_embedding_model(self, credentials: Optional[dict] = None):
credentials = self.get_credentials('text-embedding-ada-002') if not credentials else credentials
try:
result = openai.Embedding.create(input=['test'],
engine='text-embedding-ada-002',
timeout=60,
api_key=str(credentials.get('openai_api_key')),
api_base=str(credentials.get('openai_api_base')),
api_type='azure',
api_version=str(credentials.get('openai_api_version')))["data"][0][
"embedding"]
except openai.error.AuthenticationError as e:
raise AzureAuthenticationError(str(e))
except openai.error.APIConnectionError as e:
raise AzureRequestFailedError(
'Failed to request Azure OpenAI, please check your API Base Endpoint, The format is `https://xxx.openai.azure.com/`')
except openai.error.InvalidRequestError as e:
if e.http_status == 404:
raise AzureRequestFailedError("Please check your 'gpt-3.5-turbo' or 'text-embedding-ada-002' "
"deployment name is exists in Azure AI")
else:
raise AzureRequestFailedError('Failed to request Azure OpenAI. Status code: {}'.format(response.status_code))
raise AzureRequestFailedError(
'Failed to request Azure OpenAI. cause: {}'.format(str(e)))
except openai.error.OpenAIError as e:
raise AzureRequestFailedError(
'Failed to request Azure OpenAI. cause: {}'.format(str(e)))
if not isinstance(result, list):
raise AzureRequestFailedError('Failed to request Azure OpenAI.')
def get_credentials(self, model_id: Optional[str] = None) -> dict:
"""
......@@ -98,31 +106,11 @@ class AzureProvider(BaseProvider):
if 'openai_api_version' not in config:
config['openai_api_version'] = AZURE_OPENAI_API_VERSION
models = self.get_models(credentials=config)
if not models:
raise ValidateFailedError("Please add deployments for "
"'gpt-3.5-turbo', 'text-embedding-ada-002' (required) "
"and 'gpt-4', 'gpt-35-turbo-16k', 'text-davinci-003' (optional).")
fixed_model_ids = [
'gpt-35-turbo',
'text-embedding-ada-002'
]
current_model_ids = [model['id'] for model in models]
missing_model_ids = [fixed_model_id for fixed_model_id in fixed_model_ids if
fixed_model_id not in current_model_ids]
if missing_model_ids:
raise ValidateFailedError("Please add deployments for '{}'.".format(", ".join(missing_model_ids)))
self.check_embedding_model(credentials=config)
except ValidateFailedError as e:
raise e
except AzureAuthenticationError:
raise ValidateFailedError('Validation failed, please check your API Key.')
except (requests.ConnectionError, requests.RequestException):
raise ValidateFailedError('Validation failed, please check your API Base Endpoint.')
except AzureRequestFailedError as ex:
raise ValidateFailedError('Validation failed, error: {}.'.format(str(ex)))
except Exception as ex:
......
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import style from './style.module.css'
......@@ -43,10 +43,15 @@ const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
type Option = keyof typeof OPTION_MAP
type OptionStatus = {
iframe: boolean
scripts: boolean
}
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
const { t } = useTranslation()
const [option, setOption] = useState<Option>('iframe')
const [isCopied, setIsCopied] = useState({ iframe: false, scripts: false })
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false })
const [_, copy] = useCopyToClipboard()
const { langeniusVersionInfo } = useAppContext()
......@@ -56,6 +61,19 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
setIsCopied({ ...isCopied, [option]: true })
}
// when toggle option, reset then copy status
const resetCopyStatus = () => {
const cache = { ...isCopied }
Object.keys(cache).forEach((key) => {
cache[key as keyof OptionStatus] = false
})
setIsCopied(cache)
}
useEffect(() => {
resetCopyStatus()
}, [isShow])
return (
<Modal
title={t(`${prefixEmbedded}.title`)}
......@@ -77,7 +95,10 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
style[`${v}Icon`],
option === v && style.active,
)}
onClick={() => setOption(v as Option)}
onClick={() => {
setOption(v as Option)
resetCopyStatus()
}}
></div>
)
})}
......
......@@ -15,6 +15,7 @@ export type Attrs = {
export function normalizeAttrs(attrs: Attrs = {}): Attrs {
return Object.keys(attrs).reduce((acc: Attrs, key) => {
const val = attrs[key]
key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase())
switch (key) {
case 'class':
acc.className = val
......
'use client'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import copy from 'copy-to-clipboard'
import { t } from 'i18next'
import s from './style.module.css'
import { randomString } from '@/app/components/app-sidebar/basic'
import Tooltip from '@/app/components/base/tooltip'
type IInputCopyProps = {
......@@ -13,13 +14,15 @@ type IInputCopyProps = {
}
const InputCopy = ({
value,
value = '',
className,
readOnly = true,
children,
}: IInputCopyProps) => {
const [isCopied, setIsCopied] = useState(false)
const selector = useRef(`input-tooltip-${randomString(4)}`)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => {
......@@ -38,7 +41,7 @@ const InputCopy = ({
{children}
<div className='flex-grow bg-gray-50 text-[13px] relative h-full'>
<Tooltip
selector="top-uniq"
selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10'
>
......@@ -50,7 +53,7 @@ const InputCopy = ({
</div>
<div className="flex-shrink-0 h-4 bg-gray-200 border" />
<Tooltip
selector="top-uniq"
selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10'
>
......
......@@ -18,7 +18,7 @@ const SecretKeyGenerateModal = ({
isShow = false,
onClose,
newKey,
className
className,
}: ISecretKeyGenerateModalProps) => {
const { t } = useTranslation()
return (
......
......@@ -6,6 +6,7 @@ import {
import { useTranslation } from 'react-i18next'
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import useSWR, { useSWRConfig } from 'swr'
import { useContext } from 'use-context-selector'
import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
......@@ -16,7 +17,6 @@ import Tooltip from '@/app/components/base/tooltip'
import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import { useContext } from 'use-context-selector'
import I18n from '@/context/i18n'
type ISecretKeyModalProps = {
......@@ -58,12 +58,11 @@ const SecretKeyModal = ({
}
}, [copyValue])
const onDel = async () => {
setShowConfirmDelete(false)
if (!delKeyID) {
if (!delKeyID)
return
}
await delApikey({ url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} })
mutate(commonParams)
}
......@@ -80,11 +79,10 @@ const SecretKeyModal = ({
}
const formatDate = (timestamp: any) => {
if (locale === 'en') {
if (locale === 'en')
return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format((+timestamp) * 1000)
} else {
else
return new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).format((+timestamp) * 1000)
}
}
return (
......@@ -111,7 +109,7 @@ const SecretKeyModal = ({
<div className='flex-shrink-0 px-3 truncate w-28'>{api.last_used_at ? formatDate(api.last_used_at) : t('appApi.never')}</div>
<div className='flex flex-grow px-3'>
<Tooltip
selector="top-uniq"
selector={`key-${api.token}`}
content={copyValue === api.token ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10'
>
......
'use client'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { t } from 'i18next'
import s from './index.module.css'
import Tooltip from '@/app/components/base/tooltip'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import { randomString } from '@/app/components/app-sidebar/basic'
type IInvitationLinkProps = {
value?: string
......@@ -13,6 +14,7 @@ const InvitationLink = ({
value = '',
}: IInvitationLinkProps) => {
const [isCopied, setIsCopied] = useState(false)
const selector = useRef(`invite-link-${randomString(4)}`)
const [_, copy] = useCopyToClipboard()
const copyHandle = useCallback(() => {
......@@ -37,7 +39,7 @@ const InvitationLink = ({
<div className="flex items-center flex-grow h-5">
<div className='flex-grow bg-gray-100 text-[13px] relative h-full'>
<Tooltip
selector="top-uniq"
selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10'
>
......@@ -46,7 +48,7 @@ const InvitationLink = ({
</div>
<div className="flex-shrink-0 h-4 bg-gray-200 border" />
<Tooltip
selector="top-uniq"
selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10'
>
......
......@@ -48,7 +48,10 @@ const List: FC<IListProps> = ({
useInfiniteScroll(
async () => {
if (!isNoMore) {
const lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined
let lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined
if (lastId === '-1')
lastId = undefined
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
onMoreLoaded({ data: conversations, has_more })
}
......
......@@ -17,6 +17,7 @@ const InstallForm = () => {
const [email, setEmail] = React.useState('')
const [name, setName] = React.useState('')
const [password, setPassword] = React.useState('')
const [showPassword, setShowPassword] = React.useState(false)
const showErrorMessage = (message: string) => {
Toast.notify({
type: 'error',
......@@ -108,12 +109,21 @@ const InstallForm = () => {
<div className="mt-1 relative rounded-md shadow-sm">
<input
id="password"
type='password'
type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder={t('login.passwordPlaceholder') || ''}
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-gray-400 hover:text-gray-500 focus:outline-none focus:text-gray-500"
>
{showPassword ? '👀' : '😝'}
</button>
</div>
</div>
<div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</div>
......
import { useState } from 'react'
import { useCallback, useState } from 'react'
import writeText from 'copy-to-clipboard'
type CopiedValue = string | null
type CopyFn = (text: string) => Promise<boolean>
function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null)
const [copiedText, setCopiedText] = useState<CopiedValue>(null)
const copy: CopyFn = async text => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported')
return false
}
const copy: CopyFn = useCallback(async (text: string) => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported')
return false
}
try {
await navigator.clipboard.writeText(text)
setCopiedText(text)
return true
} catch (error) {
console.warn('Copy failed', error)
setCopiedText(null)
return false
}
try {
writeText(text)
setCopiedText(text)
return true
}
catch (error) {
console.warn('Copy failed', error)
setCopiedText(null)
return false
}
}, [])
return [copiedText, copy]
return [copiedText, copy]
}
export default useCopyToClipboard
\ No newline at end of file
export default useCopyToClipboard
......@@ -43,7 +43,7 @@ const translation = {
'较高的 Temperature 设置将导致更多样和创造性的输出,而较低的 Temperature 将产生更保守的输出并且类似于训练数据。',
topP: '采样范围',
topPTip:
'Top P值越高,输出与训练文本越相似,Top P值越低,输出越有创意和变化。它可用于使输出更适合特定用例。',
'Top P值越低,输出与训练文本越相似,Top P值越高,输出越有创意和变化。它可用于使输出更适合特定用例。',
presencePenalty: '词汇控制',
presencePenaltyTip:
'Presence penalty 是根据新词是否出现在目前的文本中来对其进行惩罚。正值将降低模型谈论新话题的可能性。',
......
......@@ -54,7 +54,7 @@
"katex": "^0.16.7",
"lodash-es": "^4.17.21",
"negotiator": "^0.6.3",
"next": "^13.4.7",
"next": "13.3.0",
"qs": "^6.11.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
......
......@@ -15,6 +15,7 @@ async function embedChatbot () {
return;
}
const isDev = !!difyChatbotConfig.isDev
const baseUrl = difyChatbotConfig.baseUrl || `https://${isDev ? 'dev.' : ''}udify.app`
const openIcon = `<svg
id="openIcon"
width="24"
......@@ -53,7 +54,7 @@ async function embedChatbot () {
iframe.allow = "fullscreen;microphone"
iframe.title = "dify chatbot bubble window"
iframe.id = 'dify-chatbot-bubble-window'
iframe.src = `https://${isDev ? 'dev.' : ''}udify.app/chatbot/${difyChatbotConfig.token}`;
iframe.src = `${baseUrl}/chatbot/${difyChatbotConfig.token}`
iframe.style.cssText = 'border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;'
document.body.appendChild(iframe);
}
......
async function embedChatbot(){const t=window.difyChatbotConfig;if(t&&t.token){const o=!!t.isDev,n=`<svg
async function embedChatbot(){const t=window.difyChatbotConfig;if(t&&t.token){var e=!!t.isDev;const o=t.baseUrl||`https://${e?"dev.":""}udify.app`,n=`<svg
id="openIcon"
width="24"
height="24"
......@@ -27,4 +27,4 @@ async function embedChatbot(){const t=window.difyChatbotConfig;if(t&&t.token){co
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>`;if(!document.getElementById("dify-chatbot-bubble-button")){var e=document.createElement("div");e.id="dify-chatbot-bubble-button",e.style.cssText="position: fixed; bottom: 1rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: pointer; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}";const d=document.createElement("div");d.style.cssText="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",d.innerHTML=n,e.appendChild(d),document.body.appendChild(e),e.addEventListener("click",function(){var e=document.getElementById("dify-chatbot-bubble-window");e?"none"===e.style.display?(e.style.display="block",d.innerHTML=i):(e.style.display="none",d.innerHTML=n):((e=document.createElement("iframe")).allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id="dify-chatbot-bubble-window",e.src=`https://${o?"dev.":""}udify.app/chatbot/`+t.token,e.style.cssText="border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;",document.body.appendChild(e),d.innerHTML=i)})}}else console.error("difyChatbotConfig is empty or token is not provided")}document.body.onload=embedChatbot;
\ No newline at end of file
</svg>`;if(!document.getElementById("dify-chatbot-bubble-button")){e=document.createElement("div");e.id="dify-chatbot-bubble-button",e.style.cssText="position: fixed; bottom: 1rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: pointer; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}";const d=document.createElement("div");d.style.cssText="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",d.innerHTML=n,e.appendChild(d),document.body.appendChild(e),e.addEventListener("click",function(){var e=document.getElementById("dify-chatbot-bubble-window");e?"none"===e.style.display?(e.style.display="block",d.innerHTML=i):(e.style.display="none",d.innerHTML=n):((e=document.createElement("iframe")).allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id="dify-chatbot-bubble-window",e.src=o+"/chatbot/"+t.token,e.style.cssText="border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;",document.body.appendChild(e),d.innerHTML=i)})}}else console.error("difyChatbotConfig is empty or token is not provided")}document.body.onload=embedChatbot;
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment