Skip to content

Commit 0483151

Browse files
committed
fix(ui): restore loading star orbit
1 parent bd9c8eb commit 0483151

2 files changed

Lines changed: 21 additions & 131 deletions

File tree

packages/web/components/common/MyLoading/ParticleLoading.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const config = {
88
durationMs: 3000,
99
rotationDurationMs: 16000,
1010
pulseDurationMs: 4600,
11+
strokeWidth: 8.33,
1112
orbitRadius: 7,
1213
detailAmplitude: 2.7,
1314
petalCount: 5,
@@ -46,6 +47,12 @@ const getRotation = (time: number) => {
4647
return -((time % config.rotationDurationMs) / config.rotationDurationMs) * 360;
4748
};
4849

50+
const buildPath = (detailScale: number, steps = 480) =>
51+
Array.from({ length: steps + 1 }, (_, index) => {
52+
const pathPoint = point(index / steps, detailScale);
53+
return `${index === 0 ? 'M' : 'L'} ${pathPoint.x.toFixed(2)} ${pathPoint.y.toFixed(2)}`;
54+
}).join(' ');
55+
4956
const getParticle = (index: number, progress: number, detailScale: number) => {
5057
const tailOffset = index / (config.particleCount - 1);
5158
const p = point(normalizeProgress(progress - tailOffset * config.trailSpan), detailScale);
@@ -68,6 +75,7 @@ const getParticleSize = (size: SpinnerProps['size']) => {
6875

6976
const ParticleLoading = ({ size = 'lg' }: { size?: SpinnerProps['size'] }) => {
7077
const groupRef = useRef<SVGGElement>(null);
78+
const pathRef = useRef<SVGPathElement>(null);
7179
const particleRefs = useRef<(SVGCircleElement | null)[]>([]);
7280
const boxSize = getParticleSize(size);
7381

@@ -85,6 +93,7 @@ const ParticleLoading = ({ size = 'lg' }: { size?: SpinnerProps['size'] }) => {
8593
if (groupRef.current) {
8694
groupRef.current.setAttribute('transform', `rotate(${getRotation(time)} 50 50)`);
8795
}
96+
pathRef.current?.setAttribute('d', buildPath(detailScale));
8897

8998
for (let i = 0; i < particleRefs.current.length; i++) {
9099
const node = particleRefs.current[i];
@@ -114,6 +123,14 @@ const ParticleLoading = ({ size = 'lg' }: { size?: SpinnerProps['size'] }) => {
114123
aria-hidden="true"
115124
>
116125
<g ref={groupRef}>
126+
<path
127+
ref={pathRef}
128+
stroke="#000000"
129+
strokeWidth={config.strokeWidth}
130+
strokeLinecap="round"
131+
strokeLinejoin="round"
132+
opacity={0.04}
133+
/>
117134
{indices.map((i) => (
118135
<circle
119136
key={i}

projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatLoading.tsx

Lines changed: 4 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,15 @@
1-
import React, { useEffect, useMemo, useRef } from 'react';
2-
import { Box, Flex, type BoxProps, type SpinnerProps } from '@chakra-ui/react';
1+
import React from 'react';
2+
import { Box, Flex, type SpinnerProps } from '@chakra-ui/react';
33
import { useTranslation } from 'next-i18next';
4-
5-
const config = {
6-
rotate: true,
7-
particleCount: 30,
8-
trailSpan: 0.38,
9-
durationMs: 3000,
10-
rotationDurationMs: 16000,
11-
pulseDurationMs: 4600,
12-
orbitRadius: 7,
13-
detailAmplitude: 2.7,
14-
petalCount: 5,
15-
curveScale: 3.9
16-
};
17-
18-
const TWO_PI = Math.PI * 2;
19-
const PETAL_K = Math.round(config.petalCount);
20-
const particleSizeMap: Record<string, BoxProps['boxSize']> = {
21-
xs: '24px',
22-
sm: '32px',
23-
md: '48px',
24-
lg: '64px',
25-
xl: '80px'
26-
};
27-
28-
const point = (progress: number, detailScale: number) => {
29-
const t = progress * TWO_PI;
30-
const r = config.orbitRadius - config.detailAmplitude * detailScale * Math.cos(PETAL_K * t);
31-
return {
32-
x: 50 + Math.cos(t) * r * config.curveScale,
33-
y: 50 + Math.sin(t) * r * config.curveScale
34-
};
35-
};
36-
37-
const normalizeProgress = (progress: number) => ((progress % 1) + 1) % 1;
38-
39-
const getDetailScale = (time: number) => {
40-
const pulseProgress = (time % config.pulseDurationMs) / config.pulseDurationMs;
41-
const pulseAngle = pulseProgress * TWO_PI;
42-
return 0.52 + ((Math.sin(pulseAngle + 0.55) + 1) / 2) * 0.48;
43-
};
44-
45-
const getRotation = (time: number) => {
46-
if (!config.rotate) return 0;
47-
return -((time % config.rotationDurationMs) / config.rotationDurationMs) * 360;
48-
};
49-
50-
const getParticle = (index: number, progress: number, detailScale: number) => {
51-
const tailOffset = index / (config.particleCount - 1);
52-
const p = point(normalizeProgress(progress - tailOffset * config.trailSpan), detailScale);
53-
const fade = Math.pow(1 - tailOffset, 0.56);
54-
return {
55-
x: p.x,
56-
y: p.y,
57-
radius: 0.6 + fade * 3.57,
58-
opacity: 0.04 + fade * 0.96
59-
};
60-
};
61-
62-
const getParticleSize = (size: SpinnerProps['size']) => {
63-
if (typeof size === 'string' && particleSizeMap[size]) {
64-
return particleSizeMap[size];
65-
}
66-
67-
return size || particleSizeMap.lg;
68-
};
69-
70-
const AIChatParticleLoading = ({ size = 'lg' }: { size?: SpinnerProps['size'] }) => {
71-
const groupRef = useRef<SVGGElement>(null);
72-
const particleRefs = useRef<(SVGCircleElement | null)[]>([]);
73-
const boxSize = getParticleSize(size);
74-
75-
const indices = useMemo(() => Array.from({ length: config.particleCount }, (_, i) => i), []);
76-
77-
useEffect(() => {
78-
const startedAt = performance.now();
79-
let rafId = 0;
80-
81-
const render = (now: number) => {
82-
const time = now - startedAt;
83-
const progress = (time % config.durationMs) / config.durationMs;
84-
const detailScale = getDetailScale(time);
85-
86-
if (groupRef.current) {
87-
groupRef.current.setAttribute('transform', `rotate(${getRotation(time)} 50 50)`);
88-
}
89-
90-
for (let i = 0; i < particleRefs.current.length; i++) {
91-
const node = particleRefs.current[i];
92-
if (!node) continue;
93-
94-
const particle = getParticle(i, progress, detailScale);
95-
node.setAttribute('cx', particle.x.toFixed(2));
96-
node.setAttribute('cy', particle.y.toFixed(2));
97-
node.setAttribute('r', particle.radius.toFixed(2));
98-
node.setAttribute('opacity', particle.opacity.toFixed(3));
99-
}
100-
101-
rafId = requestAnimationFrame(render);
102-
};
103-
104-
rafId = requestAnimationFrame(render);
105-
return () => cancelAnimationFrame(rafId);
106-
}, []);
107-
108-
return (
109-
<Box boxSize={boxSize} position="relative" flexShrink={0} display="grid" placeItems="center">
110-
<Box
111-
as="svg"
112-
viewBox="8 8 84 84"
113-
fill="none"
114-
sx={{ width: '100%', height: '100%', overflow: 'visible' }}
115-
aria-hidden="true"
116-
>
117-
<g ref={groupRef}>
118-
{indices.map((i) => (
119-
<circle
120-
key={i}
121-
ref={(el) => {
122-
particleRefs.current[i] = el;
123-
}}
124-
fill="#3370FF"
125-
/>
126-
))}
127-
</g>
128-
</Box>
129-
</Box>
130-
);
131-
};
4+
import ParticleLoading from '@fastgpt/web/components/common/MyLoading/ParticleLoading';
1325

1336
const AIChatLoading = ({ text, size = 'lg' }: { text?: string; size?: SpinnerProps['size'] }) => {
1347
const { t } = useTranslation();
1358
const loadingText = text ?? t('chat:ai_chat_loading');
1369

13710
return (
13811
<Flex alignItems={'center'} justifyContent={'flex-start'} gap={2}>
139-
<AIChatParticleLoading size={size} />
12+
<ParticleLoading size={size} />
14013
{loadingText && (
14114
<Box color={'myGray.600'} fontSize={'16px'} lineHeight={'24px'} fontWeight={'normal'}>
14215
{loadingText}

0 commit comments

Comments
 (0)