feat: add project-aware agent workspace
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user