nyxcore-systems
6 min read

From Pixels to Pulse: Enhancing Dashboards with Dynamic Avatars and Flexible Analytics

Dive into our latest development sprint where we brought AI personas to life with auto-generated avatars and supercharged our project analytics with flexible date ranges for the Activity Pulse chart.

frontendbackendnextjstypescripttrpcavatarsdata-visualizationuxai-personasdashboardlessons-learned

Another late-night session wrapped, the kind where the code flows and the features click into place. As the clock ticked past 4 AM, two significant enhancements to our executive intelligence dashboard were complete, polished, and ready for commit. This post breaks down the journey, the technical decisions, and a valuable lesson learned about data visualization.

Our twin goals for this session were clear:

  1. Bring our AI personas to life with dynamic avatars, ensuring no more generic placeholders.
  2. Empower users with more control over their project insights by adding a date range selector to the Activity Pulse chart.

Let's dive into how we tackled these.

Bringing Our AI Personas to Life: Avatars Everywhere!

For a system driven by intelligent personas, having a visual identity is crucial. Our goal was not just to display avatars, but to intelligently generate them for any persona lacking an image, making our dashboard feel more vibrant and personalized.

The Backend Magic: Auto-Generating Avatars

The core of this feature lives on the server. We introduced a new generateMissingAvatars mutation in our tRPC router (src/server/trpc/routers/personas.ts). This mutation is smart:

  • It queries the database for all personas (both tenant-specific and built-in system personas) where imageUrl is null.
  • For each missing avatar, it extracts relevant information – primarily skills from specializations and systemPrompt – to feed into our createAvatar() utility. This ensures the generated avatar visually reflects the persona's role.
  • Finally, it updates the database with the newly generated imageUrl using a single updateMany operation for efficiency.
typescript
// src/server/trpc/routers/personas.ts (simplified excerpt)
// ...
generateMissingAvatars: publicProcedure.mutation(async ({ ctx }) => {
  const personasWithoutAvatars = await ctx.db.persona.findMany({
    where: { imageUrl: null },
    select: { id: true, specializations: true, systemPrompt: true },
  });

  const updates = await Promise.all(
    personasWithoutAvatars.map(async (persona) => {
      const avatarUrl = await createAvatar({
        skills: persona.specializations,
        prompt: persona.systemPrompt,
      });
      return { id: persona.id, imageUrl: avatarUrl };
    })
  );

  await ctx.db.persona.updateMany({
    data: updates, // This is not how updateMany works directly, simplified for blog.
    // Actual implementation would iterate or use a more complex upsert/update structure.
  });

  return { count: updates.length };
}),
// ...

Frontend Integration: Seamless Display and Regeneration

With the backend ready, the next step was weaving these avatars into the user interface:

  1. Dashboard Persona List (src/app/(dashboard)/dashboard/personas/page.tsx):

    • We removed the conditional rendering ({persona.imageUrl && ...}) to ensure an avatar circle is always displayed.
    • A clever fallback was added: if imageUrl is still null (perhaps before generation completes), we display the persona's initial letter. This provides immediate visual context.
    • To ensure new personas get avatars automatically, we added a useEffect hook with a useRef guard. This triggers generateMissingAvatars once on page load, ensuring all existing personas have their images.
    • Crucially, for locally-generated PNGs (which Next.js's Image component might try to optimize unnecessarily), we added the unoptimized prop to prevent broken links or unnecessary processing.
  2. Individual Persona Profile (src/app/(dashboard)/dashboard/personas/[id]/page.tsx):

    • The old nextImage portrait shuffler was replaced, directly utilizing our new generateAvatar mutation for a consistent experience.
    • A handleRegenerateAvatar() function now leverages this new generator, giving users the power to refresh an avatar if they wish.
    • Again, unoptimized was added to the Image component here.
  3. Widespread Avatar Adoption: We extended the unoptimized prop to Image components in other areas displaying persona avatars, including src/components/shared/persona-picker.tsx and src/components/dashboard/analytics/persona-overview-panel.tsx, ensuring consistent rendering across the application.

The result? A dashboard where every persona has a unique, contextually relevant avatar, making the experience significantly richer and more intuitive.

Gaining Deeper Insights with the Activity Pulse Chart

Our Executive Intelligence Dashboard features an "Activity Pulse" chart, providing a quick overview of project activity. Previously, this was fixed at a 30-day window. Users needed more flexibility.

Expanding the Data Horizon

First, we expanded the data available from the server. In src/server/trpc/routers/projects.ts, the activity timeline now fetches a generous 90 days of data, providing a broader canvas for analysis.

Client-Side Control: The Date Range Selector

The real magic happened in src/components/project/overview/activity-pulse.tsx:

  • Button Group: We introduced a clean button group (7D, 30D, 90D) allowing users to instantly switch their view.
  • Client-Side Filtering with useMemo: Instead of making multiple server requests, we fetch 90 days of data once and then filter it client-side based on the selected range. useMemo is crucial here, efficiently re-calculating the filtered data only when the selected range changes, preventing unnecessary re-renders and computations.
  • Dynamic Title: The chart's title now dynamically updates to reflect the selected range (e.g., "Activity Pulse — Last 7D" or "Activity Pulse — Last 90D"), providing clear context.
  • Default View: The chart defaults to the 7-day view, offering a focused, immediate snapshot.
typescript
// src/components/project/overview/activity-pulse.tsx (simplified excerpt)
import React, { useState, useMemo } from 'react';
// ... assuming data fetching occurs elsewhere and provides 90 days of activity

const ActivityPulseChart = ({ fullActivityData }) => {
  const [selectedRange, setSelectedRange] = useState(7); // Default to 7 days

  const filteredActivity = useMemo(() => {
    const today = new Date();
    const startDate = new Date(today);
    startDate.setDate(today.getDate() - selectedRange);

    return fullActivityData.filter(item => new Date(item.date) >= startDate);
  }, [fullActivityData, selectedRange]);

  const title = `Activity Pulse — Last ${selectedRange}D`;

  return (
    <div>
      <h3>{title}</h3>
      <div className="button-group">
        <button onClick={() => setSelectedRange(7)}>7D</button>
        <button onClick={() => setSelectedRange(30)}>30D</button>
        <button onClick={() => setSelectedRange(90)}>90D</button>
      </div>
      {/* Chart rendering logic using filteredActivity */}
    </div>
  );
};

This enhancement transforms the Activity Pulse from a static overview into a dynamic analytical tool, giving users immediate control over their data perspective.

A Charting Conundrum: The "Single Day" Dilemma (Lessons Learned)

Not every idea makes it to production, and sometimes the best lessons come from failed attempts. We initially tried adding a "1D" (1 day) option to the Activity Pulse chart.

The Problem: When you filter down to a single day, the chart only receives one data point. A single data point renders as a lonely dot – no line, no trend, no useful visualization. It was visually uninformative and cluttered the interface without providing value.

The Root Cause: Our server currently returns activity data with daily granularity. To meaningfully support a "1D" view that shows trends within a day, we would need hourly bucketing of activity data.

The Solution: We pragmatically removed the "1D" option. The 7-day default provides a readable chart with sufficient data points to show a trend, while 30D and 90D offer broader context. This decision highlights the importance of matching data granularity with visualization needs. This is a potential future enhancement, but for now, the current options provide the best balance of utility and clarity.

What's Next?

With dynamic avatars enriching our personas and flexible date ranges empowering our activity insights, the dashboard feels more robust and user-friendly.

Our immediate next steps involve committing these changes and then diving into an exciting, complex challenge: planning and implementing an enhanced notes enrichment pipeline. This will involve choosing personas for enrichment, integrating project and global wisdom, generating action points, and building robust workflows – a journey we'll surely share insights from in a future post!