feat: add service connection panel and translations for service connection messages

This commit is contained in:
zhsama 2025-12-01 15:23:46 +08:00
parent 5e6053b367
commit c8807d3f89
8 changed files with 276 additions and 0 deletions

View File

@ -24,6 +24,10 @@ import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
import { formatBooleanInputs } from '@/utils/model-config'
import Avatar from '../../avatar'
import ServiceConnectionPanel from '@/app/components/base/service-connection-panel'
import type { AuthType, ServiceConnectionItem as ServiceConnectionItemType } from '@/app/components/base/service-connection-panel'
import { Notion } from '@/app/components/base/icons/src/public/common'
import { Google } from '@/app/components/base/icons/src/public/plugins'
const ChatWrapper = () => {
const {
@ -167,6 +171,53 @@ const ChatWrapper = () => {
const [collapsed, setCollapsed] = useState(!!currentConversationId)
// Demo: Service connection state
const [serviceConnections, setServiceConnections] = useState<ServiceConnectionItemType[]>([
{
id: 'notion',
name: 'Notion Page Search',
icon: <Notion className="h-6 w-6" />,
authType: 'oauth',
status: 'pending',
},
{
id: 'gmail',
name: 'Gmail Tools',
icon: <img src="https://www.gstatic.com/images/branding/product/1x/gmail_2020q4_32dp.png" alt="Gmail" className="h-6 w-6" />,
authType: 'oauth',
status: 'pending',
},
{
id: 'youtube',
name: 'YouTube Data Upload',
icon: <img src="https://www.youtube.com/s/desktop/f506bd45/img/favicon_32x32.png" alt="YouTube" className="h-6 w-6" />,
authType: 'oauth',
status: 'pending',
},
{
id: 'google-serp',
name: 'Google SerpApi Search',
icon: <Google className="h-6 w-6" />,
authType: 'api_key',
status: 'pending',
},
])
const [showServiceConnection, setShowServiceConnection] = useState(true)
const handleServiceConnect = useCallback((serviceId: string, _authType: AuthType) => {
// Demo: 模拟连接成功
setServiceConnections(prev => prev.map(service =>
service.id === serviceId
? { ...service, status: 'connected' as const }
: service,
))
}, [])
const handleServiceContinue = useCallback(() => {
setShowServiceConnection(false)
}, [])
const chatNode = useMemo(() => {
if (allInputsHidden || !inputsForms.length)
return null
@ -253,6 +304,23 @@ const ChatWrapper = () => {
/>
: null
// 如果需要显示服务连接面板,则显示面板而非聊天界面
if (showServiceConnection) {
return (
<div className={cn(
'flex h-full items-center justify-center overflow-auto bg-chatbot-bg',
isMobile && 'px-4 py-8',
)}>
<ServiceConnectionPanel
services={serviceConnections}
onConnect={handleServiceConnect}
onContinue={handleServiceContinue}
className={cn(isMobile && 'max-w-full')}
/>
</div>
)
}
return (
<div
className='h-full overflow-hidden bg-chatbot-bg'

View File

@ -0,0 +1,79 @@
'use client'
import type { FC } from 'react'
import { memo, useMemo } from 'react'
import { RiArrowRightLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import ServiceItem from './service-item'
import type { ServiceConnectionPanelProps } from './types'
import cn from '@/utils/classnames'
const ServiceConnectionPanel: FC<ServiceConnectionPanelProps> = ({
title,
description,
services,
onConnect,
onContinue,
continueDisabled,
continueText,
className,
}) => {
const { t } = useTranslation()
const allConnected = useMemo(() => {
return services.every(service => service.status === 'connected')
}, [services])
const displayTitle = title || t('share.serviceConnection.title')
const displayDescription = description || t('share.serviceConnection.description', { count: services.length })
return (
<div className={cn(
'flex w-full max-w-[600px] flex-col items-center',
className,
)}>
<div className="mb-6 text-center">
<h2 className="system-xl-semibold mb-1 text-text-primary">
{displayTitle}
</h2>
<p className="system-sm-regular text-text-tertiary">
{displayDescription}
</p>
</div>
<div className="w-full space-y-2">
{services.map(service => (
<ServiceItem
key={service.id}
service={service}
onConnect={onConnect}
/>
))}
</div>
{onContinue && (
<div className="mt-6 flex w-full justify-end">
<Button
variant="primary"
disabled={continueDisabled ?? !allConnected}
onClick={onContinue}
>
{continueText || t('share.serviceConnection.continue')}
<RiArrowRightLine className="ml-1 h-4 w-4" />
</Button>
</div>
)}
</div>
)
}
export default memo(ServiceConnectionPanel)
export { default as ServiceItem } from './service-item'
export type {
ServiceConnectionPanelProps,
ServiceConnectionItem,
AuthType,
ServiceConnectionStatus,
} from './types'

View File

@ -0,0 +1,72 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import type { AuthType, ServiceConnectionItem } from './types'
import cn from '@/utils/classnames'
type ServiceItemProps = {
service: ServiceConnectionItem
onConnect: (serviceId: string, authType: AuthType) => void
}
const ServiceItem: FC<ServiceItemProps> = ({
service,
onConnect,
}) => {
const { t } = useTranslation()
const handleConnect = () => {
onConnect(service.id, service.authType)
}
const getButtonText = () => {
if (service.status === 'connected')
return t('share.serviceConnection.connected')
if (service.authType === 'api_key')
return t('share.serviceConnection.addApiKey')
return t('share.serviceConnection.connect')
}
const isConnected = service.status === 'connected'
return (
<div className={cn(
'flex items-center justify-between gap-3 rounded-xl border border-components-panel-border-subtle bg-components-panel-bg px-4 py-3',
'hover:border-components-panel-border hover:shadow-xs',
'transition-all duration-200',
)}>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center">
{service.icon}
</div>
<div className="flex flex-col">
<span className="system-sm-medium text-text-secondary">
{service.name}
</span>
{service.description && (
<span className="system-xs-regular text-text-tertiary">
{service.description}
</span>
)}
</div>
</div>
<Button
variant={isConnected ? 'secondary' : 'secondary-accent'}
size="small"
onClick={handleConnect}
disabled={isConnected}
>
{!isConnected && <RiAddLine className="mr-0.5 h-3.5 w-3.5" />}
{getButtonText()}
</Button>
</div>
)
}
export default memo(ServiceItem)

View File

@ -0,0 +1,25 @@
import type { ReactNode } from 'react'
export type AuthType = 'oauth' | 'api_key'
export type ServiceConnectionStatus = 'pending' | 'connected' | 'error'
export type ServiceConnectionItem = {
id: string
name: string
icon: ReactNode
authType: AuthType
status: ServiceConnectionStatus
description?: string
}
export type ServiceConnectionPanelProps = {
title?: string
description?: string
services: ServiceConnectionItem[]
onConnect: (serviceId: string, authType: AuthType) => void
onContinue?: () => void
continueDisabled?: boolean
continueText?: string
className?: string
}

View File

@ -81,6 +81,14 @@ const translation = {
login: {
backToHome: 'Back to Home',
},
serviceConnection: {
title: 'Connect the required services to start',
description: 'You need to configure {{count}} connections before you can use this app',
connect: 'Connect',
addApiKey: 'Add API Key',
connected: 'Connected',
continue: 'Continue',
},
}
export default translation

View File

@ -77,6 +77,14 @@ const translation = {
login: {
backToHome: 'ホームに戻る',
},
serviceConnection: {
title: 'サービスを接続して開始',
description: 'このアプリを使用するには {{count}} 件の接続を設定する必要があります',
connect: '接続',
addApiKey: 'API Key を追加',
connected: '接続済み',
continue: '続行',
},
}
export default translation

View File

@ -77,6 +77,14 @@ const translation = {
login: {
backToHome: '返回首页',
},
serviceConnection: {
title: '连接所需服务以开始',
description: '您需要配置 {{count}} 个连接才能使用此应用',
connect: '连接',
addApiKey: '添加 API Key',
connected: '已连接',
continue: '继续',
},
}
export default translation

View File

@ -77,6 +77,14 @@ const translation = {
login: {
backToHome: '返回首頁',
},
serviceConnection: {
title: '連接所需服務以開始',
description: '您需要配置 {{count}} 個連接才能使用此應用',
connect: '連接',
addApiKey: '新增 API Key',
connected: '已連接',
continue: '繼續',
},
}
export default translation