Merge remote-tracking branch 'origin/codex/bbyy-production-156-20260820' into codex/deploy-48-bloub-20260902
This commit is contained in:
commit
e0501bbbd1
@ -16382,8 +16382,20 @@ export function CanvasChatPanel({
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
<div className="flex w-full min-w-0 items-start gap-3">
|
||||
{isClawAssistantMessage ? (
|
||||
<BloubAvatar
|
||||
animated={
|
||||
index === renderableVisibleMessages.length - 1
|
||||
}
|
||||
className="mt-0.5"
|
||||
label="Claw 助手"
|
||||
size={32}
|
||||
state="idle"
|
||||
/>
|
||||
) : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 max-w-full text-[14px] text-[#2f3640] dark:text-zinc-300",
|
||||
isImageOnlyAssistant
|
||||
@ -16788,8 +16800,9 @@ export function CanvasChatPanel({
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BloubAvatar } from "@/components/bloub-avatar";
|
||||
import type { StateId } from "@/lib/bloub-official/states";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type GenerationPhase = "idle" | "queued" | "processing" | "finalizing" | "failed" | "completed";
|
||||
type GenerationPhase =
|
||||
| "idle"
|
||||
| "queued"
|
||||
| "processing"
|
||||
| "finalizing"
|
||||
| "failed"
|
||||
| "completed";
|
||||
|
||||
interface GenerationDotPlaceholderProps {
|
||||
className?: string;
|
||||
title?: string;
|
||||
compact?: boolean;
|
||||
phase?: GenerationPhase;
|
||||
progress?: number | null;
|
||||
compact?: boolean;
|
||||
showProgress?: boolean;
|
||||
title?: string;
|
||||
viewportScale?: number;
|
||||
}
|
||||
|
||||
@ -101,6 +109,7 @@ const GENERATION_STATUS_TITLES = [
|
||||
"正在完成本次生成",
|
||||
"正在即将展示结果",
|
||||
];
|
||||
|
||||
const LONG_WAIT_TITLE_THRESHOLD_MS = 5 * 60 * 1000;
|
||||
const LONG_WAIT_STATUS_TITLES = [
|
||||
"服务繁忙,排队处理中",
|
||||
@ -108,7 +117,6 @@ const LONG_WAIT_STATUS_TITLES = [
|
||||
"仍在保持连接,等待模型返回",
|
||||
"结果返回后会自动展示",
|
||||
];
|
||||
|
||||
const STATUS_TITLE_DURATIONS_MS = GENERATION_STATUS_TITLES.map((_, index) => {
|
||||
const rhythm = [3600, 5200, 4100, 6100, 3300, 4700, 5700, 3900, 6800, 4400];
|
||||
return rhythm[index % rhythm.length];
|
||||
@ -116,147 +124,13 @@ const STATUS_TITLE_DURATIONS_MS = GENERATION_STATUS_TITLES.map((_, index) => {
|
||||
const TYPEWRITER_START_DELAY_MS = 120;
|
||||
const TYPEWRITER_BASE_STEP_MS = 46;
|
||||
|
||||
function getStatusTitleState(elapsedMs: number, initialTitle?: string) {
|
||||
const titles = initialTitle
|
||||
? [initialTitle, ...GENERATION_STATUS_TITLES.filter((item) => item !== initialTitle)]
|
||||
: GENERATION_STATUS_TITLES;
|
||||
const durations = initialTitle
|
||||
? [4200, ...STATUS_TITLE_DURATIONS_MS]
|
||||
: STATUS_TITLE_DURATIONS_MS;
|
||||
|
||||
let cursor = Math.max(0, elapsedMs);
|
||||
for (let index = 0; index < titles.length; index += 1) {
|
||||
const duration = durations[index] || 4600;
|
||||
if (cursor < duration) {
|
||||
return {
|
||||
index,
|
||||
title: titles[index] || titles[titles.length - 1] || PHASE_TITLES.processing,
|
||||
elapsedInTitle: cursor,
|
||||
};
|
||||
}
|
||||
cursor -= duration;
|
||||
}
|
||||
|
||||
return {
|
||||
index: titles.length - 1,
|
||||
title: titles[titles.length - 1] || PHASE_TITLES.processing,
|
||||
elapsedInTitle: durations[durations.length - 1] || 4600,
|
||||
};
|
||||
}
|
||||
|
||||
function getLongWaitTitleState(elapsedMs: number, initialTitle?: string) {
|
||||
const titles = initialTitle
|
||||
? [initialTitle, ...LONG_WAIT_STATUS_TITLES.filter((item) => item !== initialTitle)]
|
||||
: LONG_WAIT_STATUS_TITLES;
|
||||
const elapsedAfterThreshold = Math.max(0, elapsedMs - LONG_WAIT_TITLE_THRESHOLD_MS);
|
||||
const duration = 7600;
|
||||
const index = Math.floor(elapsedAfterThreshold / duration) % titles.length;
|
||||
|
||||
return {
|
||||
index,
|
||||
title: titles[index] || titles[0] || PHASE_TITLES.processing,
|
||||
elapsedInTitle: elapsedAfterThreshold % duration,
|
||||
};
|
||||
}
|
||||
|
||||
function getTypewriterText(text: string, elapsedInTitle: number, index: number) {
|
||||
const chars = Array.from(text);
|
||||
if (chars.length === 0) return "";
|
||||
const step = TYPEWRITER_BASE_STEP_MS + (index % 5) * 8;
|
||||
const visibleCount = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
chars.length,
|
||||
Math.floor(Math.max(0, elapsedInTitle - TYPEWRITER_START_DELAY_MS) / step) + 1
|
||||
)
|
||||
);
|
||||
return chars.slice(0, visibleCount).join("");
|
||||
}
|
||||
|
||||
const scatterVertexShader = `
|
||||
uniform float uTime;
|
||||
uniform float uPointSize;
|
||||
uniform float uEffectIntensity;
|
||||
attribute vec3 randomOffset;
|
||||
varying float vDistance;
|
||||
|
||||
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
||||
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
||||
vec3 permute(vec3 x) { return mod289(((x * 34.0) + 1.0) * x); }
|
||||
float snoise(vec2 v) {
|
||||
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
|
||||
vec2 i = floor(v + dot(v, C.yy));
|
||||
vec2 x0 = v - i + dot(i, C.xx);
|
||||
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
||||
vec4 x12 = x0.xyxy + C.xxzz;
|
||||
x12.xy -= i1;
|
||||
i = mod289(i);
|
||||
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
||||
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
||||
m = m * m;
|
||||
m = m * m;
|
||||
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
||||
vec3 h = abs(x) - 0.5;
|
||||
vec3 ox = floor(x + 0.5);
|
||||
vec3 a0 = x - ox;
|
||||
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
||||
vec3 g;
|
||||
g.x = a0.x * x0.x + h.x * x0.y;
|
||||
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
||||
return 130.0 * dot(m, g);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 pos = position;
|
||||
vec3 scatterDir = normalize(pos + randomOffset * 0.5);
|
||||
float scatterDist = length(pos) * 0.5 + randomOffset.x * 3.0;
|
||||
vec3 scattered = pos + scatterDir * scatterDist * uEffectIntensity * 1.85;
|
||||
float turb = snoise(pos.xy * 0.34 + uTime * 0.45);
|
||||
scattered += vec3(turb, turb * 0.55, turb * 0.35) * uEffectIntensity * 1.2;
|
||||
pos = mix(pos, scattered, 0.72);
|
||||
|
||||
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
|
||||
float dist = length(pos);
|
||||
vDistance = dist;
|
||||
gl_PointSize = (uPointSize / -mvPosition.z) * (1.0 + sin(uTime * 3.0 + dist * 0.16) * 0.42);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
const scatterFragmentShader = `
|
||||
uniform float uTime;
|
||||
uniform float uOpacity;
|
||||
varying float vDistance;
|
||||
|
||||
void main() {
|
||||
float dist = distance(gl_PointCoord, vec2(0.5));
|
||||
if (dist > 0.5) discard;
|
||||
float edge = 1.0 - smoothstep(0.42, 0.5, dist);
|
||||
float core = 0.92 + (1.0 - smoothstep(0.0, 0.34, dist)) * 0.08;
|
||||
float shimmer = 0.78 + sin(vDistance * 0.33 + uTime) * 0.18;
|
||||
gl_FragColor = vec4(vec3(0.11, 0.12, 0.13), edge * core * shimmer * uOpacity);
|
||||
}
|
||||
`;
|
||||
|
||||
function clampProgress(value: number) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
}
|
||||
|
||||
function normalizeViewportScale(value: number | undefined) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(0.1, Math.min(4, value));
|
||||
}
|
||||
|
||||
function getParticlePointSize(compact: boolean, viewportScale: number) {
|
||||
return (compact ? 76 : 94) * viewportScale;
|
||||
}
|
||||
|
||||
function estimateProgress(phase: GenerationPhase, elapsedMs: number) {
|
||||
if (phase === "completed") return 100;
|
||||
if (phase === "failed") return 100;
|
||||
if (phase === "completed" || phase === "failed") return 100;
|
||||
if (phase === "finalizing") return 96;
|
||||
if (phase === "queued") {
|
||||
return clampProgress(6 + Math.min(12, elapsedMs / 3500));
|
||||
@ -268,165 +142,93 @@ function estimateProgress(phase: GenerationPhase, elapsedMs: number) {
|
||||
return clampProgress(Math.min(8, elapsedMs / 1000));
|
||||
}
|
||||
|
||||
function ScatterParticleLayer({
|
||||
compact,
|
||||
opacity,
|
||||
viewportScale,
|
||||
}: {
|
||||
compact: boolean;
|
||||
opacity: number;
|
||||
viewportScale: number;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const pointSizeUniformRef = useRef<{ value: number } | null>(null);
|
||||
const viewportScaleRef = useRef(viewportScale);
|
||||
function getStatusTitleState(elapsedMs: number, initialTitle?: string) {
|
||||
const titles = initialTitle
|
||||
? [
|
||||
initialTitle,
|
||||
...GENERATION_STATUS_TITLES.filter((item) => item !== initialTitle),
|
||||
]
|
||||
: GENERATION_STATUS_TITLES;
|
||||
const durations = initialTitle
|
||||
? [4200, ...STATUS_TITLE_DURATIONS_MS]
|
||||
: STATUS_TITLE_DURATIONS_MS;
|
||||
|
||||
useEffect(() => {
|
||||
viewportScaleRef.current = viewportScale;
|
||||
if (pointSizeUniformRef.current) {
|
||||
pointSizeUniformRef.current.value = getParticlePointSize(
|
||||
compact,
|
||||
viewportScale
|
||||
);
|
||||
}
|
||||
}, [compact, viewportScale]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let cleanup = () => {};
|
||||
|
||||
async function setup() {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const THREE = await import("three");
|
||||
if (cancelled || !containerRef.current) return;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 1000);
|
||||
camera.position.set(0, 0, compact ? 48 : 44);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
renderer.setClearColor(0xffffff, 0);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2.5));
|
||||
renderer.domElement.style.display = "block";
|
||||
renderer.domElement.style.height = "100%";
|
||||
renderer.domElement.style.width = "100%";
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
const particleCount = compact ? 7200 : 12800;
|
||||
const positions = new Float32Array(particleCount * 3);
|
||||
const randomOffsets = new Float32Array(particleCount * 3);
|
||||
|
||||
for (let i = 0; i < particleCount; i += 1) {
|
||||
const i3 = i * 3;
|
||||
const t = (Math.random() - 0.5) * 4.7;
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const radiusBase = 0.35 + Math.pow(Math.abs(t), 2.18);
|
||||
const radius = radiusBase * (0.74 + Math.random() * 0.58);
|
||||
positions[i3] = Math.cos(angle) * radius * 2.25;
|
||||
positions[i3 + 1] = t * (compact ? 5.2 : 6.8);
|
||||
positions[i3 + 2] = Math.sin(angle) * radius * 2.25;
|
||||
randomOffsets[i3] = (Math.random() - 0.5) * 2;
|
||||
randomOffsets[i3 + 1] = (Math.random() - 0.5) * 2;
|
||||
randomOffsets[i3 + 2] = (Math.random() - 0.5) * 2;
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("randomOffset", new THREE.BufferAttribute(randomOffsets, 3));
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: scatterVertexShader,
|
||||
fragmentShader: scatterFragmentShader,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.NormalBlending,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uPointSize: {
|
||||
value: getParticlePointSize(compact, viewportScaleRef.current),
|
||||
},
|
||||
uEffectIntensity: { value: 1 },
|
||||
uOpacity: { value: opacity },
|
||||
},
|
||||
});
|
||||
pointSizeUniformRef.current = material.uniforms.uPointSize as {
|
||||
value: number;
|
||||
};
|
||||
|
||||
const points = new THREE.Points(geometry, material);
|
||||
points.rotation.x = -0.06;
|
||||
const baseX = 0;
|
||||
const baseY = 0;
|
||||
points.position.set(baseX, baseY, 0);
|
||||
scene.add(points);
|
||||
|
||||
const resize = () => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const width = Math.max(1, Math.floor(rect.width));
|
||||
const height = Math.max(1, Math.floor(rect.height));
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
resize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(resize);
|
||||
resizeObserver.observe(container);
|
||||
|
||||
let frameId = 0;
|
||||
let time = 0;
|
||||
const animate = () => {
|
||||
time += 0.008;
|
||||
points.rotation.y += 0.0048;
|
||||
points.rotation.z += 0.0015;
|
||||
points.rotation.x = -0.08 + Math.sin(time * 0.17) * 0.18;
|
||||
points.position.x = baseX + Math.sin(time * 0.16) * 0.04;
|
||||
points.position.y = baseY + Math.sin(time * 0.22) * 0.04;
|
||||
material.uniforms.uTime.value = time;
|
||||
renderer.render(scene, camera);
|
||||
frameId = window.requestAnimationFrame(animate);
|
||||
};
|
||||
animate();
|
||||
|
||||
cleanup = () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
resizeObserver.disconnect();
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
renderer.dispose();
|
||||
renderer.domElement.remove();
|
||||
pointSizeUniformRef.current = null;
|
||||
let cursor = Math.max(0, elapsedMs);
|
||||
for (let index = 0; index < titles.length; index += 1) {
|
||||
const duration = durations[index] || 4600;
|
||||
if (cursor < duration) {
|
||||
return {
|
||||
elapsedInTitle: cursor,
|
||||
index,
|
||||
title:
|
||||
titles[index] ||
|
||||
titles[titles.length - 1] ||
|
||||
PHASE_TITLES.processing,
|
||||
};
|
||||
}
|
||||
cursor -= duration;
|
||||
}
|
||||
|
||||
setup();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cleanup();
|
||||
};
|
||||
}, [compact, opacity]);
|
||||
return {
|
||||
elapsedInTitle: durations[durations.length - 1] || 4600,
|
||||
index: titles.length - 1,
|
||||
title: titles[titles.length - 1] || PHASE_TITLES.processing,
|
||||
};
|
||||
}
|
||||
|
||||
return <div ref={containerRef} className="absolute inset-0" />;
|
||||
function getLongWaitTitleState(elapsedMs: number, initialTitle?: string) {
|
||||
const titles = initialTitle
|
||||
? [
|
||||
initialTitle,
|
||||
...LONG_WAIT_STATUS_TITLES.filter((item) => item !== initialTitle),
|
||||
]
|
||||
: LONG_WAIT_STATUS_TITLES;
|
||||
const elapsedAfterThreshold = Math.max(
|
||||
0,
|
||||
elapsedMs - LONG_WAIT_TITLE_THRESHOLD_MS
|
||||
);
|
||||
const duration = 7600;
|
||||
const index = Math.floor(elapsedAfterThreshold / duration) % titles.length;
|
||||
|
||||
return {
|
||||
elapsedInTitle: elapsedAfterThreshold % duration,
|
||||
index,
|
||||
title: titles[index] || titles[0] || PHASE_TITLES.processing,
|
||||
};
|
||||
}
|
||||
|
||||
function getTypewriterText(text: string, elapsedInTitle: number, index: number) {
|
||||
const chars = Array.from(text);
|
||||
if (chars.length === 0) return "";
|
||||
const step = TYPEWRITER_BASE_STEP_MS + (index % 5) * 8;
|
||||
const visibleCount = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
chars.length,
|
||||
Math.floor(
|
||||
Math.max(0, elapsedInTitle - TYPEWRITER_START_DELAY_MS) / step
|
||||
) + 1
|
||||
)
|
||||
);
|
||||
return chars.slice(0, visibleCount).join("");
|
||||
}
|
||||
|
||||
function getBloubState(phase: GenerationPhase): StateId {
|
||||
if (phase === "failed") return "alert";
|
||||
if (phase === "completed") return "wink";
|
||||
if (phase === "queued") return "thinking";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
export function GenerationDotPlaceholder({
|
||||
className,
|
||||
title,
|
||||
compact = false,
|
||||
phase = "processing",
|
||||
progress,
|
||||
compact = false,
|
||||
showProgress = true,
|
||||
viewportScale,
|
||||
title,
|
||||
}: GenerationDotPlaceholderProps) {
|
||||
const [elapsedMs, setElapsedMs] = useState(0);
|
||||
const resolvedViewportScale = normalizeViewportScale(viewportScale);
|
||||
|
||||
useEffect(() => {
|
||||
const startedAt = Date.now();
|
||||
@ -451,9 +253,10 @@ export function GenerationDotPlaceholder({
|
||||
statusTitleState.index
|
||||
);
|
||||
const resolvedTitle =
|
||||
phase === "processing" || phase === "finalizing" ? rotatingTitle : title || PHASE_TITLES[phase];
|
||||
const dotOpacity = phase === "failed" ? 0.18 : 0.56;
|
||||
|
||||
phase === "processing" || phase === "finalizing"
|
||||
? rotatingTitle
|
||||
: title || PHASE_TITLES[phase];
|
||||
const playSequence = phase === "processing" || phase === "finalizing";
|
||||
const progressStyle = useMemo(
|
||||
() => ({ width: `${resolvedProgress}%` }),
|
||||
[resolvedProgress]
|
||||
@ -462,60 +265,51 @@ export function GenerationDotPlaceholder({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative isolate overflow-hidden rounded-[32px] bg-[#f4f4f4] text-[#565b63]",
|
||||
"relative isolate overflow-hidden rounded-[24px] bg-[#f4f4f4] text-[#565b63] dark:bg-[#08090b] dark:text-zinc-200",
|
||||
compact ? "min-h-[190px] p-5" : "min-h-[360px] p-8",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative z-10 flex h-full min-h-[inherit] flex-col">
|
||||
<div className="flex h-full min-h-[inherit] flex-col">
|
||||
<div
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
"flex items-center gap-2 font-semibold tracking-tight",
|
||||
compact ? "text-[12px] leading-[1.35]" : "text-[18px] leading-[1.35]"
|
||||
"font-semibold",
|
||||
compact
|
||||
? "text-[12px] leading-[1.35]"
|
||||
: "text-[18px] leading-[1.35]"
|
||||
)}
|
||||
>
|
||||
{resolvedTitle}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"relative inline-flex shrink-0 items-center justify-center text-[#5b6068]",
|
||||
compact ? "h-5 w-5" : "h-7 w-7"
|
||||
)}
|
||||
>
|
||||
<span className="absolute inset-[18%] rounded-full bg-current opacity-15 blur-[3px]" />
|
||||
<span className="absolute left-1/2 top-1/2 h-[18%] w-[18%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-current opacity-80" />
|
||||
<span className="absolute inset-0 animate-[generationSparkSpin_3.8s_linear_infinite]">
|
||||
<span className="absolute left-1/2 top-[4%] h-[28%] w-[10%] -translate-x-1/2 rounded-full bg-current opacity-80" />
|
||||
<span className="absolute bottom-[4%] left-1/2 h-[28%] w-[10%] -translate-x-1/2 rounded-full bg-current opacity-55" />
|
||||
<span className="absolute left-[4%] top-1/2 h-[10%] w-[28%] -translate-y-1/2 rounded-full bg-current opacity-65" />
|
||||
<span className="absolute right-[4%] top-1/2 h-[10%] w-[28%] -translate-y-1/2 rounded-full bg-current opacity-70" />
|
||||
</span>
|
||||
<span className="absolute inset-[8%] animate-[generationSparkPulse_1.9s_ease-in-out_infinite]">
|
||||
<span className="absolute left-[16%] top-[16%] h-[9%] w-[30%] rotate-45 rounded-full bg-current opacity-55" />
|
||||
<span className="absolute right-[16%] top-[16%] h-[9%] w-[30%] -rotate-45 rounded-full bg-current opacity-65" />
|
||||
<span className="absolute bottom-[16%] left-[16%] h-[9%] w-[30%] -rotate-45 rounded-full bg-current opacity-60" />
|
||||
<span className="absolute bottom-[16%] right-[16%] h-[9%] w-[30%] rotate-45 rounded-full bg-current opacity-50" />
|
||||
</span>
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
{resolvedTitle}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-1 inline-block h-[1em] w-[0.08em] translate-y-[0.15em] animate-[generationCursorBlink_1s_steps(1,end)_infinite] bg-current"
|
||||
/>
|
||||
</span>
|
||||
className="ml-1 inline-block h-[1em] w-[0.08em] translate-y-[0.15em] animate-[generationCursorBlink_1s_steps(1,end)_infinite] bg-current"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center py-3">
|
||||
<BloubAvatar
|
||||
label={resolvedTitle}
|
||||
playSequence={playSequence}
|
||||
size={compact ? 88 : 120}
|
||||
state={getBloubState(phase)}
|
||||
surface="page"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showProgress ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-[12px] font-medium text-[#7d838b]">
|
||||
<div className="flex items-center justify-between text-[12px] font-medium text-[#7d838b] dark:text-zinc-400">
|
||||
<span>{phase === "failed" ? "已停止" : "生成进度"}</span>
|
||||
<span>{resolvedProgress}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-white/80 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.035)]">
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-white/80 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.035)] dark:bg-white/10 dark:shadow-none">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-[width] duration-700 ease-out",
|
||||
phase === "failed" ? "bg-[#c7ccd3]" : "bg-[#9fa4aa]"
|
||||
phase === "failed"
|
||||
? "bg-[#c7ccd3] dark:bg-zinc-600"
|
||||
: "bg-[#0a0a0c] dark:bg-[#3ecf8e]"
|
||||
)}
|
||||
style={progressStyle}
|
||||
/>
|
||||
@ -523,11 +317,7 @@ export function GenerationDotPlaceholder({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<ScatterParticleLayer
|
||||
compact={compact}
|
||||
opacity={dotOpacity}
|
||||
viewportScale={resolvedViewportScale}
|
||||
/>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes generationCursorBlink {
|
||||
0%,
|
||||
@ -539,33 +329,6 @@ export function GenerationDotPlaceholder({
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes generationSparkSpin {
|
||||
0% {
|
||||
transform: rotate(0deg) scale(0.9);
|
||||
opacity: 0.55;
|
||||
}
|
||||
45% {
|
||||
transform: rotate(155deg) scale(1.06);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.9);
|
||||
opacity: 0.58;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes generationSparkPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg) scale(0.72);
|
||||
opacity: 0.35;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-18deg) scale(1.08);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user