smaller adjustments and server client seperation for projects

This commit is contained in:
2025-08-30 14:38:52 +02:00
parent dda268930c
commit 452ca1770c
12 changed files with 356 additions and 162 deletions

View File

@@ -8,35 +8,36 @@ role: Researcher, Software Developer
skills: Multi-Agent Reinforcement Learning (MARL), Emergence Analysis, AI Safety, Simulation Environment Design, Python, Gymnasium API, Software Engineering, Unity (Visualization), Industry Collaboration skills: Multi-Agent Reinforcement Learning (MARL), Emergence Analysis, AI Safety, Simulation Environment Design, Python, Gymnasium API, Software Engineering, Unity (Visualization), Industry Collaboration
--- ---
<div className="container"> {/* The InfoBox now contains ALL metadata, creating a clean sidebar. */}
<InfoBox title="Project Resources"> <InfoBox title="Project Details">
{/* Section 2: Project Info */}
<h4>Overview</h4>
<p style={{ lineHeight: '1.5', margin: 0 }}>
<strong>Project:</strong> AI-Fusion<br/>
<strong>Partner:</strong> <a href="https://www.iks.fraunhofer.de/" target="_blank" rel="noopener noreferrer">Fraunhofer IKS</a><br/>
<strong>Duration:</strong> 2022 - 2023
</p>
<Image src="/images/projects/full_domain.png" alt="logo" width="400" height="200" />
<br />
<hr />
<br />
{/* Section 1: Resources */}
<h4 style={{marginTop: 0, marginRight: 0}}>Resources</h4>
<ul style={{ listStyle: 'none', paddingLeft: '0' }}> <ul style={{ listStyle: 'none', paddingLeft: '0' }}>
<li><a href="https://github.com/illiumst/marl-factory-grid/" target="_blank" rel="noopener noreferrer"><i className="fab fa-fw fa-github" aria-hidden="true"></i> GitHub Repo</a></li> <li><a href="https://github.com/illiumst/marl-factory-grid/" target="_blank" rel="noopener noreferrer"><i className="fab fa-fw fa-github" aria-hidden="true"></i> GitHub Repo</a></li>
<li><a href="https://pypi.org/project/Marl-Factory-Grid/" target="_blank" rel="noopener noreferrer"><i className="fab fa-fw fa-python" aria-hidden="true"></i> Install via PyPI</a></li> <li><a href="https://pypi.org/project/Marl-Factory-Grid/" target="_blank" rel="noopener noreferrer"><i className="fab fa-fw fa-python" aria-hidden="true"></i> Install via PyPI</a></li>
<li><a href="https://marl-factory-grid.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer"><i className="fas fa-fw fa-book" aria-hidden="true"></i> ReadTheDocs</a></li> <li><a href="https://marl-factory-grid.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer"><i className="fas fa-fw fa-book" aria-hidden="true"></i> ReadTheDocs</a></li>
<li><i className="fas fa-fw fa-file-alt" aria-hidden="true"></i> <Cite bibtexKey="altmann2024emergence" /></li> <li><i className="fas fa-fw fa-file-alt" aria-hidden="true"></i> <Cite bibtexKey="altmann2024emergence" /></li>
</ul> </ul>
<Image src="/images/projects/full_domain.png" alt="logo" style={{ margin: '0em', padding: '0em', width: '15em' }} /> </InfoBox>
</InfoBox>
<div className="main-content" style={{ float: 'left', width: '75%' }}>
In collaboration with Fraunhofer IKS, the AI-Fusion project addressed the critical challenge of understanding and ensuring safety in multi-agent reinforcement learning (MARL) systems. Emergence, defined as the arising of complex, often unpredictable, system-level dynamics from local interactions between agents and their environment, was a central focus due to its implications for system safety and reliability.
<hr />
**Project:** AI-Fusion<br/> {/* All the main content now flows naturally in a single column. */}
**Partner:** [Fraunhofer Institute for Cognitive Systems (IKS)](https://www.iks.fraunhofer.de/)<br/> In collaboration with Fraunhofer IKS, the AI-Fusion project addressed the critical challenge of understanding and ensuring safety in multi-agent reinforcement learning (MARL) systems. Emergence, defined as the arising of complex, often unpredictable, system-level dynamics from local interactions between agents and their environment, was a central focus due to its implications for system safety and reliability. The project's objective was to investigate the detection and mitigation of potentially unsafe emergent behaviors in complex systems composed of multiple interacting AI agents, particularly in scenarios involving heterogeneous agents (e.g., mixed-vendor autonomous systems).
**Duration:** 2022 - 2023<br/>
**Objective:** To investigate the detection and mitigation of potentially unsafe emergent behaviors in complex systems composed of multiple interacting AI agents, particularly in scenarios involving heterogeneous agents (e.g., mixed-vendor autonomous systems).
</div>
</div>
---
To facilitate research into these phenomena, key contributions included the development of specialized simulation tools: To facilitate research into these phenomena, key contributions included the development of specialized simulation tools:
**1. High-Performance MARL Simulation Environment:** **1. High-Performance MARL Simulation Environment:**
* A flexible and efficient simulation environment was developed in Python, adhering to the [Gymnasium (formerly Gym) API specification](https://gymnasium.farama.org/main/). * A flexible and efficient simulation environment was developed in Python, adhering to the [Gymnasium (formerly Gym) API specification](https://gymnasium.farama.org/main/).
* **Purpose:** Designed specifically for training and evaluating reinforcement learning algorithms in multi-agent contexts prone to emergent behaviors. * **Purpose:** Designed specifically for training and evaluating reinforcement learning algorithms in multi-agent contexts prone to emergent behaviors.
* **Features:** * **Features:**
@@ -45,16 +46,13 @@ To facilitate research into these phenomena, key contributions included the deve
* **Performance:** Optimized for efficient simulation runs, enabling extensive experimentation. * **Performance:** Optimized for efficient simulation runs, enabling extensive experimentation.
**2. Unity-Based Demonstrator Unit:** **2. Unity-Based Demonstrator Unit:**
* A complementary visualization tool was created using the Unity engine. * A complementary visualization tool was created using the Unity engine.
* **Purpose:** Allows for the replay, inspection, and detailed analysis of specific simulation scenarios and agent interactions. * **Purpose:** Allows for the replay, inspection, and detailed analysis of specific simulation scenarios and agent interactions.
* **Utility:** Aids researchers in identifying and understanding the mechanisms behind observed emergent dynamics. * **Utility:** Aids researchers in identifying and understanding the mechanisms behind observed emergent dynamics.
* [View Demonstrator on GitHub](https://github.com/illiumst/F-IKS_demonstrator) * [View Demonstrator on GitHub](https://github.com/illiumst/F-IKS_demonstrator)
<div style={{ clear: 'both' }}></div>
<div className="text-center"> <div className="text-center">
<Image src="/images/projects/rel_emergence.png" alt="Diagram illustrating the concept of emergence from interactions between agents and environment" style={{ padding: '0.1em', width: '80%' }} /> <Image src="/images/projects/rel_emergence.png" alt="Diagram illustrating the concept of emergence from interactions between agents and environment" width="800" height="300"/>
<figcaption>Conceptual relationship defining emergence in multi-agent systems.</figcaption> <figcaption>Conceptual relationship defining emergence in multi-agent systems.</figcaption>
</div> </div>

View File

@@ -141,4 +141,14 @@ code[data-line-numbers-max-digits="2"] > [data-line]::before {
code[data-line-numbers-max-digits="3"] > [data-line]::before { code[data-line-numbers-max-digits="3"] > [data-line]::before {
width: 3rem; width: 3rem;
}
.prose a {
/* Use the CSS variable we defined in the component */
color: var(--accent-color);
}
.prose a:hover {
/* Optional: slightly darken or lighten the color on hover for better feedback */
opacity: 0.8;
} }

View File

@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter as FontSans } from "next/font/google"; import { Inter as FontSans } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { AccentColorProvider } from "@/context/accent-color-context";
const fontSans = FontSans({ const fontSans = FontSans({
subsets: ["latin"], subsets: ["latin"],
@@ -64,11 +65,13 @@ export default function RootLayout({
> >
<script defer src="https://umami.steffenillium.de/script.js" data-website-id="170441c3-f9ca-4dea-9f44-ba0573b0f9e5"></script> <script defer src="https://umami.steffenillium.de/script.js" data-website-id="170441c3-f9ca-4dea-9f44-ba0573b0f9e5"></script>
<ThemeProvider attribute="class" defaultTheme="light"> <ThemeProvider attribute="class" defaultTheme="light">
<TooltipProvider delayDuration={0}> <AccentColorProvider>
<Header /> <TooltipProvider delayDuration={0}>
{children} <Header />
<Navbar /> {children}
</TooltipProvider> <Navbar />
</TooltipProvider>
</AccentColorProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@@ -1,12 +1,12 @@
import { getPostBySlug, getPostSlugs } from '@/lib/mdx'; import { getPostBySlug, getPostSlugs, getPostFrontmatter } from '@/lib/mdx'; // Assume getPostFrontmatter exists
import { CitationProvider } from '@/context/citation-context';
import { ReferencesContainer } from '@/components/references-container';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Image from 'next/image';
import { DATA } from '@/data/resume'; import { DATA } from '@/data/resume';
import { getPublicationsData } from '@/lib/publications'; import { getPublicationsData } from '@/lib/publications';
import { CustomMDX } from '@/components/custom-mdx'; import { ProjectArticle } from '@/components/project-article';
import Link from 'next/link';
// Helper function to get frontmatter for a slug (you may need to create this in lib/mdx.ts)
// It's a lighter version of getPostBySlug that only parses frontmatter.
// If you don't have it, we can just use getPostBySlug, it's just slightly less performant.
export async function generateStaticParams() { export async function generateStaticParams() {
const slugs = getPostSlugs('projects'); const slugs = getPostSlugs('projects');
@@ -15,9 +15,7 @@ export async function generateStaticParams() {
export async function generateMetadata({ params }: { params: { slug: string } }) { export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPostBySlug('projects', params.slug); const post = await getPostBySlug('projects', params.slug);
if (!post) { if (!post) { return {}; }
return {};
}
return { return {
title: post.frontmatter.title, title: post.frontmatter.title,
description: post.frontmatter.description || DATA.description, description: post.frontmatter.description || DATA.description,
@@ -25,76 +23,28 @@ export async function generateMetadata({ params }: { params: { slug: string } })
} }
export default async function ProjectPage({ params }: { params: { slug: string } }) { export default async function ProjectPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug('projects', params.slug); const { slug } = params;
const post = await getPostBySlug('projects', slug);
const publications = getPublicationsData(); const publications = getPublicationsData();
if (!post) { if (!post) {
return notFound(); notFound();
} }
return ( // --- Logic for Previous/Next Navigation ---
<CitationProvider publications={publications}> const allSlugs = getPostSlugs('projects'); // Or get them from your DATA file if they are ordered there
<main className="flex flex-col min-h-[100dvh]"> const currentIndex = allSlugs.findIndex((s) => s === slug);
<article className="prose prose-stone dark:prose-invert max-w-none">
<header className="mb-8">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl">
{post.frontmatter.title}
</h1>
{post.frontmatter.excerpt && (
<p className="text-lg text-muted-foreground mt-1">{post.frontmatter.excerpt}</p>
)}
</header>
{post.frontmatter.show_teaser && post.frontmatter.teaser && ( const prevSlug = currentIndex > 0 ? allSlugs[currentIndex - 1] : null;
<div className="my-6"> const nextSlug = currentIndex < allSlugs.length - 1 ? allSlugs[currentIndex + 1] : null;
<Image
src={post.frontmatter.teaser}
alt={`${post.frontmatter.title} teaser image`}
width={800}
height={400}
className="rounded-lg"
/>
</div>
)}
{post.frontmatter.tags && ( const prevPost = prevSlug ? await getPostBySlug('projects', prevSlug) : null;
<div className="flex flex-wrap gap-2 mt-8"> const nextPost = nextSlug ? await getPostBySlug('projects', nextSlug) : null;
{post.frontmatter.tags.map((tag: string) => (
<Link const navigation = {
key={tag} prev: prevPost ? { slug: prevSlug, title: prevPost.frontmatter.title } : null,
href={`/tags/${tag}`} next: nextPost ? { slug: nextSlug, title: nextPost.frontmatter.title } : null,
className="px-3 py-1 text-sm font-medium bg-secondary text-secondary-foreground rounded-full hover:bg-primary hover:text-primary-foreground transition-colors" };
>
{tag} return <ProjectArticle post={post} publications={publications} navigation={navigation} />;
</Link> }
))}
</div>
)}
<div>
{post.frontmatter.icon ? (
// If there is an icon, render it and then the content.
// The parent div with `flow-root` contains the float correctly.
<div className="flow-root">
<Image
src={post.frontmatter.icon}
alt={`${post.frontmatter.title} icon`}
width={80}
height={80}
className="float-left mr-4 mb-2 rounded-full"
/>
<CustomMDX {...post.source} />
</div>
) : (
// If there is NO icon, wrap the content in a div for the drop-cap effect.
<div className="first-letter:float-left first-letter:mr-3 first-letter:text-5xl first-letter:font-bold first-letter:leading-none first-letter:text-primary first-letter:pr-1">
<CustomMDX {...post.source} />
</div>
)}
</div>
<ReferencesContainer />
</article>
</main>
</CitationProvider>
);
}

View File

@@ -50,13 +50,15 @@ export default function PublicationsPage() {
</Link> </Link>
))} ))}
</div> </div>
<div className="space-y-4"> <div className="space-y-8"> {/* Increased spacing between year sections */}
{years.map((year) => ( {years.map((year) => (
<div key={year} className="relative"> <div key={year} className="flex flex-col md:flex-row md:space-x-8">
<h2 className="sticky top-18 z-10 -ml-4 pl-4 pr-4 py-2 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 font-bold text-xl"> <div className="md:w-16 flex-shrink-0">
{year} <h2 className="sticky top-20 font-bold text-xl text-muted-foreground md:text-right">
</h2> {year}
<div className="space-y-4 pt-4"> </h2>
</div>
<div className="flex-grow space-y-4 pt-2 md:pt-0"> {/* Main content column */}
{publicationsByYear[year].map((pub) => ( {publicationsByYear[year].map((pub) => (
<PublicationCard <PublicationCard
key={pub.key} key={pub.key}

View File

@@ -9,13 +9,14 @@ export function InfoBox({ title, children, className, ...props }: InfoBoxProps)
return ( return (
<aside <aside
className={cn( className={cn(
"relative mt-0 md:mt-2 md:float-right md:w-64 md:ml-4 md:mb-4 p-2 border rounded-lg shadow-sm bg-card text-card-foreground", // The `not-prose` class will now work correctly.
"not-prose relative mt-4 mb-8 md:float-right md:w-64 md:ml-4 md:mb-4 p-4 border rounded-lg shadow-sm bg-card text-card-foreground",
className className
)} )}
{...props} {...props}
> >
<h3 className="font-bold mt-0 mb-0 text-lg">{title}</h3> <h3 className="font-bold mt-0 mb-2 text-lg">{title}</h3>
<div className="text-sm space-y-0 mb-0">{children}</div> <div className="text-sm space-y-2">{children}</div>
</aside> </aside>
); );
} }

View File

@@ -74,3 +74,15 @@ export const mdxComponents = {
InfoBox, InfoBox,
Cite, Cite,
}; };
const serverSafeComponents = {
// KEEP components that DO NOT use hooks
h1: mdxComponents.h1,
h2: mdxComponents.h2,
h3: mdxComponents.h3,
h4: mdxComponents.h4,
h5: mdxComponents.h5,
h6: mdxComponents.h6,
Image: mdxComponents.Image,
a: mdxComponents.a,
};

View File

@@ -0,0 +1,110 @@
"use client";
import { CitationProvider } from '@/context/citation-context';
import { ReferencesContainer } from '@/components/references-container';
import Image from 'next/image';
import { CustomMDX } from '@/components/custom-mdx';
import Link from 'next/link';
import { Publication } from '@/lib/publications';
import { useAccentColor } from '@/context/accent-color-context';
import { ProjectNavigation } from './project-navigation';
// Define the types for our props
interface Post {
source: any;
frontmatter: {
title: string;
excerpt?: string;
show_teaser?: boolean;
teaser?: string;
tags?: string[];
icon?: string;
};
}
interface NavigationLink {
slug: string;
title: string;
}
interface ProjectArticleProps {
post: Post;
publications: Publication[];
navigation: {
prev: NavigationLink | null;
next: NavigationLink | null;
};
basePath: string;
}
export function ProjectArticle({ post, publications, navigation, basePath }: ProjectArticleProps) {
const accentColor = useAccentColor();
return (
<CitationProvider publications={publications}>
<main className="flex flex-col min-h-[100dvh] py-8" style={{ '--accent-color': accentColor } as React.CSSProperties}>
<article className="prose prose-stone dark:prose-invert max-w-none">
<header className="mb-8">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl mb-2">
{post.frontmatter.title}
</h1>
{post.frontmatter.excerpt && (
<p className="text-lg text-muted-foreground mt-1">{post.frontmatter.excerpt}</p>
)}
</header>
{post.frontmatter.show_teaser && post.frontmatter.teaser && (
<div className="my-6">
<Image
src={post.frontmatter.teaser}
alt={`${post.frontmatter.title} teaser image`}
width={800}
height={400}
className="rounded-lg"
/>
</div>
)}
{post.frontmatter.tags && (
<div className="flex flex-wrap gap-2 mt-8">
{post.frontmatter.tags.map((tag: string) => (
<Link
key={tag}
href={`/tags/${tag}`}
className="px-3 py-1 text-sm font-medium bg-secondary text-secondary-foreground hover:text-primary-foreground transition-colors"
onMouseOver={(e) => e.currentTarget.style.backgroundColor = accentColor}
onMouseOut={(e) => e.currentTarget.style.backgroundColor = ''}
>
{tag}
</Link>
))}
</div>
)}
<div className="mt-8">
{post.frontmatter.icon ? (
<div className="flow-root">
<Image
src={post.frontmatter.icon}
alt={`${post.frontmatter.title} icon`}
width={80}
height={80}
className="float-left mr-4 mb-2"
/>
<CustomMDX {...post.source} />
</div>
) : (
<div className="first-letter:float-left first-letter:mr-3 first-letter:text-5xl first-letter:font-bold first-letter:leading-none first-letter:text-primary first-letter:pr-1">
<CustomMDX {...post.source} />
</div>
)}
</div>
<ReferencesContainer />
<ProjectNavigation prev={navigation.prev} next={navigation.next} basePath={basePath} />
</article>
</main>
</CitationProvider>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import Link from 'next/link';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import { useAccentColor } from '@/context/accent-color-context';
interface NavLink {
slug: string;
title: string;
}
interface ProjectNavigationProps {
prev?: NavLink | null;
next?: NavLink | null;
// --- FIX #1: Added basePath prop for reusability ---
basePath: string;
}
export function ProjectNavigation({ prev, next, basePath }: ProjectNavigationProps) {
const accentColor = useAccentColor();
if (!prev && !next) return null;
return (
// --- FIX #2: Added 'not-prose' to remove unwanted link styles ---
<div className="not-prose grid grid-cols-1 md:grid-cols-2 gap-4 mt-12 pt-8 border-t">
<div>
{prev && (
// Use the dynamic basePath
<Link href={`/${basePath}/${prev.slug}`} className="block p-4 border rounded-lg hover:border-primary transition-colors h-full">
<div className="text-sm text-muted-foreground flex items-center gap-2">
<ArrowLeft size={16} />
Previous
</div>
<div className="font-semibold truncate" style={{ color: accentColor }}>
{prev.title}
</div>
</Link>
)}
</div>
<div className="md:text-right">
{next && (
// Use the dynamic basePath
<Link href={`/${basePath}/${next.slug}`} className="block p-4 border rounded-lg hover:border-primary transition-colors h-full">
<div className="text-sm text-muted-foreground flex items-center gap-2 md:justify-end">
Next
<ArrowRight size={16} />
</div>
<div className="font-semibold truncate" style={{ color: accentColor }}>
{next.title}
</div>
</Link>
)}
</div>
</div>
);
}

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { Card, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { FileText, Copy, Download, Paperclip } from "lucide-react"; import { FileText, Copy, Download, Paperclip, BookOpen, Check, X } from "lucide-react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; // Import the Link component import Link from "next/link";
import { CardTitle } from "@/components/ui/card";
interface Props { interface Props {
bibtexKey: string; bibtexKey: string;
@@ -17,7 +17,8 @@ interface Props {
pdfUrl?: string; pdfUrl?: string;
bibtex: string; bibtex: string;
className?: string; className?: string;
pdfAvailable: boolean; // --- FIX IS HERE: Made pdfAvailable optional ---
pdfAvailable?: boolean;
} }
export function PublicationCard({ export function PublicationCard({
@@ -30,9 +31,10 @@ export function PublicationCard({
pdfUrl, pdfUrl,
bibtex, bibtex,
className, className,
pdfAvailable, // --- FIX IS HERE: Default pdfAvailable to false if not provided ---
pdfAvailable = false,
}: Props) { }: Props) {
const [copied, setCopied] = useState(false); const [copyStatus, setCopyStatus] = useState<"idle" | "success" | "error">("idle");
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null); const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
@@ -44,9 +46,16 @@ export function PublicationCard({
e.stopPropagation(); e.stopPropagation();
if (isClient && navigator.clipboard?.writeText) { if (isClient && navigator.clipboard?.writeText) {
navigator.clipboard.writeText(bibtex).then(() => { navigator.clipboard.writeText(bibtex).then(() => {
setCopied(true); setCopyStatus("success");
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopyStatus("idle"), 2000);
}).catch(err => console.error("Failed to copy BibTeX:", err)); }).catch(err => {
console.error("Failed to copy BibTeX:", err);
setCopyStatus("error");
setTimeout(() => setCopyStatus("idle"), 2000);
});
} else {
setCopyStatus("error");
setTimeout(() => setCopyStatus("idle"), 2000);
} }
}; };
@@ -69,7 +78,7 @@ export function PublicationCard({
const ImageContent = ( const ImageContent = (
<> <>
{!imageError ? ( {!imageError ? (
<Image src={imageUrl} alt={`Preview for ${title}`} fill className="rounded-md object-cover" onError={() => setImageError(true)} sizes="(max-width: 768px) 100vw, 33vw" /> <Image src={imageUrl} alt={`Preview for ${title}`} fill className="rounded-md object-cover" onError={() => setImageError(true)} sizes="96px" />
) : ( ) : (
<div className="flex items-center justify-center w-full h-full bg-muted/20 rounded-md"> <div className="flex items-center justify-center w-full h-full bg-muted/20 rounded-md">
<Paperclip className="h-6 w-6 text-muted-foreground" /> <Paperclip className="h-6 w-6 text-muted-foreground" />
@@ -79,42 +88,47 @@ export function PublicationCard({
); );
return ( return (
<div className={cn("flex items-center space-x-4 p-4 border rounded-lg shadow-sm hover:shadow-lg transition-all duration-300 ease-out", className)} id={bibtexKey}> <div className={cn("flex items-start space-x-4 p-4 border rounded-lg shadow-sm hover:shadow-lg transition-all duration-300 ease-out", className)} id={bibtexKey}>
{url ? ( <div className="flex-shrink-0 w-24 h-24 relative">
<Link href={url} target="_blank" rel="noopener noreferrer" className="flex-shrink-0 w-24 h-24 relative"> {url ? (
{ImageContent} <Link href={url} target="_blank" rel="noopener noreferrer" className="block w-full h-full">
</Link> {ImageContent}
) : ( </Link>
<div className="flex-shrink-0 w-24 h-24 relative">{ImageContent}</div> ) : (
)} ImageContent
<div className="flex-grow flex flex-col md:flex-row items-start justify-between space-y-2 md:space-y-0 md:space-x-4"> )}
<div className="flex-grow space-y-1"> </div>
<CardTitle className="text-base font-medium leading-snug"> <div className="flex-grow space-y-1">
{url ? ( <CardTitle className="text-base font-medium leading-snug">
<Link href={url} target="_blank" rel="noopener noreferrer" className="hover:underline"> {url ? (
{title} <Link href={url} target="_blank" rel="noopener noreferrer" className="hover:underline">
</Link> {title}
) : ( </Link>
title ) : (
)} title
</CardTitle>
<p className="text-sm text-muted-foreground">
{formattedAuthors}. <em>{journal}</em>. {year}
</p>
</div>
<div className="flex flex-col space-y-1 items-end self-start md:self-auto">
<Button variant="ghost" size="sm" className="px-2 h-7 w-7" onClick={handleDownload} title="Download BibTeX"><Download className="h-4 w-4" /></Button>
<Button variant="ghost" size="sm" className="px-2 h-7 w-7 relative" onClick={handleCopy} title="Copy BibTeX">
<Copy className="h-4 w-4" />
{copied && <span className="absolute -right-full top-1/2 -translate-y-1/2 ml-2 bg-green-500 text-white text-xs px-2 py-1 rounded whitespace-nowrap animate-fade-in">Copied!</span>}
</Button>
{pdfUrl && pdfAvailable && (
<Button variant="ghost" size="sm" className="px-2 h-7 w-7" asChild onClick={(e) => e.stopPropagation()}>
<a href={pdfUrl} target="_blank" rel="noopener noreferrer" title="Open PDF"><FileText className="h-4 w-4" /></a>
</Button>
)} )}
</div> </CardTitle>
<p className="text-sm text-muted-foreground">
{formattedAuthors}. <em>{journal}</em>. {year}
</p>
</div>
<div className="flex flex-col space-y-1 flex-shrink-0">
<Button variant="ghost" size="sm" className="px-2 h-7 w-7" onClick={handleDownload} title="Download BibTeX">
<FileText className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="px-2 h-7 w-7 relative" onClick={handleCopy} title="Copy BibTeX">
{copyStatus === 'idle' && <Copy className="h-4 w-4" />}
{copyStatus === 'success' && <Check className="h-4 w-4 text-green-500" />}
{copyStatus === 'error' && <X className="h-4 w-4 text-red-500" />}
</Button>
{pdfUrl && pdfAvailable && (
<Button variant="ghost" size="sm" className="px-2 h-7 w-7" asChild onClick={(e) => e.stopPropagation()}>
<a href={pdfUrl} target="_blank" rel="noopener noreferrer" title="Open PDF">
<BookOpen className="h-4 w-4" />
</a>
</Button>
)}
</div> </div>
</div> </div>
); );
} }

View File

@@ -2,11 +2,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useCitations } from '@/context/citation-context'; import { useCitations } from '@/context/citation-context';
import { PublicationCard } from './publication-card'; // Changed import import { PublicationCard } from './publication-card';
import { BookOpen } from 'lucide-react'; import { BookOpen } from 'lucide-react';
export function ReferencesContainer() { export function ReferencesContainer() {
const { citedKeys, getPublicationByKey } = useCitations(); // Added getPublicationByKey const { citedKeys, getPublicationByKey } = useCitations();
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
useEffect(() => { useEffect(() => {
@@ -20,12 +20,13 @@ export function ReferencesContainer() {
const sortedKeys = Array.from(citedKeys).sort(); const sortedKeys = Array.from(citedKeys).sort();
return ( return (
<div className="mt-8 pt-4 border-t"> {/* Adjusted whitespace */} <div className="mt-8 pt-4 border-t">
<h2 className="text-2xl font-bold mb-2 flex items-center gap-2"> <h2 className="text-2xl font-bold mb-2 flex items-center gap-2">
<BookOpen className="h-6 w-6" /> <BookOpen className="h-6 w-6" />
References References
</h2> </h2>
<div className="space-y-4"> {/* --- FIX IS IN THE LINE BELOW: Added not-prose --- */}
<div className="not-prose space-y-4">
{sortedKeys.map(key => { {sortedKeys.map(key => {
const pub = getPublicationByKey(key); const pub = getPublicationByKey(key);
if (!pub) return null; if (!pub) return null;
@@ -39,10 +40,11 @@ export function ReferencesContainer() {
year={pub.year} year={pub.year}
pdfUrl={pub.pdfUrl} pdfUrl={pub.pdfUrl}
bibtex={pub.bibtex} bibtex={pub.bibtex}
// The pdfAvailable prop is now safely omitted
/> />
); );
})} })}
</div> </div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,35 @@
"use client";
import React, { createContext, useContext, useMemo } from 'react';
// A curated palette of visually appealing accent colors from Tailwind's palette
const ACCENT_COLORS = [
'#f43f5e', // rose-500
'#3b82f6', // blue-500
'#16a34a', // green-600
'#8b5cf6', // violet-500
'#f97316', // orange-500
'#06b6d4', // cyan-500
'#d946ef', // fuchsia-500
];
const AccentColorContext = createContext<string>(ACCENT_COLORS[0]);
export function AccentColorProvider({ children }: { children: React.ReactNode }) {
const accentColor = useMemo(() => {
// This calculation ensures the color is stable for the duration
const interval = 180 * 60 * 1000; // 180 minutes in milliseconds
const timeBucket = Math.floor(Date.now() / interval);
return ACCENT_COLORS[timeBucket % ACCENT_COLORS.length];
}, []); // Empty dependency array means this runs only once on mount
return (
<AccentColorContext.Provider value={accentColor}>
{children}
</AccentColorContext.Provider>
);
}
export function useAccentColor() {
return useContext(AccentColorContext);
}