Skip to content

Feat/UI improvement #921

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
16 changes: 10 additions & 6 deletions openadapt/app/dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import './globals.css'

import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import { Notifications } from '@mantine/notifications';
import { Notifications } from '@mantine/notifications'
import { Shell } from '@/components/Shell'
import { CSPostHogProvider } from './providers';
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,12 +26,10 @@ export default function RootLayout({
<ColorSchemeScript />
</head>
<CSPostHogProvider>
<body>
<body className={poppins.className}>
<MantineProvider>
<Notifications />
<Shell>
{children}
</Shell>
<Shell>{children}</Shell>
</MantineProvider>
</body>
</CSPostHogProvider>
Expand Down
104 changes: 83 additions & 21 deletions openadapt/app/dashboard/app/recordings/RawRecordings.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,102 @@
import { SimpleTable } from '@/components/SimpleTable';
import { Recording } from '@/types/recording';
import { SimpleTable } from '@/components/SimpleTable'
import dynamic from 'next/dynamic'
import { Recording } from '@/types/recording'
import React, { useEffect, useState } from 'react'
import { timeStampToDateString } from '../utils';
import { useRouter } from 'next/navigation';
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 router = useRouter();
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(() => {
fetchRecordings();
}, []);
fetchRecordings()
}, [])

function onClickRow(recording: Recording) {
return () => router.push(`/recordings/detail/?id=${recording.id}`);
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: '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: '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}
Expand Down
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
22 changes: 20 additions & 2 deletions openadapt/app/dashboard/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
type Route = {
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