diff --git a/app-next/src/app/[locale]/(explore)/benchmarks/[id]/page.tsx b/app-next/src/app/[locale]/(explore)/benchmarks/[id]/page.tsx index 63c32a1e..ab802aa0 100644 --- a/app-next/src/app/[locale]/(explore)/benchmarks/[id]/page.tsx +++ b/app-next/src/app/[locale]/(explore)/benchmarks/[id]/page.tsx @@ -1,47 +1,17 @@ import { Metadata } from "next"; import { setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; -import { - Award, - Database, - GitBranch, - Zap, - Calendar, - User, -} from "lucide-react"; -import { getElasticsearchUrl } from "@/lib/elasticsearch"; +import { Database, Flag, Calendar, User } from "lucide-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { entityColors, ENTITY_ICONS } from "@/constants"; import { Card, CardContent } from "@/components/ui/card"; +import { CollapsibleSection } from "@/components/ui/collapsible-section"; +import { BenchmarkDatasetsSection } from "@/components/benchmark/benchmark-datasets-section"; +import { BenchmarkTasksSection } from "@/components/benchmark/benchmark-tasks-section"; +import { BenchmarkNavigationMenu } from "@/components/benchmark/benchmark-navigation-menu"; import Link from "next/link"; - -interface StudyData { - study_id: number; - study_type: string; - name: string; - description?: string; - uploader?: string; - uploader_id?: number; - date?: string; - datasets_included?: number; - tasks_included?: number; - flows_included?: number; - runs_included?: number; -} - -async function fetchStudy(id: string): Promise { - const url = getElasticsearchUrl(`study/_doc/${id}`); - const res = await fetch(url, { next: { revalidate: 3600 } }); - - if (!res.ok) { - throw new Error(`Benchmark ${id} not found`); - } - - const data = await res.json(); - if (!data.found || !data._source) { - throw new Error(`Benchmark ${id} not found`); - } - - return data._source as StudyData; -} +import { fetchStudy } from "@/lib/api/study"; +import type { StudyData } from "@/lib/api/study"; export async function generateMetadata({ params, @@ -52,8 +22,7 @@ export async function generateMetadata({ try { const study = await fetchStudy(id); - const typeLabel = - study.study_type === "task" ? "Task Suite" : "Run Study"; + const typeLabel = study.study_type === "task" ? "Task Suite" : "Run Study"; return { title: `${study.name} - ${typeLabel} - OpenML Benchmarks`, @@ -90,37 +59,50 @@ export default async function BenchmarkDetailPage({ notFound(); } - const typeLabel = - study.study_type === "task" ? "Task Suite" : "Run Study"; + const typeLabel = study.study_type === "task" ? "Task Suite" : "Run Study"; const entityCounts = [ { label: "Datasets", count: study.datasets_included || 0, icon: Database, - color: "text-green-600", - href: `/datasets?q=study_${id}`, + color: entityColors.data, }, { label: "Tasks", count: study.tasks_included || 0, - icon: Award, - color: "text-blue-600", - href: `/tasks?q=study_${id}`, + icon: Flag, + color: entityColors.task, }, { label: "Flows", count: study.flows_included || 0, - icon: GitBranch, - color: "text-orange-600", - href: `/flows?q=study_${id}`, + icon: (props: React.HTMLAttributes) => ( + + } + /> + ), + color: entityColors.flow, }, { label: "Runs", count: study.runs_included || 0, - icon: Zap, - color: "text-purple-600", - href: `/runs?q=study_${id}`, + icon: (props: React.HTMLAttributes) => ( + + } + /> + ), + color: entityColors.run, }, ]; @@ -130,14 +112,26 @@ export default async function BenchmarkDetailPage({ {/* Header */}
- + {typeLabel} #{id}

-

@@ -166,11 +160,12 @@ export default async function BenchmarkDetailPage({ {/* Entity counts grid */}
- {entityCounts.map(({ label, count, icon: Icon, color, href }) => ( - - + {entityCounts + .filter((e) => e.count > 0) + .map(({ label, count, icon: Icon, color }) => ( + - +

{Number(count).toLocaleString()} @@ -179,21 +174,59 @@ export default async function BenchmarkDetailPage({

- - ))} + ))}
- {/* Description */} - {study.description && ( - - -

Description

-
- {study.description} -
-
-
- )} + {/* Content with Sidebar Navigation */} +
+ {/* Left: Main Content */} +
+ {/* Description */} + {study.description && ( +
+ + } + defaultOpen={true} + > +
+ +
+ )} + + {/* Datasets Section */} + + + {/* Tasks Section */} + +
+ + {/* Right: Navigation Menu */} + +
); diff --git a/app-next/src/app/[locale]/(explore)/benchmarks/page.tsx b/app-next/src/app/[locale]/(explore)/benchmarks/page.tsx index ee85fc1c..9f13ca02 100644 --- a/app-next/src/app/[locale]/(explore)/benchmarks/page.tsx +++ b/app-next/src/app/[locale]/(explore)/benchmarks/page.tsx @@ -1,4 +1,10 @@ import { redirect } from "next/navigation"; -export default function BenchmarksPage() { - redirect("/benchmarks/tasks"); + +export default async function BenchmarksPage({ + searchParams, +}: { + searchParams: Promise<{ q?: string }>; +}) { + const { q } = await searchParams; + redirect(q ? `/benchmarks/tasks?q=${encodeURIComponent(q)}` : "/benchmarks/tasks"); } diff --git a/app-next/src/app/[locale]/(explore)/collections/[id]/page.tsx b/app-next/src/app/[locale]/(explore)/collections/[id]/page.tsx index 3fd15027..4154970e 100644 --- a/app-next/src/app/[locale]/(explore)/collections/[id]/page.tsx +++ b/app-next/src/app/[locale]/(explore)/collections/[id]/page.tsx @@ -1,49 +1,18 @@ import { Metadata } from "next"; import { setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; -import { - Layers, - Database, - Award, - GitBranch, - Zap, - Calendar, - User, -} from "lucide-react"; -import { getElasticsearchUrl } from "@/lib/elasticsearch"; +import { Layers, Database, Flag, Calendar, User } from "lucide-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { entityColors, ENTITY_ICONS } from "@/constants"; import { Card, CardContent } from "@/components/ui/card"; +import { CollapsibleSection } from "@/components/ui/collapsible-section"; +import { CollectionDatasetsSection } from "@/components/collection/collection-datasets-section"; +import { CollectionTasksSection } from "@/components/collection/collection-tasks-section"; +import { CollectionNavigationMenu } from "@/components/collection/collection-navigation-menu"; +import { EntityActionsMenu } from "@/components/ui/entity-actions-menu"; import Link from "next/link"; - -interface StudyData { - study_id: number; - study_type: string; - name: string; - description?: string; - uploader?: string; - uploader_id?: number; - date?: string; - visibility?: string; - datasets_included?: number; - tasks_included?: number; - flows_included?: number; - runs_included?: number; -} - -async function fetchStudy(id: string): Promise { - const url = getElasticsearchUrl(`study/_doc/${id}`); - const res = await fetch(url, { next: { revalidate: 3600 } }); - - if (!res.ok) { - throw new Error(`Study ${id} not found`); - } - - const data = await res.json(); - if (!data.found || !data._source) { - throw new Error(`Study ${id} not found`); - } - - return data._source as StudyData; -} +import { fetchStudy } from "@/lib/api/study"; +import type { StudyData } from "@/lib/api/study"; export async function generateMetadata({ params, @@ -60,8 +29,7 @@ export async function generateMetadata({ return { title: `${study.name} - ${typeLabel} - OpenML`, description: - study.description?.substring(0, 160) || - `OpenML ${typeLabel} #${id}`, + study.description?.substring(0, 160) || `OpenML ${typeLabel} #${id}`, openGraph: { title: `${study.name} - ${typeLabel}`, description: `OpenML Collection #${id}: ${study.name}`, @@ -100,29 +68,29 @@ export default async function CollectionDetailPage({ label: "Datasets", count: study.datasets_included || 0, icon: Database, - color: "text-green-600", - href: `/datasets?q=study_${id}`, + color: entityColors.data, }, { label: "Tasks", count: study.tasks_included || 0, - icon: Award, - color: "text-blue-600", - href: `/tasks?q=study_${id}`, + icon: Flag, + color: entityColors.task, }, { label: "Flows", count: study.flows_included || 0, - icon: GitBranch, - color: "text-orange-600", - href: `/flows?q=study_${id}`, + icon: (props: any) => ( + + ), + color: entityColors.flow, }, { label: "Runs", count: study.runs_included || 0, - icon: Zap, - color: "text-purple-600", - href: `/runs?q=study_${id}`, + icon: (props: any) => ( + + ), + color: entityColors.run, }, ]; @@ -132,16 +100,30 @@ export default async function CollectionDetailPage({ {/* Header */}
- + {typeLabel} #{id}
-

-

+
+

+

+ +
{study.uploader && ( @@ -168,11 +150,12 @@ export default async function CollectionDetailPage({ {/* Entity counts grid */}
- {entityCounts.map(({ label, count, icon: Icon, color, href }) => ( - - + {entityCounts + .filter((e) => e.count > 0) + .map(({ label, count, icon: Icon, color }) => ( + - +

{Number(count).toLocaleString()} @@ -181,21 +164,54 @@ export default async function CollectionDetailPage({

- - ))} + ))}
- {/* Description */} - {study.description && ( - - -

Description

-
- {study.description} -
-
-
- )} + {/* Content with Sidebar Navigation */} +
+ {/* Left: Main Content */} +
+ {/* Description */} + {study.description && ( +
+ + } + defaultOpen={true} + > +
+ +
+ )} + + {/* Datasets Section */} + + + {/* Tasks Section */} + +
+ + {/* Right: Navigation Menu */} + +
); diff --git a/app-next/src/app/[locale]/(explore)/collections/create/page.tsx b/app-next/src/app/[locale]/(explore)/collections/create/page.tsx new file mode 100644 index 00000000..ed1261d1 --- /dev/null +++ b/app-next/src/app/[locale]/(explore)/collections/create/page.tsx @@ -0,0 +1,40 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { CollectionCreateForm } from "@/components/collection/collection-create-form"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Create Collection | OpenML", + description: "Create a new benchmark collection of tasks on OpenML.", +}; + +export default async function CollectionCreatePage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const session = await getServerSession(authOptions); + + if (!session) { + redirect( + `/${locale}/auth/sign-in?reason=createCollection&callbackUrl=/${locale}/collections/create`, + ); + } + + return ( +
+
+

+ Create Collection +

+

+ Create a new collection (benchmark suite) of tasks. +

+
+ + +
+ ); +} diff --git a/app-next/src/app/[locale]/(explore)/collections/page.tsx b/app-next/src/app/[locale]/(explore)/collections/page.tsx index 78e4ecfd..015fb788 100644 --- a/app-next/src/app/[locale]/(explore)/collections/page.tsx +++ b/app-next/src/app/[locale]/(explore)/collections/page.tsx @@ -1,4 +1,10 @@ import { redirect } from "next/navigation"; -export default function CollectionsPage() { - redirect("/collections/tasks"); + +export default async function CollectionsPage({ + searchParams, +}: { + searchParams: Promise<{ q?: string }>; +}) { + const { q } = await searchParams; + redirect(q ? `/collections/tasks?q=${encodeURIComponent(q)}` : "/collections/tasks"); } diff --git a/app-next/src/app/[locale]/(explore)/datasets/[id]/edit/page.tsx b/app-next/src/app/[locale]/(explore)/datasets/[id]/edit/page.tsx index 0154c364..ad5a1f27 100644 --- a/app-next/src/app/[locale]/(explore)/datasets/[id]/edit/page.tsx +++ b/app-next/src/app/[locale]/(explore)/datasets/[id]/edit/page.tsx @@ -30,14 +30,22 @@ export default async function DatasetEditPage({ // Auth check — redirect to sign-in if not authenticated const session = await getServerSession(authOptions); if (!session?.user) { - redirect(`/auth/signin?callbackUrl=/datasets/${id}/edit`); + redirect(`/${locale}/auth/sign-in?reason=uploadDataset&callbackUrl=/${locale}/datasets/${id}/edit`); } const dataset = await fetchDataset(id); // Determine if current user is the dataset owner - const userId = (session.user as { id?: string }).id; - const isOwner = userId ? Number(userId) === dataset.uploader_id : false; + // Prefer real OpenML user ID (resolves local dev ID mismatch) + const sessionUser = session.user as { id?: string; openmlUserId?: string }; + const effectiveUserId = sessionUser.openmlUserId ?? sessionUser.id; + const isOwner = effectiveUserId ? Number(effectiveUserId) === dataset.uploader_id : false; + + // Check whether the session has a valid OpenML API key + const hasApiKey = !!(session as { apikey?: string }).apikey; + // OAuth users created in local dev environments don't have a real OpenML API key + const isLocalUser = + (session.user as { isLocalUser?: boolean }).isLocalUser ?? false; return (
@@ -45,6 +53,9 @@ export default async function DatasetEditPage({ datasetId={dataset.data_id} datasetName={dataset.name} isOwner={isOwner} + hasApiKey={hasApiKey} + isLocalUser={isLocalUser} + initialTags={(dataset.tags ?? []).map((t) => t.tag)} initialValues={{ description: dataset.description || "", creator: dataset.creator || "", diff --git a/app-next/src/app/[locale]/(explore)/datasets/[id]/page.tsx b/app-next/src/app/[locale]/(explore)/datasets/[id]/page.tsx index 212cb87b..ef678724 100644 --- a/app-next/src/app/[locale]/(explore)/datasets/[id]/page.tsx +++ b/app-next/src/app/[locale]/(explore)/datasets/[id]/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { setRequestLocale } from "next-intl/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { BarChart3 } from "lucide-react"; import { fetchDataset, @@ -9,7 +11,6 @@ import { import { DatasetHeader } from "@/components/dataset/dataset-header-new"; import { DatasetDescription } from "@/components/dataset/dataset-description"; import { QualityTable } from "@/components/dataset/quality-table"; -import { DatasetNavigationMenu } from "@/components/dataset/dataset-navigation-menu"; import { CollapsibleSection } from "@/components/ui/collapsible-section"; import { DataAnalysisSection } from "@/components/dataset/data-analysis-section"; import { MetadataSection } from "@/components/dataset/metadata-section"; @@ -17,6 +18,9 @@ import { ActivityOverview } from "@/components/dataset/activity-overview"; import { DataDetailSection } from "@/components/dataset/data-detail-section"; import { TasksSection } from "@/components/dataset/tasks-section"; import { RunsSection } from "@/components/dataset/runs-section"; +import { WorkspaceSetter } from "@/components/workspace/workspace-setter"; +import { WorkspaceInlinePanel } from "@/components/workspace/workspace-inline-panel"; +import { entityColors } from "@/constants"; export async function generateMetadata({ params, @@ -128,12 +132,18 @@ export default async function DatasetDetailPage({ const { locale, id } = await params; setRequestLocale(locale); - const [dataset, taskCount, runCount] = await Promise.all([ + const [dataset, taskCount, runCount, session] = await Promise.all([ fetchDataset(id), fetchDatasetTaskCount(id), fetchDatasetRunCount(id), + getServerSession(authOptions), ]); + const sessionUser = session?.user as { id?: string; openmlUserId?: string } | undefined; + // Prefer the real OpenML user ID (resolves local dev ID mismatch) + const effectiveUserId = sessionUser?.openmlUserId ?? sessionUser?.id; + const isOwner = effectiveUserId ? Number(effectiveUserId) === dataset.uploader_id : false; + // If dataset is deactivated, show notice if (dataset.status === "deactivated") { return ( @@ -154,16 +164,82 @@ export default async function DatasetDetailPage({
{/* Main Content */}
+ {/* Push context to the persistent workspace panel */} + 0 + ? [ + { + id: "data-analysis", + label: `Data & Analysis`, + iconName: "Database", + count: dataset.features.length, + }, + ] + : []), + { id: "metadata", label: "Metadata", iconName: "Tags" }, + { id: "activity", label: "Activity", iconName: "LineChart" }, + { id: "data-detail", label: "Data Detail", iconName: "Database" }, + { + id: "tasks", + label: "Tasks", + iconName: "ExternalLink", + count: taskCount, + }, + { + id: "runs", + label: "Runs", + iconName: "ExternalLink", + count: runCount, + }, + ...(dataset.qualities && Object.keys(dataset.qualities).length > 0 + ? [ + { + id: "qualities", + label: "Qualities", + iconName: "BarChart3", + count: Object.keys(dataset.qualities).length, + }, + ] + : []), + ]} + quickLinks={[ + ...(dataset.data_id + ? [ + { + label: `Tasks on this dataset`, + href: `/tasks?data_id=${dataset.data_id}`, + iconName: "ExternalLink", + }, + { + label: `Runs on this dataset`, + href: `/runs?data_id=${dataset.data_id}`, + iconName: "ExternalLink", + }, + ] + : []), + ]} + /> + {/* Header: Full Width - Name, stats, actions */} - {/* Content with Sidebar - Below Header */} -
- {/* Left: Main Content */} + {/* Main Content + Inline Panel */} +
{/* Description: Primary content */}
@@ -204,18 +280,7 @@ export default async function DatasetDetailPage({ )}
- - {/* Right: Navigation Menu - Responsive */} - 0} - hasQualities={ - dataset.qualities && Object.keys(dataset.qualities).length > 0 - } - featuresCount={dataset.features?.length || 0} - taskCount={taskCount} - runCount={runCount} - dataId={dataset.data_id} - /> +
diff --git a/app-next/src/app/[locale]/(explore)/datasets/upload/page.tsx b/app-next/src/app/[locale]/(explore)/datasets/upload/page.tsx new file mode 100644 index 00000000..9626256b --- /dev/null +++ b/app-next/src/app/[locale]/(explore)/datasets/upload/page.tsx @@ -0,0 +1,42 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { DatasetUploadForm } from "@/components/dataset/dataset-upload-form"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Upload Dataset | OpenML", + description: + "Upload and share a machine learning dataset with the OpenML community.", +}; + +export default async function DatasetUploadPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const session = await getServerSession(authOptions); + + if (!session) { + redirect( + `/${locale}/auth/sign-in?reason=uploadDataset&callbackUrl=/${locale}/datasets/upload`, + ); + } + + return ( +
+
+

+ Upload Dataset +

+

+ Contribute data to the OpenML community. Make sure your data is + machine-readable and properly formatted. +

+
+ + +
+ ); +} diff --git a/app-next/src/app/[locale]/(explore)/flows/[id]/page.tsx b/app-next/src/app/[locale]/(explore)/flows/[id]/page.tsx index 7c810016..3aafa4a8 100644 --- a/app-next/src/app/[locale]/(explore)/flows/[id]/page.tsx +++ b/app-next/src/app/[locale]/(explore)/flows/[id]/page.tsx @@ -1,13 +1,9 @@ import { setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; -import { - FileText, - Settings2, - List, - Cog, - History, - BarChart3, -} from "lucide-react"; +import type { Metadata } from "next"; +import { FileText, Settings2, List, History, BarChart3 } from "lucide-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ENTITY_ICONS } from "@/constants/entityIcons"; import { getFlow, fetchFlowRunCount, fetchFlowVersions } from "@/lib/api/flow"; import { FlowHeader } from "@/components/flow/flow-header"; import { FlowDescriptionSection } from "@/components/flow/flow-description-section"; @@ -17,8 +13,45 @@ import { FlowDependenciesSection } from "@/components/flow/flow-dependencies-sec import { FlowAnalysisSection } from "@/components/flow/flow-analysis-section"; import { FlowVersionsSection } from "@/components/flow/flow-versions-section"; import { FlowRunsList } from "@/components/flow/flow-runs-list"; -import { FlowNavigationMenu } from "@/components/flow/flow-navigation-menu"; import { CollapsibleSection } from "@/components/ui/collapsible-section"; +import { WorkspaceSetter } from "@/components/workspace/workspace-setter"; +import { WorkspaceInlinePanel } from "@/components/workspace/workspace-inline-panel"; +import { entityColors } from "@/constants"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; id: string }>; +}): Promise { + const { id } = await params; + const flowId = parseInt(id, 10); + + if (isNaN(flowId)) { + return { + title: "Flow Not Found | OpenML", + }; + } + + try { + const flow = await getFlow(flowId); + if (!flow) { + return { + title: "Flow Not Found | OpenML", + }; + } + + return { + title: `${flow.name} (Flow ${flow.flow_id}) | OpenML`, + description: flow.description + ? flow.description.replace(/<[^>]*>/g, "").substring(0, 160) + : `Details for OpenML Flow ${flow.name}.`, + }; + } catch (error) { + return { + title: "Error | OpenML", + }; + } +} export default async function FlowDetailPage({ params, @@ -53,116 +86,184 @@ export default async function FlowDetailPage({
{/* Main Content */}
+ {/* Push context to the persistent workspace panel */} + 0 + ? [{ id: "analysis", label: "Analyse", iconName: "BarChart3" }] + : []), + ...(flow.dependencies + ? [ + { + id: "dependencies", + label: "Dependencies", + iconName: "Layers", + }, + ] + : []), + ...(parametersCount > 0 + ? [ + { + id: "parameters", + label: "Parameters", + iconName: "Settings2", + count: parametersCount, + }, + ] + : []), + ...(componentsCount > 0 + ? [ + { + id: "components", + label: "Components", + iconName: "Layers", + count: componentsCount, + }, + ] + : []), + ...(versionsCount > 1 + ? [ + { + id: "versions", + label: "Versions", + iconName: "ExternalLink", + count: versionsCount, + }, + ] + : []), + { + id: "runs", + label: "Runs", + iconName: "ExternalLink", + count: runCount, + }, + ]} + quickLinks={[ + { + label: `Runs using this flow`, + href: `/runs?flow_id=${flow.flow_id}`, + iconName: "ExternalLink", + }, + ]} + /> + {/* Header: Full Width */} - {/* Content with Sidebar - Below Header */} -
- {/* Left: Main Content */} + {/* Main Content + Inline Panel */} +
- {/* 1. Description Section */} + {/* 1. Description Section */} + } + defaultOpen={true} + > + + + + {/* 1.2. Analysis Section */} + {runCount > 0 && ( } + id="analysis" + title="Analyse" + description="Performance analysis across tasks" + icon={} defaultOpen={true} > - + + )} - {/* 1.2. Analysis Section */} - {runCount > 0 && ( - } - defaultOpen={true} - > - - - )} - - {/* 1.5. Dependencies Section */} - {flow.dependencies && ( - } - defaultOpen={true} - > - - - )} - - {/* 2. Parameters Section */} - {parametersCount > 0 && ( - } - badge={parametersCount} - defaultOpen={true} - > - - - )} - - {/* 3. Components Section */} - {componentsCount > 0 && ( - } - badge={componentsCount} - defaultOpen={true} - > - - - )} - - {/* 4. Versions Section */} - {versionsCount > 1 && ( - } - badge={versionsCount} - defaultOpen={false} - > - - - )} + } + defaultOpen={true} + > + + + )} + + {/* 2. Parameters Section */} + {parametersCount > 0 && ( + } + badge={parametersCount} + defaultOpen={true} + > + + + )} - {/* 5. Runs List */} + {/* 3. Components Section */} + {componentsCount > 0 && ( } - badge={runCount} + id="components" + title="Components" + description="Sub-flows and nested components" + icon={ + + } + badge={componentsCount} + defaultOpen={true} + > + + + )} + + {/* 4. Versions Section */} + {versionsCount > 1 && ( + } + badge={versionsCount} defaultOpen={false} > - + -
+ )} - {/* Right: Navigation Menu - Responsive */} - + {/* 5. Runs List */} + } + badge={runCount} + defaultOpen={false} + > + + +
+
diff --git a/app-next/src/app/[locale]/(explore)/layout.tsx b/app-next/src/app/[locale]/(explore)/layout.tsx new file mode 100644 index 00000000..8407b971 --- /dev/null +++ b/app-next/src/app/[locale]/(explore)/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import { WorkspaceProvider } from "@/contexts/workspace-context"; + +/** + * Shared layout for all (explore) pages. + * + * WorkspaceProvider keeps recent-entity history and active entity context + * across navigations. Each detail page renders its own WorkspaceInlinePanel + * (sticky, inside a flex layout after the entity header). + */ +export default function ExploreLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/app-next/src/app/[locale]/(explore)/measures/[id]/page.tsx b/app-next/src/app/[locale]/(explore)/measures/[id]/page.tsx new file mode 100644 index 00000000..a090304f --- /dev/null +++ b/app-next/src/app/[locale]/(explore)/measures/[id]/page.tsx @@ -0,0 +1,201 @@ +import { Metadata } from "next"; +import { setRequestLocale } from "next-intl/server"; +import Link from "next/link"; +import { FileText, Flag, Hash, Database, BarChart3 } from "lucide-react"; +import { ENTITY_ICONS, entityColors } from "@/constants"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { fetchMeasure, fetchRelatedTasks } from "@/lib/api/measure"; +import { MeasureHeader, MeasureAnalysisSection } from "@/components/measure"; +import { MeasureDescriptionSection } from "@/components/measure/measure-description-section"; +import { MeasureNavigationMenu } from "@/components/measure/measure-navigation-menu"; +import { CollapsibleSection } from "@/components/ui/collapsible-section"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; id: string }>; +}): Promise { + const { id } = await params; + + try { + const measure = await fetchMeasure(id); + return { + title: `${measure.name} - OpenML Measure`, + description: measure.description || `OpenML Measure: ${measure.name}`, + openGraph: { + title: `${measure.name} - OpenML Measure`, + description: + measure.description || `OpenML Measure #${id}: ${measure.name}`, + type: "article", + url: `https://www.openml.org/measures/${id}`, + }, + }; + } catch { + return { + title: "Measure Not Found - OpenML", + description: "The requested measure could not be found.", + }; + } +} + +export default async function MeasureDetailPage({ + params, +}: { + params: Promise<{ locale: string; id: string }>; +}) { + const { locale, id } = await params; + setRequestLocale(locale); + + const measure = await fetchMeasure(id); + const tasks = await fetchRelatedTasks(measure.name); + + return ( +
+
+ + +
+
+ {/* 1. Description */} + + } + defaultOpen={true} + > + + + + {/* 2. Analysis */} + + } + defaultOpen={false} + > + + + + {/* 3. Related Tasks */} + + } + badge={tasks.length} + defaultOpen={false} + > + {tasks.length > 0 ? ( +
+ {tasks.map((t) => ( +
+
+
+ +
+

+ Task #{t.task_id} +

+ + {t.task_type || `Type ${t.task_type_id}`} + +
+
+ {t.source_data?.name && ( +
+ + + Dataset: {t.source_data.name} + +
+ )} + {t.runs !== undefined && t.runs > 0 && ( +
+ + + + + {Number(t.runs).toLocaleString()} + + + Runs + +
+ )} +
+ + + {t.task_id} + + + View task {t.task_id} + +
+ ))} +
+ ) : ( +
+ No tasks found using this measure. +
+ )} +
+
+ + +
+
+
+ ); +} + +export const revalidate = 3600; diff --git a/app-next/src/app/[locale]/(explore)/measures/data-qualities/page.tsx b/app-next/src/app/[locale]/(explore)/measures/data-qualities/page.tsx index ef77f478..b31349c9 100644 --- a/app-next/src/app/[locale]/(explore)/measures/data-qualities/page.tsx +++ b/app-next/src/app/[locale]/(explore)/measures/data-qualities/page.tsx @@ -1,7 +1,8 @@ import { setRequestLocale } from "next-intl/server"; import { MeasureList } from "@/components/measure/measure-list"; -import { Gauge } from "lucide-react"; import type { Metadata } from "next"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ENTITY_ICONS, entityColors } from "@/constants"; export const metadata: Metadata = { title: "Data Quality Measures - OpenML", @@ -37,7 +38,16 @@ export default async function DataQualitiesPage({
-