Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/components/Dashboard/DashboardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useMosaicWebRTCConnection } from "@/hooks/useMosaicWebRTCConnection.ts"
import { useRobotInfo } from "@/hooks/useRobotInfo.ts";
import { RobotConnector, type TabConfig, type WidgetConfig } from "@/mosaic";
import { DASHBOARD_STORAGE_KEYS } from "@/utils";
import { getWidgetDescriptor } from "@/widgets/_utils/widgetRegistry.ts";

const ResponsiveGridLayout = WidthProvider(Responsive);

Expand Down Expand Up @@ -157,7 +158,10 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) {
connectors: widget.connectors.map((connector) => {
return new RobotConnector(connector.robotId, connector.connectorId);
}),
params: widget.params,
params: {
...getWidgetDescriptor(widget.type)?.getDefaultParams(),
...widget.params,
},
onUpdateWidgetParams: (params?: any) => {
onUpdateWidgetParams(widget.id, params);
},
Expand Down
11 changes: 2 additions & 9 deletions frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,7 @@ export function ErrorWidget({ error }: ErrorWidgetProps) {
</Text>
</VStack>
</HStack>
<Box
w="100%"
p={3}
border="1px solid"
borderColor="red.200"
borderRadius="md"
bg="white"
>
<Box w="100%" p={3} border="1px solid" borderColor="red.200" borderRadius="md" bg="white">
<Text fontSize="xs" color="gray.500" mb={1}>
Error
</Text>
Expand All @@ -50,4 +43,4 @@ export function ErrorWidget({ error }: ErrorWidgetProps) {
</Box>
</Flex>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export function useMosaicWidget(): WidgetConfig {
return ctx;
}

export { MosaicWidgetContext };
export { MosaicWidgetContext };
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { WidgetHeader } from "./WidgetHeader.tsx";
interface WidgetFrameProps {
children?: ReactNode;
widgetConfig: WidgetConfig;
error?: string;
error?: string | null;
}

export function WidgetRoot({ widgetConfig, children, error }: WidgetFrameProps) {
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";

export abstract class WidgetDescriptor {
export abstract class WidgetDescriptor<T = Record<string, any>> {
public abstract getName(): string;

public getRequiredStoreType(): StoreType | null {
Expand All @@ -27,7 +27,11 @@ export abstract class WidgetDescriptor {
return false;
}

public validateParams(_params: Record<string, any>): string | null {
public getDefaultParams(): T {
return {} as T;
}

public validateParams(_params: T): string | null {
return null;
}

Expand Down
37 changes: 26 additions & 11 deletions frontend/src/components/WidgetCustomize/StoreDataPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ interface ReceivableDataPanelProps {
defaultData: string;
}

function tryFormatJson(value: string): string {
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch {
return value;
}
}

function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps) {
const [input, setInput] = useState(defaultData);
const [input, setInput] = useState(() => tryFormatJson(defaultData));

return (
<VStack gap={2} align="stretch">
Expand All @@ -18,9 +26,10 @@ function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps
size="sm"
value={input}
onChange={(e) => setInput(e.target.value)}
onBlur={(e) => setInput(tryFormatJson(e.target.value))}
placeholder="Enter raw data string..."
fontFamily="mono"
rows={4}
rows={6}
/>
<HStack justify="flex-end">
<Button size="sm" colorPalette="green" onClick={() => onInject(input)}>
Expand All @@ -32,7 +41,7 @@ function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps
}

interface SendableDataPanelProps {
sentDataLog: string[];
sentDataLog: { time: string; data: string }[];
onClearLog: () => void;
}

Expand All @@ -52,20 +61,26 @@ function SendableDataPanel({ sentDataLog, onClearLog }: SendableDataPanelProps)
borderColor="gray.200"
borderRadius="md"
p={2}
h="150px"
overflowY="auto"
h="200px"
overflow="auto"
resize="vertical"
bg="gray.50"
>
{sentDataLog.length === 0 ? (
<Text fontSize="xs" color="gray.400">
No data sent yet...
</Text>
) : (
<VStack gap={1} align="stretch">
<VStack gap={2} align="stretch">
{sentDataLog.map((entry, i) => (
<Code key={i} fontSize="xs" whiteSpace="pre-wrap" wordBreak="break-all">
{entry}
</Code>
<Box key={i}>
<Text fontSize="xs" color="gray.400" mb="1px">
{entry.time}
</Text>
<Code fontSize="xs" whiteSpace="pre-wrap" wordBreak="break-all" display="block">
{tryFormatJson(entry.data)}
</Code>
</Box>
))}
</VStack>
)}
Expand Down Expand Up @@ -111,7 +126,7 @@ interface StoreDataPanelProps {
defaultInjectData: string;
onInject: (rawData: string) => void;
onInjectMedia: (url: string) => void;
sentDataLog: string[];
sentDataLog: { time: string; data: string }[];
onClearLog: () => void;
}

Expand Down Expand Up @@ -155,4 +170,4 @@ export function StoreDataPanel({
)}
</VStack>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ export function WidgetConfigPanel({ widgetConfig }: WidgetConfigPanelProps) {
</Box>
</Box>
);
}
}
43 changes: 25 additions & 18 deletions frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Heading, Separator, Text, VStack } from "@chakra-ui/react";
import { useContext, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";

import type { RobotConfig, WidgetConfig } from "@/mosaic";
import type { MosaicStore } from "@/mosaic/store/interface/mosaic-store.ts";
Expand Down Expand Up @@ -95,7 +95,7 @@ export function WidgetCustomizePage() {

// Store interaction
const [storeType, setStoreType] = useState<"receivable" | "sendable" | "media" | null>(null);
const [sentDataLog, setSentDataLog] = useState<string[]>([]);
const [sentDataLog, setSentDataLog] = useState<{ time: string; data: string }[]>([]);

// Keep refs for cleanup
const currentStoreRef = useRef<MosaicStore | null>(null);
Expand Down Expand Up @@ -126,16 +126,16 @@ export function WidgetCustomizePage() {
const mockChannel = {
readyState: "open" as RTCDataChannelState,
send: (data: string) => {
setSentDataLog((prev) =>
[`[${new Date().toLocaleTimeString()}] ${data}`, ...prev].slice(0, 100),
);
const now = new Date();
const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
setSentDataLog((prev) => [{ time, data }, ...prev].slice(0, 100));
},
} as unknown as RTCDataChannel;
store.setDataChannel(mockChannel);
}

setStoreType(store.getStoreType());
setWidgetParams({});
setWidgetParams(getWidgetDescriptor(widgetType)?.getDefaultParams() ?? {});
setWidgetPosition({ x: 0, y: 0, w: 8, h: 6 });

updateRobotInfo(new RobotInfo(TEST_ROBOT_ID, "Test Robot", 6, fakeRobotConfig));
Expand All @@ -153,9 +153,10 @@ export function WidgetCustomizePage() {
const handleApply = () => {
if (!widgetType || !connectorId || !connectorType) return;

// Release this component's own store ref (not the widget's ref)
// Force-delete the store for this connector so Phase 2 always creates a fresh store
// of the newly selected type. The widget's own cleanup will release its ref gracefully.
if (currentConnectorRef.current) {
storeManager.releaseStore(currentConnectorRef.current);
storeManager.forceDeleteStore(currentConnectorRef.current);
currentStoreRef.current = null;
currentConnectorRef.current = null;
}
Expand Down Expand Up @@ -207,16 +208,22 @@ export function WidgetCustomizePage() {
}
};

const widgetConfig: WidgetConfig | null = appliedConfig
? {
id: "test-widget",
type: appliedConfig.widgetType,
position: widgetPosition,
connectors: [new RobotConnector(TEST_ROBOT_ID, appliedConfig.connectorId)],
params: widgetParams,
onUpdateWidgetParams: (params) => setWidgetParams(params ?? {}),
}
: null;
const handleUpdateWidgetParams = useCallback((params?: any) => setWidgetParams(params ?? {}), []);

const widgetConfig: WidgetConfig | null = useMemo(
() =>
appliedConfig
? {
id: "test-widget",
type: appliedConfig.widgetType,
position: widgetPosition,
connectors: [new RobotConnector(TEST_ROBOT_ID, appliedConfig.connectorId)],
params: widgetParams,
onUpdateWidgetParams: handleUpdateWidgetParams,
}
: null,
[appliedConfig, widgetPosition, widgetParams, handleUpdateWidgetParams],
);

const isApplyDisabled = !widgetType || !connectorId || !connectorType;

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/mosaic/store/store-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export class StoreManager {
return store;
}

public forceDeleteStore(robotConnector: RobotConnector): void {
this.mosaicStores.delete(robotConnector);
}

public releaseStore(robotConnector: RobotConnector): boolean {
const store = this.mosaicStores.get(robotConnector);
if (store === undefined) return false;
Expand Down
19 changes: 0 additions & 19 deletions frontend/src/mosaic/webrtc/webrtc-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export class WebRTCConnection {
this.peerConnection = peerConnection;
peerConnection.onicecandidate = this.onicecandidate.bind(this);
peerConnection.onconnectionstatechange = this.onconnectionstatechange.bind(this);
peerConnection.ontrack = this.ontrack.bind(this);
return peerConnection;
}

Expand Down Expand Up @@ -359,24 +358,6 @@ export class WebRTCConnection {
}
}

private ontrack(event: RTCTrackEvent): void {
console.log(
`Received remote track id: ${event.track.id},
stream id: ${event.streams?.[0]?.id}
kind: ${event.track.kind}`,
);

if (event.track.kind !== "video" || !event.streams?.[0]) {
console.log(`[${this.robotInfo.id}] Video track is not of kind video or has no stream`);
return;
}

const stream = event.streams[0];
this.mediaStreams.set(stream.id, stream);

// TODO: need to connect to the store
}

private onConnectionConnected(): void {
console.log(
`[${this.robotInfo.id}] Connection established!, notifying related stores: `,
Expand Down
Loading
Loading