Commit 6c1fa275 authored by StyleZhang's avatar StyleZhang

add plugin setting

parent 59ee4215
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="link-external-02">
<path id="Icon" d="M10.5 4.5L10.5 1.5M10.5 1.5H7.49999M10.5 1.5L6 6M5 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V7" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="alert-circle">
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM8.66667 5.33329C8.66667 4.9651 8.36819 4.66663 8 4.66663C7.63181 4.66663 7.33334 4.9651 7.33334 5.33329V7.99996C7.33334 8.36815 7.63181 8.66663 8 8.66663C8.36819 8.66663 8.66667 8.36815 8.66667 7.99996V5.33329ZM8 9.99996C7.63181 9.99996 7.33334 10.2984 7.33334 10.6666C7.33334 11.0348 7.63181 11.3333 8 11.3333H8.00667C8.37486 11.3333 8.67334 11.0348 8.67334 10.6666C8.67334 10.2984 8.37486 9.99996 8.00667 9.99996H8Z" fill="#D92D20"/>
</g>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="check-circle">
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM11.4714 6.47136C11.7318 6.21101 11.7318 5.7889 11.4714 5.52855C11.2111 5.26821 10.7889 5.26821 10.5286 5.52855L7 9.05715L5.47141 7.52855C5.21106 7.2682 4.78895 7.2682 4.5286 7.52855C4.26825 7.7889 4.26825 8.21101 4.5286 8.47136L6.5286 10.4714C6.78895 10.7317 7.21106 10.7317 7.47141 10.4714L11.4714 6.47136Z" fill="#039855"/>
</g>
</svg>
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "link-external-02"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M10.5 4.5L10.5 1.5M10.5 1.5H7.49999M10.5 1.5L6 6M5 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V7",
"stroke": "currentColor",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "LinkExternal02"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './LinkExternal02.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
export default Icon
export { default as LinkExternal02 } from './LinkExternal02'
export { default as Loading02 } from './Loading02' export { default as Loading02 } from './Loading02'
export { default as Trash03 } from './Trash03' export { default as Trash03 } from './Trash03'
export { default as XClose } from './XClose' export { default as XClose } from './XClose'
......
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "alert-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM8.66667 5.33329C8.66667 4.9651 8.36819 4.66663 8 4.66663C7.63181 4.66663 7.33334 4.9651 7.33334 5.33329V7.99996C7.33334 8.36815 7.63181 8.66663 8 8.66663C8.36819 8.66663 8.66667 8.36815 8.66667 7.99996V5.33329ZM8 9.99996C7.63181 9.99996 7.33334 10.2984 7.33334 10.6666C7.33334 11.0348 7.63181 11.3333 8 11.3333H8.00667C8.37486 11.3333 8.67334 11.0348 8.67334 10.6666C8.67334 10.2984 8.37486 9.99996 8.00667 9.99996H8Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "AlertCircle"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AlertCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
export default Icon
export { default as AlertCircle } from './AlertCircle'
export { default as AlertTriangle } from './AlertTriangle' export { default as AlertTriangle } from './AlertTriangle'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "check-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM11.4714 6.47136C11.7318 6.21101 11.7318 5.7889 11.4714 5.52855C11.2111 5.26821 10.7889 5.26821 10.5286 5.52855L7 9.05715L5.47141 7.52855C5.21106 7.2682 4.78895 7.2682 4.5286 7.52855C4.26825 7.7889 4.26825 8.21101 4.5286 8.47136L6.5286 10.4714C6.78895 10.7317 7.21106 10.7317 7.47141 10.4714L11.4714 6.47136Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "CheckCircle"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './CheckCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
export default Icon
export { default as CheckCircle } from './CheckCircle'
export { default as Download02 } from './Download02' export { default as Download02 } from './Download02'
export { default as XCircle } from './XCircle' export { default as XCircle } from './XCircle'
...@@ -12,6 +12,7 @@ import AccountSetting from '../account-setting' ...@@ -12,6 +12,7 @@ import AccountSetting from '../account-setting'
import AccountAbout from '../account-about' import AccountAbout from '../account-about'
import WorkplaceSelector from './workplace-selector' import WorkplaceSelector from './workplace-selector'
import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import { KeyValidatorContextProvider } from '@/context/key-validator'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import Avatar from '@/app/components/base/avatar' import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common' import { logout } from '@/service/common'
...@@ -27,7 +28,7 @@ export default function AppSelector({ userProfile, langeniusVersionInfo }: IAppS ...@@ -27,7 +28,7 @@ export default function AppSelector({ userProfile, langeniusVersionInfo }: IAppS
rounded-lg font-normal hover:bg-gray-100 cursor-pointer rounded-lg font-normal hover:bg-gray-100 cursor-pointer
` `
const router = useRouter() const router = useRouter()
const [settingVisible, setSettingVisible] = useState(false) const [settingVisible, setSettingVisible] = useState(true)
const [aboutVisible, setAboutVisible] = useState(false) const [aboutVisible, setAboutVisible] = useState(false)
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
...@@ -130,7 +131,11 @@ export default function AppSelector({ userProfile, langeniusVersionInfo }: IAppS ...@@ -130,7 +131,11 @@ export default function AppSelector({ userProfile, langeniusVersionInfo }: IAppS
</Transition> </Transition>
</Menu> </Menu>
{ {
settingVisible && <AccountSetting onCancel={() => setSettingVisible(false)} /> settingVisible && (
<KeyValidatorContextProvider>
<AccountSetting onCancel={() => setSettingVisible(false)} />
</KeyValidatorContextProvider>
)
} }
{ {
aboutVisible && <AccountAbout onCancel={() => setAboutVisible(false)} langeniusVersionInfo={langeniusVersionInfo} /> aboutVisible && <AccountAbout onCancel={() => setAboutVisible(false)} langeniusVersionInfo={langeniusVersionInfo} />
......
...@@ -8,11 +8,12 @@ import MembersPage from './members-page' ...@@ -8,11 +8,12 @@ import MembersPage from './members-page'
import IntegrationsPage from './Integrations-page' import IntegrationsPage from './Integrations-page'
import LanguagePage from './language-page' import LanguagePage from './language-page'
import ProviderPage from './provider-page' import ProviderPage from './provider-page'
import PluginPage from './plugin-page'
import DataSourcePage from './data-source-page' import DataSourcePage from './data-source-page'
import s from './index.module.css' import s from './index.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import { Database03 } from '@/app/components/base/icons/src/vender/line/development' import { Database03, PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development'
import { Database03 as Database03Solid } from '@/app/components/base/icons/src/vender/solid/development' import { Database03 as Database03Solid, PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development'
const iconClassName = ` const iconClassName = `
w-4 h-4 ml-3 mr-2 w-4 h-4 ml-3 mr-2
...@@ -24,7 +25,7 @@ type IAccountSettingProps = { ...@@ -24,7 +25,7 @@ type IAccountSettingProps = {
} }
export default function AccountSetting({ export default function AccountSetting({
onCancel, onCancel,
activeTab = 'account', activeTab = 'plugin',
}: IAccountSettingProps) { }: IAccountSettingProps) {
const [activeMenu, setActiveMenu] = useState(activeTab) const [activeMenu, setActiveMenu] = useState(activeTab)
const { t } = useTranslation() const { t } = useTranslation()
...@@ -75,6 +76,12 @@ export default function AccountSetting({ ...@@ -75,6 +76,12 @@ export default function AccountSetting({
icon: <Database03 className={iconClassName} />, icon: <Database03 className={iconClassName} />,
activeIcon: <Database03Solid className={iconClassName} />, activeIcon: <Database03Solid className={iconClassName} />,
}, },
{
key: 'plugin',
name: t('common.settings.plugin'),
icon: <PuzzlePiece01 className={iconClassName} />,
activeIcon: <PuzzlePiece01Solid className={iconClassName} />,
},
], ],
}, },
] ]
...@@ -138,6 +145,9 @@ export default function AccountSetting({ ...@@ -138,6 +145,9 @@ export default function AccountSetting({
{ {
activeMenu === 'data-source' && <DataSourcePage /> activeMenu === 'data-source' && <DataSourcePage />
} }
{
activeMenu === 'plugin' && <PluginPage />
}
</div> </div>
</div> </div>
</Modal> </Modal>
......
import type { ChangeEvent } from 'react'
import {
ValidatedErrorIcon,
ValidatedErrorMessage,
ValidatedSuccessIcon,
ValidatingTip,
} from './ValidateStatus'
import { ValidatedStatus } from './declarations'
import type { ValidatedStatusState } from './declarations'
type KeyInputProps = {
value?: string
name: string
placeholder: string
className?: string
onChange: (v: string) => void
onFocus?: () => void
validating: boolean
validatedStatusState: ValidatedStatusState
}
const KeyInput = ({
value,
name,
placeholder,
className,
onChange,
onFocus,
validating,
validatedStatusState,
}: KeyInputProps) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value
onChange(inputValue)
}
const getValidatedIcon = () => {
if (validatedStatusState.status === ValidatedStatus.Error || validatedStatusState.status === ValidatedStatus.Exceed)
return <ValidatedErrorIcon />
if (validatedStatusState.status === ValidatedStatus.Success)
return <ValidatedSuccessIcon />
}
const getValidatedTip = () => {
if (validating)
return <ValidatingTip />
if (validatedStatusState.status === ValidatedStatus.Error)
return <ValidatedErrorMessage errorMessage={validatedStatusState.message ?? ''} />
}
return (
<div className={className}>
<div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div>
<div className='
flex items-center px-3 bg-white rounded-lg
shadow-[0_1px_2px_rgba(16,24,40,0.05)]
'>
<input
className='
w-full py-[9px]
text-xs font-medium text-gray-700 leading-[18px]
appearance-none outline-none bg-transparent
'
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={onFocus}
/>
{getValidatedIcon()}
</div>
{getValidatedTip()}
</div>
)
}
export default KeyInput
import { useTranslation } from 'react-i18next'
import Indicator from '../../indicator'
import type { Status } from './declarations'
type OperateProps = {
isOpen: boolean
status: Status
onCancel: () => void
onSave: () => void
onAdd: () => void
onEdit: () => void
}
const Operate = ({
isOpen,
status,
onCancel,
onSave,
onAdd,
onEdit,
}: OperateProps) => {
const { t } = useTranslation()
if (isOpen) {
return (
<div className='flex items-center'>
<div className='
flex items-center
mr-[5px] px-3 h-7 rounded-md cursor-pointer
text-xs font-medium text-gray-700
' onClick={onCancel} >
{t('common.operation.cancel')}
</div>
<div className='
flex items-center
px-3 h-7 rounded-md cursor-pointer bg-primary-700
text-xs font-medium text-white
' onClick={onSave}>
{t('common.operation.save')}
</div>
</div>
)
}
if (status === 'add') {
return (
<div className='
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
' onClick={onAdd}>
{t('common.provider.addKey')}
</div>
)
}
if (status === 'fail' || status === 'success') {
return (
<div className='flex items-center'>
{
status === 'fail' && (
<div className='flex items-center mr-4'>
<div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>
<Indicator color='red' className='ml-2' />
</div>
)
}
{
status === 'success' && (
<Indicator color='green' className='mr-4' />
)
}
<div className='
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
' onClick={onEdit}>
{t('common.provider.editKey')}
</div>
</div>
)
}
return null
}
export default Operate
import { useTranslation } from 'react-i18next'
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
export const ValidatedErrorIcon = () => {
return <AlertCircle className='w-4 h-4 text-[#D92D20]' />
}
export const ValidatedSuccessIcon = () => {
return <CheckCircle className='w-4 h-4 text-[#039855]' />
}
export const ValidatingTip = () => {
const { t } = useTranslation()
return (
<div className={'mt-2 text-primary-600 text-xs font-normal'}>
{t('common.provider.validating')}
</div>
)
}
export const ValidatedErrorMessage = ({ errorMessage }: { errorMessage: string }) => {
const { t } = useTranslation()
return (
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
{t('common.provider.validatedError')}{errorMessage}
</div>
)
}
import type { Dispatch, SetStateAction } from 'react'
export enum ValidatedStatus {
Success = 'success',
Error = 'error',
Exceed = 'exceed',
}
export type ValidatedStatusState = {
status?: ValidatedStatus
message?: string
}
export type Status = 'add' | 'fail' | 'success'
export type ValidateValue = Record<string, string | undefined>
export type ValidateCallback = {
before: (v?: ValidateValue) => boolean | undefined
run?: (v?: ValidateValue) => Promise<ValidatedStatusState>
}
export type Form = {
key: string
title: string
placeholder: string
value?: string
validate?: ValidateCallback
handleFocus?: (v: ValidateValue, dispatch: Dispatch<SetStateAction<ValidateValue>>) => void
}
export type KeyFrom = {
text: string
link: string
}
export type KeyValidatorProps = {
type: string
title: React.ReactNode
status: Status
forms: Form[]
keyFrom: KeyFrom
}
import { useState } from 'react'
import { useDebounceFn } from 'ahooks'
import type { DebouncedFunc } from 'lodash-es'
import { ValidatedStatus } from './declarations'
import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations'
export const useValidate: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise<void>>, boolean, ValidatedStatusState] = (value) => {
const [validating, setValidating] = useState(false)
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => {
if (!validateCallback.before(value)) {
setValidating(false)
setValidatedStatus({})
return
}
setValidating(true)
if (validateCallback.run) {
const res = await validateCallback?.run(value)
setValidatedStatus(
res.status === 'success'
? { status: ValidatedStatus.Success }
: { status: ValidatedStatus.Error, message: res.message })
setValidating(false)
}
}, { wait: 500 })
return [run, validating, validatedStatus]
}
import { useState } from 'react'
import Operate from './Operate'
import KeyInput from './KeyInput'
import { useValidate } from './hooks'
import type { Form, KeyFrom, Status, ValidateValue } from './declarations'
import { useKeyValidatorContext } from '@/context/key-validator'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
export type KeyValidatorProps = {
type: string
title: React.ReactNode
status: Status
forms: Form[]
keyFrom: KeyFrom
onSave: (v: ValidateValue) => Promise<boolean | undefined>
}
const KeyValidator = ({
type,
title,
status,
forms,
keyFrom,
onSave,
}: KeyValidatorProps) => {
const { trigger } = useKeyValidatorContext()
const [isOpen, setIsOpen] = useState(false)
const prevValue = forms.reduce((prev: ValidateValue, next: Form) => {
prev[next.key] = next.value
return prev
}, {})
const [value, setValue] = useState(prevValue)
const [validate, validating, validatedStatusState] = useValidate(value)
trigger?.useSubscription((v) => {
if (v !== type) {
setIsOpen(false)
setValue(prevValue)
validate({ before: () => false })
}
})
const handleCancel = () => {
trigger?.emit('')
}
const handleSave = async () => {
if (await onSave(value))
trigger?.emit('')
}
const handleAdd = () => {
setIsOpen(true)
trigger?.emit(type)
}
const handleEdit = () => {
setIsOpen(true)
trigger?.emit(type)
}
const handleChange = (form: Form, val: string) => {
setValue({ ...value, [form.key]: val })
if (form.validate)
validate(form.validate)
}
const handleFocus = (form: Form) => {
if (form.handleFocus)
form.handleFocus(value, setValue)
}
return (
<div className='mb-2 border-[0.5px] border-gray-200 bg-gray-50 rounded-md'>
<div className={
`flex items-center justify-between px-4 h-[52px] cursor-pointer ${isOpen && 'border-b-[0.5px] border-b-gray-200'}`
}>
{title}
<Operate
isOpen={isOpen}
status={status}
onCancel={handleCancel}
onSave={handleSave}
onAdd={handleAdd}
onEdit={handleEdit}
/>
</div>
{
isOpen && (
<div className='px-4 py-3'>
{
forms.map(form => (
<KeyInput
key={form.key}
className='mb-4'
name={form.title}
placeholder={form.placeholder}
value={value[form.key] || ''}
onChange={v => handleChange(form, v)}
onFocus={() => handleFocus(form)}
validating={validating}
validatedStatusState={validatedStatusState}
/>
))
}
<a className="flex items-center text-xs cursor-pointer text-primary-600" href={keyFrom.link} target={'_blank'}>
{keyFrom.text}
<LinkExternal02 className='w-3 h-3 ml-1 text-primary-600' />
</a>
</div>
)
}
</div>
)
}
export default KeyValidator
import { useTranslation } from 'react-i18next'
import Image from 'next/image'
import SerpapiLogo from '../../assets/serpapi.png'
import KeyValidator from '../key-validator'
import type { Form, ValidateValue } from '../key-validator/declarations'
import { updatePluginKey, validatePluginKey } from './utils'
import type { PluginProvider } from '@/models/common'
type SerpapiPluginProps = {
plugin: PluginProvider
onUpdate: () => void
}
const SerpapiPlugin = ({
plugin,
onUpdate,
}: SerpapiPluginProps) => {
const { t } = useTranslation()
const forms: Form[] = [{
key: 'api_key',
title: t('common.plugin.serpapi.apiKey'),
placeholder: t('common.plugin.serpapi.apiKeyPlaceholder'),
value: plugin.credentials?.api_key,
validate: {
before: (v) => {
if (v?.api_key)
return true
},
run: async (v) => {
return validatePluginKey('serpapi', {
credentials: {
api_key: v?.api_key,
},
})
},
},
handleFocus: (v, dispatch) => {
if (v.api_key === plugin.credentials?.api_key)
dispatch({ ...v, api_key: '' })
},
}]
const handleSave = async (v: ValidateValue) => {
if (!v?.api_key || v?.api_key === plugin.credentials?.api_key)
return
const res = await updatePluginKey('serpapi', {
credentials: {
api_key: v?.api_key,
},
})
if (res.status === 'success') {
onUpdate()
return true
}
}
return (
<KeyValidator
type='serpapi'
title={<Image alt='serpapi logo' src={SerpapiLogo} width={64} />}
status={plugin.credentials?.api_key ? 'success' : 'add'}
forms={forms}
keyFrom={{
text: t('common.plugin.serpapi.keyFrom'),
link: 'https://serpapi.com/manage-api-key',
}}
onSave={handleSave}
/>
)
}
export default SerpapiPlugin
import useSWR from 'swr'
import { LockClosedIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import SerpapiPlugin from './SerpapiPlugin'
import { fetchPluginProviders } from '@/service/common'
import type { PluginProvider } from '@/models/common'
const PluginPage = () => {
const { t } = useTranslation()
const { data: plugins, mutate } = useSWR('/workspaces/current/tool-providers', fetchPluginProviders)
const Plugin_MAP: Record<string, any> = {
serpapi: (plugin: PluginProvider) => <SerpapiPlugin plugin={plugin} onUpdate={() => mutate()} />,
}
return (
<div className='pb-7'>
<div>
{plugins?.map(plugin => Plugin_MAP[plugin.tool_name](plugin))}
</div>
<div className='absolute bottom-0 w-full h-[42px] flex items-center bg-white text-xs text-gray-500'>
<LockClosedIcon className='w-3 h-3 mr-1' />
{t('common.provider.encrypted.front')}
<Link
className='text-primary-600 mx-1'
target={'_blank'}
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</Link>
{t('common.provider.encrypted.back')}
</div>
</div>
)
}
export default PluginPage
import { ValidatedStatus } from '../key-validator/declarations'
import { updatePluginProviderAIKey, validatePluginProviderKey } from '@/service/common'
export const validatePluginKey = async (pluginType: string, body: any) => {
try {
const res = await validatePluginProviderKey({
url: `/workspaces/current/tool-providers/${pluginType}/credentials-validate`,
body,
})
if (res.result === 'success')
return Promise.resolve({ status: ValidatedStatus.Success })
else
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error })
}
catch (e: any) {
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
}
}
export const updatePluginKey = async (pluginType: string, body: any) => {
try {
const res = await updatePluginProviderAIKey({
url: `/workspaces/current/tool-providers/${pluginType}/credentials`,
body,
})
if (res.result === 'success')
return Promise.resolve({ status: ValidatedStatus.Success })
else
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error })
}
catch (e: any) {
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
}
}
import { createContext, useContext } from 'use-context-selector'
import { useEventEmitter } from 'ahooks'
import type { EventEmitter } from 'ahooks/lib/useEventEmitter'
const KeyValidatorContext = createContext<{ trigger: EventEmitter<string> | null }>({
trigger: null,
})
export const useKeyValidatorContext = () => useContext(KeyValidatorContext)
type KeyValidatorContextProviderProps = {
children: React.ReactNode
}
export const KeyValidatorContextProvider = ({
children,
}: KeyValidatorContextProviderProps) => {
const trigger = useEventEmitter<string>()
return (
<KeyValidatorContext.Provider value={{ trigger }}>
{children}
</KeyValidatorContext.Provider>
)
}
export default KeyValidatorContext
...@@ -90,6 +90,7 @@ const translation = { ...@@ -90,6 +90,7 @@ const translation = {
language: 'Language', language: 'Language',
provider: 'Model Provider', provider: 'Model Provider',
dataSource: 'Data Source', dataSource: 'Data Source',
plugin: 'Plugins',
}, },
account: { account: {
avatar: 'Avatar', avatar: 'Avatar',
...@@ -198,6 +199,13 @@ const translation = { ...@@ -198,6 +199,13 @@ const translation = {
}, },
}, },
}, },
plugin: {
serpapi: {
apiKey: 'API Key',
apiKeyPlaceholder: 'Enter your API key',
keyFrom: 'Get your SerpAPI key from SerpAPI Account Page',
},
},
about: { about: {
changeLog: 'Changlog', changeLog: 'Changlog',
updateNow: 'Update now', updateNow: 'Update now',
......
...@@ -90,6 +90,7 @@ const translation = { ...@@ -90,6 +90,7 @@ const translation = {
language: '语言', language: '语言',
provider: '模型供应商', provider: '模型供应商',
dataSource: '数据来源', dataSource: '数据来源',
plugin: '插件',
}, },
account: { account: {
avatar: '头像', avatar: '头像',
...@@ -199,6 +200,13 @@ const translation = { ...@@ -199,6 +200,13 @@ const translation = {
}, },
}, },
}, },
plugin: {
serpapi: {
apiKey: 'API Key',
apiKeyPlaceholder: '输入你的 API 密钥',
keyFrom: '从 SerpAPI 帐户页面获取您的 SerpAPI 密钥',
},
},
about: { about: {
changeLog: '更新日志', changeLog: '更新日志',
updateNow: '现在更新', updateNow: '现在更新',
......
...@@ -135,3 +135,11 @@ export type DataSourceNotion = { ...@@ -135,3 +135,11 @@ export type DataSourceNotion = {
export type GithubRepo = { export type GithubRepo = {
stargazers_count: number stargazers_count: number
} }
export type PluginProvider = {
tool_name: string
is_enabled: boolean
credentials: {
api_key: string
} | null
}
...@@ -3,7 +3,7 @@ import { del, get, patch, post, put } from './base' ...@@ -3,7 +3,7 @@ import { del, get, patch, post, put } from './base'
import type { import type {
AccountIntegrate, CommonResponse, DataSourceNotion, AccountIntegrate, CommonResponse, DataSourceNotion,
IWorkspace, LangGeniusVersionResponse, Member, IWorkspace, LangGeniusVersionResponse, Member,
OauthResponse, Provider, ProviderAzureToken, TenantInfoResponse, OauthResponse, PluginProvider, Provider, ProviderAzureToken, TenantInfoResponse,
UserProfileOriginResponse, UserProfileOriginResponse,
} from '@/models/common' } from '@/models/common'
import type { import type {
...@@ -101,3 +101,14 @@ export const syncDataSourceNotion: Fetcher<CommonResponse, { url: string }> = ({ ...@@ -101,3 +101,14 @@ export const syncDataSourceNotion: Fetcher<CommonResponse, { url: string }> = ({
export const updateDataSourceNotionAction: Fetcher<CommonResponse, { url: string }> = ({ url }) => { export const updateDataSourceNotionAction: Fetcher<CommonResponse, { url: string }> = ({ url }) => {
return patch(url) as Promise<CommonResponse> return patch(url) as Promise<CommonResponse>
} }
export const fetchPluginProviders: Fetcher<PluginProvider[] | null, string> = (url) => {
return get(url) as Promise<PluginProvider[] | null>
}
export const validatePluginProviderKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { credentials: any } }> = ({ url, body }) => {
return post(url, { body }) as Promise<ValidateOpenAIKeyResponse>
}
export const updatePluginProviderAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { credentials: any } }> = ({ url, body }) => {
return post(url, { body }) as Promise<UpdateOpenAIKeyResponse>
}
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