Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/UI improvement #921

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
22 changes: 22 additions & 0 deletions openadapt/app/dashboard/api/recordings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def attach_routes(self) -> APIRouter:
self.app.add_api_route("/start", self.start_recording)
self.app.add_api_route("/stop", self.stop_recording)
self.app.add_api_route("/status", self.recording_status)
self.app.add_api_route(
"/{recording_id}/screenshots", self.get_recording_screenshots
)
self.recording_detail_route()
return self.app

Expand Down Expand Up @@ -63,6 +66,25 @@ def recording_status() -> dict[str, bool]:
"""Get the recording status."""
return {"recording": cards.is_recording()}

@staticmethod
def get_recording_screenshots(recording_id: int) -> dict[str, list[str]]:
"""Get all screenshots for a specific recording."""
session = crud.get_new_session(read_only=True)
recording = crud.get_recording_by_id(session, recording_id)
action_events = get_events(session, recording)

screenshots = []
for action_event in action_events:
try:
image = display_event(action_event)
if image:
screenshots.append(image2utf8(image))
except Exception as e:
logger.info(f"Failed to display event: {e}")
continue

return {"screenshots": screenshots}

def recording_detail_route(self) -> None:
"""Add the recording detail route as a websocket."""

Expand Down
8 changes: 7 additions & 1 deletion openadapt/app/dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import { Notifications } from '@mantine/notifications';
import { Shell } from '@/components/Shell'
import { CSPostHogProvider } from './providers';
import { Poppins } from 'next/font/google';

const poppins = Poppins({
subsets: ['latin'],
weight: ['400', '700'],
});

export const metadata = {
title: 'OpenAdapt.AI',
Expand All @@ -20,7 +26,7 @@ export default function RootLayout({
<ColorSchemeScript />
</head>
<CSPostHogProvider>
<body>
<body className={poppins.className}>
<MantineProvider>
<Notifications />
<Shell>
Expand Down
83 changes: 70 additions & 13 deletions openadapt/app/dashboard/app/recordings/RawRecordings.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,59 @@
import { SimpleTable } from '@/components/SimpleTable';
import dynamic from 'next/dynamic';
import { Recording } from '@/types/recording';
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react';
import { timeStampToDateString } from '../utils';
import { useRouter } from 'next/navigation';

const FramePlayer = dynamic<{ recording: Recording; frameRate: number }>(
() => import('@/components/FramePlayer').then((mod) => mod.FramePlayer),
{ ssr: false }
);

async function fetchRecordingWithScreenshots(recordingId: string | number) {
try {
const response = await fetch(`/api/recordings/${recordingId}/screenshots`);
if (!response.ok) {
throw new Error('Failed to fetch screenshots');
}
const data = await response.json();
return data.screenshots || [];
} catch (error) {
console.error('Error fetching screenshots:', error);
return [];
}
}

export const RawRecordings = () => {
const [recordings, setRecordings] = useState<Recording[]>([]);
const [loading, setLoading] = useState(true);
const router = useRouter();

function fetchRecordings() {
fetch('/api/recordings').then(res => {
async function fetchRecordings() {
try {
setLoading(true);
const res = await fetch('/api/recordings');
if (res.ok) {
res.json().then((data) => {
setRecordings(data.recordings);
});
const data = await res.json();

// Fetch screenshots for all recordings in parallel
const recordingsWithScreenshots = await Promise.all(
data.recordings.map(async (rec: Recording) => {
const screenshots = await fetchRecordingWithScreenshots(rec.id);
return {
...rec,
screenshots
};
})
);

setRecordings(recordingsWithScreenshots);
}
})
} catch (error) {
console.error('Error fetching recordings:', error);
} finally {
setLoading(false);
}
}

useEffect(() => {
Expand All @@ -26,19 +64,38 @@ export const RawRecordings = () => {
return () => router.push(`/recordings/detail/?id=${recording.id}`);
}

if (loading) {
return <div className="text-center py-4">Loading recordings...</div>;
}

return (
<SimpleTable
columns={[
{name: 'ID', accessor: 'id'},
{name: 'Description', accessor: 'task_description'},
{name: 'Start time', accessor: (recording: Recording) => recording.video_start_time ? timeStampToDateString(recording.video_start_time) : 'N/A'},
{name: 'Timestamp', accessor: (recording: Recording) => recording.timestamp ? timeStampToDateString(recording.timestamp) : 'N/A'},
{name: 'Monitor Width/Height', accessor: (recording: Recording) => `${recording.monitor_width}/${recording.monitor_height}`},
{name: 'Double Click Interval Seconds/Pixels', accessor: (recording: Recording) => `${recording.double_click_interval_seconds}/${recording.double_click_distance_pixels}`},
{name: 'Start time', accessor: (recording: Recording) =>
recording.video_start_time
? timeStampToDateString(recording.video_start_time)
: 'N/A'
},
{name: 'Monitor Width/Height', accessor: (recording: Recording) =>
`${recording.monitor_width}/${recording.monitor_height}`
},
{
name: 'Video',
accessor: (recording: Recording) => (
<div className="min-w-[200px]">
<FramePlayer
recording={recording}
frameRate={1}
/>
</div>
),
}
]}
data={recordings}
refreshData={fetchRecordings}
onClickRow={onClickRow}
/>
)
}
);
};
4 changes: 2 additions & 2 deletions openadapt/app/dashboard/app/recordings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ export default function Recordings() {
return (
<Box>
{recordingStatus === RecordingStatus.RECORDING && (
<Button onClick={stopRecording} variant="outline" color="red">
<Button onClick={stopRecording} variant="outline" className="hover:bg-red-900 bg-red-600 hover:text-zinc-100 text-zinc-200 hover:text-white">
Stop recording
</Button>
)}
{recordingStatus === RecordingStatus.STOPPED && (
<Button onClick={startRecording} variant="outline" color="blue">
<Button onClick={startRecording} variant="outline" className="hover:bg-accent bg-primary/80 hover:text-zinc-100 text-zinc-200 hover:text-white">
Start recording
</Button>
)}
Expand Down
41 changes: 30 additions & 11 deletions openadapt/app/dashboard/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
type Route = {
name: string
path: string
}
import {
IconFileText,
IconSettings,
IconEraser,
IconBook
} from '@tabler/icons-react';


export interface Route {
name: string;
path: string;
icon: typeof IconFileText; // Simple type that works for all Tabler icons
active?: boolean;
badge?: string | number;
}

export const routes: Route[] = [
{
name: 'Recordings',
path: '/recordings',
},
{
icon: IconFileText,
active: true
},
{
name: 'Settings',
path: '/settings',
},
{
icon: IconSettings,
active: false
},
{
name: 'Scrubbing',
path: '/scrubbing',
},
{
icon: IconEraser,
active: false
},
{
name: 'Onboarding',
path: '/onboarding',
}
icon: IconBook,
active: false
}
]
Loading
Loading