feat: expand MCP server functionality with customer, catalogue, marketing, and user tools and integrate new health indicators and configuration models.
This commit is contained in:
@@ -7,18 +7,24 @@ import { useChatStore } from '@/store/useChatStore'
|
||||
import { useConnectionStore } from '@/store/useConnectionStore'
|
||||
import { SendHorizontal, AlertCircle, Clock, Mic } from 'lucide-react'
|
||||
|
||||
export function ChatBottombar() {
|
||||
export function ChatBottombar({ onSend }: { onSend?: (prompt: string) => void }) {
|
||||
const [input, setInput] = useState('')
|
||||
const sendMessage = useChatStore((state) => state.sendMessage)
|
||||
const conversationId = useChatStore((state) => state.conversationId)
|
||||
const connectionStatus = useConnectionStore((state) => state.status)
|
||||
const queuedCount = useConnectionStore((state) => state.queuedCount)
|
||||
|
||||
const isQueueFull = queuedCount >= 20
|
||||
const isDisconnected = connectionStatus === 'disconnected' || connectionStatus === 'error'
|
||||
const isReady = !isQueueFull
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || isQueueFull) return
|
||||
sendMessage(input.trim())
|
||||
if (!input.trim() || !isReady) return
|
||||
if (onSend) {
|
||||
onSend(input.trim())
|
||||
} else {
|
||||
sendMessage(input.trim())
|
||||
}
|
||||
setInput('')
|
||||
}
|
||||
|
||||
@@ -40,11 +46,13 @@ export function ChatBottombar() {
|
||||
placeholder={
|
||||
isQueueFull
|
||||
? 'Mất kết nối. Hàng chờ đã đầy, vui lòng chờ...'
|
||||
: 'Hỏi Loyalty Agent...'
|
||||
: !conversationId
|
||||
? 'Đang khởi tạo cuộc trò chuyện...'
|
||||
: 'Hỏi Loyalty Agent...'
|
||||
}
|
||||
className="flex-1 min-h-[36px] max-h-[120px] py-1.5 px-1 bg-transparent border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-sm leading-relaxed text-foreground placeholder:text-muted-foreground/60 resize-none overflow-y-auto"
|
||||
rows={1}
|
||||
disabled={isQueueFull}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
@@ -71,7 +79,7 @@ export function ChatBottombar() {
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm disabled:opacity-30 shrink-0 transition-all active:scale-95"
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isQueueFull}
|
||||
disabled={!input.trim() || !isReady}
|
||||
>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { ChatList } from './ChatList'
|
||||
import { ChatBottombar } from './ChatBottombar'
|
||||
import { ChatMobileSidebar } from './ChatMobileSidebar'
|
||||
@@ -52,6 +52,19 @@ export function ChatLayout() {
|
||||
}
|
||||
}, [messages, activeId, conversations, fetchConversations])
|
||||
|
||||
const handleSendMessage = useCallback(async (prompt: string) => {
|
||||
let currentId = activeId
|
||||
if (!currentId) {
|
||||
const newConv = await createNewConversation()
|
||||
if (newConv) {
|
||||
currentId = newConv.id
|
||||
}
|
||||
}
|
||||
if (currentId) {
|
||||
useChatStore.getState().sendMessage(prompt)
|
||||
}
|
||||
}, [activeId, createNewConversation])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full bg-background text-foreground overflow-hidden">
|
||||
{/* Mobile Drawer & Sidebar */}
|
||||
@@ -97,8 +110,8 @@ export function ChatLayout() {
|
||||
{/* Center Content & Floating Input */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<div className="flex-1 flex flex-col overflow-hidden relative">
|
||||
<ChatList messages={messages} />
|
||||
<ChatBottombar />
|
||||
<ChatList messages={messages} onSend={handleSendMessage} />
|
||||
<ChatBottombar onSend={handleSendMessage} />
|
||||
</div>
|
||||
|
||||
{/* Right Info Panel */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Message } from '@/store/useChatStore'
|
||||
import { ArrowDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function ChatList({ messages }: { messages: Message[] }) {
|
||||
export function ChatList({ messages, onSend }: { messages: Message[], onSend?: (prompt: string) => void }) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const [showScrollBottom, setShowScrollBottom] = useState(false)
|
||||
@@ -32,7 +32,7 @@ export function ChatList({ messages }: { messages: Message[] }) {
|
||||
}, [messages])
|
||||
|
||||
if (messages.length === 0) {
|
||||
return <WelcomeScreen />
|
||||
return <WelcomeScreen onSend={onSend} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,11 +6,39 @@ interface ToolStatusIndicatorProps {
|
||||
}
|
||||
|
||||
const TOOL_NAME_MAP: Record<string, string> = {
|
||||
// Campaign tools
|
||||
searchCampaigns: '🔍 Đang tìm kiếm chiến dịch...',
|
||||
getCampaignDetails: '📋 Đang lấy thông tin chi tiết chiến dịch...',
|
||||
countCampaigns: '📊 Đang đếm chiến dịch...',
|
||||
getCampaignById: '📋 Đang lấy thông tin chiến dịch...',
|
||||
getCampaignsByIds: '📋 Đang lấy thông tin các chiến dịch...',
|
||||
generateCampaignId: '🔑 Đang tạo mã chiến dịch...',
|
||||
checkCampaignId: '✅ Đang kiểm tra mã chiến dịch...',
|
||||
createCampaign: '✨ Đang khởi tạo chiến dịch mới...',
|
||||
executeWorkflow: '⚡ Đang xử lý quy trình...',
|
||||
queryCustomerPoints: '💳 Đang tra cứu điểm thưởng khách hàng...',
|
||||
// Rule tools
|
||||
searchRules: '🔍 Đang tìm kiếm thể lệ...',
|
||||
countRules: '📊 Đang đếm thể lệ...',
|
||||
getRuleById: '📋 Đang lấy thông tin thể lệ...',
|
||||
getRulesByIds: '📋 Đang lấy thông tin các thể lệ...',
|
||||
generateRuleId: '🔑 Đang tạo mã thể lệ...',
|
||||
checkRuleId: '✅ Đang kiểm tra mã thể lệ...',
|
||||
createRule: '✨ Đang tạo thể lệ mới...',
|
||||
findMatchingRules: '🎯 Đang tìm thể lệ phù hợp...',
|
||||
// Pool tools
|
||||
searchPools: '🏦 Đang tìm kiếm pool điểm...',
|
||||
getPoolDefinitionById: '🏦 Đang lấy thông tin pool...',
|
||||
getPoolDefinitionsByIds: '🏦 Đang lấy thông tin các pool...',
|
||||
// Customer tools
|
||||
searchCustomers: '👤 Đang tìm kiếm khách hàng...',
|
||||
// Transaction tools
|
||||
searchTransactions: '💰 Đang tìm kiếm giao dịch...',
|
||||
// Reference data tools
|
||||
searchTransactionCodes: '🏷️ Đang tìm mã giao dịch...',
|
||||
searchCounterDefinitions: '📊 Đang tìm bộ đếm...',
|
||||
searchDeductionSequences: '📑 Đang tìm trình tự khấu trừ...',
|
||||
searchCatalogues: '🎁 Đang tìm danh mục...',
|
||||
searchAppParams: '⚙️ Đang tìm tham số hệ thống...',
|
||||
searchDynamicAttributes: '🔧 Đang tìm thuộc tính...',
|
||||
searchJourneys: '🗺️ Đang tìm hành trình...',
|
||||
}
|
||||
|
||||
export function ToolStatusIndicator({ toolName }: ToolStatusIndicatorProps) {
|
||||
|
||||
@@ -37,9 +37,17 @@ const ACTION_MATRIX = [
|
||||
},
|
||||
]
|
||||
|
||||
export function WelcomeScreen() {
|
||||
export function WelcomeScreen({ onSend }: { onSend?: (prompt: string) => void }) {
|
||||
const sendMessage = useChatStore((s) => s.sendMessage)
|
||||
|
||||
const handleAction = (prompt: string) => {
|
||||
if (onSend) {
|
||||
onSend(prompt)
|
||||
} else {
|
||||
sendMessage(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center max-w-3xl mx-auto animate-in fade-in duration-300">
|
||||
|
||||
@@ -57,7 +65,7 @@ export function WelcomeScreen() {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => sendMessage(item.prompt)}
|
||||
onClick={() => handleAction(item.prompt)}
|
||||
className="group cursor-pointer p-4 rounded-2xl bg-card border border-border/70 hover:border-primary/50 hover:shadow-md hover:bg-secondary/40 transition-all duration-200 flex flex-col justify-between min-h-[140px] relative overflow-hidden"
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -65,12 +65,16 @@ class StompService {
|
||||
onWebSocketClose: () => {
|
||||
console.warn('[StompService] WebSocket connection closed')
|
||||
useConnectionStore.getState().setStatus('disconnected')
|
||||
this.scheduleReconnect()
|
||||
if (this.client) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
},
|
||||
onStompError: (frame) => {
|
||||
console.error('[StompService] STOMP error frame received:', frame.headers['message'])
|
||||
useConnectionStore.getState().setStatus('error')
|
||||
this.scheduleReconnect()
|
||||
if (this.client) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
},
|
||||
onWebSocketError: (event) => {
|
||||
console.error('[StompService] WebSocket error:', event)
|
||||
@@ -105,13 +109,18 @@ class StompService {
|
||||
this.reconnectAttempt++
|
||||
console.log(`[StompService] Scheduling reconnect attempt #${this.reconnectAttempt} in ${delay}ms`)
|
||||
|
||||
this.reconnectTimeoutId = setTimeout(() => {
|
||||
this.reconnectTimeoutId = setTimeout(async () => {
|
||||
this.reconnectTimeoutId = null
|
||||
if (this.client) {
|
||||
this.client.activate()
|
||||
} else {
|
||||
this.connect()
|
||||
const oldClient = this.client
|
||||
this.client = null // Clear reference to prevent loops in onWebSocketClose
|
||||
try {
|
||||
await oldClient.deactivate()
|
||||
} catch (e) {
|
||||
console.error('[StompService] Error deactivating old client:', e)
|
||||
}
|
||||
}
|
||||
this.connect()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user