初始化模版工程
This commit is contained in:
250
components/nova-sdk/nova-chat/index.tsx
Normal file
250
components/nova-sdk/nova-chat/index.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import React, { useRef, useCallback, useMemo, useState } from 'react'
|
||||
import { FolderOpen, Share2, Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/utils/cn'
|
||||
import { MessageList, type MessageListRef } from '../message-list'
|
||||
import { TaskPanel } from '../task-panel'
|
||||
import { NovaKitProvider } from '../context/NovaKitProvider'
|
||||
import { useNovaChatLogic } from '../hooks/useNovaChatLogic'
|
||||
import { ChatInputArea } from './ChatInputArea'
|
||||
import type { PlatformConfig, ApiEvent } from '../hooks/useNovaEvents'
|
||||
import { toast } from 'sonner'
|
||||
import { request } from '@/http/request'
|
||||
|
||||
// 导出类型供外部使用
|
||||
export type { ApiEvent, PlatformConfig }
|
||||
|
||||
export interface NovaChatProps {
|
||||
/** 模式 */
|
||||
mode?: 'chat' | 'share'
|
||||
/** 是否显示文件面板 */
|
||||
panelMode?: 'sidebar' | 'dialog'
|
||||
/** 会话 ID */
|
||||
conversationId?: string
|
||||
/** 代理 ID */
|
||||
agentId?: string
|
||||
/** 侧边面板宽度 */
|
||||
panelWidth?: number | string
|
||||
/** 是否正在加载 */
|
||||
loading?: boolean
|
||||
/** 输入框占位符 */
|
||||
placeholder?: string
|
||||
/** 是否禁用输入 */
|
||||
disabled?: boolean
|
||||
/** 空状态渲染 */
|
||||
emptyRender?: React.ReactNode
|
||||
/** 自定义类名 */
|
||||
className?: string
|
||||
|
||||
// useNovaEvents 相关配置
|
||||
/** 平台配置(如果提供,则自动建立 WebSocket 连接和获取历史记录) */
|
||||
platformConfig?: PlatformConfig
|
||||
/** WebSocket 重连次数限制,默认 3 */
|
||||
reconnectLimit?: number
|
||||
/** WebSocket 重连间隔(毫秒),默认 3000 */
|
||||
reconnectInterval?: number
|
||||
/** 获取认证 Token */
|
||||
getToken?: () => string | undefined
|
||||
/** 获取租户 ID */
|
||||
getTenantId?: () => string | undefined
|
||||
/** 新事件回调 */
|
||||
onEvent?: (event: ApiEvent) => void
|
||||
/** 连接状态变化回调 */
|
||||
onConnectionChange?: (connected: boolean) => void
|
||||
/** 错误回调 */
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Nova 聊天组件 - 整合消息列表、输入框和文件面板
|
||||
*/
|
||||
function InnerNovaChat({
|
||||
mode = 'chat',
|
||||
panelMode = 'sidebar',
|
||||
conversationId,
|
||||
agentId,
|
||||
panelWidth = '50%',
|
||||
placeholder,
|
||||
disabled = false,
|
||||
emptyRender,
|
||||
className,
|
||||
platformConfig,
|
||||
reconnectLimit = 3,
|
||||
reconnectInterval = 3000,
|
||||
getToken,
|
||||
getTenantId,
|
||||
onEvent,
|
||||
onConnectionChange,
|
||||
onError,
|
||||
}: NovaChatProps) {
|
||||
const messageListRef = useRef<MessageListRef>(null)
|
||||
|
||||
// 使用主业务逻辑 Hook
|
||||
const {
|
||||
messages,
|
||||
loading,
|
||||
taskStatus,
|
||||
artifacts,
|
||||
hasArtifacts,
|
||||
panelVisible,
|
||||
selectedAttachment,
|
||||
handleSend,
|
||||
handlePanelToggle,
|
||||
handlePanelClose,
|
||||
handleAttachmentClick,
|
||||
handleImageAttachmentClick,
|
||||
handleToolCallClick,
|
||||
setLoading,
|
||||
api,
|
||||
} = useNovaChatLogic({
|
||||
conversationId,
|
||||
agentId,
|
||||
platformConfig,
|
||||
reconnectLimit,
|
||||
reconnectInterval,
|
||||
getToken,
|
||||
getTenantId,
|
||||
onEvent,
|
||||
onConnectionChange,
|
||||
onError,
|
||||
mode
|
||||
})
|
||||
|
||||
const [shareLoading, setShareLoading] = useState(false)
|
||||
|
||||
// 包装 handleSend 以添加滚动逻辑
|
||||
const handleSendWithScroll = useCallback(
|
||||
(payload: Parameters<typeof handleSend>[0]) => {
|
||||
handleSend(payload)
|
||||
|
||||
// 延迟滚动到底部,确保消息已添加到列表中
|
||||
setTimeout(() => {
|
||||
messageListRef.current?.scrollToBottom('smooth')
|
||||
}, 100)
|
||||
},
|
||||
[handleSend]
|
||||
)
|
||||
|
||||
const providerAgentId = agentId ?? platformConfig?.agentId ?? ''
|
||||
|
||||
const shareUrl = useMemo(() => {
|
||||
if (!conversationId || !providerAgentId) return ''
|
||||
return `${window.location.origin}/share?conversationId=${encodeURIComponent(conversationId)}&agentId=${encodeURIComponent(providerAgentId)}`
|
||||
}, [conversationId, providerAgentId])
|
||||
|
||||
return (
|
||||
<NovaKitProvider panelMode={panelMode} conversationId={conversationId} api={api} agentName={platformConfig?.agentName || 'Autonomous Agent'} agentId={providerAgentId} setLoading={setLoading} loading={loading} mode={mode}>
|
||||
<div className={cn('flex h-full w-full overflow-hidden text-foreground bg-white', className)}>
|
||||
{/* 主聊天区域 */}
|
||||
<div className="relative flex flex-col h-full min-w-0 w-full flex-1">
|
||||
{/* 头部通栏:白色磨砂,图标黑色 */}
|
||||
<div className="shrink-0 z-20 flex items-center justify-end px-4 py-2 bg-white">
|
||||
{hasArtifacts && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handlePanelToggle}
|
||||
title={panelVisible ? '关闭文件面板' : '打开文件面板'}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-xl',
|
||||
'border-none bg-transparent',
|
||||
'shadow-none flex items-center justify-center',
|
||||
'text-gray-800 hover:text-black hover:scale-105 transition-all duration-200 hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{mode === 'chat' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!shareUrl || shareLoading}
|
||||
title={shareUrl ? '复制分享链接' : '缺少会话或 Agent 信息,无法分享'}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-xl',
|
||||
'border-none bg-transparent',
|
||||
'shadow-none flex items-center justify-center',
|
||||
'text-gray-800 hover:text-black hover:scale-105 transition-all duration-200 hover:bg-gray-100'
|
||||
)}
|
||||
onClick={async () => {
|
||||
if (!shareUrl || shareLoading) return
|
||||
|
||||
setShareLoading(true)
|
||||
try {
|
||||
await request.post('/v1/super_agent/chat/update_public', {
|
||||
conversation_id: conversationId,
|
||||
is_public: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await navigator.clipboard?.writeText?.(shareUrl)
|
||||
toast.success('分享链接已复制到剪贴板')
|
||||
} catch {
|
||||
window.open(shareUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
} catch {
|
||||
toast.error('分享链接更新失败')
|
||||
} finally {
|
||||
setShareLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{shareLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Share2 className="w-6 h-6" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 消息列表 - 铺满全部高度,消息可滚动到输入框下方 */}
|
||||
<MessageList
|
||||
ref={messageListRef}
|
||||
messages={messages}
|
||||
taskStatus={taskStatus}
|
||||
loading={loading}
|
||||
emptyRender={emptyRender}
|
||||
onAttachmentClick={handleAttachmentClick}
|
||||
onImageAttachmentClick={handleImageAttachmentClick}
|
||||
onToolCallClick={handleToolCallClick}
|
||||
onSendMessage={content => handleSendWithScroll({ content })}
|
||||
/>
|
||||
|
||||
{/* 输入区域 - absolute 固定在底部,磨砂透明,消息可穿过 */}
|
||||
{mode === 'chat' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10">
|
||||
<ChatInputArea
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
taskStatus={taskStatus}
|
||||
onSend={handleSendWithScroll}
|
||||
hasArtifacts={hasArtifacts}
|
||||
panelVisible={panelVisible}
|
||||
onPanelToggle={handlePanelToggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件面板:支持 sidebar 与 dialog 两种模式 */}
|
||||
{(hasArtifacts || selectedAttachment) && panelVisible && (
|
||||
<TaskPanel
|
||||
artifacts={selectedAttachment ? [selectedAttachment] : artifacts}
|
||||
visible={panelVisible}
|
||||
width={panelWidth}
|
||||
onClose={handlePanelClose}
|
||||
initialSelected={selectedAttachment}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NovaKitProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const NovaChat = React.memo(InnerNovaChat)
|
||||
export default NovaChat
|
||||
Reference in New Issue
Block a user