diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx
index aecfa2b97..ca46bb4b5 100644
--- a/components/canvas/CanvasChatPanel.tsx
+++ b/components/canvas/CanvasChatPanel.tsx
@@ -16382,8 +16382,20 @@ export function CanvasChatPanel({
>
) : (
- <>
-
+ {isClawAssistantMessage ? (
+
+ ) : null}
+
- >
+
)}
diff --git a/components/generation-dot-placeholder.tsx b/components/generation-dot-placeholder.tsx
index ad6dacad6..c5c416488 100644
--- a/components/generation-dot-placeholder.tsx
+++ b/components/generation-dot-placeholder.tsx
@@ -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(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 ;
+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 (
-
+
+ {resolvedTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {resolvedTitle}
-
-
+ className="ml-1 inline-block h-[1em] w-[0.08em] translate-y-[0.15em] animate-[generationCursorBlink_1s_steps(1,end)_infinite] bg-current"
+ />
-
+
+
+
+
+
{showProgress ? (
-
+
{phase === "failed" ? "已停止" : "生成进度"}
{resolvedProgress}%
-
+
@@ -523,11 +317,7 @@ export function GenerationDotPlaceholder({
) : null}
-
+
);