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 ...@@ -2,6 +2,7 @@ import datetime
import logging import logging
import random import random
import string import string
import time
import click import click
from flask import current_app from flask import current_app
...@@ -13,7 +14,7 @@ from libs.helper import email as email_validate ...@@ -13,7 +14,7 @@ from libs.helper import email as email_validate
from extensions.ext_database import db from extensions.ext_database import db
from libs.rsa import generate_key_pair from libs.rsa import generate_key_pair
from models.account import InvitationCode, Tenant from models.account import InvitationCode, Tenant
from models.dataset import Dataset from models.dataset import Dataset, DatasetQuery, Document, DocumentSegment
from models.model import Account from models.model import Account
import secrets import secrets
import base64 import base64
...@@ -172,7 +173,7 @@ def recreate_all_dataset_indexes(): ...@@ -172,7 +173,7 @@ def recreate_all_dataset_indexes():
page = 1 page = 1
while True: while True:
try: 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) .order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
except NotFound: except NotFound:
break break
...@@ -188,12 +189,66 @@ def recreate_all_dataset_indexes(): ...@@ -188,12 +189,66 @@ def recreate_all_dataset_indexes():
else: else:
click.echo('passed.') click.echo('passed.')
except Exception as e: 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 continue
click.echo(click.style('Congratulations! Recreate {} dataset indexes.'.format(recreate_count), fg='green')) 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.') @click.command('sync-anthropic-hosted-providers', help='Sync anthropic hosted providers.')
def sync_anthropic_hosted_providers(): def sync_anthropic_hosted_providers():
click.echo(click.style('Start sync anthropic hosted providers.', fg='green')) click.echo(click.style('Start sync anthropic hosted providers.', fg='green'))
...@@ -218,7 +273,9 @@ def sync_anthropic_hosted_providers(): ...@@ -218,7 +273,9 @@ def sync_anthropic_hosted_providers():
) )
count += 1 count += 1
except Exception as e: 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 continue
click.echo(click.style('Congratulations! Synced {} anthropic hosted providers.'.format(count), fg='green')) click.echo(click.style('Congratulations! Synced {} anthropic hosted providers.'.format(count), fg='green'))
...@@ -231,3 +288,4 @@ def register_commands(app): ...@@ -231,3 +288,4 @@ def register_commands(app):
app.cli.add_command(reset_encrypt_key_pair) app.cli.add_command(reset_encrypt_key_pair)
app.cli.add_command(recreate_all_dataset_indexes) app.cli.add_command(recreate_all_dataset_indexes)
app.cli.add_command(sync_anthropic_hosted_providers) app.cli.add_command(sync_anthropic_hosted_providers)
app.cli.add_command(clean_unused_dataset_indexes)
...@@ -53,7 +53,8 @@ DEFAULTS = { ...@@ -53,7 +53,8 @@ DEFAULTS = {
'DEFAULT_LLM_PROVIDER': 'openai', 'DEFAULT_LLM_PROVIDER': 'openai',
'OPENAI_HOSTED_QUOTA_LIMIT': 200, 'OPENAI_HOSTED_QUOTA_LIMIT': 200,
'ANTHROPIC_HOSTED_QUOTA_LIMIT': 1000, 'ANTHROPIC_HOSTED_QUOTA_LIMIT': 1000,
'TENANT_DOCUMENT_COUNT': 100 'TENANT_DOCUMENT_COUNT': 100,
'CLEAN_DAY_SETTING': 30
} }
...@@ -215,6 +216,7 @@ class Config: ...@@ -215,6 +216,7 @@ class Config:
self.NOTION_INTEGRATION_TOKEN = get_env('NOTION_INTEGRATION_TOKEN') self.NOTION_INTEGRATION_TOKEN = get_env('NOTION_INTEGRATION_TOKEN')
self.TENANT_DOCUMENT_COUNT = get_env('TENANT_DOCUMENT_COUNT') self.TENANT_DOCUMENT_COUNT = get_env('TENANT_DOCUMENT_COUNT')
self.CLEAN_DAY_SETTING = get_env('CLEAN_DAY_SETTING')
class CloudEditionConfig(Config): class CloudEditionConfig(Config):
......
...@@ -3,7 +3,6 @@ from flask import request ...@@ -3,7 +3,6 @@ from flask import request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_restful import Resource, reqparse, fields, marshal, marshal_with from flask_restful import Resource, reqparse, fields, marshal, marshal_with
from werkzeug.exceptions import NotFound, Forbidden from werkzeug.exceptions import NotFound, Forbidden
import services import services
from controllers.console import api from controllers.console import api
from controllers.console.datasets.error import DatasetNameDuplicateError from controllers.console.datasets.error import DatasetNameDuplicateError
......
...@@ -2,6 +2,7 @@ import json ...@@ -2,6 +2,7 @@ import json
import logging import logging
from typing import Optional, Union from typing import Optional, Union
import openai
import requests import requests
from core.llm.provider.base import BaseProvider from core.llm.provider.base import BaseProvider
...@@ -14,30 +15,37 @@ AZURE_OPENAI_API_VERSION = '2023-07-01-preview' ...@@ -14,30 +15,37 @@ AZURE_OPENAI_API_VERSION = '2023-07-01-preview'
class AzureProvider(BaseProvider): class AzureProvider(BaseProvider):
def get_models(self, model_id: Optional[str] = None, credentials: Optional[dict] = None) -> list[dict]: 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 return []
url = "{}/openai/deployments?api-version={}".format(
str(credentials.get('openai_api_base')), def check_embedding_model(self, credentials: Optional[dict] = None):
str(credentials.get('openai_api_version')) credentials = self.get_credentials('text-embedding-ada-002') if not credentials else credentials
) try:
result = openai.Embedding.create(input=['test'],
headers = { engine='text-embedding-ada-002',
"api-key": str(credentials.get('openai_api_key')), timeout=60,
"content-type": "application/json; charset=utf-8" api_key=str(credentials.get('openai_api_key')),
} api_base=str(credentials.get('openai_api_base')),
api_type='azure',
response = requests.get(url, headers=headers) api_version=str(credentials.get('openai_api_version')))["data"][0][
"embedding"]
if response.status_code == 200: except openai.error.AuthenticationError as e:
result = response.json() raise AzureAuthenticationError(str(e))
return [{ except openai.error.APIConnectionError as e:
'id': deployment['id'], raise AzureRequestFailedError(
'name': '{} ({})'.format(deployment['id'], deployment['model']) 'Failed to request Azure OpenAI, please check your API Base Endpoint, The format is `https://xxx.openai.azure.com/`')
} for deployment in result['data'] if deployment['status'] == 'succeeded'] except openai.error.InvalidRequestError as e:
else: if e.http_status == 404:
if response.status_code == 401: raise AzureRequestFailedError("Please check your 'gpt-3.5-turbo' or 'text-embedding-ada-002' "
raise AzureAuthenticationError() "deployment name is exists in Azure AI")
else: 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: def get_credentials(self, model_id: Optional[str] = None) -> dict:
""" """
...@@ -98,31 +106,11 @@ class AzureProvider(BaseProvider): ...@@ -98,31 +106,11 @@ class AzureProvider(BaseProvider):
if 'openai_api_version' not in config: if 'openai_api_version' not in config:
config['openai_api_version'] = AZURE_OPENAI_API_VERSION config['openai_api_version'] = AZURE_OPENAI_API_VERSION
models = self.get_models(credentials=config) self.check_embedding_model(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)))
except ValidateFailedError as e: except ValidateFailedError as e:
raise e raise e
except AzureAuthenticationError: except AzureAuthenticationError:
raise ValidateFailedError('Validation failed, please check your API Key.') 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: except AzureRequestFailedError as ex:
raise ValidateFailedError('Validation failed, error: {}.'.format(str(ex))) raise ValidateFailedError('Validation failed, error: {}.'.format(str(ex)))
except Exception as ex: except Exception as ex:
......
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import style from './style.module.css' import style from './style.module.css'
...@@ -43,10 +43,15 @@ const prefixEmbedded = 'appOverview.overview.appInfo.embedded' ...@@ -43,10 +43,15 @@ const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
type Option = keyof typeof OPTION_MAP type Option = keyof typeof OPTION_MAP
type OptionStatus = {
iframe: boolean
scripts: boolean
}
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => { const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const [option, setOption] = useState<Option>('iframe') 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 [_, copy] = useCopyToClipboard()
const { langeniusVersionInfo } = useAppContext() const { langeniusVersionInfo } = useAppContext()
...@@ -56,6 +61,19 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => { ...@@ -56,6 +61,19 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
setIsCopied({ ...isCopied, [option]: true }) 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 ( return (
<Modal <Modal
title={t(`${prefixEmbedded}.title`)} title={t(`${prefixEmbedded}.title`)}
...@@ -77,7 +95,10 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => { ...@@ -77,7 +95,10 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
style[`${v}Icon`], style[`${v}Icon`],
option === v && style.active, option === v && style.active,
)} )}
onClick={() => setOption(v as Option)} onClick={() => {
setOption(v as Option)
resetCopyStatus()
}}
></div> ></div>
) )
})} })}
......
...@@ -15,6 +15,7 @@ export type Attrs = { ...@@ -15,6 +15,7 @@ export type Attrs = {
export function normalizeAttrs(attrs: Attrs = {}): Attrs { export function normalizeAttrs(attrs: Attrs = {}): Attrs {
return Object.keys(attrs).reduce((acc: Attrs, key) => { return Object.keys(attrs).reduce((acc: Attrs, key) => {
const val = attrs[key] const val = attrs[key]
key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase())
switch (key) { switch (key) {
case 'class': case 'class':
acc.className = val acc.className = val
......
'use client' 'use client'
import React, { useEffect, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { t } from 'i18next' import { t } from 'i18next'
import s from './style.module.css' import s from './style.module.css'
import { randomString } from '@/app/components/app-sidebar/basic'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
type IInputCopyProps = { type IInputCopyProps = {
...@@ -13,13 +14,15 @@ type IInputCopyProps = { ...@@ -13,13 +14,15 @@ type IInputCopyProps = {
} }
const InputCopy = ({ const InputCopy = ({
value, value = '',
className, className,
readOnly = true, readOnly = true,
children, children,
}: IInputCopyProps) => { }: IInputCopyProps) => {
const [isCopied, setIsCopied] = useState(false) const [isCopied, setIsCopied] = useState(false)
const selector = useRef(`input-tooltip-${randomString(4)}`)
useEffect(() => { useEffect(() => {
if (isCopied) { if (isCopied) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
...@@ -38,7 +41,7 @@ const InputCopy = ({ ...@@ -38,7 +41,7 @@ const InputCopy = ({
{children} {children}
<div className='flex-grow bg-gray-50 text-[13px] relative h-full'> <div className='flex-grow bg-gray-50 text-[13px] relative h-full'>
<Tooltip <Tooltip
selector="top-uniq" selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10' className='z-10'
> >
...@@ -50,7 +53,7 @@ const InputCopy = ({ ...@@ -50,7 +53,7 @@ const InputCopy = ({
</div> </div>
<div className="flex-shrink-0 h-4 bg-gray-200 border" /> <div className="flex-shrink-0 h-4 bg-gray-200 border" />
<Tooltip <Tooltip
selector="top-uniq" selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10' className='z-10'
> >
......
...@@ -18,7 +18,7 @@ const SecretKeyGenerateModal = ({ ...@@ -18,7 +18,7 @@ const SecretKeyGenerateModal = ({
isShow = false, isShow = false,
onClose, onClose,
newKey, newKey,
className className,
}: ISecretKeyGenerateModalProps) => { }: ISecretKeyGenerateModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid' import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import useSWR, { useSWRConfig } from 'swr' import useSWR, { useSWRConfig } from 'swr'
import { useContext } from 'use-context-selector'
import SecretKeyGenerateModal from './secret-key-generate' import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css' import s from './style.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
...@@ -16,7 +17,6 @@ import Tooltip from '@/app/components/base/tooltip' ...@@ -16,7 +17,6 @@ import Tooltip from '@/app/components/base/tooltip'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard' import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import { useContext } from 'use-context-selector'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
type ISecretKeyModalProps = { type ISecretKeyModalProps = {
...@@ -58,12 +58,11 @@ const SecretKeyModal = ({ ...@@ -58,12 +58,11 @@ const SecretKeyModal = ({
} }
}, [copyValue]) }, [copyValue])
const onDel = async () => { const onDel = async () => {
setShowConfirmDelete(false) setShowConfirmDelete(false)
if (!delKeyID) { if (!delKeyID)
return return
}
await delApikey({ url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }) await delApikey({ url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} })
mutate(commonParams) mutate(commonParams)
} }
...@@ -80,11 +79,10 @@ const SecretKeyModal = ({ ...@@ -80,11 +79,10 @@ const SecretKeyModal = ({
} }
const formatDate = (timestamp: any) => { 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) 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 new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).format((+timestamp) * 1000)
}
} }
return ( return (
...@@ -111,7 +109,7 @@ const SecretKeyModal = ({ ...@@ -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-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'> <div className='flex flex-grow px-3'>
<Tooltip <Tooltip
selector="top-uniq" selector={`key-${api.token}`}
content={copyValue === api.token ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} content={copyValue === api.token ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10' className='z-10'
> >
......
'use client' 'use client'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { t } from 'i18next' import { t } from 'i18next'
import s from './index.module.css' import s from './index.module.css'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard' import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import { randomString } from '@/app/components/app-sidebar/basic'
type IInvitationLinkProps = { type IInvitationLinkProps = {
value?: string value?: string
...@@ -13,6 +14,7 @@ const InvitationLink = ({ ...@@ -13,6 +14,7 @@ const InvitationLink = ({
value = '', value = '',
}: IInvitationLinkProps) => { }: IInvitationLinkProps) => {
const [isCopied, setIsCopied] = useState(false) const [isCopied, setIsCopied] = useState(false)
const selector = useRef(`invite-link-${randomString(4)}`)
const [_, copy] = useCopyToClipboard() const [_, copy] = useCopyToClipboard()
const copyHandle = useCallback(() => { const copyHandle = useCallback(() => {
...@@ -37,7 +39,7 @@ const InvitationLink = ({ ...@@ -37,7 +39,7 @@ const InvitationLink = ({
<div className="flex items-center flex-grow h-5"> <div className="flex items-center flex-grow h-5">
<div className='flex-grow bg-gray-100 text-[13px] relative h-full'> <div className='flex-grow bg-gray-100 text-[13px] relative h-full'>
<Tooltip <Tooltip
selector="top-uniq" selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10' className='z-10'
> >
...@@ -46,7 +48,7 @@ const InvitationLink = ({ ...@@ -46,7 +48,7 @@ const InvitationLink = ({
</div> </div>
<div className="flex-shrink-0 h-4 bg-gray-200 border" /> <div className="flex-shrink-0 h-4 bg-gray-200 border" />
<Tooltip <Tooltip
selector="top-uniq" selector={selector.current}
content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} content={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
className='z-10' className='z-10'
> >
......
...@@ -48,7 +48,10 @@ const List: FC<IListProps> = ({ ...@@ -48,7 +48,10 @@ const List: FC<IListProps> = ({
useInfiniteScroll( useInfiniteScroll(
async () => { async () => {
if (!isNoMore) { 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) const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
onMoreLoaded({ data: conversations, has_more }) onMoreLoaded({ data: conversations, has_more })
} }
......
...@@ -17,6 +17,7 @@ const InstallForm = () => { ...@@ -17,6 +17,7 @@ const InstallForm = () => {
const [email, setEmail] = React.useState('') const [email, setEmail] = React.useState('')
const [name, setName] = React.useState('') const [name, setName] = React.useState('')
const [password, setPassword] = React.useState('') const [password, setPassword] = React.useState('')
const [showPassword, setShowPassword] = React.useState(false)
const showErrorMessage = (message: string) => { const showErrorMessage = (message: string) => {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
...@@ -108,12 +109,21 @@ const InstallForm = () => { ...@@ -108,12 +109,21 @@ const InstallForm = () => {
<div className="mt-1 relative rounded-md shadow-sm"> <div className="mt-1 relative rounded-md shadow-sm">
<input <input
id="password" id="password"
type='password' type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
placeholder={t('login.passwordPlaceholder') || ''} 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'} 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>
<div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</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 CopiedValue = string | null
type CopyFn = (text: string) => Promise<boolean> type CopyFn = (text: string) => Promise<boolean>
function useCopyToClipboard(): [CopiedValue, CopyFn] { function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null) const [copiedText, setCopiedText] = useState<CopiedValue>(null)
const copy: CopyFn = async text => { const copy: CopyFn = useCallback(async (text: string) => {
if (!navigator?.clipboard) { if (!navigator?.clipboard) {
console.warn('Clipboard not supported') console.warn('Clipboard not supported')
return false return false
} }
try { try {
await navigator.clipboard.writeText(text) writeText(text)
setCopiedText(text) setCopiedText(text)
return true return true
} catch (error) { }
console.warn('Copy failed', error) catch (error) {
setCopiedText(null) console.warn('Copy failed', error)
return false setCopiedText(null)
} return false
} }
}, [])
return [copiedText, copy] return [copiedText, copy]
} }
export default useCopyToClipboard export default useCopyToClipboard
\ No newline at end of file
...@@ -43,7 +43,7 @@ const translation = { ...@@ -43,7 +43,7 @@ const translation = {
'较高的 Temperature 设置将导致更多样和创造性的输出,而较低的 Temperature 将产生更保守的输出并且类似于训练数据。', '较高的 Temperature 设置将导致更多样和创造性的输出,而较低的 Temperature 将产生更保守的输出并且类似于训练数据。',
topP: '采样范围', topP: '采样范围',
topPTip: topPTip:
'Top P值越高,输出与训练文本越相似,Top P值越低,输出越有创意和变化。它可用于使输出更适合特定用例。', 'Top P值越低,输出与训练文本越相似,Top P值越高,输出越有创意和变化。它可用于使输出更适合特定用例。',
presencePenalty: '词汇控制', presencePenalty: '词汇控制',
presencePenaltyTip: presencePenaltyTip:
'Presence penalty 是根据新词是否出现在目前的文本中来对其进行惩罚。正值将降低模型谈论新话题的可能性。', 'Presence penalty 是根据新词是否出现在目前的文本中来对其进行惩罚。正值将降低模型谈论新话题的可能性。',
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
"katex": "^0.16.7", "katex": "^0.16.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"negotiator": "^0.6.3", "negotiator": "^0.6.3",
"next": "^13.4.7", "next": "13.3.0",
"qs": "^6.11.1", "qs": "^6.11.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
......
...@@ -15,6 +15,7 @@ async function embedChatbot () { ...@@ -15,6 +15,7 @@ async function embedChatbot () {
return; return;
} }
const isDev = !!difyChatbotConfig.isDev const isDev = !!difyChatbotConfig.isDev
const baseUrl = difyChatbotConfig.baseUrl || `https://${isDev ? 'dev.' : ''}udify.app`
const openIcon = `<svg const openIcon = `<svg
id="openIcon" id="openIcon"
width="24" width="24"
...@@ -53,7 +54,7 @@ async function embedChatbot () { ...@@ -53,7 +54,7 @@ async function embedChatbot () {
iframe.allow = "fullscreen;microphone" iframe.allow = "fullscreen;microphone"
iframe.title = "dify chatbot bubble window" iframe.title = "dify chatbot bubble window"
iframe.id = '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;' 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); 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" id="openIcon"
width="24" width="24"
height="24" height="24"
...@@ -27,4 +27,4 @@ async function embedChatbot(){const t=window.difyChatbotConfig;if(t&&t.token){co ...@@ -27,4 +27,4 @@ async function embedChatbot(){const t=window.difyChatbotConfig;if(t&&t.token){co
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="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; </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 \ 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