generated from nhcarrigan/template
chore: fix lints and tests
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}">
|
||||||
|
|||||||
@@ -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
@@ -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[],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user