Unverified Commit 8e112003 authored by Joel's avatar Joel Committed by GitHub

feat: frontend support claude (#573)

Co-authored-by: 's avatarStyleZhang <jasonapring2015@outlook.com>
parent 7599f79a
......@@ -372,7 +372,7 @@ const Debug: FC<IDebug> = ({
{/* Chat */}
{mode === AppType.chat && (
<div className="mt-[34px] h-full flex flex-col">
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
<div className="h-full overflow-y-auto overflow-x-hidden" ref={chatListDomRef}>
<Chat
chatList={chatList}
......
......@@ -16,6 +16,7 @@ import ConfigModel from '@/app/components/app/configuration/config-model'
import Config from '@/app/components/app/configuration/config'
import Debug from '@/app/components/app/configuration/debug'
import Confirm from '@/app/components/base/confirm'
import { ProviderType } from '@/types/app'
import type { AppDetailResponse } from '@/models/app'
import { ToastContext } from '@/app/components/base/toast'
import { fetchTenantInfo } from '@/service/common'
......@@ -67,7 +68,7 @@ const Configuration: FC = () => {
frequency_penalty: 1, // -2-2
})
const [modelConfig, doSetModelConfig] = useState<ModelConfig>({
provider: 'openai',
provider: ProviderType.openai,
model_id: 'gpt-3.5-turbo',
configs: {
prompt_template: '',
......@@ -84,8 +85,9 @@ const Configuration: FC = () => {
doSetModelConfig(newModelConfig)
}
const setModelId = (modelId: string) => {
const setModelId = (modelId: string, provider: ProviderType) => {
const newModelConfig = produce(modelConfig, (draft: any) => {
draft.provider = provider
draft.model_id = modelId
})
setModelConfig(newModelConfig)
......
......@@ -19,6 +19,7 @@ const AutoHeightTextarea = forwardRef(
{ value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
outerRef: any,
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const ref = outerRef || useRef<HTMLTextAreaElement>(null)
const doFocus = () => {
......@@ -54,13 +55,20 @@ const AutoHeightTextarea = forwardRef(
return (
<div className='relative'>
<div className={cn(className, 'invisible whitespace-pre-wrap break-all overflow-y-auto')} style={{ minHeight, maxHeight }}>
<div className={cn(className, 'invisible whitespace-pre-wrap break-all overflow-y-auto')} style={{
minHeight,
maxHeight,
paddingRight: (value && value.trim().length > 10000) ? 140 : 130,
}}>
{!value ? placeholder : value.replace(/\n$/, '\n ')}
</div>
<textarea
ref={ref}
autoFocus={autoFocus}
className={cn(className, 'absolute inset-0 resize-none overflow-hidden')}
className={cn(className, 'absolute inset-0 resize-none overflow-auto')}
style={{
paddingRight: (value && value.trim().length > 10000) ? 140 : 130,
}}
placeholder={placeholder}
onChange={onChange}
onKeyDown={onKeyDown}
......
.icon {
width: 24px;
height: 24px;
margin-right: 12px;
background: url(../../../assets/anthropic.svg) center center no-repeat;
background-size: contain;
}
.bar {
background: linear-gradient(90deg, rgba(41, 112, 255, 0.9) 0%, rgba(21, 94, 239, 0.9) 100%);
}
.bar-error {
background: linear-gradient(90deg, rgba(240, 68, 56, 0.72) 0%, rgba(217, 45, 32, 0.9) 100%);
}
.bar-item {
width: 10%;
border-right: 1px solid rgba(255, 255, 255, 0.5);
}
.bar-item:last-of-type {
border-right: 0;
}
\ No newline at end of file
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './index.module.css'
import type { ProviderHosted } from '@/models/common'
type AnthropicHostedProviderProps = {
provider: ProviderHosted
}
const AnthropicHostedProvider = ({
provider,
}: AnthropicHostedProviderProps) => {
const { t } = useTranslation()
const exhausted = provider.quota_used > provider.quota_limit
return (
<div className={`
border-[0.5px] border-gray-200 rounded-xl
${exhausted ? 'bg-[#FFFBFA]' : 'bg-gray-50'}
`}>
<div className='pt-4 px-4 pb-3'>
<div className='flex items-center mb-3'>
<div className={s.icon} />
<div className='grow text-sm font-medium text-gray-800'>
{t('common.provider.anthropicHosted.anthropicHosted')}
</div>
<div className={`
px-2 h-[22px] flex items-center rounded-md border
text-xs font-semibold
${exhausted ? 'border-[#D92D20] text-[#D92D20]' : 'border-primary-600 text-primary-600'}
`}>
{exhausted ? t('common.provider.anthropicHosted.exhausted') : t('common.provider.anthropicHosted.onTrial')}
</div>
</div>
<div className='text-[13px] text-gray-500'>{t('common.provider.anthropicHosted.desc')}</div>
</div>
<div className='flex items-center h-[42px] px-4 border-t-[0.5px] border-t-[rgba(0, 0, 0, 0.05)]'>
<div className='text-[13px] text-gray-700'>{t('common.provider.anthropicHosted.callTimes')}</div>
<div className='relative grow h-2 flex bg-gray-200 rounded-md mx-2 overflow-hidden'>
<div
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
style={{ width: `${(provider.quota_used / provider.quota_limit * 100).toFixed(2)}%` }}
/>
{Array(10).fill(0).map((i, k) => (
<div key={k} className={s['bar-item']} />
))}
</div>
<div className={`
text-[13px] font-medium ${exhausted ? 'text-[#D92D20]' : 'text-gray-700'}
`}>{provider.quota_used}/{provider.quota_limit}</div>
</div>
{
exhausted && (
<div className='
px-4 py-3 leading-[18px] flex items-center text-[13px] text-gray-700 font-medium
bg-[#FFFAEB] border-t border-t-[rgba(0, 0, 0, 0.05)] rounded-b-xl
'>
{t('common.provider.anthropicHosted.usedUp')}
</div>
)
}
</div>
)
}
export default AnthropicHostedProvider
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import ProviderInput from '../provider-input'
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
import {
ValidatedErrorIcon,
ValidatedErrorOnOpenaiTip,
ValidatedSuccessIcon,
ValidatingTip,
} from '../provider-input/Validate'
import type { Provider, ProviderAnthropicToken } from '@/models/common'
type AnthropicProviderProps = {
provider: Provider
onValidatedStatus: (status?: ValidatedStatusState) => void
onTokenChange: (token: ProviderAnthropicToken) => void
}
const AnthropicProvider = ({
provider,
onValidatedStatus,
onTokenChange,
}: AnthropicProviderProps) => {
const { t } = useTranslation()
const [token, setToken] = useState<ProviderAnthropicToken>((provider.token as ProviderAnthropicToken) || { anthropic_api_key: '' })
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
const handleFocus = () => {
if (token.anthropic_api_key === (provider.token as ProviderAnthropicToken).anthropic_api_key) {
setToken({ anthropic_api_key: '' })
onTokenChange({ anthropic_api_key: '' })
setValidatedStatus({})
}
}
const handleChange = (v: string) => {
const apiKey = { anthropic_api_key: v }
setToken(apiKey)
onTokenChange(apiKey)
validate(apiKey, {
beforeValidating: () => {
if (!v) {
setValidatedStatus({})
return false
}
return true
},
})
}
useEffect(() => {
if (typeof onValidatedStatus === 'function')
onValidatedStatus(validatedStatus)
}, [validatedStatus])
const getValidatedIcon = () => {
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
return <ValidatedErrorIcon />
if (validatedStatus.status === ValidatedStatus.Success)
return <ValidatedSuccessIcon />
}
const getValidatedTip = () => {
if (validating)
return <ValidatingTip />
if (validatedStatus?.status === ValidatedStatus.Error)
return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
}
return (
<div className='px-4 pt-3 pb-4'>
<ProviderInput
value={token.anthropic_api_key}
name={t('common.provider.apiKey')}
placeholder={t('common.provider.enterYourKey')}
onChange={handleChange}
onFocus={handleFocus}
validatedIcon={getValidatedIcon()}
validatedTip={getValidatedTip()}
/>
<Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://docs.anthropic.com/claude/reference/getting-started-with-the-api" target={'_blank'}>
{t('common.provider.anthropic.keyFrom')}
<ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
</Link>
</div>
)
}
export default AnthropicProvider
......@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import ProviderItem from './provider-item'
import OpenaiHostedProvider from './openai-hosted-provider'
import AnthropicHostedProvider from './anthropic-hosted-provider'
import type { ProviderHosted } from '@/models/common'
import { fetchProviders } from '@/service/common'
import { IS_CE_EDITION } from '@/config'
......@@ -18,6 +19,10 @@ const providersMap: { [k: string]: any } = {
icon: 'azure',
name: 'Azure OpenAI Service',
},
'anthropic-custom': {
icon: 'anthropic',
name: 'Anthropic',
},
}
// const providersList = [
......@@ -65,6 +70,8 @@ const ProviderPage = () => {
}
})
const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0]
const anthropicHosted = data?.filter(provider => provider.provider_name === 'anthropic' && provider.provider_type === 'system')?.[0]
const providedOpenaiProvider = data?.find(provider => provider.is_enabled && (provider.provider_name === 'openai' || provider.provider_name === 'azure_openai'))
return (
<div className='pb-7'>
......@@ -78,6 +85,16 @@ const ProviderPage = () => {
</>
)
}
{
anthropicHosted && !IS_CE_EDITION && (
<>
<div>
<AnthropicHostedProvider provider={anthropicHosted as ProviderHosted} />
</div>
<div className='my-5 w-full h-0 border-[0.5px] border-gray-100' />
</>
)
}
<div>
{
providers?.map(providerItem => (
......@@ -89,6 +106,7 @@ const ProviderPage = () => {
activeId={activeProviderId}
onActive={aid => setActiveProviderId(aid)}
onSave={() => mutate()}
providedOpenaiProvider={providedOpenaiProvider}
/>
))
}
......
......@@ -5,14 +5,20 @@ import { useTranslation } from 'react-i18next'
import Indicator from '../../../indicator'
import OpenaiProvider from '../openai-provider'
import AzureProvider from '../azure-provider'
import AnthropicProvider from '../anthropic-provider'
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 type { Provider, ProviderAnthropicToken, ProviderAzureToken } from '@/models/common'
import { ProviderName } from '@/models/common'
import { updateProviderAIKey } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
const providerNameMap: Record<string, string> = {
openai: 'OpenAI',
azure_openai: 'Azure OpenAI Service',
}
type IProviderItemProps = {
icon: string
name: string
......@@ -20,6 +26,7 @@ type IProviderItemProps = {
activeId: string
onActive: (v: string) => void
onSave: () => void
providedOpenaiProvider?: Provider
}
const ProviderItem = ({
activeId,
......@@ -28,15 +35,18 @@ const ProviderItem = ({
provider,
onActive,
onSave,
providedOpenaiProvider,
}: IProviderItemProps) => {
const { t } = useTranslation()
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>()
const [loading, setLoading] = useState(false)
const { notify } = useContext(ToastContext)
const [token, setToken] = useState<ProviderAzureToken | string>(
const [token, setToken] = useState<ProviderAzureToken | string | ProviderAnthropicToken>(
provider.provider_name === 'azure_openai'
? { openai_api_base: '', openai_api_key: '' }
: '',
: provider.provider_name === 'anthropic'
? { anthropic_api_key: '' }
: '',
)
const id = `${provider.provider_name}-${provider.provider_type}`
const isOpen = id === activeId
......@@ -54,6 +64,8 @@ const ProviderItem = ({
}
if (provider.provider_name === ProviderName.OPENAI)
return provider.token
if (provider.provider_name === ProviderName.ANTHROPIC)
return provider.token?.anthropic_api_key
}
const handleUpdateToken = async () => {
if (loading)
......@@ -81,7 +93,7 @@ const ProviderItem = ({
<div className={cn(s[`icon-${icon}`], 'mr-3 w-6 h-6 rounded-md')} />
<div className='grow text-sm font-medium text-gray-800'>{name}</div>
{
providerTokenHasSetted() && !comingSoon && !isOpen && (
providerTokenHasSetted() && !comingSoon && !isOpen && provider.provider_name !== ProviderName.ANTHROPIC && (
<div className='flex items-center mr-4'>
{!isValid && <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>}
<Indicator color={!isValid ? 'red' : 'green'} className='ml-2' />
......@@ -89,7 +101,27 @@ const ProviderItem = ({
)
}
{
!comingSoon && !isOpen && (
(providerTokenHasSetted() && !comingSoon && !isOpen && provider.provider_name === ProviderName.ANTHROPIC) && (
<div className='flex items-center mr-4'>
{
providedOpenaiProvider?.is_valid
? !isValid
? <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>
: null
: <div className='text-xs text-[#DC6803]'>{t('common.provider.anthropic.notEnabled')}</div>
}
<Indicator color={
providedOpenaiProvider?.is_valid
? isValid
? 'green'
: 'red'
: 'yellow'
} className='ml-2' />
</div>
)
}
{
!comingSoon && !isOpen && provider.provider_name !== ProviderName.ANTHROPIC && (
<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
......@@ -98,6 +130,34 @@ const ProviderItem = ({
</div>
)
}
{
(!comingSoon && !isOpen && provider.provider_name === ProviderName.ANTHROPIC)
? providedOpenaiProvider?.is_enabled
? (
<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={() => providedOpenaiProvider.is_valid && onActive(id)}>
{providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')}
</div>
)
: (
<Tooltip
htmlContent={<div className='w-[320px]'>
{t('common.provider.anthropic.enableTip')}
</div>}
position='bottom'
selector='anthropic-provider-enable-top-tooltip'>
<div className='
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-not-allowed
text-xs font-medium text-gray-700 flex items-center opacity-50
'>
{t('common.provider.addKey')}
</div>
</Tooltip>
)
: null
}
{
comingSoon && !isOpen && (
<div className='
......@@ -147,6 +207,29 @@ const ProviderItem = ({
/>
)
}
{
provider.provider_name === ProviderName.ANTHROPIC && isOpen && (
<AnthropicProvider
provider={provider}
onValidatedStatus={v => setValidatedStatus(v)}
onTokenChange={v => setToken(v)}
/>
)
}
{
provider.provider_name === ProviderName.ANTHROPIC && !isOpen && providerTokenHasSetted() && providedOpenaiProvider?.is_valid && (
<div className='px-4 py-3 text-[13px] font-medium text-gray-700'>
{t('common.provider.anthropic.using')} {providerNameMap[providedOpenaiProvider.provider_name as string]}
</div>
)
}
{
provider.provider_name === ProviderName.ANTHROPIC && !isOpen && providerTokenHasSetted() && !providedOpenaiProvider?.is_valid && (
<div className='px-4 py-3 bg-[#FFFAEB] text-[13px] font-medium text-gray-700'>
{t('common.provider.anthropic.enableTip')}
</div>
)
}
</div>
)
}
......
......@@ -620,7 +620,7 @@ const Main: FC<IMainProps> = ({
{
hasSetInputs && (
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
<div className='h-full overflow-y-auto' ref={chatListDomRef}>
<Chat
chatList={chatList}
......
......@@ -609,7 +609,7 @@ const Main: FC<IMainProps> = ({
{
hasSetInputs && (
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
<div className='h-full overflow-y-auto' ref={chatListDomRef}>
<Chat
chatList={chatList}
......
......@@ -54,7 +54,7 @@ const translation = {
maxTokenTip:
'Max tokens depending on the model. Prompt and completion share this limit. One token is roughly 1 English character.',
maxTokenSettingTip: 'Your max token setting is high, potentially limiting space for prompts, queries, and data. Consider setting it below 2/3.',
setToCurrentModelMaxTokenTip: 'Max token is updated to the maximum token of the current model 4,000.',
setToCurrentModelMaxTokenTip: 'Max token is updated to the maximum token of the current model {{maxToken}}.',
},
tone: {
Creative: 'Creative',
......@@ -180,6 +180,22 @@ const translation = {
useYourModel: 'Currently using own Model Provider.',
close: 'Close',
},
anthropicHosted: {
anthropicHosted: 'Anthropic Claude',
onTrial: 'ON TRIAL',
exhausted: 'QUOTA EXHAUSTED',
desc: 'Powerful model, which excels at a wide range of tasks from sophisticated dialogue and creative content generation to detailed instruction.',
callTimes: 'Call times',
usedUp: 'Trial quota used up. Add own Model Provider.',
useYourModel: 'Currently using own Model Provider.',
close: 'Close',
},
anthropic: {
using: 'The embedding capability is using',
enableTip: 'To enable the Anthropic model, you need to bind to OpenAI or Azure OpenAI Service first.',
notEnabled: 'Not enabled',
keyFrom: 'Get your API key from Anthropic',
},
encrypted: {
front: 'Your API KEY will be encrypted and stored using',
back: ' technology.',
......
......@@ -54,7 +54,7 @@ const translation = {
maxTokenTip:
'生成的最大令牌数取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 半个中文字符。',
maxTokenSettingTip: '您设置的最大 tokens 数较大,可能会导致 prompt、用户问题、数据集内容没有 token 空间进行处理,建议设置到 2/3 以下。',
setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 4,000。',
setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 {{maxToken}}。',
},
tone: {
Creative: '创意',
......@@ -180,6 +180,22 @@ const translation = {
useYourModel: '当前正在使用你自己的模型供应商。',
close: '关闭',
},
anthropicHosted: {
anthropicHosted: 'Anthropic Claude',
onTrial: '体验',
exhausted: '超出限额',
desc: '功能强大的模型,擅长执行从复杂对话和创意内容生成到详细指导的各种任务。',
callTimes: '调用次数',
usedUp: '试用额度已用完,请在下方添加自己的模型供应商',
useYourModel: '当前正在使用你自己的模型供应商。',
close: '关闭',
},
anthropic: {
using: '嵌入能力正在使用',
enableTip: '要启用 Anthropic 模型,您需要先绑定 OpenAI 或 Azure OpenAI 服务。',
notEnabled: '未启用',
keyFrom: '从 Anthropic 获取您的 API 密钥',
},
encrypted: {
front: '密钥将使用 ',
back: ' 技术进行加密和存储。',
......
......@@ -59,14 +59,19 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l
export enum ProviderName {
OPENAI = 'openai',
AZURE_OPENAI = 'azure_openai',
ANTHROPIC = 'anthropic',
}
export type ProviderAzureToken = {
openai_api_base?: string
openai_api_key?: string
}
export type ProviderAnthropicToken = {
anthropic_api_key?: string
}
export type ProviderTokenType = {
[ProviderName.OPENAI]: string
[ProviderName.AZURE_OPENAI]: ProviderAzureToken
[ProviderName.ANTHROPIC]: ProviderAnthropicToken
}
export type Provider = {
[Name in ProviderName]: {
......
......@@ -3,7 +3,7 @@ import { del, get, patch, post, put } from './base'
import type {
AccountIntegrate, CommonResponse, DataSourceNotion,
IWorkspace, LangGeniusVersionResponse, Member,
OauthResponse, Provider, ProviderAzureToken, TenantInfoResponse,
OauthResponse, Provider, ProviderAnthropicToken, ProviderAzureToken, TenantInfoResponse,
UserProfileOriginResponse,
} from '@/models/common'
import type {
......@@ -58,7 +58,7 @@ export const fetchProviders: Fetcher<Provider[] | null, { url: string; params: R
export const validateProviderKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
return post(url, { body }) as Promise<ValidateOpenAIKeyResponse>
}
export const updateProviderAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string | ProviderAzureToken } }> = ({ url, body }) => {
export const updateProviderAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string | ProviderAzureToken | ProviderAnthropicToken } }> = ({ url, body }) => {
return post(url, { body }) as Promise<UpdateOpenAIKeyResponse>
}
......
export enum ProviderType {
openai = 'openai',
anthropic = 'anthropic',
}
export enum AppType {
'chat' = 'chat',
'completion' = 'completion',
......
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