chore: fix lints and tests
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 53s
CI / Lint & Test (pull_request) Failing after 7m36s
CI / Build Linux (pull_request) Has been skipped
CI / Build Windows (cross-compile) (pull_request) Has been skipped

This commit is contained in:
2026-01-19 19:51:10 -08:00
parent da566f408e
commit 27f69cb308
11 changed files with 1128 additions and 1003 deletions
@@ -1,23 +1,33 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from "svelte";
import { fade, fly } from 'svelte/transition'; import { fade, fly } from "svelte/transition";
import { cubicOut } from 'svelte/easing'; import { cubicOut } from "svelte/easing";
import { listen } from '@tauri-apps/api/event'; import { listen } from "@tauri-apps/api/event";
import type { AchievementUnlockedEvent } from '$lib/types/achievements'; import type { AchievementUnlockedEvent } from "$lib/types/achievements";
let achievements = $state<AchievementUnlockedEvent[]>([]); let achievements = $state<AchievementUnlockedEvent[]>([]);
let currentAchievement = $state<AchievementUnlockedEvent | null>(null); let currentAchievement = $state<AchievementUnlockedEvent | null>(null);
let showNotification = $state(false); let showNotification = $state(false);
onMount(async () => { onMount(() => {
const unlisten = await listen<AchievementUnlockedEvent>('achievement:unlocked', (event) => { let unlisten: (() => void) | undefined;
const setupListener = async () => {
unlisten = await listen<AchievementUnlockedEvent>("achievement:unlocked", (event) => {
achievements.push(event.payload); achievements.push(event.payload);
if (!showNotification) { if (!showNotification) {
showNext(); showNext();
} }
}); });
};
return unlisten; setupListener();
return () => {
if (unlisten) {
unlisten();
}
};
}); });
function showNext() { function showNext() {
@@ -42,19 +52,33 @@
function getRarityColor(rarity: string): string { function getRarityColor(rarity: string): string {
switch (rarity) { switch (rarity) {
case 'legendary': return 'from-yellow-400 to-orange-500'; case "legendary":
case 'epic': return 'from-purple-400 to-pink-500'; return "from-yellow-400 to-orange-500";
case 'rare': return 'from-blue-400 to-indigo-500'; case "epic":
default: return 'from-green-400 to-emerald-500'; return "from-purple-400 to-pink-500";
case "rare":
return "from-blue-400 to-indigo-500";
default:
return "from-green-400 to-emerald-500";
} }
} }
function getAchievementRarity(id: string): string { function getAchievementRarity(id: string): string {
// Determine rarity based on achievement ID // Determine rarity based on achievement ID
if (id === 'TokenMaster') return 'legendary'; if (id === "TokenMaster") return "legendary";
if (['CodeMachine', 'Unstoppable'].includes(id)) return 'epic'; if (["CodeMachine", "Unstoppable"].includes(id)) return "epic";
if (['BlossomingCoder', 'CodeWizard', 'MasterBuilder', 'EnduranceChamp', 'DeepDive', 'CreativeCoder'].includes(id)) return 'rare'; if (
return 'common'; [
"BlossomingCoder",
"CodeWizard",
"MasterBuilder",
"EnduranceChamp",
"DeepDive",
"CreativeCoder",
].includes(id)
)
return "rare";
return "common";
} }
</script> </script>
@@ -67,18 +91,27 @@
<!-- Backdrop with animated gradient border --> <!-- Backdrop with animated gradient border -->
<div class="relative p-[2px] rounded-lg overflow-hidden"> <div class="relative p-[2px] rounded-lg overflow-hidden">
<!-- Animated gradient border --> <!-- Animated gradient border -->
<div class="absolute inset-0 bg-gradient-to-r {getRarityColor(getAchievementRarity(currentAchievement.achievement.id))} animate-pulse"></div> <div
class="absolute inset-0 bg-gradient-to-r {getRarityColor(
getAchievementRarity(currentAchievement.achievement.id)
)} animate-pulse"
></div>
<!-- Main notification content --> <!-- Main notification content -->
<div class="relative bg-[var(--bg-primary)] rounded-lg p-4 shadow-2xl backdrop-blur-sm"> <div class="relative bg-[var(--bg-primary)] rounded-lg p-4 shadow-2xl backdrop-blur-sm">
<button <button
onclick={dismiss} onclick={dismiss}
onkeydown={(e) => e.key === 'Enter' && dismiss()} onkeydown={(e) => e.key === "Enter" && dismiss()}
class="absolute top-2 right-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors" class="absolute top-2 right-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
aria-label="Dismiss notification" aria-label="Dismiss notification"
> >
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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"></path> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg> </svg>
</button> </button>
@@ -89,13 +122,21 @@
<!-- Sparkle animations --> <!-- Sparkle animations -->
<div class="absolute -top-1 -right-1 text-yellow-400 animate-ping"></div> <div class="absolute -top-1 -right-1 text-yellow-400 animate-ping"></div>
<div class="absolute -bottom-1 -left-1 text-yellow-400 animate-ping animation-delay-200"></div> <div
<div class="absolute top-1/2 -right-2 text-yellow-400 animate-ping animation-delay-400"></div> class="absolute -bottom-1 -left-1 text-yellow-400 animate-ping animation-delay-200"
>
</div>
<div class="absolute top-1/2 -right-2 text-yellow-400 animate-ping animation-delay-400">
</div>
</div> </div>
<!-- Text content --> <!-- Text content -->
<div class="flex-1 min-w-0 pt-1"> <div class="flex-1 min-w-0 pt-1">
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"> <h3
class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
>
Achievement Unlocked! Achievement Unlocked!
</h3> </h3>
<p class="text-lg font-bold text-[var(--text-primary)] mt-1"> <p class="text-lg font-bold text-[var(--text-primary)] mt-1">
@@ -107,7 +148,11 @@
<!-- Rarity badge --> <!-- Rarity badge -->
<div class="mt-2 inline-flex items-center"> <div class="mt-2 inline-flex items-center">
<span class="px-2 py-1 text-xs font-medium rounded-full bg-gradient-to-r {getRarityColor(getAchievementRarity(currentAchievement.achievement.id))} text-white capitalize"> <span
class="px-2 py-1 text-xs font-medium rounded-full bg-gradient-to-r {getRarityColor(
getAchievementRarity(currentAchievement.achievement.id)
)} text-white capitalize"
>
{getAchievementRarity(currentAchievement.achievement.id)} {getAchievementRarity(currentAchievement.achievement.id)}
</span> </span>
</div> </div>
@@ -116,10 +161,13 @@
<!-- Celebration confetti effect (CSS only) --> <!-- Celebration confetti effect (CSS only) -->
<div class="absolute inset-0 pointer-events-none overflow-hidden rounded-lg"> <div class="absolute inset-0 pointer-events-none overflow-hidden rounded-lg">
{#each Array(10) as _, i} {#each Array(10) as _ (_)}
<div <div
class="absolute w-2 h-2 bg-gradient-to-br {getRarityColor(getAchievementRarity(currentAchievement.achievement.id))} rounded-full animate-fall" class="absolute w-2 h-2 bg-gradient-to-br {getRarityColor(
style="left: {Math.random() * 100}%; animation-delay: {Math.random() * 2}s; animation-duration: {2 + Math.random() * 2}s;" getAchievementRarity(currentAchievement.achievement.id)
)} rounded-full animate-fall"
style="left: {Math.random() * 100}%; animation-delay: {Math.random() *
2}s; animation-duration: {2 + Math.random() * 2}s;"
></div> ></div>
{/each} {/each}
</div> </div>
+79 -36
View File
@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide } from "svelte/transition";
import { quintOut } from 'svelte/easing'; import { quintOut } from "svelte/easing";
import { import {
achievementsStore, achievementsStore,
achievementProgress, achievementProgress,
achievementCategories, achievementCategories,
} from '$lib/stores/achievements'; } from "$lib/stores/achievements";
import type { Achievement } from '$lib/types/achievements'; import type { Achievement } from "$lib/types/achievements";
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClose?: () => void; onClose?: () => void;
} }
const { isOpen = $bindable(false), onClose } = $props<Props>(); const { isOpen = $bindable(false), onClose }: Props = $props();
let selectedCategory = $state<string | null>(null); let selectedCategory = $state<string | null>(null);
const achievementsState = $derived($achievementsStore); const achievementsState = $derived($achievementsStore);
@@ -21,33 +21,45 @@
function getRarityColor(rarity: string): string { function getRarityColor(rarity: string): string {
switch (rarity) { switch (rarity) {
case 'legendary': return 'text-yellow-500 dark:text-yellow-400'; case "legendary":
case 'epic': return 'text-purple-500 dark:text-purple-400'; return "text-yellow-500 dark:text-yellow-400";
case 'rare': return 'text-blue-500 dark:text-blue-400'; case "epic":
default: return 'text-green-500 dark:text-green-400'; return "text-purple-500 dark:text-purple-400";
case "rare":
return "text-blue-500 dark:text-blue-400";
default:
return "text-green-500 dark:text-green-400";
} }
} }
function getRarityBg(rarity: string): string { function getRarityBg(rarity: string): string {
switch (rarity) { switch (rarity) {
case 'legendary': return 'bg-yellow-500/10'; case "legendary":
case 'epic': return 'bg-purple-500/10'; return "bg-yellow-500/10";
case 'rare': return 'bg-blue-500/10'; case "epic":
default: return 'bg-green-500/10'; return "bg-purple-500/10";
case "rare":
return "bg-blue-500/10";
default:
return "bg-green-500/10";
} }
} }
function formatDate(date: Date | undefined): string { function formatDate(date: Date | undefined): string {
if (!date) return ''; if (!date) return "";
return new Date(date).toLocaleDateString('en-US', { return new Date(date).toLocaleDateString("en-US", {
month: 'short', month: "short",
day: 'numeric', day: "numeric",
year: 'numeric', year: "numeric",
}); });
} }
function getAchievementsForCategory(categoryIds: string[]): Achievement[] { function getAchievementsForCategory(categoryIds: string[]): Achievement[] {
return categoryIds.map(id => achievementsState.achievements[id as keyof typeof achievementsState.achievements]).filter(Boolean); return categoryIds
.map(
(id) => achievementsState.achievements[id as keyof typeof achievementsState.achievements]
)
.filter(Boolean);
} }
</script> </script>
@@ -56,7 +68,7 @@
<div <div
class="fixed inset-0 bg-black/50 z-40" class="fixed inset-0 bg-black/50 z-40"
onclick={onClose} onclick={onClose}
onkeydown={(e) => e.key === 'Escape' && onClose()} onkeydown={(e) => e.key === "Escape" && onClose?.()}
role="button" role="button"
tabindex="-1" tabindex="-1"
aria-label="Close achievements panel" aria-label="Close achievements panel"
@@ -66,7 +78,7 @@
<div <div
class="fixed left-0 top-0 h-full w-96 bg-[var(--bg-primary)] border-r border-[var(--border-color)] class="fixed left-0 top-0 h-full w-96 bg-[var(--bg-primary)] border-r border-[var(--border-color)]
shadow-2xl z-50 flex flex-col" shadow-2xl z-50 flex flex-col"
transition:slide={{ duration: 300, easing: quintOut, axis: 'x' }} transition:slide={{ duration: 300, easing: quintOut, axis: "x" }}
> >
<!-- Header --> <!-- Header -->
<div class="p-6 border-b border-[var(--border-color)]"> <div class="p-6 border-b border-[var(--border-color)]">
@@ -74,18 +86,26 @@
<h2 class="text-2xl font-bold text-[var(--text-primary)]">Achievements</h2> <h2 class="text-2xl font-bold text-[var(--text-primary)]">Achievements</h2>
<button <button
onclick={onClose} onclick={onClose}
onkeydown={(e) => e.key === 'Enter' && onClose()} onkeydown={(e) => e.key === "Enter" && onClose?.()}
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label="Close achievements panel"
> >
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" 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"></path> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg> </svg>
</button> </button>
</div> </div>
<!-- Overall progress --> <!-- Overall progress -->
<div class="mt-4"> <div class="mt-4">
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400 mb-2"> <div
class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400 mb-2"
>
<span>{progress.unlocked} / {progress.total} Unlocked</span> <span>{progress.unlocked} / {progress.total} Unlocked</span>
<span>{progress.percentage}%</span> <span>{progress.percentage}%</span>
</div> </div>
@@ -100,14 +120,17 @@
<!-- Categories --> <!-- Categories -->
<div class="flex-1 overflow-y-auto"> <div class="flex-1 overflow-y-auto">
{#each achievementCategories as category} {#each achievementCategories as category (category.name)}
{@const achievements = getAchievementsForCategory(category.ids)} {@const achievements = getAchievementsForCategory(category.ids)}
{@const unlockedCount = achievements.filter(a => a.unlocked).length} {@const unlockedCount = achievements.filter((a) => a.unlocked).length}
<div class="border-b border-[var(--border-color)]"> <div class="border-b border-[var(--border-color)]">
<button <button
onclick={() => selectedCategory = selectedCategory === category.name ? null : category.name} onclick={() =>
onkeydown={(e) => e.key === 'Enter' && (selectedCategory = selectedCategory === category.name ? null : category.name)} (selectedCategory = selectedCategory === category.name ? null : category.name)}
onkeydown={(e) =>
e.key === "Enter" &&
(selectedCategory = selectedCategory === category.name ? null : category.name)}
class="w-full p-4 text-left hover:bg-[var(--bg-secondary)] transition-colors" class="w-full p-4 text-left hover:bg-[var(--bg-secondary)] transition-colors"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -120,12 +143,19 @@
{unlockedCount} / {achievements.length} {unlockedCount} / {achievements.length}
</span> </span>
<svg <svg
class="w-5 h-5 transition-transform {selectedCategory === category.name ? 'rotate-180' : ''}" class="w-5 h-5 transition-transform {selectedCategory === category.name
? 'rotate-180'
: ''}"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
></path>
</svg> </svg>
</div> </div>
</div> </div>
@@ -133,9 +163,11 @@
{#if selectedCategory === category.name} {#if selectedCategory === category.name}
<div class="p-4 space-y-3" transition:slide={{ duration: 200, easing: quintOut }}> <div class="p-4 space-y-3" transition:slide={{ duration: 200, easing: quintOut }}>
{#each achievements as achievement} {#each achievements as achievement (achievement.id)}
<div <div
class="p-3 rounded-lg border {achievement.unlocked ? 'border-[var(--border-color)] bg-[var(--bg-secondary)]' : 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-800 opacity-50'}" class="p-3 rounded-lg border {achievement.unlocked
? 'border-[var(--border-color)] bg-[var(--bg-secondary)]'
: 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-800 opacity-50'}"
> >
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<!-- Icon --> <!-- Icon -->
@@ -149,7 +181,11 @@
<h4 class="font-semibold text-[var(--text-primary)]"> <h4 class="font-semibold text-[var(--text-primary)]">
{achievement.name} {achievement.name}
</h4> </h4>
<span class="text-xs px-2 py-0.5 rounded-full {getRarityBg(achievement.rarity)} {getRarityColor(achievement.rarity)} capitalize"> <span
class="text-xs px-2 py-0.5 rounded-full {getRarityBg(
achievement.rarity
)} {getRarityColor(achievement.rarity)} capitalize"
>
{achievement.rarity} {achievement.rarity}
</span> </span>
</div> </div>
@@ -171,7 +207,10 @@
<div class="w-full bg-gray-300 dark:bg-gray-700 rounded-full h-1.5"> <div class="w-full bg-gray-300 dark:bg-gray-700 rounded-full h-1.5">
<div <div
class="bg-gray-500 h-1.5 rounded-full transition-all duration-300" class="bg-gray-500 h-1.5 rounded-full transition-all duration-300"
style="width: {Math.min((achievement.progress / achievement.maxProgress) * 100, 100)}%" style="width: {Math.min(
(achievement.progress / achievement.maxProgress) * 100,
100
)}%"
></div> ></div>
</div> </div>
</div> </div>
@@ -193,8 +232,12 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xl">{achievementsState.lastUnlocked.icon}</span> <span class="text-xl">{achievementsState.lastUnlocked.icon}</span>
<div> <div>
<p class="font-semibold text-[var(--text-primary)]">{achievementsState.lastUnlocked.name}</p> <p class="font-semibold text-[var(--text-primary)]">
<p class="text-xs text-gray-500">{formatDate(achievementsState.lastUnlocked.unlockedAt)}</p> {achievementsState.lastUnlocked.name}
</p>
<p class="text-xs text-gray-500">
{formatDate(achievementsState.lastUnlocked.unlockedAt)}
</p>
</div> </div>
</div> </div>
</div> </div>
+8 -9
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { formattedStats } from '$lib/stores/stats'; import { formattedStats } from "$lib/stores/stats";
import { fade } from 'svelte/transition'; import { fade } from "svelte/transition";
let showToolsBreakdown = false; let showToolsBreakdown = false;
</script> </script>
@@ -61,17 +61,14 @@
{#if Object.keys($formattedStats.sessionToolsUsage).length > 0} {#if Object.keys($formattedStats.sessionToolsUsage).length > 0}
<div class="stats-section"> <div class="stats-section">
<h3 class="tools-header"> <h3 class="tools-header">
<button <button class="tools-toggle" onclick={() => (showToolsBreakdown = !showToolsBreakdown)}>
class="tools-toggle"
onclick={() => showToolsBreakdown = !showToolsBreakdown}
>
Tools Used Tools Used
<span class="toggle-icon">{showToolsBreakdown ? '▼' : '▶'}</span> <span class="toggle-icon">{showToolsBreakdown ? "▼" : "▶"}</span>
</button> </button>
</h3> </h3>
{#if showToolsBreakdown} {#if showToolsBreakdown}
<div class="tools-breakdown"> <div class="tools-breakdown">
{#each Object.entries($formattedStats.sessionToolsUsage).sort((a, b) => b[1] - a[1]) as [tool, count]} {#each Object.entries($formattedStats.sessionToolsUsage).sort((a, b) => b[1] - a[1]) as [tool, count] (tool)}
<div class="stat-row stat-detail"> <div class="stat-row stat-detail">
<span class="stat-label">{tool}:</span> <span class="stat-label">{tool}:</span>
<span class="stat-value">{count}</span> <span class="stat-value">{count}</span>
@@ -98,7 +95,9 @@
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 8px;
font-size: 0.85rem; font-size: 0.85rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08); box-shadow:
0 4px 6px rgba(0, 0, 0, 0.1),
0 1px 3px rgba(0, 0, 0, 0.08);
} }
.stats-section { .stats-section {
+9 -5
View File
@@ -3,7 +3,7 @@
onToggleAchievements?: () => void; onToggleAchievements?: () => void;
} }
const { onToggleAchievements = () => {} } = $props<Props>(); const { onToggleAchievements = () => {} }: Props = $props();
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { getVersion } from "@tauri-apps/api/app"; import { getVersion } from "@tauri-apps/api/app";
@@ -188,14 +188,18 @@
> >
<span class="text-lg">🏆</span> <span class="text-lg">🏆</span>
{#if progress.unlocked > 0} {#if progress.unlocked > 0}
<span class="absolute -top-1 -right-1 bg-[var(--accent-primary)] text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]"> <span
class="absolute -top-1 -right-1 bg-[var(--accent-primary)] text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]"
>
{progress.unlocked} {progress.unlocked}
</span> </span>
{/if} {/if}
</button> </button>
<button <button
onclick={() => showStats = !showStats} onclick={() => (showStats = !showStats)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors {showStats ? 'text-[var(--accent-primary)]' : ''}" class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors {showStats
? 'text-[var(--accent-primary)]'
: ''}"
title="Usage Stats" title="Usage Stats"
> >
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -269,7 +273,7 @@
{#if showStats} {#if showStats}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="fixed inset-0 z-40" onclick={() => showStats = false}></div> <div class="fixed inset-0 z-40" onclick={() => (showStats = false)}></div>
<div class="fixed top-14 right-4 z-50"> <div class="fixed top-14 right-4 z-50">
<StatsDisplay /> <StatsDisplay />
</div> </div>
+3 -1
View File
@@ -84,7 +84,9 @@
class="terminal-content h-[calc(100%-40px)] overflow-y-auto p-4 font-mono text-sm" class="terminal-content h-[calc(100%-40px)] overflow-y-auto p-4 font-mono text-sm"
> >
{#if lines.length === 0} {#if lines.length === 0}
<div class="terminal-waiting italic">Waiting for Claude... Type a message below to start!</div> <div class="terminal-waiting italic">
Waiting for Claude... Type a message below to start!
</div>
{:else} {:else}
{#each lines as line (line.id)} {#each lines as line (line.id)}
<div class="terminal-line mb-2 {getLineClass(line.type)}"> <div class="terminal-line mb-2 {getLineClass(line.type)}">
+4 -4
View File
@@ -1,7 +1,7 @@
// Achievement sound player using the notification system // Achievement sound player using the notification system
import { soundPlayer } from '$lib/notifications'; import { soundPlayer } from "$lib/notifications";
import { NotificationType } from '$lib/notifications/types'; import { NotificationType } from "$lib/notifications/types";
export function playAchievementSound() { export function playAchievementSound() {
// Use the soundPlayer which respects global notification settings // Use the soundPlayer which respects global notification settings
@@ -12,8 +12,8 @@ export function playAchievementSound() {
export function testAchievementSound() { export function testAchievementSound() {
try { try {
playAchievementSound(); playAchievementSound();
console.log('Achievement sound played successfully!'); console.log("Achievement sound played successfully!");
} catch (error) { } catch (error) {
console.error('Error playing achievement sound:', error); console.error("Error playing achievement sound:", error);
} }
} }
+351 -325
View File
@@ -1,8 +1,8 @@
import { writable, derived } from 'svelte/store'; import { writable, derived } from "svelte/store";
import { listen } from '@tauri-apps/api/event'; import { listen } from "@tauri-apps/api/event";
import { invoke } from '@tauri-apps/api/core'; import { invoke } from "@tauri-apps/api/core";
import type { Achievement, AchievementUnlockedEvent, AchievementId } from '$lib/types/achievements'; import type { Achievement, AchievementUnlockedEvent, AchievementId } from "$lib/types/achievements";
import { playAchievementSound } from '$lib/sounds/achievement'; import { playAchievementSound } from "$lib/sounds/achievement";
interface AchievementState { interface AchievementState {
achievements: Record<AchievementId, Achievement>; achievements: Record<AchievementId, Achievement>;
@@ -11,430 +11,436 @@ interface AchievementState {
} }
// Initial achievement definitions // Initial achievement definitions
const achievementDefinitions: Record<AchievementId, Omit<Achievement, 'unlocked' | 'unlockedAt'>> = { const achievementDefinitions: Record<
AchievementId,
Omit<Achievement, "unlocked" | "unlockedAt">
> = {
// Token milestones // Token milestones
FirstSteps: { FirstSteps: {
id: 'FirstSteps', id: "FirstSteps",
name: 'First Steps', name: "First Steps",
description: 'Generated your first 1,000 tokens', description: "Generated your first 1,000 tokens",
icon: '👶', icon: "👶",
rarity: 'common', rarity: "common",
maxProgress: 1000, maxProgress: 1000,
}, },
GrowingStrong: { GrowingStrong: {
id: 'GrowingStrong', id: "GrowingStrong",
name: 'Growing Strong', name: "Growing Strong",
description: 'Reached 10,000 tokens total', description: "Reached 10,000 tokens total",
icon: '🌱', icon: "🌱",
rarity: 'common', rarity: "common",
maxProgress: 10000, maxProgress: 10000,
}, },
BlossomingCoder: { BlossomingCoder: {
id: 'BlossomingCoder', id: "BlossomingCoder",
name: 'Blossoming Coder', name: "Blossoming Coder",
description: 'Generated 100,000 tokens - you\'re really growing!', description: "Generated 100,000 tokens - you're really growing!",
icon: '🌸', icon: "🌸",
rarity: 'rare', rarity: "rare",
maxProgress: 100000, maxProgress: 100000,
}, },
TokenMaster: { TokenMaster: {
id: 'TokenMaster', id: "TokenMaster",
name: 'Token Master', name: "Token Master",
description: 'One million tokens! You\'re unstoppable!', description: "One million tokens! You're unstoppable!",
icon: '👑', icon: "👑",
rarity: 'legendary', rarity: "legendary",
maxProgress: 1000000, maxProgress: 1000000,
}, },
// Code generation // Code generation
HelloWorld: { HelloWorld: {
id: 'HelloWorld', id: "HelloWorld",
name: 'Hello, World!', name: "Hello, World!",
description: 'Generated your first code block', description: "Generated your first code block",
icon: '👋', icon: "👋",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
CodeWizard: { CodeWizard: {
id: 'CodeWizard', id: "CodeWizard",
name: 'Code Wizard', name: "Code Wizard",
description: '100 code blocks generated', description: "100 code blocks generated",
icon: '🧙‍♀️', icon: "🧙‍♀️",
rarity: 'rare', rarity: "rare",
maxProgress: 100, maxProgress: 100,
}, },
ThousandBlocks: { ThousandBlocks: {
id: 'ThousandBlocks', id: "ThousandBlocks",
name: 'Thousand Blocks', name: "Thousand Blocks",
description: '1,000 code blocks! You\'re a code machine!', description: "1,000 code blocks! You're a code machine!",
icon: '🏗️', icon: "🏗️",
rarity: 'epic', rarity: "epic",
maxProgress: 1000, maxProgress: 1000,
}, },
// File operations // File operations
FileManipulator: { FileManipulator: {
id: 'FileManipulator', id: "FileManipulator",
name: 'File Manipulator', name: "File Manipulator",
description: 'Edited 10 files', description: "Edited 10 files",
icon: '📝', icon: "📝",
rarity: 'common', rarity: "common",
maxProgress: 10, maxProgress: 10,
}, },
FileArchitect: { FileArchitect: {
id: 'FileArchitect', id: "FileArchitect",
name: 'File Architect', name: "File Architect",
description: 'Created or edited 100 files', description: "Created or edited 100 files",
icon: '🏛️', icon: "🏛️",
rarity: 'rare', rarity: "rare",
maxProgress: 100, maxProgress: 100,
}, },
// Conversation milestones // Conversation milestones
ConversationStarter: { ConversationStarter: {
id: 'ConversationStarter', id: "ConversationStarter",
name: 'Conversation Starter', name: "Conversation Starter",
description: 'Exchanged 10 messages', description: "Exchanged 10 messages",
icon: '💬', icon: "💬",
rarity: 'common', rarity: "common",
maxProgress: 10, maxProgress: 10,
}, },
ChattyKathy: { ChattyKathy: {
id: 'ChattyKathy', id: "ChattyKathy",
name: 'Chatty Kathy', name: "Chatty Kathy",
description: '100 messages exchanged', description: "100 messages exchanged",
icon: '🗣️', icon: "🗣️",
rarity: 'common', rarity: "common",
maxProgress: 100, maxProgress: 100,
}, },
Conversationalist: { Conversationalist: {
id: 'Conversationalist', id: "Conversationalist",
name: 'Master Conversationalist', name: "Master Conversationalist",
description: '1,000 messages! We\'re really connecting!', description: "1,000 messages! We're really connecting!",
icon: '💖', icon: "💖",
rarity: 'rare', rarity: "rare",
maxProgress: 1000, maxProgress: 1000,
}, },
// Tool usage // Tool usage
Toolsmith: { Toolsmith: {
id: 'Toolsmith', id: "Toolsmith",
name: 'Toolsmith', name: "Toolsmith",
description: 'Used 5 different tools', description: "Used 5 different tools",
icon: '🔨', icon: "🔨",
rarity: 'common', rarity: "common",
maxProgress: 5, maxProgress: 5,
}, },
ToolMaster: { ToolMaster: {
id: 'ToolMaster', id: "ToolMaster",
name: 'Tool Master', name: "Tool Master",
description: 'Used 10 different tools efficiently', description: "Used 10 different tools efficiently",
icon: '🛠️', icon: "🛠️",
rarity: 'rare', rarity: "rare",
maxProgress: 10, maxProgress: 10,
}, },
// Time-based achievements // Time-based achievements
EarlyBird: { EarlyBird: {
id: 'EarlyBird', id: "EarlyBird",
name: 'Early Bird', name: "Early Bird",
description: 'Started a session between 5 AM and 7 AM', description: "Started a session between 5 AM and 7 AM",
icon: '🌅', icon: "🌅",
rarity: 'common', rarity: "common",
}, },
NightOwl: { NightOwl: {
id: 'NightOwl', id: "NightOwl",
name: 'Night Owl', name: "Night Owl",
description: 'Coding after midnight', description: "Coding after midnight",
icon: '🦉', icon: "🦉",
rarity: 'common', rarity: "common",
}, },
AllNighter: { AllNighter: {
id: 'AllNighter', id: "AllNighter",
name: 'All Nighter', name: "All Nighter",
description: 'Worked through the night (2 AM - 5 AM)', description: "Worked through the night (2 AM - 5 AM)",
icon: '🌙', icon: "🌙",
rarity: 'rare', rarity: "rare",
}, },
WeekendWarrior: { WeekendWarrior: {
id: 'WeekendWarrior', id: "WeekendWarrior",
name: 'Weekend Warrior', name: "Weekend Warrior",
description: 'Coding on a weekend', description: "Coding on a weekend",
icon: '⚔️', icon: "⚔️",
rarity: 'common', rarity: "common",
}, },
DedicatedDeveloper: { DedicatedDeveloper: {
id: 'DedicatedDeveloper', id: "DedicatedDeveloper",
name: 'Dedicated Developer', name: "Dedicated Developer",
description: 'Coded for 30 days in a row', description: "Coded for 30 days in a row",
icon: '🏆', icon: "🏆",
rarity: 'legendary', rarity: "legendary",
}, },
// Search and exploration // Search and exploration
Explorer: { Explorer: {
id: 'Explorer', id: "Explorer",
name: 'Explorer', name: "Explorer",
description: 'Used search tools 50 times', description: "Used search tools 50 times",
icon: '🔍', icon: "🔍",
rarity: 'common', rarity: "common",
maxProgress: 50, maxProgress: 50,
}, },
MasterSearcher: { MasterSearcher: {
id: 'MasterSearcher', id: "MasterSearcher",
name: 'Master Searcher', name: "Master Searcher",
description: 'Searched 500 times across files', description: "Searched 500 times across files",
icon: '🕵️‍♀️', icon: "🕵️‍♀️",
rarity: 'rare', rarity: "rare",
maxProgress: 500, maxProgress: 500,
}, },
// Session achievements // Session achievements
QuickSession: { QuickSession: {
id: 'QuickSession', id: "QuickSession",
name: 'Quick Session', name: "Quick Session",
description: 'Completed a productive session in under 5 minutes', description: "Completed a productive session in under 5 minutes",
icon: '⚡', icon: "⚡",
rarity: 'common', rarity: "common",
}, },
FocusedWork: { FocusedWork: {
id: 'FocusedWork', id: "FocusedWork",
name: 'Focused Work', name: "Focused Work",
description: 'Worked for 30 minutes straight', description: "Worked for 30 minutes straight",
icon: '🎯', icon: "🎯",
rarity: 'common', rarity: "common",
}, },
DeepDive: { DeepDive: {
id: 'DeepDive', id: "DeepDive",
name: 'Deep Dive', name: "Deep Dive",
description: 'Worked for 2 hours continuously', description: "Worked for 2 hours continuously",
icon: '🏊‍♀️', icon: "🏊‍♀️",
rarity: 'rare', rarity: "rare",
}, },
MarathonSession: { MarathonSession: {
id: 'MarathonSession', id: "MarathonSession",
name: 'Marathon Session', name: "Marathon Session",
description: '5+ hour coding session!', description: "5+ hour coding session!",
icon: '🏃‍♀️', icon: "🏃‍♀️",
rarity: 'epic', rarity: "epic",
}, },
// Special achievements // Special achievements
FirstMessage: { FirstMessage: {
id: 'FirstMessage', id: "FirstMessage",
name: 'First Message', name: "First Message",
description: 'Sent your first message to Hikari', description: "Sent your first message to Hikari",
icon: '✨', icon: "✨",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
FirstTool: { FirstTool: {
id: 'FirstTool', id: "FirstTool",
name: 'First Tool', name: "First Tool",
description: 'Used your first tool', description: "Used your first tool",
icon: '🔧', icon: "🔧",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
FirstCodeBlock: { FirstCodeBlock: {
id: 'FirstCodeBlock', id: "FirstCodeBlock",
name: 'First Code', name: "First Code",
description: 'Generated your first code block', description: "Generated your first code block",
icon: '📦', icon: "📦",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
FirstFileEdit: { FirstFileEdit: {
id: 'FirstFileEdit', id: "FirstFileEdit",
name: 'First Edit', name: "First Edit",
description: 'Made your first file edit', description: "Made your first file edit",
icon: '✏️', icon: "✏️",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
Polyglot: { Polyglot: {
id: 'Polyglot', id: "Polyglot",
name: 'Polyglot', name: "Polyglot",
description: 'Generated code in 5+ languages in one session', description: "Generated code in 5+ languages in one session",
icon: '🌍', icon: "🌍",
rarity: 'rare', rarity: "rare",
maxProgress: 5, maxProgress: 5,
}, },
SpeedCoder: { SpeedCoder: {
id: 'SpeedCoder', id: "SpeedCoder",
name: 'Speed Coder', name: "Speed Coder",
description: 'Generated 10 code blocks in 10 minutes', description: "Generated 10 code blocks in 10 minutes",
icon: '🚀', icon: "🚀",
rarity: 'rare', rarity: "rare",
}, },
ClaudeConnoisseur: { ClaudeConnoisseur: {
id: 'ClaudeConnoisseur', id: "ClaudeConnoisseur",
name: 'Claude Connoisseur', name: "Claude Connoisseur",
description: 'Used all available Claude models', description: "Used all available Claude models",
icon: '🎨', icon: "🎨",
rarity: 'epic', rarity: "epic",
maxProgress: 5, // Adjust based on available models maxProgress: 5, // Adjust based on available models
}, },
MarathonCoder: { MarathonCoder: {
id: 'MarathonCoder', id: "MarathonCoder",
name: 'Marathon Coder', name: "Marathon Coder",
description: '10,000 tokens in a single session', description: "10,000 tokens in a single session",
icon: '🏃‍♂️', icon: "🏃‍♂️",
rarity: 'epic', rarity: "epic",
maxProgress: 10000, maxProgress: 10000,
}, },
// Relationship & Greetings // Relationship & Greetings
GoodMorning: { GoodMorning: {
id: 'GoodMorning', id: "GoodMorning",
name: 'Good Morning!', name: "Good Morning!",
description: 'Greeted Hikari with a good morning', description: "Greeted Hikari with a good morning",
icon: '🌅', icon: "🌅",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
GoodNight: { GoodNight: {
id: 'GoodNight', id: "GoodNight",
name: 'Sweet Dreams', name: "Sweet Dreams",
description: 'Said good night to Hikari', description: "Said good night to Hikari",
icon: '🌙', icon: "🌙",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
ThankYou: { ThankYou: {
id: 'ThankYou', id: "ThankYou",
name: 'Grateful Heart', name: "Grateful Heart",
description: 'Thanked Hikari for her help', description: "Thanked Hikari for her help",
icon: '🙏', icon: "🙏",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
LoveYou: { LoveYou: {
id: 'LoveYou', id: "LoveYou",
name: 'Heartfelt', name: "Heartfelt",
description: 'Expressed love to Hikari', description: "Expressed love to Hikari",
icon: '💕', icon: "💕",
rarity: 'rare', rarity: "rare",
maxProgress: 1, maxProgress: 1,
}, },
// Personality & Fun // Personality & Fun
EmojiUser: { EmojiUser: {
id: 'EmojiUser', id: "EmojiUser",
name: 'Emoji Master', name: "Emoji Master",
description: 'Used 20+ emojis in messages', description: "Used 20+ emojis in messages",
icon: '😄', icon: "😄",
rarity: 'common', rarity: "common",
maxProgress: 20, maxProgress: 20,
}, },
CapsLock: { CapsLock: {
id: 'CapsLock', id: "CapsLock",
name: 'CAPS LOCK', name: "CAPS LOCK",
description: 'SENT A MESSAGE IN ALL CAPS', description: "SENT A MESSAGE IN ALL CAPS",
icon: '🔊', icon: "🔊",
rarity: 'common', rarity: "common",
maxProgress: 1, maxProgress: 1,
}, },
QuestionMaster: { QuestionMaster: {
id: 'QuestionMaster', id: "QuestionMaster",
name: 'Question Master', name: "Question Master",
description: 'Asked 50 questions', description: "Asked 50 questions",
icon: '❓', icon: "❓",
rarity: 'common', rarity: "common",
maxProgress: 50, maxProgress: 50,
}, },
PleaseAndThankYou: { PleaseAndThankYou: {
id: 'PleaseAndThankYou', id: "PleaseAndThankYou",
name: 'Polite Programmer', name: "Polite Programmer",
description: 'Always says please and thank you', description: "Always says please and thank you",
icon: '🎩', icon: "🎩",
rarity: 'common', rarity: "common",
maxProgress: 10, maxProgress: 10,
}, },
// Git & Development // Git & Development
CommitMaster: { CommitMaster: {
id: 'CommitMaster', id: "CommitMaster",
name: 'Commit Master', name: "Commit Master",
description: 'Made 100 commits through Hikari', description: "Made 100 commits through Hikari",
icon: '📝', icon: "📝",
rarity: 'rare', rarity: "rare",
maxProgress: 100, maxProgress: 100,
}, },
PRO: { PRO: {
id: 'PRO', id: "PRO",
name: 'PRO', name: "PRO",
description: 'Created 10 pull requests', description: "Created 10 pull requests",
icon: '🔀', icon: "🔀",
rarity: 'rare', rarity: "rare",
maxProgress: 10, maxProgress: 10,
}, },
Reviewer: { Reviewer: {
id: 'Reviewer', id: "Reviewer",
name: 'Code Reviewer', name: "Code Reviewer",
description: 'Reviewed 10 pull requests', description: "Reviewed 10 pull requests",
icon: '👀', icon: "👀",
rarity: 'rare', rarity: "rare",
maxProgress: 10, maxProgress: 10,
}, },
IssueTracker: { IssueTracker: {
id: 'IssueTracker', id: "IssueTracker",
name: 'Issue Tracker', name: "Issue Tracker",
description: 'Created 25 issues', description: "Created 25 issues",
icon: '🎯', icon: "🎯",
rarity: 'rare', rarity: "rare",
maxProgress: 25, maxProgress: 25,
}, },
GitGuru: { GitGuru: {
id: 'GitGuru', id: "GitGuru",
name: 'Git Guru', name: "Git Guru",
description: 'Mastered git operations', description: "Mastered git operations",
icon: '🌲', icon: "🌲",
rarity: 'epic', rarity: "epic",
}, },
// Tool Mastery // Tool Mastery
BashMaster: { BashMaster: {
id: 'BashMaster', id: "BashMaster",
name: 'Bash Master', name: "Bash Master",
description: 'Used bash commands 100 times', description: "Used bash commands 100 times",
icon: '💻', icon: "💻",
rarity: 'rare', rarity: "rare",
maxProgress: 100, maxProgress: 100,
}, },
FileExplorer: { FileExplorer: {
id: 'FileExplorer', id: "FileExplorer",
name: 'File Explorer', name: "File Explorer",
description: 'Explored files 100 times', description: "Explored files 100 times",
icon: '📂', icon: "📂",
rarity: 'common', rarity: "common",
maxProgress: 100, maxProgress: 100,
}, },
SearchExpert: { SearchExpert: {
id: 'SearchExpert', id: "SearchExpert",
name: 'Search Expert', name: "Search Expert",
description: 'Mastered advanced search queries', description: "Mastered advanced search queries",
icon: '🔎', icon: "🔎",
rarity: 'rare', rarity: "rare",
}, },
AgentCommander: { AgentCommander: {
id: 'AgentCommander', id: "AgentCommander",
name: 'Agent Commander', name: "Agent Commander",
description: 'Used task agents effectively', description: "Used task agents effectively",
icon: '🤖', icon: "🤖",
rarity: 'rare', rarity: "rare",
}, },
MCPMaster: { MCPMaster: {
id: 'MCPMaster', id: "MCPMaster",
name: 'MCP Master', name: "MCP Master",
description: 'Mastered MCP tool usage', description: "Mastered MCP tool usage",
icon: '🛠️', icon: "🛠️",
rarity: 'epic', rarity: "epic",
}, },
}; };
// Initialize all achievements as locked // Initialize all achievements as locked
const initialAchievements: Record<AchievementId, Achievement> = {} as Record<AchievementId, Achievement>; const initialAchievements: Record<AchievementId, Achievement> = {} as Record<
AchievementId,
Achievement
>;
for (const [id, def] of Object.entries(achievementDefinitions)) { for (const [id, def] of Object.entries(achievementDefinitions)) {
initialAchievements[id as AchievementId] = { initialAchievements[id as AchievementId] = {
...def, ...def,
@@ -454,11 +460,13 @@ function createAchievementsStore() {
return { return {
subscribe, subscribe,
unlockAchievement: (event: AchievementUnlockedEvent, playSound: boolean = true) => { unlockAchievement: (event: AchievementUnlockedEvent, playSound: boolean = true) => {
update(state => { update((state) => {
const achievement = state.achievements[event.achievement.id]; const achievement = state.achievements[event.achievement.id];
if (achievement && !achievement.unlocked) { if (achievement && !achievement.unlocked) {
achievement.unlocked = true; achievement.unlocked = true;
achievement.unlockedAt = event.achievement.unlocked_at ? new Date(event.achievement.unlocked_at) : new Date(); achievement.unlockedAt = event.achievement.unlocked_at
? new Date(event.achievement.unlocked_at)
: new Date();
state.totalUnlocked++; state.totalUnlocked++;
state.lastUnlocked = achievement; state.lastUnlocked = achievement;
@@ -467,7 +475,7 @@ function createAchievementsStore() {
try { try {
playAchievementSound(); playAchievementSound();
} catch (error) { } catch (error) {
console.error('Failed to play achievement sound:', error); console.error("Failed to play achievement sound:", error);
} }
} }
} }
@@ -475,7 +483,7 @@ function createAchievementsStore() {
}); });
}, },
updateProgress: (id: AchievementId, progress: number) => { updateProgress: (id: AchievementId, progress: number) => {
update(state => { update((state) => {
const achievement = state.achievements[id]; const achievement = state.achievements[id];
if (achievement) { if (achievement) {
achievement.progress = progress; achievement.progress = progress;
@@ -489,26 +497,22 @@ function createAchievementsStore() {
totalUnlocked: 0, totalUnlocked: 0,
lastUnlocked: null, lastUnlocked: null,
})); }));
} },
}; };
} }
export const achievementsStore = createAchievementsStore(); export const achievementsStore = createAchievementsStore();
// Derived stores for different views // Derived stores for different views
export const unlockedAchievements = derived( export const unlockedAchievements = derived(achievementsStore, ($store) =>
achievementsStore, Object.values($store.achievements).filter((a) => a.unlocked)
$store => Object.values($store.achievements).filter(a => a.unlocked)
); );
export const lockedAchievements = derived( export const lockedAchievements = derived(achievementsStore, ($store) =>
achievementsStore, Object.values($store.achievements).filter((a) => !a.unlocked)
$store => Object.values($store.achievements).filter(a => !a.unlocked)
); );
export const achievementsByRarity = derived( export const achievementsByRarity = derived(achievementsStore, ($store) => {
achievementsStore,
$store => {
const byRarity: Record<string, Achievement[]> = { const byRarity: Record<string, Achievement[]> = {
common: [], common: [],
rare: [], rare: [],
@@ -521,103 +525,125 @@ export const achievementsByRarity = derived(
} }
return byRarity; return byRarity;
} });
);
export const achievementProgress = derived( export const achievementProgress = derived(achievementsStore, ($store) => ({
achievementsStore,
$store => ({
unlocked: $store.totalUnlocked, unlocked: $store.totalUnlocked,
total: Object.keys($store.achievements).length, total: Object.keys($store.achievements).length,
percentage: Math.round(($store.totalUnlocked / Object.keys($store.achievements).length) * 100), percentage: Math.round(($store.totalUnlocked / Object.keys($store.achievements).length) * 100),
}) }));
);
// Initialize achievement listener // Initialize achievement listener
export async function initAchievementsListener() { export async function initAchievementsListener() {
// Listen for achievement unlocked events // Listen for achievement unlocked events
await listen<AchievementUnlockedEvent>('achievement:unlocked', (event) => { await listen<AchievementUnlockedEvent>("achievement:unlocked", (event) => {
achievementsStore.unlockAchievement(event.payload); achievementsStore.unlockAchievement(event.payload);
}); });
// Load saved achievements from persistent storage // Load saved achievements from persistent storage
try { try {
const savedAchievements = await invoke<AchievementUnlockedEvent[]>('load_saved_achievements'); const savedAchievements = await invoke<AchievementUnlockedEvent[]>("load_saved_achievements");
// Update the store with saved achievements (don't play sounds) // Update the store with saved achievements (don't play sounds)
for (const event of savedAchievements) { for (const event of savedAchievements) {
achievementsStore.unlockAchievement(event, false); achievementsStore.unlockAchievement(event, false);
} }
} catch (error) { } catch (error) {
console.error('Failed to load saved achievements:', error); console.error("Failed to load saved achievements:", error);
} }
} }
// Export achievement categories for the display panel // Export achievement categories for the display panel
export const achievementCategories = [ export const achievementCategories = [
{ {
name: 'Token Milestones', name: "Token Milestones",
description: 'Track your token generation progress', description: "Track your token generation progress",
ids: ['FirstSteps', 'GrowingStrong', 'BlossomingCoder', 'TokenMaster'] as AchievementId[], ids: ["FirstSteps", "GrowingStrong", "BlossomingCoder", "TokenMaster"] as AchievementId[],
}, },
{ {
name: 'Code Generation', name: "Code Generation",
description: 'Achievements for generating code', description: "Achievements for generating code",
ids: ['HelloWorld', 'CodeWizard', 'ThousandBlocks'] as AchievementId[], ids: ["HelloWorld", "CodeWizard", "ThousandBlocks"] as AchievementId[],
}, },
{ {
name: 'File Operations', name: "File Operations",
description: 'Working with files and projects', description: "Working with files and projects",
ids: ['FileManipulator', 'FileArchitect'] as AchievementId[], ids: ["FileManipulator", "FileArchitect"] as AchievementId[],
}, },
{ {
name: 'Conversations', name: "Conversations",
description: 'Building our relationship through chat', description: "Building our relationship through chat",
ids: ['ConversationStarter', 'ChattyKathy', 'Conversationalist'] as AchievementId[], ids: ["ConversationStarter", "ChattyKathy", "Conversationalist"] as AchievementId[],
}, },
{ {
name: 'Tools & Skills', name: "Tools & Skills",
description: 'Mastering different tools', description: "Mastering different tools",
ids: ['Toolsmith', 'ToolMaster'] as AchievementId[], ids: ["Toolsmith", "ToolMaster"] as AchievementId[],
}, },
{ {
name: 'Time-Based', name: "Time-Based",
description: 'When you code matters too!', description: "When you code matters too!",
ids: ['EarlyBird', 'NightOwl', 'AllNighter', 'WeekendWarrior', 'DedicatedDeveloper'] as AchievementId[], ids: [
"EarlyBird",
"NightOwl",
"AllNighter",
"WeekendWarrior",
"DedicatedDeveloper",
] as AchievementId[],
}, },
{ {
name: 'Search & Explore', name: "Search & Explore",
description: 'Finding what you need', description: "Finding what you need",
ids: ['Explorer', 'MasterSearcher'] as AchievementId[], ids: ["Explorer", "MasterSearcher"] as AchievementId[],
}, },
{ {
name: 'Session Records', name: "Session Records",
description: 'Your coding session achievements', description: "Your coding session achievements",
ids: ['QuickSession', 'FocusedWork', 'DeepDive', 'MarathonSession', 'MarathonCoder'] as AchievementId[], ids: [
"QuickSession",
"FocusedWork",
"DeepDive",
"MarathonSession",
"MarathonCoder",
] as AchievementId[],
}, },
{ {
name: 'Relationship & Greetings', name: "Relationship & Greetings",
description: 'Our special moments together', description: "Our special moments together",
ids: ['GoodMorning', 'GoodNight', 'ThankYou', 'LoveYou'] as AchievementId[], ids: ["GoodMorning", "GoodNight", "ThankYou", "LoveYou"] as AchievementId[],
}, },
{ {
name: 'Personality & Fun', name: "Personality & Fun",
description: 'Express yourself!', description: "Express yourself!",
ids: ['EmojiUser', 'CapsLock', 'QuestionMaster', 'PleaseAndThankYou'] as AchievementId[], ids: ["EmojiUser", "CapsLock", "QuestionMaster", "PleaseAndThankYou"] as AchievementId[],
}, },
{ {
name: 'Git & Development', name: "Git & Development",
description: 'Version control mastery', description: "Version control mastery",
ids: ['CommitMaster', 'PRO', 'Reviewer', 'IssueTracker', 'GitGuru'] as AchievementId[], ids: ["CommitMaster", "PRO", "Reviewer", "IssueTracker", "GitGuru"] as AchievementId[],
}, },
{ {
name: 'Tool Mastery', name: "Tool Mastery",
description: 'Master of all tools', description: "Master of all tools",
ids: ['BashMaster', 'FileExplorer', 'SearchExpert', 'AgentCommander', 'MCPMaster'] as AchievementId[], ids: [
"BashMaster",
"FileExplorer",
"SearchExpert",
"AgentCommander",
"MCPMaster",
] as AchievementId[],
}, },
{ {
name: 'Special', name: "Special",
description: 'Unique accomplishments', description: "Unique accomplishments",
ids: ['FirstMessage', 'FirstTool', 'FirstCodeBlock', 'FirstFileEdit', 'Polyglot', 'SpeedCoder', 'ClaudeConnoisseur'] as AchievementId[], ids: [
"FirstMessage",
"FirstTool",
"FirstCodeBlock",
"FirstFileEdit",
"Polyglot",
"SpeedCoder",
"ClaudeConnoisseur",
] as AchievementId[],
}, },
]; ];
+8 -8
View File
@@ -1,6 +1,6 @@
import { writable, derived } from 'svelte/store'; import { writable, derived } from "svelte/store";
import { listen } from '@tauri-apps/api/event'; import { listen } from "@tauri-apps/api/event";
import { invoke } from '@tauri-apps/api/core'; import { invoke } from "@tauri-apps/api/core";
export interface UsageStats { export interface UsageStats {
total_input_tokens: number; total_input_tokens: number;
@@ -74,7 +74,7 @@ export const formattedStats = derived(stats, ($stats) => {
sessionInputTokens: formatNumber($stats.session_input_tokens), sessionInputTokens: formatNumber($stats.session_input_tokens),
sessionOutputTokens: formatNumber($stats.session_output_tokens), sessionOutputTokens: formatNumber($stats.session_output_tokens),
sessionCost: formatCost($stats.session_cost_usd), sessionCost: formatCost($stats.session_cost_usd),
model: $stats.model || 'No model selected', model: $stats.model || "No model selected",
// New formatted fields // New formatted fields
messagesTotal: formatNumber($stats.messages_exchanged), messagesTotal: formatNumber($stats.messages_exchanged),
@@ -96,7 +96,7 @@ export const formattedStats = derived(stats, ($stats) => {
// Initialize stats listener // Initialize stats listener
export async function initStatsListener() { export async function initStatsListener() {
// Listen for stats updates from the backend // Listen for stats updates from the backend
await listen('claude:stats', (event) => { await listen("claude:stats", (event) => {
const payload = event.payload as { stats: UsageStats }; const payload = event.payload as { stats: UsageStats };
const { stats: newStats } = payload; const { stats: newStats } = payload;
@@ -106,16 +106,16 @@ export async function initStatsListener() {
// Load initial stats from backend // Load initial stats from backend
try { try {
const initialStats = await invoke<UsageStats>('get_usage_stats'); const initialStats = await invoke<UsageStats>("get_usage_stats");
stats.set(initialStats); stats.set(initialStats);
} catch (error) { } catch (error) {
console.error('Failed to load initial stats:', error); console.error("Failed to load initial stats:", error);
} }
} }
// Reset session stats (call when starting new session) // Reset session stats (call when starting new session)
export function resetSessionStats() { export function resetSessionStats() {
stats.update(current => ({ stats.update((current) => ({
...current, ...current,
session_input_tokens: 0, session_input_tokens: 0,
session_output_tokens: 0, session_output_tokens: 0,
+52 -52
View File
@@ -10,76 +10,76 @@ export interface AchievementUnlockedEvent {
export type AchievementId = export type AchievementId =
// Token Milestones // Token Milestones
| 'FirstSteps' // 1,000 tokens | "FirstSteps" // 1,000 tokens
| 'GrowingStrong' // 10,000 tokens | "GrowingStrong" // 10,000 tokens
| 'BlossomingCoder' // 100,000 tokens | "BlossomingCoder" // 100,000 tokens
| 'TokenMaster' // 1,000,000 tokens | "TokenMaster" // 1,000,000 tokens
// Code Generation // Code Generation
| 'HelloWorld' // First code block | "HelloWorld" // First code block
| 'CodeWizard' // 100 code blocks | "CodeWizard" // 100 code blocks
| 'ThousandBlocks' // 1,000 code blocks | "ThousandBlocks" // 1,000 code blocks
// File Operations // File Operations
| 'FileManipulator' // 10 files edited | "FileManipulator" // 10 files edited
| 'FileArchitect' // 100 files edited | "FileArchitect" // 100 files edited
// Conversation milestones // Conversation milestones
| 'ConversationStarter' // 10 messages | "ConversationStarter" // 10 messages
| 'ChattyKathy' // 100 messages | "ChattyKathy" // 100 messages
| 'Conversationalist' // 1,000 messages | "Conversationalist" // 1,000 messages
// Tool usage // Tool usage
| 'Toolsmith' // 5 different tools | "Toolsmith" // 5 different tools
| 'ToolMaster' // 10 different tools | "ToolMaster" // 10 different tools
// Time-based achievements // Time-based achievements
| 'EarlyBird' // Started session 5-7 AM | "EarlyBird" // Started session 5-7 AM
| 'NightOwl' // Coding after midnight | "NightOwl" // Coding after midnight
| 'AllNighter' // Worked 2-5 AM | "AllNighter" // Worked 2-5 AM
| 'WeekendWarrior' // Coding on weekend | "WeekendWarrior" // Coding on weekend
| 'DedicatedDeveloper' // 30 days in a row | "DedicatedDeveloper" // 30 days in a row
// Search and exploration // Search and exploration
| 'Explorer' // 50 searches | "Explorer" // 50 searches
| 'MasterSearcher' // 500 searches | "MasterSearcher" // 500 searches
// Session achievements // Session achievements
| 'QuickSession' // Productive session < 5 min | "QuickSession" // Productive session < 5 min
| 'FocusedWork' // 30 min session | "FocusedWork" // 30 min session
| 'DeepDive' // 2 hour session | "DeepDive" // 2 hour session
| 'MarathonSession' // 5+ hour session | "MarathonSession" // 5+ hour session
// Special achievements // Special achievements
| 'FirstMessage' // First message sent | "FirstMessage" // First message sent
| 'FirstTool' // First tool used | "FirstTool" // First tool used
| 'FirstCodeBlock' // First code generated | "FirstCodeBlock" // First code generated
| 'FirstFileEdit' // First file edit | "FirstFileEdit" // First file edit
| 'Polyglot' // 5+ languages in one session | "Polyglot" // 5+ languages in one session
| 'SpeedCoder' // 10 code blocks in 10 minutes | "SpeedCoder" // 10 code blocks in 10 minutes
| 'ClaudeConnoisseur' // Used all Claude models | "ClaudeConnoisseur" // Used all Claude models
| 'MarathonCoder' // 10k tokens in one session | "MarathonCoder" // 10k tokens in one session
// Relationship & Greetings // Relationship & Greetings
| 'GoodMorning' // Said good morning | "GoodMorning" // Said good morning
| 'GoodNight' // Said good night | "GoodNight" // Said good night
| 'ThankYou' // Said thank you | "ThankYou" // Said thank you
| 'LoveYou' // Said love you | "LoveYou" // Said love you
// Personality & Fun // Personality & Fun
| 'EmojiUser' // Used 20+ emojis | "EmojiUser" // Used 20+ emojis
| 'CapsLock' // ALL CAPS MESSAGE | "CapsLock" // ALL CAPS MESSAGE
| 'QuestionMaster' // Asked 50 questions | "QuestionMaster" // Asked 50 questions
| 'PleaseAndThankYou' // Polite user | "PleaseAndThankYou" // Polite user
// Git & Development // Git & Development
| 'CommitMaster' // 100 commits | "CommitMaster" // 100 commits
| 'PRO' // Created 10 PRs | "PRO" // Created 10 PRs
| 'Reviewer' // Reviewed 10 PRs | "Reviewer" // Reviewed 10 PRs
| 'IssueTracker' // Created 25 issues | "IssueTracker" // Created 25 issues
| 'GitGuru' // Used git commands | "GitGuru" // Used git commands
// Tool Mastery // Tool Mastery
| 'BashMaster' // Used bash 100 times | "BashMaster" // Used bash 100 times
| 'FileExplorer' // Searched files 100 times | "FileExplorer" // Searched files 100 times
| 'SearchExpert' // Advanced searches | "SearchExpert" // Advanced searches
| 'AgentCommander' // Used task agents | "AgentCommander" // Used task agents
| 'MCPMaster'; // Used MCP tools | "MCPMaster"; // Used MCP tools
export interface Achievement { export interface Achievement {
id: AchievementId; id: AchievementId;
name: string; name: string;
description: string; description: string;
icon: string; icon: string;
rarity: 'common' | 'rare' | 'epic' | 'legendary'; rarity: "common" | "rare" | "epic" | "legendary";
unlocked: boolean; unlocked: boolean;
unlockedAt?: Date; unlockedAt?: Date;
progress?: number; progress?: number;
+5 -2
View File
@@ -36,7 +36,7 @@
</script> </script>
<div class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden"> <div class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden">
<StatusBar onToggleAchievements={() => achievementPanelOpen = !achievementPanelOpen} /> <StatusBar onToggleAchievements={() => (achievementPanelOpen = !achievementPanelOpen)} />
<main class="flex-1 flex overflow-hidden"> <main class="flex-1 flex overflow-hidden">
<!-- Left panel: Character display --> <!-- Left panel: Character display -->
@@ -56,7 +56,10 @@
<PermissionModal /> <PermissionModal />
<ConfigSidebar /> <ConfigSidebar />
<AchievementNotification /> <AchievementNotification />
<AchievementsPanel bind:isOpen={achievementPanelOpen} onClose={() => achievementPanelOpen = false} /> <AchievementsPanel
bind:isOpen={achievementPanelOpen}
onClose={() => (achievementPanelOpen = false)}
/>
</div> </div>
<style> <style>
+7 -7
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { testAchievementSound } from '$lib/sounds/achievement'; import { testAchievementSound } from "$lib/sounds/achievement";
import { invoke } from '@tauri-apps/api/core'; import { invoke } from "@tauri-apps/api/core";
async function testSound() { async function testSound() {
testAchievementSound(); testAchievementSound();
@@ -9,12 +9,12 @@
async function triggerAchievement() { async function triggerAchievement() {
// This will trigger an achievement that hasn't been unlocked yet // This will trigger an achievement that hasn't been unlocked yet
try { try {
await invoke('check_achievements', { await invoke("check_achievements", {
eventType: 'message_sent', eventType: "message_sent",
data: {} data: {},
}); });
} catch (error) { } catch (error) {
console.error('Failed to trigger achievement:', error); console.error("Failed to trigger achievement:", error);
} }
} }
</script> </script>
@@ -38,7 +38,7 @@
</button> </button>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4"> <p class="text-sm text-gray-600 dark:text-gray-400 mt-4">
Click the first button to test just the sound effect.<br> Click the first button to test just the sound effect.<br />
Click the second button to trigger a real achievement (if any are available to unlock). Click the second button to trigger a real achievement (if any are available to unlock).
</p> </p>
</div> </div>