Files
mai-bot/dashboard/src/routes/config/model/hooks/useModelTour.ts

192 lines
5.8 KiB
TypeScript

/**
* 模型配置页面 Tour 引导 Hook
*/
import { useCallback, useEffect, useRef } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useTour } from '@/components/tour'
import { MODEL_ASSIGNMENT_TOUR_ID, STEP_ROUTE_MAP, modelAssignmentTourSteps } from '@/components/tour/tours/model-assignment-tour'
interface UseModelTourOptions {
/** 打开模型编辑对话框回调 */
onOpenEditDialog?: () => void
/** 关闭模型编辑对话框回调 */
onCloseEditDialog?: () => void
/** 打开提供商编辑对话框回调 */
onOpenProviderDialog?: () => void
/** 关闭提供商编辑对话框回调 */
onCloseProviderDialog?: () => void
/** 切换到模型厂商设置标签页 */
onOpenProvidersTab?: () => void
/** 切换到添加模型标签页 */
onOpenModelsTab?: () => void
/** 切换到模型分配标签页 */
onOpenTasksTab?: () => void
}
interface UseModelTourReturn {
/** 开始引导 */
startTour: () => void
/** Tour 是否正在运行 */
isRunning: boolean
/** 当前步骤索引 */
stepIndex: number
}
export function useModelTour(options: UseModelTourOptions = {}): UseModelTourReturn {
const {
onOpenEditDialog,
onCloseEditDialog,
onOpenProviderDialog,
onCloseProviderDialog,
onOpenProvidersTab,
onOpenModelsTab,
onOpenTasksTab,
} = options
const navigate = useNavigate()
const { registerTour, startTour: startTourFn, state: tourState, goToStep } = useTour()
const prevTourStepRef = useRef(tourState.stepIndex)
const didClickTourTarget = useCallback((event: MouseEvent, selector: string) => {
const target = event.target instanceof Element ? event.target : null
if (target?.closest(selector)) {
return true
}
const element = document.querySelector(selector)
if (!element) {
return false
}
const rect = element.getBoundingClientRect()
return (
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom
)
}, [])
useEffect(() => {
registerTour(MODEL_ASSIGNMENT_TOUR_ID, modelAssignmentTourSteps)
}, [registerTour])
useEffect(() => {
if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) {
return
}
const targetRoute = STEP_ROUTE_MAP[tourState.stepIndex]
if (targetRoute && window.location.pathname !== targetRoute) {
navigate({ to: targetRoute })
}
}, [tourState.stepIndex, tourState.activeTourId, tourState.isRunning, navigate])
useEffect(() => {
if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) {
return
}
const prevStep = prevTourStepRef.current
const currentStep = tourState.stepIndex
if (currentStep <= 2) {
onOpenProvidersTab?.()
}
if (prevStep >= 3 && prevStep <= 9 && currentStep < 3) {
onCloseProviderDialog?.()
}
if (prevStep <= 2 && currentStep >= 3 && currentStep <= 9) {
onOpenProviderDialog?.()
}
if (currentStep === 10 || currentStep === 11) {
onCloseProviderDialog?.()
onOpenModelsTab?.()
}
if (prevStep >= 12 && prevStep <= 17 && currentStep < 12) {
onCloseEditDialog?.()
}
if (prevStep <= 11 && currentStep >= 12 && currentStep <= 17) {
onOpenEditDialog?.()
}
if (currentStep === 19) {
onOpenTasksTab?.()
}
prevTourStepRef.current = currentStep
}, [
tourState.stepIndex,
tourState.activeTourId,
tourState.isRunning,
onOpenEditDialog,
onCloseEditDialog,
onOpenProviderDialog,
onCloseProviderDialog,
onOpenProvidersTab,
onOpenModelsTab,
onOpenTasksTab,
])
useEffect(() => {
if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) return
const handleTourClick = (event: MouseEvent) => {
const currentStep = tourState.stepIndex
if (currentStep === 1 && didClickTourTarget(event, '[data-tour="providers-tab-trigger"]')) {
onOpenProvidersTab?.()
setTimeout(() => goToStep(2), 300)
} else if (currentStep === 2 && didClickTourTarget(event, '[data-tour="add-provider-button"]')) {
onOpenProviderDialog?.()
setTimeout(() => goToStep(3), 300)
} else if (currentStep === 9 && didClickTourTarget(event, '[data-tour="provider-cancel-button"]')) {
onCloseProviderDialog?.()
setTimeout(() => goToStep(10), 300)
} else if (currentStep === 10 && didClickTourTarget(event, '[data-tour="models-tab-trigger"]')) {
onOpenModelsTab?.()
setTimeout(() => goToStep(11), 300)
} else if (currentStep === 11 && didClickTourTarget(event, '[data-tour="add-model-button"]')) {
onOpenEditDialog?.()
setTimeout(() => goToStep(12), 300)
} else if (currentStep === 17 && didClickTourTarget(event, '[data-tour="model-cancel-button"]')) {
onCloseEditDialog?.()
setTimeout(() => goToStep(18), 300)
} else if (currentStep === 18 && didClickTourTarget(event, '[data-tour="tasks-tab-trigger"]')) {
onOpenTasksTab?.()
setTimeout(() => goToStep(19), 300)
}
}
document.addEventListener('click', handleTourClick, true)
return () => document.removeEventListener('click', handleTourClick, true)
}, [
tourState,
goToStep,
onOpenEditDialog,
onCloseEditDialog,
onOpenProviderDialog,
onCloseProviderDialog,
onOpenProvidersTab,
onOpenModelsTab,
onOpenTasksTab,
didClickTourTarget,
])
const handleStartTour = useCallback(() => {
onOpenProvidersTab?.()
startTourFn(MODEL_ASSIGNMENT_TOUR_ID)
}, [startTourFn, onOpenProvidersTab])
return {
startTour: handleStartTour,
isRunning: tourState.isRunning && tourState.activeTourId === MODEL_ASSIGNMENT_TOUR_ID,
stepIndex: tourState.stepIndex,
}
}