chore: clean up web ui repo hygiene
This commit is contained in:
parent
94e62c9456
commit
2fcc9db903
4786 changed files with 1271 additions and 1275231 deletions
|
|
@ -6,11 +6,19 @@ import { LogPanel } from "./components/LogPanel";
|
|||
import { useAudioMeter } from "./hooks/useAudioMeter";
|
||||
import { usePTT } from "./hooks/usePTT";
|
||||
import { useWebRTC } from "./hooks/useWebRTC";
|
||||
import type { CardItem, CardMessageMetadata, CardSelectionRange, JsonValue } from "./types";
|
||||
import type {
|
||||
AgentState,
|
||||
CardItem,
|
||||
CardMessageMetadata,
|
||||
CardSelectionRange,
|
||||
JsonValue,
|
||||
LogLine,
|
||||
} from "./types";
|
||||
|
||||
const SWIPE_THRESHOLD_PX = 64;
|
||||
const SWIPE_DIRECTION_RATIO = 1.15;
|
||||
const CARD_SELECTION_EVENT = "nanobot:card-selection-change";
|
||||
type WorkspaceView = "agent" | "feed";
|
||||
|
||||
interface AppRtcActions {
|
||||
connect(): Promise<void>;
|
||||
|
|
@ -118,8 +126,8 @@ function AgentCardContext({
|
|||
|
||||
function useSwipeHandlers(
|
||||
composing: boolean,
|
||||
view: "agent" | "feed",
|
||||
setView: (view: "agent" | "feed") => void,
|
||||
view: WorkspaceView,
|
||||
setView: (view: WorkspaceView) => void,
|
||||
isInteractiveTarget: (target: EventTarget | null) => boolean,
|
||||
) {
|
||||
const swipeStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
|
|
@ -155,7 +163,7 @@ function useSwipeHandlers(
|
|||
}
|
||||
|
||||
function useCardActions(
|
||||
setView: (view: "agent" | "feed") => void,
|
||||
setView: (view: WorkspaceView) => void,
|
||||
setSelectedCardId: (cardId: string | null) => void,
|
||||
) {
|
||||
const handleAskCard = useCallback(
|
||||
|
|
@ -170,6 +178,29 @@ function useCardActions(
|
|||
return { handleAskCard };
|
||||
}
|
||||
|
||||
function useGlobalPointerBindings({
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
}: {
|
||||
handlePointerDown: (event: Event) => void;
|
||||
handlePointerMove: (event: Event) => void;
|
||||
handlePointerUp: (event: Event) => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
document.addEventListener("pointerdown", handlePointerDown, { passive: false });
|
||||
document.addEventListener("pointermove", handlePointerMove, { passive: true });
|
||||
document.addEventListener("pointerup", handlePointerUp, { passive: false });
|
||||
document.addEventListener("pointercancel", handlePointerUp, { passive: false });
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
document.removeEventListener("pointercancel", handlePointerUp);
|
||||
};
|
||||
}, [handlePointerDown, handlePointerMove, handlePointerUp]);
|
||||
}
|
||||
|
||||
function useControlActions(rtc: AppRtcActions) {
|
||||
const handleReset = useCallback(async () => {
|
||||
const confirmed = window.confirm("Clear the current conversation context and start fresh?");
|
||||
|
|
@ -189,21 +220,63 @@ function useControlActions(rtc: AppRtcActions) {
|
|||
return { handleReset, handleToggleTextOnly };
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const rtc = useWebRTC();
|
||||
const remoteAudioLevel = useAudioMeter(rtc.remoteStream);
|
||||
const audioLevel = rtc.textOnly ? 0 : remoteAudioLevel;
|
||||
function useSelectedCardActions({
|
||||
rtc,
|
||||
selectedCardId,
|
||||
setSelectedCardId,
|
||||
selectedCardMetadata,
|
||||
}: {
|
||||
rtc: AppRtcActions & {
|
||||
sendTextMessage(text: string, metadata?: CardMessageMetadata): Promise<void>;
|
||||
};
|
||||
selectedCardId: string | null;
|
||||
setSelectedCardId: (cardId: string | null) => void;
|
||||
selectedCardMetadata: () => CardMessageMetadata | undefined;
|
||||
}) {
|
||||
const clearSelectedCardContext = useCallback(() => {
|
||||
if (selectedCardId) window.__nanobotClearCardSelection?.(selectedCardId);
|
||||
setSelectedCardId(null);
|
||||
}, [selectedCardId, setSelectedCardId]);
|
||||
|
||||
const [view, setView] = useState<"agent" | "feed">("agent");
|
||||
const [composing, setComposing] = useState(false);
|
||||
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
|
||||
const [selectionVersion, setSelectionVersion] = useState(0);
|
||||
const autoOpenedFeedRef = useRef(false);
|
||||
const handleCardChoice = useCallback(
|
||||
(cardId: string, value: string) => {
|
||||
rtc.sendJson({ type: "card-response", card_id: cardId, value });
|
||||
},
|
||||
[rtc],
|
||||
);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (text: string) => {
|
||||
await rtc.sendTextMessage(text, selectedCardMetadata());
|
||||
},
|
||||
[rtc, selectedCardMetadata],
|
||||
);
|
||||
|
||||
const handleResetWithSelection = useCallback(async () => {
|
||||
const confirmed = window.confirm("Clear the current conversation context and start fresh?");
|
||||
if (!confirmed) return;
|
||||
clearSelectedCardContext();
|
||||
await rtc.connect();
|
||||
rtc.sendJson({ type: "command", command: "reset" });
|
||||
}, [clearSelectedCardContext, rtc]);
|
||||
|
||||
return {
|
||||
clearSelectedCardContext,
|
||||
handleCardChoice,
|
||||
handleSendMessage,
|
||||
handleResetWithSelection,
|
||||
};
|
||||
}
|
||||
|
||||
function useSelectedCardContext(
|
||||
cards: CardItem[],
|
||||
selectedCardId: string | null,
|
||||
selectionVersion: number,
|
||||
) {
|
||||
const selectedCard = useMemo(
|
||||
() =>
|
||||
selectedCardId ? (rtc.cards.find((card) => card.serverId === selectedCardId) ?? null) : null,
|
||||
[rtc.cards, selectedCardId],
|
||||
selectedCardId ? (cards.find((card) => card.serverId === selectedCardId) ?? null) : null,
|
||||
[cards, selectedCardId],
|
||||
);
|
||||
const selectedCardSelection = useMemo(
|
||||
() => (selectedCardId ? readCardSelection(selectedCardId) : null),
|
||||
|
|
@ -214,6 +287,155 @@ export function App() {
|
|||
[selectedCard, selectionVersion],
|
||||
);
|
||||
|
||||
return { selectedCard, selectedCardSelection, selectedCardMetadata };
|
||||
}
|
||||
|
||||
function useCardSelectionLifecycle({
|
||||
cards,
|
||||
selectedCardId,
|
||||
setSelectedCardId,
|
||||
setSelectionVersion,
|
||||
setView,
|
||||
}: {
|
||||
cards: CardItem[];
|
||||
selectedCardId: string | null;
|
||||
setSelectedCardId: (cardId: string | null) => void;
|
||||
setSelectionVersion: (updater: (current: number) => number) => void;
|
||||
setView: (view: WorkspaceView) => void;
|
||||
}) {
|
||||
const autoOpenedFeedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoOpenedFeedRef.current || cards.length === 0) return;
|
||||
autoOpenedFeedRef.current = true;
|
||||
setView("feed");
|
||||
}, [cards.length, setView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCardId) return;
|
||||
if (cards.some((card) => card.serverId === selectedCardId)) return;
|
||||
setSelectedCardId(null);
|
||||
}, [cards, selectedCardId, setSelectedCardId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelectionChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ cardId?: string; selection?: JsonValue | null }>)
|
||||
.detail;
|
||||
setSelectionVersion((current) => current + 1);
|
||||
const cardId = typeof detail?.cardId === "string" ? detail.cardId : "";
|
||||
if (cardId && detail?.selection) setSelectedCardId(cardId);
|
||||
};
|
||||
|
||||
window.addEventListener(CARD_SELECTION_EVENT, handleSelectionChange as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener(CARD_SELECTION_EVENT, handleSelectionChange as EventListener);
|
||||
};
|
||||
}, [setSelectedCardId, setSelectionVersion]);
|
||||
}
|
||||
|
||||
function AgentWorkspace({
|
||||
active,
|
||||
selectedCard,
|
||||
selectedCardSelection,
|
||||
onClearSelectedCardContext,
|
||||
onReset,
|
||||
textOnly,
|
||||
onToggleTextOnly,
|
||||
logLines,
|
||||
connected,
|
||||
onSendMessage,
|
||||
onExpandChange,
|
||||
effectiveAgentState,
|
||||
connecting,
|
||||
audioLevel,
|
||||
}: {
|
||||
active: boolean;
|
||||
selectedCard: CardItem | null;
|
||||
selectedCardSelection: CardSelectionRange | null;
|
||||
onClearSelectedCardContext(): void;
|
||||
onReset(): Promise<void>;
|
||||
textOnly: boolean;
|
||||
onToggleTextOnly(enabled: boolean): Promise<void>;
|
||||
logLines: LogLine[];
|
||||
connected: boolean;
|
||||
onSendMessage(text: string): Promise<void>;
|
||||
onExpandChange(expanded: boolean): void;
|
||||
effectiveAgentState: AgentState;
|
||||
connecting: boolean;
|
||||
audioLevel: number;
|
||||
}) {
|
||||
return (
|
||||
<section class="workspace-panel workspace-agent">
|
||||
{active && (
|
||||
<ControlBar onReset={onReset} textOnly={textOnly} onToggleTextOnly={onToggleTextOnly} />
|
||||
)}
|
||||
{active && selectedCard && (
|
||||
<AgentCardContext
|
||||
card={selectedCard}
|
||||
selection={selectedCardSelection}
|
||||
onClear={onClearSelectedCardContext}
|
||||
/>
|
||||
)}
|
||||
{active && (
|
||||
<LogPanel
|
||||
lines={logLines}
|
||||
disabled={!connected}
|
||||
onSendMessage={onSendMessage}
|
||||
onExpandChange={onExpandChange}
|
||||
/>
|
||||
)}
|
||||
<AgentIndicator
|
||||
state={effectiveAgentState}
|
||||
connected={connected}
|
||||
connecting={connecting}
|
||||
audioLevel={audioLevel}
|
||||
viewActive
|
||||
onPointerDown={() => {}}
|
||||
onPointerUp={() => {}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FeedWorkspace({
|
||||
cards,
|
||||
onDismiss,
|
||||
onChoice,
|
||||
onAskCard,
|
||||
}: {
|
||||
cards: CardItem[];
|
||||
onDismiss(id: number): void;
|
||||
onChoice(cardId: string, value: string): void;
|
||||
onAskCard(card: CardItem): void;
|
||||
}) {
|
||||
return (
|
||||
<section class="workspace-panel workspace-feed">
|
||||
<CardFeed
|
||||
cards={cards}
|
||||
viewActive
|
||||
onDismiss={onDismiss}
|
||||
onChoice={onChoice}
|
||||
onAskCard={onAskCard}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const rtc = useWebRTC();
|
||||
const remoteAudioLevel = useAudioMeter(rtc.remoteStream);
|
||||
const audioLevel = rtc.textOnly ? 0 : remoteAudioLevel;
|
||||
|
||||
const [view, setView] = useState<WorkspaceView>("agent");
|
||||
const [composing, setComposing] = useState(false);
|
||||
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
|
||||
const [selectionVersion, setSelectionVersion] = useState(0);
|
||||
const { selectedCard, selectedCardSelection, selectedCardMetadata } = useSelectedCardContext(
|
||||
rtc.cards,
|
||||
selectedCardId,
|
||||
selectionVersion,
|
||||
);
|
||||
|
||||
const { agentStateOverride, handlePointerDown, handlePointerMove, handlePointerUp } = usePTT({
|
||||
connected: rtc.connected && !rtc.textOnly,
|
||||
onSendPtt: (pressed) =>
|
||||
|
|
@ -232,122 +454,55 @@ export function App() {
|
|||
setView,
|
||||
isInteractiveTarget,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("pointerdown", handlePointerDown, { passive: false });
|
||||
document.addEventListener("pointermove", handlePointerMove, { passive: true });
|
||||
document.addEventListener("pointerup", handlePointerUp, { passive: false });
|
||||
document.addEventListener("pointercancel", handlePointerUp, { passive: false });
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
document.removeEventListener("pointercancel", handlePointerUp);
|
||||
};
|
||||
}, [handlePointerDown, handlePointerMove, handlePointerUp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoOpenedFeedRef.current || rtc.cards.length === 0) return;
|
||||
autoOpenedFeedRef.current = true;
|
||||
setView("feed");
|
||||
}, [rtc.cards.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCardId) return;
|
||||
if (rtc.cards.some((card) => card.serverId === selectedCardId)) return;
|
||||
setSelectedCardId(null);
|
||||
}, [rtc.cards, selectedCardId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelectionChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ cardId?: string; selection?: JsonValue | null }>)
|
||||
.detail;
|
||||
setSelectionVersion((current) => current + 1);
|
||||
const cardId = typeof detail?.cardId === "string" ? detail.cardId : "";
|
||||
if (cardId && detail?.selection) setSelectedCardId(cardId);
|
||||
};
|
||||
|
||||
window.addEventListener(CARD_SELECTION_EVENT, handleSelectionChange as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener(CARD_SELECTION_EVENT, handleSelectionChange as EventListener);
|
||||
};
|
||||
}, []);
|
||||
useGlobalPointerBindings({ handlePointerDown, handlePointerMove, handlePointerUp });
|
||||
useCardSelectionLifecycle({
|
||||
cards: rtc.cards,
|
||||
selectedCardId,
|
||||
setSelectedCardId,
|
||||
setSelectionVersion,
|
||||
setView,
|
||||
});
|
||||
|
||||
const { handleToggleTextOnly } = useControlActions(rtc);
|
||||
const { handleAskCard } = useCardActions(setView, setSelectedCardId);
|
||||
|
||||
const clearSelectedCardContext = useCallback(() => {
|
||||
if (selectedCardId) window.__nanobotClearCardSelection?.(selectedCardId);
|
||||
setSelectedCardId(null);
|
||||
}, [selectedCardId]);
|
||||
|
||||
const handleCardChoice = useCallback(
|
||||
(cardId: string, value: string) => {
|
||||
rtc.sendJson({ type: "card-response", card_id: cardId, value });
|
||||
},
|
||||
[rtc],
|
||||
);
|
||||
const handleSendMessage = useCallback(
|
||||
async (text: string) => {
|
||||
await rtc.sendTextMessage(text, selectedCardMetadata());
|
||||
},
|
||||
[rtc, selectedCardMetadata],
|
||||
);
|
||||
const handleResetWithSelection = useCallback(async () => {
|
||||
const confirmed = window.confirm("Clear the current conversation context and start fresh?");
|
||||
if (!confirmed) return;
|
||||
if (selectedCardId) window.__nanobotClearCardSelection?.(selectedCardId);
|
||||
setSelectedCardId(null);
|
||||
await rtc.connect();
|
||||
rtc.sendJson({ type: "command", command: "reset" });
|
||||
}, [rtc, selectedCardId]);
|
||||
const {
|
||||
clearSelectedCardContext,
|
||||
handleCardChoice,
|
||||
handleSendMessage,
|
||||
handleResetWithSelection,
|
||||
} = useSelectedCardActions({
|
||||
rtc,
|
||||
selectedCardId,
|
||||
setSelectedCardId,
|
||||
selectedCardMetadata,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="swipe-shell" onPointerDown={onSwipeStart} onPointerUp={onSwipeEnd}>
|
||||
<div id="swipe-track" class={view === "feed" ? "feed-active" : ""}>
|
||||
<section class="workspace-panel workspace-agent">
|
||||
{view === "agent" && (
|
||||
<ControlBar
|
||||
onReset={handleResetWithSelection}
|
||||
textOnly={rtc.textOnly}
|
||||
onToggleTextOnly={handleToggleTextOnly}
|
||||
/>
|
||||
)}
|
||||
{view === "agent" && selectedCard && (
|
||||
<AgentCardContext
|
||||
card={selectedCard}
|
||||
selection={selectedCardSelection}
|
||||
onClear={clearSelectedCardContext}
|
||||
/>
|
||||
)}
|
||||
{view === "agent" && (
|
||||
<LogPanel
|
||||
lines={rtc.logLines}
|
||||
disabled={!rtc.connected}
|
||||
onSendMessage={handleSendMessage}
|
||||
onExpandChange={setComposing}
|
||||
/>
|
||||
)}
|
||||
<AgentIndicator
|
||||
state={effectiveAgentState}
|
||||
connected={rtc.connected}
|
||||
connecting={rtc.connecting}
|
||||
audioLevel={audioLevel}
|
||||
viewActive
|
||||
onPointerDown={() => {}}
|
||||
onPointerUp={() => {}}
|
||||
/>
|
||||
</section>
|
||||
<section class="workspace-panel workspace-feed">
|
||||
<CardFeed
|
||||
cards={rtc.cards}
|
||||
viewActive
|
||||
onDismiss={rtc.dismissCard}
|
||||
onChoice={handleCardChoice}
|
||||
onAskCard={handleAskCard}
|
||||
/>
|
||||
</section>
|
||||
<AgentWorkspace
|
||||
active={view === "agent"}
|
||||
selectedCard={selectedCard}
|
||||
selectedCardSelection={selectedCardSelection}
|
||||
onClearSelectedCardContext={clearSelectedCardContext}
|
||||
onReset={handleResetWithSelection}
|
||||
textOnly={rtc.textOnly}
|
||||
onToggleTextOnly={handleToggleTextOnly}
|
||||
logLines={rtc.logLines}
|
||||
connected={rtc.connected}
|
||||
onSendMessage={handleSendMessage}
|
||||
onExpandChange={setComposing}
|
||||
effectiveAgentState={effectiveAgentState}
|
||||
connecting={rtc.connecting}
|
||||
audioLevel={audioLevel}
|
||||
/>
|
||||
<FeedWorkspace
|
||||
cards={rtc.cards}
|
||||
onDismiss={rtc.dismissCard}
|
||||
onChoice={handleCardChoice}
|
||||
onAskCard={handleAskCard}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<VoiceStatus text={rtc.voiceStatus} visible={rtc.statusVisible} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue