smaller adjustments and server client seperation for projects
This commit is contained in:
@@ -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
|
||||
---
|
||||
|
||||
<div className="container">
|
||||
<InfoBox title="Project Resources">
|
||||
{/* The InfoBox now contains ALL metadata, creating a clean sidebar. */}
|
||||
<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' }}>
|
||||
<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://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>
|
||||
</ul>
|
||||
<Image src="/images/projects/full_domain.png" alt="logo" style={{ margin: '0em', padding: '0em', width: '15em' }} />
|
||||
</InfoBox>
|
||||
<div className="main-content" style={{ float: 'left', width: '75%' }}>
|
||||
</InfoBox>
|
||||
|
||||
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/>
|
||||
**Partner:** [Fraunhofer Institute for Cognitive Systems (IKS)](https://www.iks.fraunhofer.de/)<br/>
|
||||
**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>
|
||||
|
||||
---
|
||||
{/* All the main content now flows naturally in a single column. */}
|
||||
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).
|
||||
|
||||
To facilitate research into these phenomena, key contributions included the development of specialized simulation tools:
|
||||
|
||||
**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/).
|
||||
* **Purpose:** Designed specifically for training and evaluating reinforcement learning algorithms in multi-agent contexts prone to emergent behaviors.
|
||||
* **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.
|
||||
|
||||
**2. Unity-Based Demonstrator Unit:**
|
||||
|
||||
* 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.
|
||||
* **Utility:** Aids researchers in identifying and understanding the mechanisms behind observed emergent dynamics.
|
||||
* [View Demonstrator on GitHub](https://github.com/illiumst/F-IKS_demonstrator)
|
||||
|
||||
<div style={{ clear: 'both' }}></div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -142,3 +142,13 @@ code[data-line-numbers-max-digits="2"] > [data-line]::before {
|
||||
code[data-line-numbers-max-digits="3"] > [data-line]::before {
|
||||
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;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter as FontSans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { AccentColorProvider } from "@/context/accent-color-context";
|
||||
|
||||
const fontSans = FontSans({
|
||||
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>
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
<AccentColorProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Header />
|
||||
{children}
|
||||
<Navbar />
|
||||
</TooltipProvider>
|
||||
</AccentColorProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getPostBySlug, getPostSlugs } from '@/lib/mdx';
|
||||
import { CitationProvider } from '@/context/citation-context';
|
||||
import { ReferencesContainer } from '@/components/references-container';
|
||||
import { getPostBySlug, getPostSlugs, getPostFrontmatter } from '@/lib/mdx'; // Assume getPostFrontmatter exists
|
||||
import { notFound } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { DATA } from '@/data/resume';
|
||||
import { getPublicationsData } from '@/lib/publications';
|
||||
import { CustomMDX } from '@/components/custom-mdx';
|
||||
import Link from 'next/link';
|
||||
import { ProjectArticle } from '@/components/project-article';
|
||||
|
||||
// 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() {
|
||||
const slugs = getPostSlugs('projects');
|
||||
@@ -15,9 +15,7 @@ export async function generateStaticParams() {
|
||||
|
||||
export async function generateMetadata({ params }: { params: { slug: string } }) {
|
||||
const post = await getPostBySlug('projects', params.slug);
|
||||
if (!post) {
|
||||
return {};
|
||||
}
|
||||
if (!post) { return {}; }
|
||||
return {
|
||||
title: post.frontmatter.title,
|
||||
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 } }) {
|
||||
const post = await getPostBySlug('projects', params.slug);
|
||||
const { slug } = params;
|
||||
const post = await getPostBySlug('projects', slug);
|
||||
const publications = getPublicationsData();
|
||||
|
||||
if (!post) {
|
||||
return notFound();
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<CitationProvider publications={publications}>
|
||||
<main className="flex flex-col min-h-[100dvh]">
|
||||
<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>
|
||||
// --- Logic for Previous/Next Navigation ---
|
||||
const allSlugs = getPostSlugs('projects'); // Or get them from your DATA file if they are ordered there
|
||||
const currentIndex = allSlugs.findIndex((s) => s === slug);
|
||||
|
||||
{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>
|
||||
)}
|
||||
const prevSlug = currentIndex > 0 ? allSlugs[currentIndex - 1] : null;
|
||||
const nextSlug = currentIndex < allSlugs.length - 1 ? allSlugs[currentIndex + 1] : null;
|
||||
|
||||
{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 rounded-full hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
const prevPost = prevSlug ? await getPostBySlug('projects', prevSlug) : null;
|
||||
const nextPost = nextSlug ? await getPostBySlug('projects', nextSlug) : null;
|
||||
|
||||
<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>
|
||||
);
|
||||
const navigation = {
|
||||
prev: prevPost ? { slug: prevSlug, title: prevPost.frontmatter.title } : null,
|
||||
next: nextPost ? { slug: nextSlug, title: nextPost.frontmatter.title } : null,
|
||||
};
|
||||
|
||||
return <ProjectArticle post={post} publications={publications} navigation={navigation} />;
|
||||
}
|
||||
@@ -50,13 +50,15 @@ export default function PublicationsPage() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-8"> {/* Increased spacing between year sections */}
|
||||
{years.map((year) => (
|
||||
<div key={year} className="relative">
|
||||
<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 key={year} className="flex flex-col md:flex-row md:space-x-8">
|
||||
<div className="md:w-16 flex-shrink-0">
|
||||
<h2 className="sticky top-20 font-bold text-xl text-muted-foreground md:text-right">
|
||||
{year}
|
||||
</h2>
|
||||
<div className="space-y-4 pt-4">
|
||||
</div>
|
||||
<div className="flex-grow space-y-4 pt-2 md:pt-0"> {/* Main content column */}
|
||||
{publicationsByYear[year].map((pub) => (
|
||||
<PublicationCard
|
||||
key={pub.key}
|
||||
|
||||
@@ -9,13 +9,14 @@ export function InfoBox({ title, children, className, ...props }: InfoBoxProps)
|
||||
return (
|
||||
<aside
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<h3 className="font-bold mt-0 mb-0 text-lg">{title}</h3>
|
||||
<div className="text-sm space-y-0 mb-0">{children}</div>
|
||||
<h3 className="font-bold mt-0 mb-2 text-lg">{title}</h3>
|
||||
<div className="text-sm space-y-2">{children}</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -74,3 +74,15 @@ export const mdxComponents = {
|
||||
InfoBox,
|
||||
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,
|
||||
};
|
||||
110
src/components/project-article.tsx
Normal file
110
src/components/project-article.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
src/components/project-navigation.tsx
Normal file
57
src/components/project-navigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
import { Card, CardTitle } from "@/components/ui/card";
|
||||
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 { cn } from "@/lib/utils";
|
||||
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 {
|
||||
bibtexKey: string;
|
||||
@@ -17,7 +17,8 @@ interface Props {
|
||||
pdfUrl?: string;
|
||||
bibtex: string;
|
||||
className?: string;
|
||||
pdfAvailable: boolean;
|
||||
// --- FIX IS HERE: Made pdfAvailable optional ---
|
||||
pdfAvailable?: boolean;
|
||||
}
|
||||
|
||||
export function PublicationCard({
|
||||
@@ -30,9 +31,10 @@ export function PublicationCard({
|
||||
pdfUrl,
|
||||
bibtex,
|
||||
className,
|
||||
pdfAvailable,
|
||||
// --- FIX IS HERE: Default pdfAvailable to false if not provided ---
|
||||
pdfAvailable = false,
|
||||
}: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copyStatus, setCopyStatus] = useState<"idle" | "success" | "error">("idle");
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
@@ -44,9 +46,16 @@ export function PublicationCard({
|
||||
e.stopPropagation();
|
||||
if (isClient && navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(bibtex).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}).catch(err => console.error("Failed to copy BibTeX:", err));
|
||||
setCopyStatus("success");
|
||||
setTimeout(() => setCopyStatus("idle"), 2000);
|
||||
}).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 = (
|
||||
<>
|
||||
{!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">
|
||||
<Paperclip className="h-6 w-6 text-muted-foreground" />
|
||||
@@ -79,15 +88,16 @@ export function PublicationCard({
|
||||
);
|
||||
|
||||
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}>
|
||||
<div className="flex-shrink-0 w-24 h-24 relative">
|
||||
{url ? (
|
||||
<Link href={url} target="_blank" rel="noopener noreferrer" className="flex-shrink-0 w-24 h-24 relative">
|
||||
<Link href={url} target="_blank" rel="noopener noreferrer" className="block w-full h-full">
|
||||
{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>
|
||||
<div className="flex-grow space-y-1">
|
||||
<CardTitle className="text-base font-medium leading-snug">
|
||||
{url ? (
|
||||
@@ -102,19 +112,23 @@ export function PublicationCard({
|
||||
{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>
|
||||
<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">
|
||||
<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>}
|
||||
{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"><FileText className="h-4 w-4" /></a>
|
||||
<a href={pdfUrl} target="_blank" rel="noopener noreferrer" title="Open PDF">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useCitations } from '@/context/citation-context';
|
||||
import { PublicationCard } from './publication-card'; // Changed import
|
||||
import { PublicationCard } from './publication-card';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
|
||||
export function ReferencesContainer() {
|
||||
const { citedKeys, getPublicationByKey } = useCitations(); // Added getPublicationByKey
|
||||
const { citedKeys, getPublicationByKey } = useCitations();
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,12 +20,13 @@ export function ReferencesContainer() {
|
||||
const sortedKeys = Array.from(citedKeys).sort();
|
||||
|
||||
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">
|
||||
<BookOpen className="h-6 w-6" />
|
||||
References
|
||||
</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 => {
|
||||
const pub = getPublicationByKey(key);
|
||||
if (!pub) return null;
|
||||
@@ -39,6 +40,7 @@ export function ReferencesContainer() {
|
||||
year={pub.year}
|
||||
pdfUrl={pub.pdfUrl}
|
||||
bibtex={pub.bibtex}
|
||||
// The pdfAvailable prop is now safely omitted
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
35
src/context/accent-color-context.tsx
Normal file
35
src/context/accent-color-context.tsx
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user