generated from nhcarrigan/template
c241544743
### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #2 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
232 lines
4.8 KiB
Svelte
232 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import { characterState, characterInfo } from "$lib/stores/character";
|
|
import type { CharacterState, CharacterStateInfo } from "$lib/types/states";
|
|
|
|
let currentState: CharacterState = $state("idle");
|
|
let info: CharacterStateInfo = $state({
|
|
state: "idle",
|
|
label: "Ready",
|
|
description: "Waiting for your command~",
|
|
spriteFile: "idle.png",
|
|
});
|
|
|
|
characterState.subscribe((state) => {
|
|
currentState = state;
|
|
});
|
|
|
|
characterInfo.subscribe((i) => {
|
|
info = i;
|
|
});
|
|
|
|
function getAnimationClass(): string {
|
|
switch (currentState) {
|
|
case "thinking":
|
|
return "animate-thinking";
|
|
case "typing":
|
|
return "animate-typing";
|
|
case "searching":
|
|
return "animate-searching";
|
|
case "success":
|
|
return "animate-celebrate";
|
|
case "error":
|
|
return "animate-shake";
|
|
default:
|
|
return "animate-idle";
|
|
}
|
|
}
|
|
|
|
function getBackgroundGlow(): string {
|
|
switch (currentState) {
|
|
case "thinking":
|
|
return "shadow-thinking";
|
|
case "typing":
|
|
return "shadow-typing";
|
|
case "searching":
|
|
return "shadow-searching";
|
|
case "coding":
|
|
return "shadow-coding";
|
|
case "mcp":
|
|
return "shadow-mcp";
|
|
case "success":
|
|
return "shadow-success";
|
|
case "error":
|
|
return "shadow-error";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="anime-girl-container flex flex-col items-center justify-end h-full p-4">
|
|
<div class="character-frame relative {getBackgroundGlow()} w-full max-w-md">
|
|
<div class="sprite-container {getAnimationClass()}">
|
|
<img
|
|
src="/sprites/{info.spriteFile}"
|
|
alt="Hikari - {info.label}"
|
|
class="character-sprite w-full h-auto object-contain"
|
|
onerror={(e) => {
|
|
const target = e.currentTarget as HTMLImageElement;
|
|
target.src = "/sprites/placeholder.svg";
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="state-indicator absolute -bottom-2 left-1/2 transform -translate-x-1/2">
|
|
<div
|
|
class="px-3 py-1 rounded-full text-xs font-medium bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--accent-primary)]"
|
|
>
|
|
{info.label}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="speech-bubble mt-4 max-w-xs">
|
|
<div
|
|
class="relative bg-[var(--bg-secondary)] rounded-lg px-4 py-2 border border-[var(--border-color)]"
|
|
>
|
|
<div
|
|
class="absolute -top-2 left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-8 border-r-8 border-b-8 border-transparent border-b-[var(--bg-secondary)]"
|
|
></div>
|
|
<p class="text-sm text-gray-300 text-center italic">{info.description}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.character-frame {
|
|
border-radius: 50%;
|
|
transition: box-shadow 0.3s ease;
|
|
}
|
|
|
|
.shadow-thinking {
|
|
box-shadow: 0 0 30px rgba(147, 51, 234, 0.5);
|
|
}
|
|
|
|
.shadow-typing {
|
|
box-shadow: 0 0 30px rgba(59, 130, 246, 0.5);
|
|
}
|
|
|
|
.shadow-searching {
|
|
box-shadow: 0 0 30px rgba(234, 179, 8, 0.5);
|
|
}
|
|
|
|
.shadow-coding {
|
|
box-shadow: 0 0 30px rgba(34, 197, 94, 0.5);
|
|
}
|
|
|
|
.shadow-mcp {
|
|
box-shadow: 0 0 30px rgba(236, 72, 153, 0.5);
|
|
}
|
|
|
|
.shadow-success {
|
|
box-shadow: 0 0 30px rgba(16, 185, 129, 0.5);
|
|
}
|
|
|
|
.shadow-error {
|
|
box-shadow: 0 0 30px rgba(239, 68, 68, 0.5);
|
|
}
|
|
|
|
@keyframes idle-bob {
|
|
0%,
|
|
100% {
|
|
transform: translateY(0);
|
|
}
|
|
50% {
|
|
transform: translateY(-5px);
|
|
}
|
|
}
|
|
|
|
@keyframes thinking-sway {
|
|
0%,
|
|
100% {
|
|
transform: rotate(-2deg);
|
|
}
|
|
50% {
|
|
transform: rotate(2deg);
|
|
}
|
|
}
|
|
|
|
@keyframes typing-bounce {
|
|
0%,
|
|
100% {
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
50% {
|
|
transform: translateY(-3px) scale(1.02);
|
|
}
|
|
}
|
|
|
|
@keyframes searching-look {
|
|
0%,
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
25% {
|
|
transform: translateX(-5px);
|
|
}
|
|
75% {
|
|
transform: translateX(5px);
|
|
}
|
|
}
|
|
|
|
@keyframes celebrate {
|
|
0%,
|
|
100% {
|
|
transform: scale(1) rotate(0deg);
|
|
}
|
|
25% {
|
|
transform: scale(1.1) rotate(-5deg);
|
|
}
|
|
50% {
|
|
transform: scale(1.1) rotate(5deg);
|
|
}
|
|
75% {
|
|
transform: scale(1.05) rotate(-3deg);
|
|
}
|
|
}
|
|
|
|
@keyframes shake {
|
|
0%,
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
10%,
|
|
30%,
|
|
50%,
|
|
70%,
|
|
90% {
|
|
transform: translateX(-5px);
|
|
}
|
|
20%,
|
|
40%,
|
|
60%,
|
|
80% {
|
|
transform: translateX(5px);
|
|
}
|
|
}
|
|
|
|
.animate-idle {
|
|
animation: idle-bob 3s ease-in-out infinite;
|
|
}
|
|
|
|
.animate-thinking {
|
|
animation: thinking-sway 2s ease-in-out infinite;
|
|
}
|
|
|
|
.animate-typing {
|
|
animation: typing-bounce 0.5s ease-in-out infinite;
|
|
}
|
|
|
|
.animate-searching {
|
|
animation: searching-look 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
.animate-celebrate {
|
|
animation: celebrate 0.8s ease-in-out;
|
|
}
|
|
|
|
.animate-shake {
|
|
animation: shake 0.5s ease-in-out;
|
|
}
|
|
</style>
|