feat: add project-aware agent workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s

This commit is contained in:
2026-10-22 18:52:18 +08:00
parent c6a0e74e72
commit 2d8506fea0
191 changed files with 12373 additions and 3556 deletions
+48 -10
View File
@@ -1,9 +1,10 @@
import type { RiveParameters } from "@rive-app/react-webgl2";
import { useRive, useStateMachineInput } from "@rive-app/react-webgl2";
import { EventType, useRive, useStateMachineInput } from "@rive-app/react-webgl2";
import { memo, useEffect, useState } from "react";
import type { PointerEvent as ReactPointerEvent } from "react";
import { cn } from "@/shared/ui/cn";
import { loadPersonaResource } from "./persona-resource";
const RIVE_SOURCE = "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/obsidian-2.0.riv";
const STATE_MACHINE = "default";
function useDeferredRiveInit() {
@@ -21,24 +22,45 @@ export type PersonaState = "idle" | "listening" | "thinking" | "speaking" | "asl
export type PersonaProps = {
className?: string;
onLoad?: RiveParameters["onLoad"];
onLoadError?: RiveParameters["onLoadError"];
state?: PersonaState;
};
export const Persona = memo(function Persona({
className,
onLoad,
onLoadError,
state = "idle"
}: PersonaProps) {
const ready = useDeferredRiveInit();
const [buffer, setBuffer] = useState<ArrayBuffer>();
const { rive, RiveComponent } = useRive(
ready
useEffect(() => {
if (!ready) return;
let cancelled = false;
void loadPersonaResource().then(
(bytes) => {
if (!cancelled) setBuffer(bytes);
},
(error: unknown) => {
if (!cancelled) onLoadError?.({ type: EventType.LoadError, data: String(error) });
}
);
return () => {
cancelled = true;
};
}, [ready, onLoadError]);
const { rive, canvas, RiveComponent } = useRive(
ready && buffer
? {
autoplay: true,
onLoad,
onLoadError,
src: RIVE_SOURCE,
stateMachines: STATE_MACHINE
buffer,
stateMachines: STATE_MACHINE,
isTouchScrollEnabled: true
}
: null
);
@@ -63,10 +85,26 @@ export const Persona = memo(function Persona({
}
}, [state, listeningInput, thinkingInput, speakingInput, asleepInput]);
const forwardPointer = (event: ReactPointerEvent<HTMLSpanElement>, type: "mouseover" | "mousemove" | "mouseout") => {
if (!event.isPrimary || event.pointerType === "touch") return;
// Let Rive transform CSS coordinates into artboard coordinates and update
// its original pointer constraint. Only forward hover events, never presses
// or touch-generated mouse events, so mobile taps retain their button action.
canvas?.dispatchEvent(new MouseEvent(type, {
clientX: event.clientX,
clientY: event.clientY
}));
};
return (
<RiveComponent
className={cn("size-16 shrink-0", className)}
style={{ filter: "brightness(0.45) contrast(1.35)" }}
/>
<span
className={cn("relative block size-16 shrink-0", className)}
onPointerEnter={(event) => forwardPointer(event, "mouseover")}
onPointerMove={(event) => forwardPointer(event, "mousemove")}
onPointerLeave={(event) => forwardPointer(event, "mouseout")}
onPointerCancel={(event) => forwardPointer(event, "mouseout")}
>
<RiveComponent className="pointer-events-none size-full" />
</span>
);
});