generated from nhcarrigan/template
feat: add "Meet the Team" cast panel
This commit is contained in:
@@ -0,0 +1,140 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { CHARACTER_POOL } from "$lib/utils/agentCharacters";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onClose }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
onclick={onClose}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) => e.key === "Escape" && onClose()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[90vh] overflow-y-auto"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="cast-title"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 id="cast-title" class="text-xl font-semibold text-[var(--text-primary)]">
|
||||||
|
Meet the Team
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onclick={onClose}
|
||||||
|
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Principal cast: Hikari + Naomi -->
|
||||||
|
<div class="grid grid-cols-1 gap-3 mb-6 sm:grid-cols-2">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 p-4 rounded-lg bg-[var(--bg-secondary)] border border-[var(--accent-primary)]/40"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://cdn.nhcarrigan.com/hikari.png"
|
||||||
|
alt="Hikari"
|
||||||
|
class="w-16 h-16 object-cover rounded-full border-2 border-[var(--border-color)] shrink-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="font-semibold text-[var(--text-primary)]">Hikari</span>
|
||||||
|
<span
|
||||||
|
class="text-xs px-2 py-0.5 rounded-full bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] font-medium"
|
||||||
|
>
|
||||||
|
Chief Operating Officer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)]">
|
||||||
|
Holds the line so the others don't have to. Never without her clipboard — or her
|
||||||
|
glasses.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 p-4 rounded-lg bg-[var(--bg-secondary)] border border-[var(--accent-primary)]/40"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://cdn.nhcarrigan.com/profile.png"
|
||||||
|
alt="Naomi"
|
||||||
|
class="w-16 h-16 object-cover rounded-full border-2 border-[var(--border-color)] shrink-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="font-semibold text-[var(--text-primary)]">Naomi</span>
|
||||||
|
<span
|
||||||
|
class="text-xs px-2 py-0.5 rounded-full bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] font-medium"
|
||||||
|
>
|
||||||
|
Chief hEx-ecutive Officer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)]">
|
||||||
|
A 525-year-old vampire running a tech company from behind a VTuber avatar. Fixes server
|
||||||
|
crashes at 4 AM.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subagent girls grid -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-[var(--text-secondary)] uppercase tracking-wider mb-3">
|
||||||
|
Subagent Squad
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||||
|
{#each CHARACTER_POOL as character (character.name)}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center gap-2 p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)] text-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={character.avatar}
|
||||||
|
alt={character.name}
|
||||||
|
class="w-14 h-14 object-cover rounded-full border-2 border-[var(--border-color)]"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-[var(--text-primary)]">{character.name}</span>
|
||||||
|
<span
|
||||||
|
class="text-xs px-2 py-0.5 rounded-full bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] font-medium"
|
||||||
|
>
|
||||||
|
{character.title}
|
||||||
|
</span>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)] leading-snug">{character.description}</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[role="dialog"] {
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
import GitPanel from "./GitPanel.svelte";
|
import GitPanel from "./GitPanel.svelte";
|
||||||
import ProfilePanel from "./ProfilePanel.svelte";
|
import ProfilePanel from "./ProfilePanel.svelte";
|
||||||
import AgentMonitorPanel from "./AgentMonitorPanel.svelte";
|
import AgentMonitorPanel from "./AgentMonitorPanel.svelte";
|
||||||
|
import CastPanel from "./CastPanel.svelte";
|
||||||
import PluginManagementPanel from "./PluginManagementPanel.svelte";
|
import PluginManagementPanel from "./PluginManagementPanel.svelte";
|
||||||
import McpManagementPanel from "./McpManagementPanel.svelte";
|
import McpManagementPanel from "./McpManagementPanel.svelte";
|
||||||
import { conversationsStore } from "$lib/stores/conversations";
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
let showGitPanel = $state(false);
|
let showGitPanel = $state(false);
|
||||||
let showProfile = $state(false);
|
let showProfile = $state(false);
|
||||||
let showAgentMonitor = $state(false);
|
let showAgentMonitor = $state(false);
|
||||||
|
let showCastPanel = $state(false);
|
||||||
let showPluginPanel = $state(false);
|
let showPluginPanel = $state(false);
|
||||||
let showMcpPanel = $state(false);
|
let showMcpPanel = $state(false);
|
||||||
let isSummarising = $state(false);
|
let isSummarising = $state(false);
|
||||||
@@ -519,6 +521,20 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => (showCastPanel = true)}
|
||||||
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
|
title="Meet the Team"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showAgentMonitor = !showAgentMonitor)}
|
onclick={() => (showAgentMonitor = !showAgentMonitor)}
|
||||||
class="p-1 text-gray-500 icon-trans-hover relative {showAgentMonitor
|
class="p-1 text-gray-500 icon-trans-hover relative {showAgentMonitor
|
||||||
@@ -737,6 +753,10 @@
|
|||||||
<AgentMonitorPanel isOpen={showAgentMonitor} onClose={() => (showAgentMonitor = false)} />
|
<AgentMonitorPanel isOpen={showAgentMonitor} onClose={() => (showAgentMonitor = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showCastPanel}
|
||||||
|
<CastPanel onClose={() => (showCastPanel = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showPluginPanel}
|
{#if showPluginPanel}
|
||||||
<PluginManagementPanel onClose={() => (showPluginPanel = false)} />
|
<PluginManagementPanel onClose={() => (showPluginPanel = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ describe("agentCharacters", () => {
|
|||||||
expect(CHARACTER_POOL).toHaveLength(6);
|
expect(CHARACTER_POOL).toHaveLength(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("each character has a name and avatar", () => {
|
it("each character has a name, avatar, title, and description", () => {
|
||||||
for (const character of CHARACTER_POOL) {
|
for (const character of CHARACTER_POOL) {
|
||||||
expect(character.name).toBeTruthy();
|
expect(character.name).toBeTruthy();
|
||||||
expect(character.avatar).toBeTruthy();
|
expect(character.avatar).toBeTruthy();
|
||||||
expect(character.avatar).toMatch(/^https:\/\//u);
|
expect(character.avatar).toMatch(/^https:\/\//u);
|
||||||
|
expect(character.title).toBeTruthy();
|
||||||
|
expect(character.description).toBeTruthy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,10 +57,12 @@ describe("agentCharacters", () => {
|
|||||||
expect(seen.size).toBeGreaterThan(1);
|
expect(seen.size).toBeGreaterThan(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a character with both name and avatar", () => {
|
it("returns a character with name, avatar, title, and description", () => {
|
||||||
const character = assignCharacter([]);
|
const character = assignCharacter([]);
|
||||||
expect(character.name).toBeTruthy();
|
expect(character.name).toBeTruthy();
|
||||||
expect(character.avatar).toBeTruthy();
|
expect(character.avatar).toBeTruthy();
|
||||||
|
expect(character.title).toBeTruthy();
|
||||||
|
expect(character.description).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("works when the active list is empty", () => {
|
it("works when the active list is empty", () => {
|
||||||
|
|||||||
@@ -1,15 +1,53 @@
|
|||||||
export interface AgentCharacter {
|
export interface AgentCharacter {
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHARACTER_POOL: readonly AgentCharacter[] = [
|
export const CHARACTER_POOL: readonly AgentCharacter[] = [
|
||||||
{ name: "Amari", avatar: "https://cdn.nhcarrigan.com/amari.png" },
|
{
|
||||||
{ name: "Keiko", avatar: "https://cdn.nhcarrigan.com/keiko.png" },
|
name: "Amari",
|
||||||
{ name: "Minori", avatar: "https://cdn.nhcarrigan.com/minori.png" },
|
avatar: "https://cdn.nhcarrigan.com/amari.png",
|
||||||
{ name: "Reina", avatar: "https://cdn.nhcarrigan.com/reina.png" },
|
title: "Executive Assistant",
|
||||||
{ name: "Tatsumi", avatar: "https://cdn.nhcarrigan.com/tatsumi.png" },
|
description:
|
||||||
{ name: "Yumiko", avatar: "https://cdn.nhcarrigan.com/yumiko.png" },
|
"Fey-blooded PA and healer of the team. She always knows when you need a break — and makes sure you take one.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Keiko",
|
||||||
|
avatar: "https://cdn.nhcarrigan.com/keiko.png",
|
||||||
|
title: "Chief Security Officer",
|
||||||
|
description:
|
||||||
|
"Bodyguard and shadow of the family. Conceals blades beneath evening gowns; always watching from the dark.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Minori",
|
||||||
|
avatar: "https://cdn.nhcarrigan.com/minori.png",
|
||||||
|
title: "Chief Compliance Officer",
|
||||||
|
description:
|
||||||
|
"An ancient Automaton built to guard the Great Library. Perfect memory, perfect logic, perfect dedication.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reina",
|
||||||
|
avatar: "https://cdn.nhcarrigan.com/reina.png",
|
||||||
|
title: "Chief Legal Officer",
|
||||||
|
description:
|
||||||
|
"Demon of the Crossroads turned corporate lawyer. Her binding contracts have held for millennia.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tatsumi",
|
||||||
|
avatar: "https://cdn.nhcarrigan.com/tatsumi.png",
|
||||||
|
title: "Chief Design Officer",
|
||||||
|
description:
|
||||||
|
"A Siren who traded the ocean for a stylus. Uses her glamour to make every interface welcoming and beautiful.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Yumiko",
|
||||||
|
avatar: "https://cdn.nhcarrigan.com/yumiko.png",
|
||||||
|
title: "Chief Technology Officer",
|
||||||
|
description:
|
||||||
|
"Technomancer and machine whisperer. She communes with machine spirits and keeps the digital world running.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user