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:
2026-08-11 16:31:52 +07:00
parent 9a26c6ce2d
commit 8ecd27dd0e
188 changed files with 5672 additions and 983 deletions

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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 (

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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)
}