Feat: Support re-segmentation (#114)

Co-authored-by: John Wang <takatost@gmail.com>
Co-authored-by: Jyong <718720800@qq.com>
Co-authored-by: 金伟强 <iamjoel007@gmail.com>
This commit is contained in:
KVOJJJin
2023-06-01 23:19:36 +08:00
committed by GitHub
parent f65a3ad1cc
commit c67f626b66
61 changed files with 1166 additions and 759 deletions

View File

@@ -1,14 +1,14 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useBoolean, useClickAway } from 'ahooks'
import { ChevronDownIcon, Cog8ToothIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
import ParamItem from './param-item'
import Radio from '@/app/components/base/radio'
import Panel from '@/app/components/base/panel'
import type { CompletionParams } from '@/models/debug'
import { Cog8ToothIcon, InformationCircleIcon, ChevronDownIcon } from '@heroicons/react/24/outline'
import { AppType } from '@/types/app'
import { TONE_LIST } from '@/config'
import Toast from '@/app/components/base/toast'
@@ -51,7 +51,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
}) => {
const { t } = useTranslation()
const isChatApp = mode === AppType.chat
const availableModels = options.filter((item) => item.type === mode)
const availableModels = options.filter(item => item.type === mode)
const [isShowConfig, { setFalse: hideConfig, toggle: toogleShowConfig }] = useBoolean(false)
const configContentRef = React.useRef(null)
useClickAway(() => {
@@ -116,14 +116,14 @@ const ConifgModel: FC<IConifgModelProps> = ({
onShowUseGPT4Confirm()
return
}
if(id !== 'gpt-4' && completionParams.max_tokens > 4000) {
if (id !== 'gpt-4' && completionParams.max_tokens > 4000) {
Toast.notify({
type: 'warning',
message: t('common.model.params.setToCurrentModelMaxTokenTip')
message: t('common.model.params.setToCurrentModelMaxTokenTip'),
})
onCompletionParamsChange({
...completionParams,
max_tokens: 4000
max_tokens: 4000,
})
}
setModelId(id)
@@ -153,7 +153,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
setToneId(id)
onCompletionParamsChange({
...tone.config,
max_tokens: completionParams.max_tokens
max_tokens: completionParams.max_tokens,
} as CompletionParams)
}
}
@@ -178,7 +178,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
return (
<div className='relative' ref={configContentRef}>
<div
className={cn(`flex items-center border h-8 px-2.5 space-x-2 rounded-lg`, disabled ? diabledStyle : ableStyle)}
className={cn('flex items-center border h-8 px-2.5 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
onClick={() => !disabled && toogleShowConfig()}
>
<ModelIcon />
@@ -206,14 +206,14 @@ const ConifgModel: FC<IConifgModelProps> = ({
<div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div>
{/* model selector */}
<div className="relative" style={{zIndex: 30}}>
<div ref={triggerRef} onClick={() => !selectModelDisabled && toogleOption()} className={cn(selectModelDisabled ? 'cursor-not-allowed' : 'cursor-pointer', "flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ")}>
<div className="relative" style={{ zIndex: 30 }}>
<div ref={triggerRef} onClick={() => !selectModelDisabled && toogleOption()} className={cn(selectModelDisabled ? 'cursor-not-allowed' : 'cursor-pointer', 'flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ')}>
<ModelIcon />
<div className="text-sm gray-900">{selectedModel?.name}</div>
{!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
</div>
{isShowOption && (
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', "absolute right-0 bg-gray-50 rounded-lg shadow")}>
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', 'absolute right-0 bg-gray-50 rounded-lg shadow')}>
{availableModels.map(item => (
<div key={item.id} onClick={handleSelectModel(item.id)} className="flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100">
<ModelIcon className='mr-2' />

View File

@@ -1,19 +1,19 @@
'use client'
import React, { FC, useEffect } from 'react'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { DataSet } from '@/models/datasets'
import Link from 'next/link'
import TypeIcon from '../type-icon'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import type { DataSet } from '@/models/datasets'
import Button from '@/app/components/base/button'
import { fetchDatasets } from '@/service/datasets'
import Loading from '@/app/components/base/loading'
import { formatNumber } from '@/utils/format'
import Link from 'next/link'
import s from './style.module.css'
export interface ISelectDataSetProps {
export type ISelectDataSetProps = {
isShow: boolean
onClose: () => void
selectedIds: string[]
@@ -37,20 +37,19 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
setDataSets(data)
setLoaded(true)
setSelected(data.filter((item) => selectedIds.includes(item.id)))
setSelected(data.filter(item => selectedIds.includes(item.id)))
})()
}, [])
const toggleSelect = (dataSet: DataSet) => {
const isSelected = selected.some((item) => item.id === dataSet.id)
const isSelected = selected.some(item => item.id === dataSet.id)
if (isSelected) {
setSelected(selected.filter((item) => item.id !== dataSet.id))
setSelected(selected.filter(item => item.id !== dataSet.id))
}
else {
if (canSelectMulti) {
if (canSelectMulti)
setSelected([...selected, dataSet])
} else {
else
setSelected([dataSet])
}
}
}
@@ -74,7 +73,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
<div className='flex items-center justify-center mt-6 rounded-lg space-x-1 h-[128px] text-[13px] border'
style={{
background: 'rgba(0, 0, 0, 0.02)',
borderColor: 'rgba(0, 0, 0, 0.02'
borderColor: 'rgba(0, 0, 0, 0.02',
}}
>
<span className='text-gray-500'>{t('appDebug.feature.dataSet.noDataSet')}</span>
@@ -85,7 +84,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
{datasets && datasets?.length > 0 && (
<>
<div className='mt-7 space-y-1 max-h-[286px] overflow-y-auto'>
{datasets.map((item) => (
{datasets.map(item => (
<div
key={item.id}
className={cn(s.item, selected.some(i => i.id === item.id) && s.selected, 'flex justify-between items-center h-10 px-2 rounded-lg bg-white border border-gray-200 cursor-pointer')}

View File

@@ -1,11 +1,13 @@
/* eslint-disable multiline-ternary */
'use client'
import React, { FC, useEffect, useRef, useState } from 'react'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import { useContext } from 'use-context-selector'
import ConfigContext from '@/context/debug-configuration'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import ConfigContext from '@/context/debug-configuration'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import Button from '@/app/components/base/button'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
@@ -14,7 +16,7 @@ import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/conf
import { getNewVar } from '@/utils/var'
import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
export interface IOpeningStatementProps {
export type IOpeningStatementProps = {
promptTemplate: string
value: string
onChange: (value: string) => void
@@ -25,7 +27,7 @@ const regex = /\{\{([^}]+)\}\}/g
const OpeningStatement: FC<IOpeningStatementProps> = ({
value = '',
onChange
onChange,
}) => {
const { t } = useTranslation()
const {
@@ -60,8 +62,6 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
const handleEdit = () => {
setFocus()
@@ -76,15 +76,15 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
const handleConfirm = () => {
const keys = getInputKeys(tempValue)
const promptKeys = promptVariables.map((item) => item.key)
const promptKeys = promptVariables.map(item => item.key)
let notIncludeKeys: string[] = []
if (promptKeys.length === 0) {
if (keys.length > 0) {
if (keys.length > 0)
notIncludeKeys = keys
}
} else {
notIncludeKeys = keys.filter((key) => !promptKeys.includes(key))
}
else {
notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
}
if (notIncludeKeys.length > 0) {
@@ -104,7 +104,7 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
const autoAddVar = () => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...notIncludeKeys.map((key) => getNewVar(key))]
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...notIncludeKeys.map(key => getNewVar(key))]
})
onChange(tempValue)
setModelConfig(newModelConfig)
@@ -130,26 +130,30 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
isFocus={isFocus}
>
<div className='text-gray-700 text-sm'>
{(hasValue || (!hasValue && isFocus)) ? (
<>
{isFocus ? (
<textarea
ref={inputRef}
value={tempValue}
rows={3}
onChange={e => setTempValue(e.target.value)}
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
placeholder={t('appDebug.openingStatement.placeholder') as string}
>
</textarea>
) : (
<div dangerouslySetInnerHTML={{
__html: coloredContent
}}></div>
)}
{(hasValue || (!hasValue && isFocus))
? (
<>
{isFocus
? (
<textarea
ref={inputRef}
value={tempValue}
rows={3}
onChange={e => setTempValue(e.target.value)}
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
placeholder={t('appDebug.openingStatement.placeholder') as string}
>
</textarea>
)
: (
<div dangerouslySetInnerHTML={{
__html: coloredContent,
}}></div>
)}
{/* Operation Bar */}
{isFocus && (
{/* Operation Bar */}
{isFocus
&& (
<div className='mt-2 flex items-center justify-between'>
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.varTip')}</div>
@@ -160,9 +164,9 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
</div>
)}
</>) : (
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
)}
</>) : (
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
)}
{isShowConfirmAddVar && (
<ConfirmAddVar

View File

@@ -6,12 +6,12 @@ import { useContext } from 'use-context-selector'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
import VarIcon from '../base/icons/var-icon'
import ConfigContext from '@/context/debug-configuration'
import type { PromptVariable } from '@/models/debug'
import { AppType } from '@/types/app'
import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import VarIcon from '../base/icons/var-icon'
import Button from '@/app/components/base/button'
export type IPromptValuePanelProps = {
@@ -71,17 +71,19 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
</div>
<div className='mt-2 leading-normal'>
{
(promptTemplate && promptTemplate?.trim()) ? (
<div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}}
>
</div>
) : (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
)
(promptTemplate && promptTemplate?.trim())
? (
<div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}}
>
</div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
)
}
</div>
</div>
@@ -105,37 +107,41 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
)}
</div>
{
promptVariables.length > 0 ? (
<div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="flex items-center justify-between">
<div className="mr-1 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
{type === 'select' ? (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
) : (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="text"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
promptVariables.length > 0
? (
<div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="flex items-center justify-between">
<div className="mr-1 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
{type === 'select'
? (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)
: (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="text"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div>
))}
</div>
) : (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
)
</div>
))}
</div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
)
}
</div>

View File

@@ -1,18 +1,19 @@
'use client'
import React, { FC, useState } from 'react'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import copy from 'copy-to-clipboard'
import Toast from '@/app/components/base/toast'
import { Feedbacktype } from '@/app/components/app/chat'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import type { Feedbacktype } from '@/app/components/app/chat'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
const MAX_DEPTH = 3
export interface IGenerationItemProps {
export type IGenerationItemProps = {
className?: string
content: string
messageId?: string | null
@@ -24,13 +25,13 @@ export interface IGenerationItemProps {
onFeedback?: (feedback: Feedbacktype) => void
onSave?: (messageId: string) => void
isMobile?: boolean
isInstalledApp: boolean,
installedAppId?: string,
isInstalledApp: boolean
installedAppId?: string
}
export const SimpleBtn = ({ className, onClick, children }: {
className?: string
onClick?: () => void,
onClick?: () => void
children: React.ReactNode
}) => (
<div
@@ -88,7 +89,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
const [childMessageId, setChildMessageId] = useState<string | null>(null)
const hasChild = !!childMessageId
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
rating: null
rating: null,
})
const handleFeedback = async (childFeedback: Feedbacktype) => {
@@ -126,115 +127,120 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}
const mainStyle = (() => {
const res: any = !isTop ? {
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff'
} : {}
const res: any = !isTop
? {
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
}
: {}
if (hasChild) {
if (hasChild)
res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
}
return res
})()
return (
<div className={cn(className, isTop ? 'rounded-xl border border-gray-200 bg-white' : 'rounded-br-xl !mt-0')}
style={isTop ? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'
} : {}}
style={isTop
? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}
: {}}
>
{isLoading ? (
<div className='flex items-center h-10'><Loading type='area' /></div>
) : (
<div
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
style={mainStyle}
>
<Markdown content={content} />
{messageId && (
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
{copyIcon}
{!isMobile && <div>{t('common.operation.copy')}</div>}
</SimpleBtn>
{isInWebApp && (
<>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }}
>
{saveIcon}
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
{(moreLikeThis && depth < MAX_DEPTH) && (
{isLoading
? (
<div className='flex items-center h-10'><Loading type='area' /></div>
)
: (
<div
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
style={mainStyle}
>
<Markdown content={content} />
{messageId && (
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
{copyIcon}
{!isMobile && <div>{t('common.operation.copy')}</div>}
</SimpleBtn>
{isInWebApp && (
<>
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={handleMoreLikeThis}
onClick={() => { onSave?.(messageId as string) }}
>
{moreLikeThisIcon}
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
{!feedback?.rating && (
<SimpleBtn className="!px-0">
<>
<div
onClick={() => {
onFeedback?.({
rating: 'like'
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbUpIcon width={16} height={16} />
</div>
<div
onClick={() => {
onFeedback?.({
rating: 'dislike'
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbDownIcon width={16} height={16} />
</div>
</>
{saveIcon}
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
)}
{feedback?.rating === 'like' && (
<div
onClick={() => {
onFeedback?.({
rating: null
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
<HandThumbUpIcon width={16} height={16} />
</div>
)}
{feedback?.rating === 'dislike' && (
<div
onClick={() => {
onFeedback?.({
rating: null
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
<HandThumbDownIcon width={16} height={16} />
</div>
)}
</>
)}
{(moreLikeThis && depth < MAX_DEPTH) && (
<SimpleBtn
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={handleMoreLikeThis}
>
{moreLikeThisIcon}
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
{!feedback?.rating && (
<SimpleBtn className="!px-0">
<>
<div
onClick={() => {
onFeedback?.({
rating: 'like',
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbUpIcon width={16} height={16} />
</div>
<div
onClick={() => {
onFeedback?.({
rating: 'dislike',
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbDownIcon width={16} height={16} />
</div>
</>
</SimpleBtn>
)}
{feedback?.rating === 'like' && (
<div
onClick={() => {
onFeedback?.({
rating: null,
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
<HandThumbUpIcon width={16} height={16} />
</div>
)}
{feedback?.rating === 'dislike' && (
<div
onClick={() => {
onFeedback?.({
rating: null,
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
<HandThumbDownIcon width={16} height={16} />
</div>
)}
</>
)}
</div>
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
</div>
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
</div>
)}
</div>
)}
)}
</div>
)}
{((childMessageId || isQuerying) && depth < 3) && (
<div className='pl-4'>

View File

@@ -1,9 +1,9 @@
import type { FC } from 'react'
import classNames from 'classnames'
import style from './style.module.css'
import data from '@emoji-mart/data'
import { init } from 'emoji-mart'
import style from './style.module.css'
init({ data })
@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({
}}
onClick={onClick}
>
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
{innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)}
</span>
)
}

View File

@@ -3,11 +3,11 @@
import type { ChangeEvent, FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import classNames from 'classnames'
import { checkKeys } from '@/utils/var'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Toast from '../toast'
import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
import Button from '@/app/components/base/button'
import { checkKeys } from '@/utils/var'
// regex to match the {{}} and replace it with a span
const regex = /\{\{([^}]+)\}\}/g
@@ -55,9 +55,9 @@ const BlockInput: FC<IBlockInputProps> = ({
useEffect(() => {
if (isEditing && contentEditableRef.current) {
// TODO: Focus at the click positon
if (currentValue) {
if (currentValue)
contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
}
contentEditableRef.current.focus()
}
}, [isEditing])
@@ -72,7 +72,6 @@ const BlockInput: FC<IBlockInputProps> = ({
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
// Not use useCallback. That will cause out callback get old data.
const handleSubmit = () => {
@@ -83,7 +82,7 @@ const BlockInput: FC<IBlockInputProps> = ({
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey })
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
})
return
}
@@ -125,9 +124,9 @@ const BlockInput: FC<IBlockInputProps> = ({
value={currentValue}
onBlur={() => {
blur()
if (!isContentChanged) {
if (!isContentChanged)
setIsEditing(false)
}
// click confirm also make blur. Then outter value is change. So below code has problem.
// setTimeout(() => {
// handleCancel()
@@ -143,31 +142,33 @@ const BlockInput: FC<IBlockInputProps> = ({
{textAreaContent}
{/* footer */}
<div className='flex item-center h-14 px-4'>
{isContentChanged ? (
<div className='flex items-center justify-between w-full'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue.length}</div>
<div className='flex space-x-2'>
<Button
onClick={handleCancel}
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleSubmit}
type="primary"
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.confirm')}
</Button>
</div>
{isContentChanged
? (
<div className='flex items-center justify-between w-full'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue.length}</div>
<div className='flex space-x-2'>
<Button
onClick={handleCancel}
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleSubmit}
type="primary"
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.confirm')}
</Button>
</div>
</div>
) : (
<p className="leading-5 text-xs text-gray-500">
{t('appDebug.promptTip')}
</p>
)}
</div>
)
: (
<p className="leading-5 text-xs text-gray-500">
{t('appDebug.promptTip')}
</p>
)}
</div>
</div>

View File

@@ -1,26 +1,28 @@
/* eslint-disable multiline-ternary */
'use client'
import React from 'react'
import { useState, FC, ChangeEvent } from 'react'
import type { ChangeEvent, FC } from 'react'
import React, { useState } from 'react'
import data from '@emoji-mart/data'
import { init, SearchIndex } from 'emoji-mart'
import { SearchIndex, init } from 'emoji-mart'
import cn from 'classnames'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import s from './style.module.css'
import {
MagnifyingGlassIcon
} from '@heroicons/react/24/outline'
import Modal from '@/app/components/base/modal'
import { useTranslation } from 'react-i18next'
declare global {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements {
'em-emoji': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
React.HTMLAttributes<HTMLElement>,
HTMLElement
>
}
}
}
@@ -57,7 +59,7 @@ const backgroundColors = [
'#ECE9FE',
'#FFE4E8',
]
interface IEmojiPickerProps {
type IEmojiPickerProps = {
isModal?: boolean
onSelect?: (emoji: string, background: string) => void
onClose?: () => void
@@ -66,7 +68,7 @@ interface IEmojiPickerProps {
const EmojiPicker: FC<IEmojiPickerProps> = ({
isModal = true,
onSelect,
onClose
onClose,
}) => {
const { t } = useTranslation()
@@ -97,8 +99,8 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
onChange={async (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.value === '') {
setIsSearching(false)
return
} else {
}
else {
setIsSearching(true)
const emojis = await search(e.target.value)
setSearchedEmojis(emojis)
@@ -111,7 +113,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
<div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3">
{isSearching && <>
<div key={`category-search`} className='flex flex-col'>
<div key={'category-search'} className='flex flex-col'>
<p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p>
<div className='w-full h-full grid grid-cols-8 gap-1'>
{searchedEmojis.map((emoji: string, index: number) => {
@@ -131,7 +133,6 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
</div>
</>}
{categories.map((category: any, index: number) => {
return <div key={`category-${index}`} className='flex flex-col'>
<p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p>
@@ -156,7 +157,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
</div>
{/* Color Select */}
<div className={cn('p-3 ', selectedEmoji == '' ? 'opacity-25' : '')}>
<div className={cn('p-3 ', selectedEmoji === '' ? 'opacity-25' : '')}>
<p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p>
<div className='w-full h-full grid grid-cols-8 gap-1'>
{backgroundColors.map((color) => {
@@ -165,9 +166,9 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
className={
cn(
'cursor-pointer',
`hover:ring-1 ring-offset-1`,
'hover:ring-1 ring-offset-1',
'inline-flex w-10 h-10 rounded-lg items-center justify-center',
color === selectedBackground ? `ring-1 ring-gray-300` : '',
color === selectedBackground ? 'ring-1 ring-gray-300' : '',
)}
onClick={() => {
setSelectedBackground(color)
@@ -191,7 +192,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
{t('app.emoji.cancel')}
</Button>
<Button
disabled={selectedEmoji == ''}
disabled={selectedEmoji === ''}
type="primary"
className='w-full'
onClick={() => {

View File

@@ -9,7 +9,7 @@ import {
createDocument,
fetchFileIndexingEstimate as didFetchFileIndexingEstimate,
} from '@/service/datasets'
import type { CreateDocumentReq, createDocumentResponse } from '@/models/datasets'
import type { CreateDocumentReq, createDocumentResponse, FullDocumentDetail } from '@/models/datasets'
import Button from '@/app/components/base/button'
import PreviewItem from './preview-item'
import Loading from '@/app/components/base/loading'
@@ -22,14 +22,18 @@ import Toast from '@/app/components/base/toast'
import { formatNumber } from '@/utils/format'
type StepTwoProps = {
isSetting?: boolean,
documentDetail?: FullDocumentDetail
hasSetAPIKEY: boolean,
onSetting: () => void,
datasetId?: string,
indexingType?: string,
file?: File,
onStepChange: (delta: number) => void,
updateIndexingTypeCache: (type: string) => void,
updateResultCache: (res: createDocumentResponse) => void
onStepChange?: (delta: number) => void,
updateIndexingTypeCache?: (type: string) => void,
updateResultCache?: (res: createDocumentResponse) => void
onSave?: () => void
onCancel?: () => void
}
enum SegmentType {
@@ -42,6 +46,8 @@ enum IndexingType {
}
const StepTwo = ({
isSetting,
documentDetail,
hasSetAPIKEY,
onSetting,
datasetId,
@@ -50,6 +56,8 @@ const StepTwo = ({
onStepChange,
updateIndexingTypeCache,
updateResultCache,
onSave,
onCancel,
}: StepTwoProps) => {
const { t } = useTranslation()
const scrollRef = useRef<HTMLDivElement>(null)
@@ -171,15 +179,23 @@ const StepTwo = ({
}
const getCreationParams = () => {
const params = {
data_source: {
type: 'upload_file',
info: file?.id,
name: file?.name,
},
indexing_technique: getIndexing_technique(),
process_rule: getProcessRule(),
} as CreateDocumentReq
let params
if (isSetting) {
params = {
original_document_id: documentDetail?.id,
process_rule: getProcessRule(),
} as CreateDocumentReq
} else {
params = {
data_source: {
type: 'upload_file',
info: file?.id,
name: file?.name,
},
indexing_technique: getIndexing_technique(),
process_rule: getProcessRule(),
} as CreateDocumentReq
}
return params
}
@@ -196,6 +212,25 @@ const StepTwo = ({
console.log(err)
}
}
const getRulesFromDetail = () => {
if (documentDetail) {
const rules = documentDetail.dataset_process_rule.rules
const separator = rules.segmentation.separator
const max = rules.segmentation.max_tokens
setSegmentIdentifier(separator === '\n' ? '\\n' : separator || '\\n')
setMax(max)
setRules(rules.pre_processing_rules)
setDefaultConfig(rules)
}
}
const getDefaultMode = () => {
if (documentDetail) {
setSegmentationType(documentDetail.dataset_process_rule.mode)
}
}
const createHandle = async () => {
try {
let res;
@@ -204,19 +239,20 @@ const StepTwo = ({
res = await createFirstDocument({
body: params
})
updateIndexingTypeCache(indexType)
updateResultCache(res)
updateIndexingTypeCache && updateIndexingTypeCache(indexType)
updateResultCache && updateResultCache(res)
} else {
res = await createDocument({
datasetId,
body: params
})
updateIndexingTypeCache(indexType)
updateResultCache({
updateIndexingTypeCache && updateIndexingTypeCache(indexType)
updateResultCache && updateResultCache({
document: res,
})
}
onStepChange(+1)
onStepChange && onStepChange(+1)
isSetting && onSave && onSave()
}
catch (err) {
Toast.notify({
@@ -228,7 +264,12 @@ const StepTwo = ({
useEffect(() => {
// fetch rules
getRules()
if (!isSetting) {
getRules()
} else {
getRulesFromDetail()
getDefaultMode()
}
}, [])
useEffect(() => {
@@ -444,11 +485,18 @@ const StepTwo = ({
</div>
</div>
</div>
<div className='flex items-center mt-8 py-2'>
<Button onClick={() => onStepChange(-1)}>{t('datasetCreation.stepTwo.lastStep')}</Button>
<div className={s.divider} />
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.nextStep')}</Button>
</div>
{!isSetting ? (
<div className='flex items-center mt-8 py-2'>
<Button onClick={() => onStepChange && onStepChange(-1)}>{t('datasetCreation.stepTwo.lastStep')}</Button>
<div className={s.divider} />
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.nextStep')}</Button>
</div>
) : (
<div className='flex items-center mt-8 py-2'>
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.save')}</Button>
<Button className='ml-2' onClick={onCancel}>{t('datasetCreation.stepTwo.cancel')}</Button>
</div>
)}
</div>
</div>
</div>

View File

@@ -19,7 +19,7 @@ import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
import type { CommonResponse } from '@/models/common'
import { asyncRunSafe } from '@/utils'
import { formatNumber } from '@/utils/format'
import { fetchIndexingEstimate, fetchIndexingStatus, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import { fetchIndexingEstimate, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'
@@ -118,14 +118,45 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
const localDocumentId = docId ?? documentId
const localIndexingTechnique = indexingType ?? indexingTechnique
const { data: indexingStatusDetail, error: indexingStatusErr, mutate: statusMutate } = useSWR({
action: 'fetchIndexingStatus',
datasetId: localDatasetId,
documentId: localDocumentId,
}, apiParams => fetchIndexingStatus(omit(apiParams, 'action')), {
refreshInterval: 5000,
revalidateOnFocus: false,
})
// const { data: indexingStatusDetailFromApi, error: indexingStatusErr, mutate: statusMutate } = useSWR({
// action: 'fetchIndexingStatus',
// datasetId: localDatasetId,
// documentId: localDocumentId,
// }, apiParams => fetchIndexingStatus(omit(apiParams, 'action')), {
// refreshInterval: 2500,
// revalidateOnFocus: false,
// })
const [indexingStatusDetail, setIndexingStatusDetail, getIndexingStatusDetail] = useGetState<any>(null)
const fetchIndexingStatus = async () => {
const status = await doFetchIndexingStatus({ datasetId: localDatasetId, documentId: localDocumentId })
setIndexingStatusDetail(status)
}
const [runId, setRunId, getRunId] = useGetState<any>(null)
const startQueryStatus = () => {
const runId = setInterval(() => {
const indexingStatusDetail = getIndexingStatusDetail()
if (indexingStatusDetail?.indexing_status === 'completed') {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stopQueryStatus()
return
}
fetchIndexingStatus()
}, 2500)
setRunId(runId)
}
const stopQueryStatus = () => {
clearInterval(getRunId())
}
useEffect(() => {
fetchIndexingStatus()
startQueryStatus()
return () => {
stopQueryStatus()
}
}, [])
const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({
action: 'fetchIndexingEstimate',
@@ -168,7 +199,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>)
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
statusMutate()
setIndexingStatusDetail(null)
}
else {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })

View File

@@ -0,0 +1,90 @@
'use client'
import React, { useState, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import DatasetDetailContext from '@/context/dataset-detail'
import type { FullDocumentDetail } from '@/models/datasets'
import { fetchTenantInfo } from '@/service/common'
import { fetchDocumentDetail, MetadataType } from '@/service/datasets'
import Loading from '@/app/components/base/loading'
import StepTwo from '@/app/components/datasets/create/step-two'
import AccountSetting from '@/app/components/header/account-setting'
import AppUnavailable from '@/app/components/base/app-unavailable'
type DocumentSettingsProps = {
datasetId: string;
documentId: string;
}
const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
const { t } = useTranslation()
const router = useRouter()
const [hasSetAPIKEY, setHasSetAPIKEY] = useState(true)
const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
const [hasError, setHasError] = useState(false)
const { indexingTechnique, dataset } = useContext(DatasetDetailContext)
const saveHandler = () => router.push(`/datasets/${datasetId}/documents/${documentId}`)
const cancelHandler = () => router.back()
const checkAPIKey = async () => {
const data = await fetchTenantInfo({ url: '/info' })
const hasSetKey = data.providers.some(({ is_valid }) => is_valid)
setHasSetAPIKEY(hasSetKey)
}
useEffect(() => {
checkAPIKey()
}, [])
const [documentDetail, setDocumentDetail] = useState<FullDocumentDetail | null>(null)
useEffect(() => {
(async () => {
try {
const detail = await fetchDocumentDetail({
datasetId,
documentId,
params: { metadata: 'without' as MetadataType }
})
setDocumentDetail(detail)
} catch (e) {
setHasError(true)
}
})()
}, [datasetId, documentId])
if (hasError) {
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
}
return (
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
<div className="grow bg-white">
{!documentDetail && <Loading type='app' />}
{dataset && documentDetail && (
<StepTwo
hasSetAPIKEY={hasSetAPIKEY}
onSetting={showSetAPIKey}
datasetId={datasetId}
indexingType={indexingTechnique || ''}
isSetting
documentDetail={documentDetail}
file={documentDetail.data_source_info.upload_file}
onSave={saveHandler}
onCancel={cancelHandler}
/>
)}
</div>
{isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
await checkAPIKey()
hideSetAPIkey()
}} />}
</div>
)
}
export default DocumentSettings

View File

@@ -95,6 +95,7 @@ export const OperationAction: FC<{
const [showModal, setShowModal] = useState(false)
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const router = useRouter()
const isListScene = scene === 'list'
@@ -166,15 +167,19 @@ export const OperationAction: FC<{
</div>
<Divider />
</>}
{/* <div className={s.actionItem}>
<SettingsIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
</div>
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/create`)}>
<FilePlusIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.uploadFile')}</span>
</div>
<Divider className='my-1' /> */}
{!archived && (
<>
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
<SettingsIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
</div>
{/* <div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/create`)}>
<FilePlusIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.uploadFile')}</span>
</div> */}
<Divider className='my-1' />
</>
)}
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
<ArchiveIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>

View File

@@ -72,7 +72,7 @@
.txtIcon {
background-image: url(./assets/txt.svg);
}
.mdIcon {
.markdownIcon {
background-image: url(./assets/md.svg);
}
.statusItemDetail {

View File

@@ -1,15 +1,15 @@
'use client'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { BookOpenIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import { ToastContext } from '@/app/components/base/toast'
import PermissionsRadio from '../permissions-radio'
import IndexMethodRadio from '../index-method-radio'
import { ToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import { updateDatasetSetting, fetchDataDetail } from '@/service/datasets'
import { DataSet } from '@/models/datasets'
import { fetchDataDetail, updateDatasetSetting } from '@/service/datasets'
import type { DataSet } from '@/models/datasets'
const rowClass = `
flex justify-between py-4
@@ -20,8 +20,7 @@ const labelClass = `
const inputClass = `
w-[480px] px-3 bg-gray-100 text-sm text-gray-800 rounded-lg outline-none appearance-none
`
const useInitialValue = <T,>(depend: T, dispatch: Dispatch<SetStateAction<T>>) => {
const useInitialValue = (depend: any, dispatch: any) => {
useEffect(() => {
dispatch(depend)
}, [depend])
@@ -32,7 +31,7 @@ type Props = {
}
const Form = ({
datasetId
datasetId,
}: Props) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
@@ -44,7 +43,8 @@ const Form = ({
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
const handleSave = async () => {
if (loading) return
if (loading)
return
if (!name?.trim()) {
notify({ type: 'error', message: t('datasetSettings.form.nameError') })
return
@@ -57,14 +57,16 @@ const Form = ({
name,
description,
permission,
indexing_technique: indexMethod
}
indexing_technique: indexMethod,
},
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
await mutateDatasets()
} catch (e) {
}
catch (e) {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
} finally {
}
finally {
setLoading(false)
}
}
@@ -142,4 +144,4 @@ const Form = ({
)
}
export default Form
export default Form

View File

@@ -1,9 +1,9 @@
'use client'
import React, { useEffect, useState } from 'react'
import copy from 'copy-to-clipboard'
import Tooltip from '@/app/components/base/tooltip'
import { t } from 'i18next'
import s from './style.module.css'
import Tooltip from '@/app/components/base/tooltip'
type IInputCopyProps = {
value?: string

View File

@@ -1,22 +1,22 @@
'use client'
import React, { FC, useEffect } from 'react'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import Toast from '../../base/toast'
import s from './style.module.css'
import ExploreContext from '@/context/explore-context'
import { App } from '@/models/explore'
import type { App } from '@/models/explore'
import Category from '@/app/components/explore/category'
import AppCard from '@/app/components/explore/app-card'
import { fetchAppList, installApp, fetchAppDetail } from '@/service/explore'
import { fetchAppDetail, fetchAppList, installApp } from '@/service/explore'
import { createApp } from '@/service/apps'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import Loading from '@/app/components/base/loading'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import s from './style.module.css'
import Toast from '../../base/toast'
const Apps: FC = ({ }) => {
const Apps: FC = () => {
const { t } = useTranslation()
const router = useRouter()
const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext)
@@ -25,13 +25,14 @@ const Apps: FC = ({ }) => {
const [isLoaded, setIsLoaded] = React.useState(false)
const currList = (() => {
if(currCategory === '') return allList
if (currCategory === '')
return allList
return allList.filter(item => item.category === currCategory)
})()
const [categories, setCategories] = React.useState([])
useEffect(() => {
(async () => {
const {categories, recommended_apps}:any = await fetchAppList()
const { categories, recommended_apps }: any = await fetchAppList()
setCategories(categories)
setAllList(recommended_apps)
setIsLoaded(true)
@@ -49,9 +50,9 @@ const Apps: FC = ({ }) => {
const [currApp, setCurrApp] = React.useState<App | null>(null)
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
const onCreate = async ({name, icon, icon_background}: any) => {
const onCreate = async ({ name, icon, icon_background }: any) => {
const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string)
try {
const app = await createApp({
name,
@@ -67,12 +68,13 @@ const Apps: FC = ({ }) => {
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
router.push(`/app/${app.id}/overview`)
} catch (e) {
}
catch (e) {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
if(!isLoaded) {
if (!isLoaded) {
return (
<div className='flex h-full items-center'>
<Loading type='area' />
@@ -92,16 +94,16 @@ const Apps: FC = ({ }) => {
value={currCategory}
onChange={setCurrCategory}
/>
<div
<div
className='flex mt-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'
style={{
maxHeight: 'calc(100vh - 243px)'
maxHeight: 'calc(100vh - 243px)',
}}
>
<nav
className={`${s.appList} grid content-start grid-cols-1 gap-4 px-12 pb-10grow shrink-0`}>
{currList.map(app => (
<AppCard
<AppCard
key={app.app_id}
app={app}
canCreate={hasEditPermission}
@@ -116,13 +118,13 @@ const Apps: FC = ({ }) => {
</div>
{isShowCreateModal && (
<CreateAppModal
appName={currApp?.app.name || ''}
show={isShowCreateModal}
onConfirm={onCreate}
onHide={() => setIsShowCreateModal(false)}
/>
)}
<CreateAppModal
appName={currApp?.app.name || ''}
show={isShowCreateModal}
onConfirm={onCreate}
onHide={() => setIsShowCreateModal(false)}
/>
)}
</div>
)
}

View File

@@ -1,12 +1,13 @@
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import exploreI18n from '@/i18n/lang/explore.en'
import cn from 'classnames'
import exploreI18n from '@/i18n/lang/explore.en'
const categoryI18n = exploreI18n.category
export interface ICategoryProps {
export type ICategoryProps = {
className?: string
list: string[]
value: string
@@ -17,23 +18,23 @@ const Category: FC<ICategoryProps> = ({
className,
list,
value,
onChange
onChange,
}) => {
const { t } = useTranslation()
const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium','flex items-center h-7 px-3 border cursor-pointer rounded-lg')
const itemStyle = (isSelected: boolean) => isSelected ? {boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'} : {}
const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium', 'flex items-center h-7 px-3 border cursor-pointer rounded-lg')
const itemStyle = (isSelected: boolean) => isSelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}
return (
<div className={cn(className, 'flex space-x-1 text-[13px]')}>
<div
className={itemClassName('' === value)}
style={itemStyle('' === value)}
onClick={() => onChange('')}
>
{t('explore.apps.allCategories')}
</div>
<div
className={itemClassName(value === '')}
style={itemStyle(value === '')}
onClick={() => onChange('')}
>
{t('explore.apps.allCategories')}
</div>
{list.map(name => (
<div
<div
key={name}
className={itemClassName(name === value)}
style={itemStyle(name === value)}

View File

@@ -2,19 +2,18 @@
import React, { useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker'
import s from './style.module.css'
type IProps = {
appName: string,
show: boolean,
onConfirm: (info: any) => void,
onHide: () => void,
appName: string
show: boolean
onConfirm: (info: any) => void
onHide: () => void
}
const CreateAppModal = ({
@@ -31,7 +30,7 @@ const CreateAppModal = ({
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const submit = () => {
if(!name.trim()) {
if (!name.trim()) {
Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
return
}
@@ -44,42 +43,42 @@ const CreateAppModal = ({
return (
<>
<Modal
isShow={show}
onClose={onHide}
className={cn(s.modal, '!max-w-[480px]', 'px-8')}
>
<span className={s.close} onClick={onHide}/>
<div className={s.title}>{t('explore.appCustomize.title', {name: appName})}</div>
<div className={s.content}>
<div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div>
<div className='flex items-center justify-between space-x-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input
value={name}
onChange={e => setName(e.target.value)}
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
/>
<Modal
isShow={show}
onClose={onHide}
className={cn(s.modal, '!max-w-[480px]', 'px-8')}
>
<span className={s.close} onClick={onHide}/>
<div className={s.title}>{t('explore.appCustomize.title', { name: appName })}</div>
<div className={s.content}>
<div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div>
<div className='flex items-center justify-between space-x-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input
value={name}
onChange={e => setName(e.target.value)}
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
/>
</div>
</div>
</div>
<div className='flex flex-row-reverse'>
<Button className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
</div>
</Modal>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
console.log(icon, icon_background)
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
<div className='flex flex-row-reverse'>
<Button className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
</div>
</Modal>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
console.log(icon, icon_background)
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
</>
)
}

View File

@@ -1,18 +1,19 @@
'use client'
import React, { FC, useEffect, useState } from 'react'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
import { fetchMembers } from '@/service/common'
import { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
import type { InstalledApp } from '@/models/explore'
export interface IExploreProps {
export type IExploreProps = {
children: React.ReactNode
}
const Explore: FC<IExploreProps> = ({
children
children,
}) => {
const { t } = useTranslation()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
@@ -23,8 +24,9 @@ const Explore: FC<IExploreProps> = ({
useEffect(() => {
document.title = `${t('explore.title')} - Dify`;
(async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}})
if(!accounts) return
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)
return
const currUser = accounts.find(account => account.id === userProfile.id)
setHasEditPermission(currUser?.role !== 'normal')
})()
@@ -39,7 +41,7 @@ const Explore: FC<IExploreProps> = ({
setControlUpdateInstalledApps,
hasEditPermission,
installedApps,
setInstalledApps
setInstalledApps,
}
}
>

View File

@@ -1,12 +1,13 @@
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import ExploreContext from '@/context/explore-context'
import ChatApp from '@/app/components/share/chat'
import TextGenerationApp from '@/app/components/share/text-generation'
import Loading from '@/app/components/base/loading'
export interface IInstalledAppProps {
export type IInstalledAppProps = {
id: string
}
@@ -14,23 +15,25 @@ const InstalledApp: FC<IInstalledAppProps> = ({
id,
}) => {
const { installedApps } = useContext(ExploreContext)
const installedApp = installedApps.find(item => item.id === id)
if(!installedApp) {
const installedApp = installedApps.find(item => item.id === id)
if (!installedApp) {
return (
<div className='flex h-full items-center'>
<Loading type='area' />
</div>
)
}
return (
<div className='h-full p-2'>
{installedApp?.app.mode === 'chat' ? (
<ChatApp isInstalledApp installedAppInfo={installedApp}/>
): (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
)}
{installedApp?.app.mode === 'chat'
? (
<ChatApp isInstalledApp installedAppInfo={installedApp}/>
)
: (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
)}
</div>
)
}

View File

@@ -1,11 +1,12 @@
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import Popover from '@/app/components/base/popover'
import { TrashIcon } from '@heroicons/react/24/outline'
import s from './style.module.css'
import Popover from '@/app/components/base/popover'
const PinIcon = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -13,7 +14,7 @@ const PinIcon = (
</svg>
)
export interface IItemOperationProps {
export type IItemOperationProps = {
className?: string
isPinned: boolean
isShowDelete: boolean
@@ -26,7 +27,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
isPinned,
isShowDelete,
togglePin,
onDelete
onDelete,
}) => {
const { t } = useTranslation()
@@ -42,18 +43,18 @@ const ItemOperation: FC<IItemOperationProps> = ({
</div>
{isShowDelete && (
<div className={cn(s.actionItem, s.deleteActionItem, 'hover:bg-gray-50 group')} onClick={onDelete} >
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('explore.sidebar.action.delete')}</span>
</div>
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('explore.sidebar.action.delete')}</span>
</div>
)}
</div>
}
trigger='click'
position='br'
btnElement={<div />}
btnClassName={(open) => cn(className, s.btn, 'h-6 w-6 rounded-md border-none p-1', open && '!bg-gray-100 !shadow-none')}
className={`!w-[120px] h-fit !z-20`}
btnClassName={open => cn(className, s.btn, 'h-6 w-6 rounded-md border-none p-1', open && '!bg-gray-100 !shadow-none')}
className={'!w-[120px] h-fit !z-20'}
/>
)
}

View File

@@ -1,12 +1,11 @@
'use client'
import cn from 'classnames'
import { useRouter } from 'next/navigation'
import s from './style.module.css'
import ItemOperation from '@/app/components/explore/item-operation'
import AppIcon from '@/app/components/base/app-icon'
import s from './style.module.css'
export interface IAppNavItemProps {
export type IAppNavItemProps = {
name: string
id: string
icon: string
@@ -31,7 +30,7 @@ export default function AppNavItem({
}: IAppNavItemProps) {
const router = useRouter()
const url = `/explore/installed/${id}`
return (
<div
key={id}
@@ -40,7 +39,7 @@ export default function AppNavItem({
isSelected ? s.active : 'hover:bg-gray-200',
'flex h-8 justify-between px-2 rounded-lg text-sm font-normal ',
)}
onClick={() => {
onClick={() => {
router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation().
}}
>
@@ -53,7 +52,7 @@ export default function AppNavItem({
borderColor: '0.5px solid rgba(0, 0, 0, 0.05)'
}}
/> */}
<AppIcon size='tiny' icon={icon} background={icon_background} />
<AppIcon size='tiny' icon={icon} background={icon_background} />
<div className='overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div>
</div>
{

View File

@@ -1,92 +1,90 @@
import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
import ProviderInput from '../provider-input'
import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedErrorOnAzureOpenaiTip,
ValidatedSuccessIcon,
ValidatingTip,
ValidatedErrorOnAzureOpenaiTip
} from '../provider-input/Validate'
import { ProviderName } from '@/models/common'
import type { Provider, ProviderAzureToken } from '@/models/common'
interface IAzureProviderProps {
type IAzureProviderProps = {
provider: Provider
onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: ProviderAzureToken) => void
}
const AzureProvider = ({
provider,
provider,
onTokenChange,
onValidatedStatus
onValidatedStatus,
}: IAzureProviderProps) => {
const { t } = useTranslation()
const [token, setToken] = useState<ProviderAzureToken>(provider.provider_name === ProviderName.AZURE_OPENAI ? {...provider.token}: {})
const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name)
const [token, setToken] = useState<ProviderAzureToken>(provider.provider_name === ProviderName.AZURE_OPENAI ? { ...provider.token } : {})
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
const handleFocus = (type: keyof ProviderAzureToken) => {
if (token[type] === (provider?.token as ProviderAzureToken)[type]) {
token[type] = ''
setToken({...token})
onTokenChange({...token})
setToken({ ...token })
onTokenChange({ ...token })
setValidatedStatus({})
}
}
const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => {
token[type] = v
setToken({...token})
onTokenChange({...token})
validate({...token}, {
setToken({ ...token })
onTokenChange({ ...token })
validate({ ...token }, {
beforeValidating: () => {
if (!token.openai_api_base || !token.openai_api_key) {
setValidatedStatus({})
return false
}
return true
}
},
})
}
const getValidatedIcon = () => {
if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
return <ValidatedErrorIcon />
}
if (validatedStatus.status === ValidatedStatus.Success) {
if (validatedStatus.status === ValidatedStatus.Success)
return <ValidatedSuccessIcon />
}
}
const getValidatedTip = () => {
if (validating) {
if (validating)
return <ValidatingTip />
}
if (validatedStatus.status === ValidatedStatus.Error) {
if (validatedStatus.status === ValidatedStatus.Error)
return <ValidatedErrorOnAzureOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
}
useEffect(() => {
if (typeof onValidatedStatus === 'function') {
if (typeof onValidatedStatus === 'function')
onValidatedStatus(validatedStatus)
}
}, [validatedStatus])
return (
<div className='px-4 py-3'>
<ProviderInput
<ProviderInput
className='mb-4'
name={t('common.provider.azure.apiBase')}
placeholder={t('common.provider.azure.apiBasePlaceholder')}
value={token.openai_api_base}
onChange={(v) => handleChange('openai_api_base', v, validate)}
onChange={v => handleChange('openai_api_base', v, validate)}
onFocus={() => handleFocus('openai_api_base')}
validatedIcon={getValidatedIcon()}
/>
<ProviderInput
<ProviderInput
className='mb-4'
name={t('common.provider.azure.apiKey')}
placeholder={t('common.provider.azure.apiKeyPlaceholder')}
value={token.openai_api_key}
onChange={(v) => handleChange('openai_api_key', v, validate)}
onChange={v => handleChange('openai_api_key', v, validate)}
onFocus={() => handleFocus('openai_api_key')}
validatedIcon={getValidatedIcon()}
validatedTip={getValidatedTip()}

View File

@@ -1,15 +1,15 @@
import { useState } from 'react'
import useSWR from 'swr'
import { fetchProviders } from '@/service/common'
import ProviderItem from './provider-item'
import OpenaiHostedProvider from './openai-hosted-provider'
import type { ProviderHosted } from '@/models/common'
import { LockClosedIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import ProviderItem from './provider-item'
import OpenaiHostedProvider from './openai-hosted-provider'
import type { ProviderHosted } from '@/models/common'
import { fetchProviders } from '@/service/common'
import { IS_CE_EDITION } from '@/config'
const providersMap: {[k: string]: any} = {
const providersMap: { [k: string]: any } = {
'openai-custom': {
icon: 'openai',
name: 'OpenAI',
@@ -17,7 +17,7 @@ const providersMap: {[k: string]: any} = {
'azure_openai-custom': {
icon: 'azure',
name: 'Azure OpenAI Service',
}
},
}
// const providersList = [
@@ -56,7 +56,7 @@ const ProviderPage = () => {
const { t } = useTranslation()
const [activeProviderId, setActiveProviderId] = useState('')
const { data, mutate } = useSWR({ url: '/workspaces/current/providers' }, fetchProviders)
const providers = data?.filter(provider => providersMap[`${provider.provider_name}-${provider.provider_type}`])?.map(provider => {
const providers = data?.filter(provider => providersMap[`${provider.provider_name}-${provider.provider_type}`])?.map((provider) => {
const providerKey = `${provider.provider_name}-${provider.provider_type}`
return {
provider,

View File

@@ -1,19 +1,19 @@
import type { Provider } from '@/models/common'
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ProviderInput from '../provider-input'
import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
import ProviderInput from '../provider-input'
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedErrorOnOpenaiTip,
ValidatedSuccessIcon,
ValidatingTip,
ValidatedExceedOnOpenaiTip,
ValidatedErrorOnOpenaiTip
} from '../provider-input/Validate'
import type { Provider } from '@/models/common'
interface IOpenaiProviderProps {
type IOpenaiProviderProps = {
provider: Provider
onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: string) => void
@@ -22,11 +22,11 @@ interface IOpenaiProviderProps {
const OpenaiProvider = ({
provider,
onValidatedStatus,
onTokenChange
onTokenChange,
}: IOpenaiProviderProps) => {
const { t } = useTranslation()
const [token, setToken] = useState(provider.token as string || '')
const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name)
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
const handleFocus = () => {
if (token === provider.token) {
setToken('')
@@ -44,35 +44,32 @@ const OpenaiProvider = ({
return false
}
return true
}
},
})
}
useEffect(() => {
if (typeof onValidatedStatus === 'function') {
if (typeof onValidatedStatus === 'function')
onValidatedStatus(validatedStatus)
}
}, [validatedStatus])
const getValidatedIcon = () => {
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) {
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
return <ValidatedErrorIcon />
}
if (validatedStatus.status === ValidatedStatus.Success) {
if (validatedStatus.status === ValidatedStatus.Success)
return <ValidatedSuccessIcon />
}
}
const getValidatedTip = () => {
if (validating) {
if (validating)
return <ValidatingTip />
}
if (validatedStatus?.status === ValidatedStatus.Error) {
if (validatedStatus?.status === ValidatedStatus.Error)
return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
}
return (
<div className='px-4 pt-3 pb-4'>
<ProviderInput
<ProviderInput
value={token}
name={t('common.provider.apiKey')}
placeholder={t('common.provider.enterYourKey')}
@@ -89,4 +86,4 @@ const OpenaiProvider = ({
)
}
export default OpenaiProvider
export default OpenaiProvider

View File

@@ -15,7 +15,7 @@ export const ValidatedSuccessIcon = () => {
export const ValidatingTip = () => {
const { t } = useTranslation()
return (
<div className={`mt-2 text-primary-600 text-xs font-normal`}>
<div className={'mt-2 text-primary-600 text-xs font-normal'}>
{t('common.provider.validating')}
</div>
)
@@ -26,11 +26,11 @@ export const ValidatedExceedOnOpenaiTip = () => {
const { locale } = useContext(I18n)
return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.apiKeyExceedBill')}&nbsp;
<Link
<Link
className='underline'
href="https://platform.openai.com/account/api-keys"
href="https://platform.openai.com/account/api-keys"
target={'_blank'}>
{locale === 'en' ? 'this link' : '这篇文档'}
</Link>
@@ -42,7 +42,7 @@ export const ValidatedErrorOnOpenaiTip = ({ errorMessage }: { errorMessage: stri
const { t } = useTranslation()
return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.validatedError')}{errorMessage}
</div>
)
@@ -52,8 +52,8 @@ export const ValidatedErrorOnAzureOpenaiTip = ({ errorMessage }: { errorMessage:
const { t } = useTranslation()
return (
<div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.validatedError')}{errorMessage}
</div>
)
}
}

View File

@@ -1,7 +1,7 @@
import { ChangeEvent } from 'react'
import { ReactElement } from 'react-markdown/lib/react-markdown'
import type { ChangeEvent } from 'react'
import type { ReactElement } from 'react-markdown/lib/react-markdown'
interface IProviderInputProps {
type IProviderInputProps = {
value?: string
name: string
placeholder: string
@@ -20,9 +20,8 @@ const ProviderInput = ({
onChange,
onFocus,
validatedIcon,
validatedTip
validatedTip,
}: IProviderInputProps) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value
onChange(inputValue)
@@ -35,12 +34,12 @@ const ProviderInput = ({
flex items-center px-3 bg-white rounded-lg
shadow-[0_1px_2px_rgba(16,24,40,0.05)]
'>
<input
<input
className='
w-full py-[9px]
text-xs font-medium text-gray-700 leading-[18px]
appearance-none outline-none bg-transparent
'
appearance-none outline-none bg-transparent
'
value={value}
placeholder={placeholder}
onChange={handleChange}
@@ -53,4 +52,4 @@ const ProviderInput = ({
)
}
export default ProviderInput
export default ProviderInput

View File

@@ -1,25 +1,26 @@
import { useState, useCallback, SetStateAction, Dispatch } from 'react'
import type { Dispatch, SetStateAction } from 'react'
import { useCallback, useState } from 'react'
import debounce from 'lodash-es/debounce'
import { DebouncedFunc } from 'lodash-es'
import type { DebouncedFunc } from 'lodash-es'
import { validateProviderKey } from '@/service/common'
export enum ValidatedStatus {
Success = 'success',
Error = 'error',
Exceed = 'exceed'
Exceed = 'exceed',
}
export type ValidatedStatusState = {
status?: ValidatedStatus,
status?: ValidatedStatus
message?: string
}
// export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError
export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatusState>>
export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void>
type ValidateTokenReturn = [
boolean,
ValidatedStatusState,
boolean,
ValidatedStatusState,
SetValidatedStatus,
ValidateFn
ValidateFn,
]
export type ValidateFnConfig = {
beforeValidating: (token: any) => boolean
@@ -29,19 +30,21 @@ const useValidateToken = (providerName: string): ValidateTokenReturn => {
const [validating, setValidating] = useState(false)
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => {
if (!config.beforeValidating(token)) {
if (!config.beforeValidating(token))
return false
}
setValidating(true)
try {
const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } })
setValidatedStatus(
res.result === 'success'
? { status: ValidatedStatus.Success }
res.result === 'success'
? { status: ValidatedStatus.Success }
: { status: ValidatedStatus.Error, message: res.error })
} catch (e: any) {
}
catch (e: any) {
setValidatedStatus({ status: ValidatedStatus.Error, message: e.message })
} finally {
}
finally {
setValidating(false)
}
}, 500), [])
@@ -50,8 +53,8 @@ const useValidateToken = (providerName: string): ValidateTokenReturn => {
validating,
validatedStatus,
setValidatedStatus,
validate
validate,
]
}
export default useValidateToken
export default useValidateToken

View File

@@ -1,18 +1,19 @@
import { useState } from 'react'
import cn from 'classnames'
import s from './index.module.css'
import { useContext } from 'use-context-selector'
import Indicator from '../../../indicator'
import { useTranslation } from 'react-i18next'
import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import Indicator from '../../../indicator'
import OpenaiProvider from '../openai-provider'
import AzureProvider from '../azure-provider'
import { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken'
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import { ValidatedStatus } from '../provider-input/useValidateToken'
import s from './index.module.css'
import type { Provider, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import { updateProviderAIKey } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast'
interface IProviderItemProps {
type IProviderItemProps = {
icon: string
name: string
provider: Provider
@@ -26,17 +27,17 @@ const ProviderItem = ({
name,
provider,
onActive,
onSave
onSave,
}: IProviderItemProps) => {
const { t } = useTranslation()
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>()
const [loading, setLoading] = useState(false)
const { notify } = useContext(ToastContext)
const [token, setToken] = useState<ProviderAzureToken | string>(
provider.provider_name === 'azure_openai'
provider.provider_name === 'azure_openai'
? { openai_api_base: '', openai_api_key: '' }
: ''
)
: '',
)
const id = `${provider.provider_name}-${provider.provider_type}`
const isOpen = id === activeId
const comingSoon = false
@@ -44,26 +45,30 @@ const ProviderItem = ({
const providerTokenHasSetted = () => {
if (provider.provider_name === ProviderName.AZURE_OPENAI) {
return provider.token && provider.token.openai_api_base && provider.token.openai_api_key ? {
openai_api_base: provider.token.openai_api_base,
openai_api_key: provider.token.openai_api_key
}: undefined
return (provider.token && provider.token.openai_api_base && provider.token.openai_api_key)
? {
openai_api_base: provider.token.openai_api_base,
openai_api_key: provider.token.openai_api_key,
}
: undefined
}
if (provider.provider_name === ProviderName.OPENAI) {
if (provider.provider_name === ProviderName.OPENAI)
return provider.token
}
}
const handleUpdateToken = async () => {
if (loading) return
if (loading)
return
if (validatedStatus?.status === ValidatedStatus.Success) {
try {
setLoading(true)
await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onActive('')
} catch (e) {
}
catch (e) {
notify({ type: 'error', message: t('common.provider.saveFailed') })
} finally {
}
finally {
setLoading(false)
onSave()
}
@@ -126,18 +131,18 @@ const ProviderItem = ({
</div>
{
provider.provider_name === ProviderName.OPENAI && isOpen && (
<OpenaiProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
<OpenaiProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
onTokenChange={v => setToken(v)}
/>
)
}
{
provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && (
<AzureProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
<AzureProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
onTokenChange={v => setToken(v)}
/>
)

View File

@@ -1,7 +1,7 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectedLayoutSegment, useRouter } from 'next/navigation'
import { useRouter, useSelectedLayoutSegment } from 'next/navigation'
import classNames from 'classnames'
import { CircleStackIcon, PuzzlePieceIcon } from '@heroicons/react/24/outline'
import { CommandLineIcon, Squares2X2Icon } from '@heroicons/react/24/solid'
@@ -15,9 +15,9 @@ import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog'
import { WorkspaceProvider } from '@/context/workspace-context'
import { useDatasetsContext } from '@/context/datasets-context'
const BuildAppsIcon = ({isSelected}: {isSelected: boolean}) => (
const BuildAppsIcon = ({ isSelected }: { isSelected: boolean }) => (
<svg className='mr-1' width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6666 4.85221L7.99998 8.00036M7.99998 8.00036L2.33331 4.85221M7.99998 8.00036L8 14.3337M14 10.7061V5.29468C14 5.06625 14 4.95204 13.9663 4.85017C13.9366 4.76005 13.8879 4.67733 13.8236 4.60754C13.7509 4.52865 13.651 4.47318 13.4514 4.36224L8.51802 1.6215C8.32895 1.51646 8.23442 1.46395 8.1343 1.44336C8.0457 1.42513 7.95431 1.42513 7.8657 1.44336C7.76559 1.46395 7.67105 1.51646 7.48198 1.6215L2.54865 4.36225C2.34896 4.47318 2.24912 4.52865 2.17642 4.60754C2.11211 4.67733 2.06343 4.76005 2.03366 4.85017C2 4.95204 2 5.06625 2 5.29468V10.7061C2 10.9345 2 11.0487 2.03366 11.1506C2.06343 11.2407 2.11211 11.3234 2.17642 11.3932C2.24912 11.4721 2.34897 11.5276 2.54865 11.6385L7.48198 14.3793C7.67105 14.4843 7.76559 14.5368 7.8657 14.5574C7.95431 14.5756 8.0457 14.5756 8.1343 14.5574C8.23442 14.5368 8.32895 14.4843 8.51802 14.3793L13.4514 11.6385C13.651 11.5276 13.7509 11.4721 13.8236 11.3932C13.8879 11.3234 13.9366 11.2407 13.9663 11.1506C14 11.0487 14 10.9345 14 10.7061Z" stroke={isSelected ? '#155EEF': '#667085'} strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M13.6666 4.85221L7.99998 8.00036M7.99998 8.00036L2.33331 4.85221M7.99998 8.00036L8 14.3337M14 10.7061V5.29468C14 5.06625 14 4.95204 13.9663 4.85017C13.9366 4.76005 13.8879 4.67733 13.8236 4.60754C13.7509 4.52865 13.651 4.47318 13.4514 4.36224L8.51802 1.6215C8.32895 1.51646 8.23442 1.46395 8.1343 1.44336C8.0457 1.42513 7.95431 1.42513 7.8657 1.44336C7.76559 1.46395 7.67105 1.51646 7.48198 1.6215L2.54865 4.36225C2.34896 4.47318 2.24912 4.52865 2.17642 4.60754C2.11211 4.67733 2.06343 4.76005 2.03366 4.85017C2 4.95204 2 5.06625 2 5.29468V10.7061C2 10.9345 2 11.0487 2.03366 11.1506C2.06343 11.2407 2.11211 11.3234 2.17642 11.3932C2.24912 11.4721 2.34897 11.5276 2.54865 11.6385L7.48198 14.3793C7.67105 14.4843 7.76559 14.5368 7.8657 14.5574C7.95431 14.5756 8.0457 14.5756 8.1343 14.5574C8.23442 14.5368 8.32895 14.4843 8.51802 14.3793L13.4514 11.6385C13.651 11.5276 13.7509 11.4721 13.8236 11.3932C13.8879 11.3234 13.9366 11.2407 13.9663 11.1506C14 11.0487 14 10.9345 14 10.7061Z" stroke={isSelected ? '#155EEF' : '#667085'} strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
@@ -51,22 +51,22 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
<div className={classNames(
'sticky top-0 left-0 right-0 z-20 flex bg-gray-100 grow-0 shrink-0 basis-auto h-14',
s.header,
isBordered ? 'border-b border-gray-200' : ''
isBordered ? 'border-b border-gray-200' : '',
)}>
<div className={classNames(
s[`header-${langeniusVersionInfo.current_env}`],
'flex flex-1 items-center justify-between px-4'
'flex flex-1 items-center justify-between px-4',
)}>
<div className='flex items-center'>
<Link href="/apps" className='flex items-center mr-3'>
<div className={s['logo']} />
<div className={s.logo} />
</Link>
{/* Add it when has many stars */}
<div className='
flex items-center h-[26px] px-2 bg-white
border border-solid border-[#E5E7EB] rounded-l-[6px] rounded-r-[6px]
'>
<div className={s['alpha']} />
<div className={s.alpha} />
<div className='ml-1 text-xs font-semibold text-gray-700'>{t('common.menus.status')}</div>
</div>
</div>
@@ -74,7 +74,7 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
<Link href="/explore/apps" className={classNames(
navClassName, 'group',
isExplore && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
isExplore ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
isExplore ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700',
)}>
<Squares2X2Icon className='mr-1 w-[18px] h-[18px]' />
{t('common.menus.explore')}
@@ -84,13 +84,13 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
text={t('common.menus.apps')}
activeSegment={['apps', 'app']}
link='/apps'
curNav={curApp && { id: curApp.id, name: curApp.name ,icon: curApp.icon, icon_background: curApp.icon_background}}
curNav={curApp && { id: curApp.id, name: curApp.name, icon: curApp.icon, icon_background: curApp.icon_background }}
navs={appItems.map(item => ({
id: item.id,
name: item.name,
link: `/app/${item.id}/overview`,
icon: item.icon,
icon_background: item.icon_background
icon_background: item.icon_background,
}))}
createText={t('common.menus.newApp')}
onCreate={() => setShowNewAppDialog(true)}
@@ -98,7 +98,7 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
<Link href="/plugins-coming-soon" className={classNames(
navClassName, 'group',
isPluginsComingSoon && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
isPluginsComingSoon ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700'
isPluginsComingSoon ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200 hover:text-gray-700',
)}>
<PuzzlePieceIcon className='mr-1 w-[18px] h-[18px]' />
{t('common.menus.plugins')}
@@ -114,7 +114,7 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
name: dataset.name,
link: `/datasets/${dataset.id}/documents`,
icon: dataset.icon,
icon_background: dataset.icon_background
icon_background: dataset.icon_background,
}))}
createText={t('common.menus.newDataset')}
onCreate={() => router.push('/datasets/create')}

View File

@@ -1,10 +1,11 @@
'use client'
import React, { FC } from 'react'
import cn from 'classnames'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import { appDefaultIconBackground } from '@/config/index'
import AppIcon from '@/app/components/base/app-icon'
export interface IAppInfoProps {
export type IAppInfoProps = {
className?: string
icon: string
icon_background?: string
@@ -15,7 +16,7 @@ const AppInfo: FC<IAppInfoProps> = ({
className,
icon,
icon_background,
name
name,
}) => {
return (
<div className={cn(className, 'flex items-center space-x-3')}>

View File

@@ -1,16 +1,16 @@
import React, { useEffect, useRef } from 'react'
import React, { useRef } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
ChatBubbleOvalLeftEllipsisIcon,
PencilSquareIcon
PencilSquareIcon,
} from '@heroicons/react/24/outline'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon, } from '@heroicons/react/24/solid'
import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'
import { useInfiniteScroll } from 'ahooks'
import Button from '../../../base/button'
import AppInfo from '@/app/components/share/chat/sidebar/app-info'
// import Card from './card'
import type { ConversationItem, SiteInfo } from '@/models/share'
import { useInfiniteScroll } from 'ahooks'
import { fetchConversations } from '@/service/share'
function classNames(...classes: any[]) {
@@ -25,7 +25,7 @@ export type ISidebarProps = {
isInstalledApp: boolean
installedAppId?: string
siteInfo: SiteInfo
onMoreLoaded: (res: {data: ConversationItem[], has_more: boolean}) => void
onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
isNoMore: boolean
}
@@ -45,19 +45,19 @@ const Sidebar: FC<ISidebarProps> = ({
useInfiniteScroll(
async () => {
if(!isNoMore) {
if (!isNoMore) {
const lastId = list[list.length - 1].id
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId)
onMoreLoaded({ data: conversations, has_more })
}
return {list: []}
return { list: [] }
},
{
target: listRef,
isNoMore: () => {
return isNoMore
},
reloadDeps: [isNoMore]
reloadDeps: [isNoMore],
},
)
@@ -66,7 +66,7 @@ const Sidebar: FC<ISidebarProps> = ({
className={
classNames(
isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]',
"shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen"
'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen',
)
}
>

View File

@@ -38,25 +38,27 @@ const ConfigSence: FC<IConfigSenceProps> = ({
<div className='w-full mt-4' key={item.key}>
<label className='text-gray-900 text-sm font-medium'>{item.name}</label>
<div className='mt-2'>
{item.type === 'select' ? (
<Select
className='w-full'
defaultValue={inputs[item.key]}
onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
) : (
<input
type="text"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{item.type === 'select'
? (
<Select
className='w-full'
defaultValue={inputs[item.key]}
onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)
: (
<input
type="text"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div>
</div>
))}