feat: initialize TJWater WebGIS frontend

This commit is contained in:
2026-07-21 18:31:08 +08:00
commit ad5e5d3133
191 changed files with 37963 additions and 0 deletions
@@ -0,0 +1,75 @@
import { Bot } from "lucide-react";
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
import type { PersonaState } from "@/shared/ai-elements/persona";
import { cn } from "@/lib/utils";
const LazyPersona = lazy(async () => {
const module = await import("@/shared/ai-elements/persona");
return { default: module.Persona };
});
type AgentPersonaProps = {
className?: string;
state?: PersonaState;
};
function useElementVisibility() {
const containerRef = useRef<HTMLSpanElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let intersectsViewport = false;
const updateVisibility = () => {
setVisible(intersectsViewport && document.visibilityState === "visible");
};
const observer = new IntersectionObserver(([entry]) => {
intersectsViewport = Boolean(entry?.isIntersecting);
updateVisibility();
});
observer.observe(container);
document.addEventListener("visibilitychange", updateVisibility);
return () => {
observer.disconnect();
document.removeEventListener("visibilitychange", updateVisibility);
};
}, []);
return { containerRef, visible };
}
function PersonaFallback() {
return (
<span className="grid h-full w-full place-items-center rounded-full border border-slate-300/70 bg-slate-100 text-slate-500">
<Bot className="h-2/5 w-2/5" aria-hidden="true" />
</span>
);
}
export function AgentPersona({ className, state = "idle" }: AgentPersonaProps) {
const { containerRef, visible } = useElementVisibility();
const [failed, setFailed] = useState(false);
const handleLoadError = useCallback(() => setFailed(true), []);
const shouldRenderAnimation = !failed && visible;
return (
<span ref={containerRef} className={cn("relative block shrink-0", className)}>
{shouldRenderAnimation ? (
<Suspense fallback={<PersonaFallback />}>
<LazyPersona
className="h-full w-full scale-110"
onLoadError={handleLoadError}
state={state}
/>
</Suspense>
) : (
<PersonaFallback />
)}
</span>
);
}