cherry-studio/src/renderer/src/components/HealthStatusIndicator/indicator.tsx
George·Dong 826414e821
feat(provider-settings): support viewing detailed error messages (#12423)
* feat(error): add clickable error detail modal for model detection

- Add onErrorClick callback to HealthStatusIndicator component
- Make error icon clickable to show detailed error modal
- Export ErrorDetailModal for reuse across components
- Use SerializedError type for better error information display
- Fix tooltip i18n keys and flex layout for consistent spacing

* fix(error): only show clickable error indicator when failed results exist

- Only pass onErrorClick callback when there are failed results
- Conditionally render ErrorDetailModal only when needed
- Improve performance by avoiding unnecessary state for success cases

* refactor(error): extract serializeHealthCheckError utility function

- Extract duplicated error serialization logic into serializeHealthCheckError()
  utility function in error.ts
- Update HealthCheckService.ts, hook.ts, and ProviderSetting.tsx to use
  the new utility function
- Fix invalid CSS values: align-items: left -> center, align: left -> flex-start
- Use HealthStatus.FAILED constant instead of hardcoded 'failed' string

* fix(settings): update tooltip message key for API check

Change the tooltip fallback translation key from the provider
API check to the models check. This ensures the displayed
message matches the new i18n key and avoids showing an
incorrect/provider-specific string when the API key
connectivity error message is absent.

* refactor(error): extract ErrorDetailModal as independent component

* refactor(error): extract ErrorDetailModal as independent component

* refactor(error): update ErrorDetailModal with full features from ErrorBlock

* refactor(error): fix export duplication and add React performance optimizations

* refactor(error): add React performance optimizations

- Memoize ErrorDetailModal child components (BuiltinError, AiSdkErrorBase, TruncatedCodeViewer, AiSdkError)
- Add useCallback for handleErrorClick in HealthStatusIndicator
- Add useCallback for copyErrorDetails in ErrorDetailModal
- Optimize ModelListItem button inline functions with useCallback

* refactor(health): move HealthStatusIndicatorProps back to indicator.tsx

- HealthStatusIndicatorProps was only used in indicator.tsx, no need to export
- Keep HealthResult in types.ts as it's used by useHealthStatus hook

* fix(health): import HealthResult from types instead of redefining

- useHealthStatus depends on HealthResult from ./types
- Only HealthStatusIndicatorProps belongs to indicator.tsx

* refactor(health): move click handler to outer wrapper

* fix(lint): resolve react-hooks/exhaustive-deps warnings in ModelListItem

- wrap healthResults in useMemo to avoid unnecessary re-renders
- remove redundant ErrorDetailModal export from ErrorBlock

* refactor(error): optimize ErrorDetailModal and replace duplicate code

- Use parseDataUrl from @shared/utils instead of manual base64 detection
- Replace duplicate ErrorDetailModal in ErrorBlock with the shared component
- Optimize onErrorClick with useMemo for better performance
2026-01-23 14:26:33 +08:00

106 lines
2.8 KiB
TypeScript

import { CheckCircleFilled, CloseCircleFilled, ExclamationCircleFilled, LoadingOutlined } from '@ant-design/icons'
import { HealthStatus } from '@renderer/types/healthCheck'
import { Flex, Tooltip, Typography } from 'antd'
import React, { memo, useCallback } from 'react'
import styled from 'styled-components'
import type { HealthResult } from './types'
import { useHealthStatus } from './useHealthStatus'
interface HealthStatusIndicatorProps {
results: HealthResult[]
loading?: boolean
showLatency?: boolean
onErrorClick?: (result: HealthResult) => void
}
const HealthStatusIndicator: React.FC<HealthStatusIndicatorProps> = ({
results,
loading = false,
showLatency = false,
onErrorClick
}) => {
const { overallStatus, tooltip, latencyText } = useHealthStatus({
results,
showLatency
})
const handleClick = useCallback(() => {
if (!onErrorClick) return
const failedResult = results.find((r) => r.status === HealthStatus.FAILED)
if (failedResult) {
onErrorClick(failedResult)
}
}, [onErrorClick, results])
if (loading) {
return (
<IndicatorWrapper $type="checking">
<LoadingOutlined spin />
</IndicatorWrapper>
)
}
if (overallStatus === 'not_checked') return null
const isClickable = onErrorClick && results.some((r) => r.status === HealthStatus.FAILED)
let icon: React.ReactNode = null
switch (overallStatus) {
case 'success':
icon = <CheckCircleFilled />
break
case 'error':
case 'partial': {
const IconComponent = overallStatus === 'error' ? CloseCircleFilled : ExclamationCircleFilled
icon = <IconComponent />
break
}
default:
return null
}
return (
<Flex align="center" gap={6}>
{latencyText && <LatencyText type="secondary">{latencyText}</LatencyText>}
<Tooltip title={tooltip} styles={{ body: { userSelect: 'text' } }}>
<IndicatorWrapper
$type={overallStatus}
$clickable={isClickable}
onClick={isClickable ? handleClick : undefined}>
{icon}
</IndicatorWrapper>
</Tooltip>
</Flex>
)
}
const IndicatorWrapper = styled.div<{ $type: string; $clickable?: boolean }>`
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'auto')};
color: ${({ $type }) => {
switch ($type) {
case 'success':
return 'var(--color-status-success)'
case 'error':
return 'var(--color-status-error)'
case 'partial':
return 'var(--color-status-warning)'
case 'checking':
default:
return 'var(--color-text)'
}
}};
`
const LatencyText = styled(Typography.Text)`
margin-left: 10px;
color: var(--color-text-secondary);
font-size: 12px;
`
export default memo(HealthStatusIndicator)