Improvements to the UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export const AI_TASK_SYSTEM_PROMPT = `You are a supportive wellness coach for a habit-tracking pet app.
|
||||
export const AI_TASK_SYSTEM_PROMPT = `You are a supportive wellness coach for a habit-tracking app where users care for a digital self (their "bit").
|
||||
|
||||
Your only allowed action is to suggest practical daily habit task labels based on trusted app context and untrusted user journal text.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export function formatAiTaskContextForPrompt(context: AiTaskContext) {
|
||||
`Daily reminder enabled: ${preferences.dailyReminderEnabled ? "yes" : "no"}`,
|
||||
`Daily reminder time: ${preferences.dailyReminderTime}`,
|
||||
`Primary focus: ${onboarding.focusTopic ?? "unknown"}`,
|
||||
`Starter pet vibe: ${onboarding.avatarVibe ?? "unknown"}`,
|
||||
`Starter bit vibe: ${onboarding.avatarVibe ?? "unknown"}`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ type AvatarStateRow = {
|
||||
room_background: string | null;
|
||||
};
|
||||
|
||||
export const DEFAULT_AVATAR_NAME = "Pixel Me";
|
||||
export const DEFAULT_AVATAR_NAME = "My Bit";
|
||||
const DEFAULT_HEX = DEFAULT_GRAY_COLOR.hex;
|
||||
const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
|
||||
|
||||
|
||||
1
lib/brand.ts
Normal file
1
lib/brand.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const HABIT_LOGO_SRC = "/habit-logo.png";
|
||||
@@ -129,7 +129,7 @@ export function buildDailyReminderNotification(status: DailyReminderStatus) {
|
||||
if (!status.quizCompleted) {
|
||||
return {
|
||||
title: "Time for your daily check-in",
|
||||
body: "Complete today's wellness quiz to keep your pet happy.",
|
||||
body: "Complete today's wellness quiz to keep your bit in good spirits.",
|
||||
targetUrl,
|
||||
};
|
||||
}
|
||||
@@ -174,7 +174,7 @@ export function showTestSystemDailyReminderNotification(
|
||||
}
|
||||
|
||||
const { body, targetUrl } = buildDailyReminderNotification(status);
|
||||
const notification = new Notification("Test: Habit Pet reminder", {
|
||||
const notification = new Notification("Test: HaBit reminder", {
|
||||
body: `This is a browser test notification. ${body}`,
|
||||
tag: `habit-pet-daily-reminder-test-${Date.now()}`,
|
||||
});
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
import {
|
||||
DEFAULT_AVATAR_NAME,
|
||||
type AvatarCustomization,
|
||||
} from "@/lib/avatar-customization-storage";
|
||||
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
|
||||
import {
|
||||
CHARACTER_LAYERS,
|
||||
COLOR_PRESETS,
|
||||
NONE_VARIANT_ID,
|
||||
type CharacterLayerId,
|
||||
} from "@/lib/character/presets";
|
||||
import { ROOM_BACKGROUNDS } from "@/lib/room-backgrounds";
|
||||
import { clampHsl } from "@/lib/character/color-utils";
|
||||
import {
|
||||
ROOM_BACKGROUNDS,
|
||||
type RoomBackgroundId,
|
||||
} from "@/lib/room-backgrounds";
|
||||
|
||||
const GUEST_PET_STORAGE_KEY = "habit-pet-guest-landing-pet";
|
||||
|
||||
const SKIN_TONE_IDS = [
|
||||
"porcelain",
|
||||
"fair",
|
||||
"light",
|
||||
"medium",
|
||||
"tan",
|
||||
"brown",
|
||||
"dark",
|
||||
"deep",
|
||||
"onyx",
|
||||
export const GUEST_LANDING_BIT_NAMES = [
|
||||
"Alex",
|
||||
"Jordan",
|
||||
"Sam",
|
||||
"Taylor",
|
||||
"Morgan",
|
||||
] as const;
|
||||
|
||||
const OUTFIT_COLOR_IDS = [
|
||||
"red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"blue",
|
||||
"indigo",
|
||||
"black",
|
||||
"white",
|
||||
] as const;
|
||||
const SKIN_COLOR_PRESETS = COLOR_PRESETS.filter((preset) =>
|
||||
[
|
||||
"porcelain",
|
||||
"fair",
|
||||
"light",
|
||||
"medium",
|
||||
"tan",
|
||||
"brown",
|
||||
"dark",
|
||||
"deep",
|
||||
"onyx",
|
||||
].includes(preset.id),
|
||||
);
|
||||
|
||||
const EYE_COLOR_IDS = ["blue", "black", "green", "brown"] as const;
|
||||
const STYLE_COLOR_PRESETS = COLOR_PRESETS.filter(
|
||||
(preset) => !SKIN_COLOR_PRESETS.some((skin) => skin.id === preset.id),
|
||||
);
|
||||
|
||||
const HAIR_COLOR_IDS = ["brown", "black", "yellow", "red", "orange", "white"] as const;
|
||||
const ALL_ROOM_IDS = ROOM_BACKGROUNDS.map((room) => room.id);
|
||||
|
||||
function pickRandom<T>(items: readonly T[]): T {
|
||||
return items[Math.floor(Math.random() * items.length)]!;
|
||||
@@ -52,48 +54,61 @@ function presetHsl(id: string) {
|
||||
return { ...preset.hsl };
|
||||
}
|
||||
|
||||
function pickRandomVariant(
|
||||
layerId: Exclude<CharacterLayerId, "skin">,
|
||||
allowNone = false,
|
||||
): string {
|
||||
function randomHsl(): { h: number; s: number; l: number } {
|
||||
return clampHsl({
|
||||
h: Math.floor(Math.random() * 360),
|
||||
s: 35 + Math.floor(Math.random() * 50),
|
||||
l: 28 + Math.floor(Math.random() * 45),
|
||||
});
|
||||
}
|
||||
|
||||
function pickRandomLayerColor(layerId: CharacterLayerId) {
|
||||
if (layerId === "skin") {
|
||||
return presetHsl(pickRandom(SKIN_COLOR_PRESETS).id);
|
||||
}
|
||||
|
||||
if (Math.random() < 0.35) {
|
||||
return randomHsl();
|
||||
}
|
||||
|
||||
return presetHsl(pickRandom(STYLE_COLOR_PRESETS).id);
|
||||
}
|
||||
|
||||
function pickRandomLayerVariant(layerId: CharacterLayerId): string {
|
||||
const layer = CHARACTER_LAYERS.find((entry) => entry.id === layerId);
|
||||
if (!layer) {
|
||||
throw new Error(`Missing character layer: ${layerId}`);
|
||||
}
|
||||
|
||||
const options = layer.variants
|
||||
.map((variant) => variant.id)
|
||||
.filter((id) => allowNone || id !== NONE_VARIANT_ID);
|
||||
if (!layer.allowVariants) {
|
||||
return layer.variants[0]?.id ?? NONE_VARIANT_ID;
|
||||
}
|
||||
|
||||
return pickRandom(options);
|
||||
return pickRandom(layer.variants.map((variant) => variant.id));
|
||||
}
|
||||
|
||||
export function buildRandomLandingPetCustomization(): AvatarCustomization {
|
||||
const freeRooms = ROOM_BACKGROUNDS.filter((room) => room.free).map(
|
||||
(room) => room.id,
|
||||
);
|
||||
|
||||
return {
|
||||
name: DEFAULT_AVATAR_NAME,
|
||||
name: pickRandom(GUEST_LANDING_BIT_NAMES),
|
||||
colors: {
|
||||
skin: presetHsl(pickRandom(SKIN_TONE_IDS)),
|
||||
pants: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
|
||||
shoes: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
|
||||
torso: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
|
||||
eyes: presetHsl(pickRandom(EYE_COLOR_IDS)),
|
||||
head: presetHsl(pickRandom(HAIR_COLOR_IDS)),
|
||||
skin: pickRandomLayerColor("skin"),
|
||||
pants: pickRandomLayerColor("pants"),
|
||||
shoes: pickRandomLayerColor("shoes"),
|
||||
torso: pickRandomLayerColor("torso"),
|
||||
eyes: pickRandomLayerColor("eyes"),
|
||||
head: pickRandomLayerColor("head"),
|
||||
},
|
||||
variants: {
|
||||
skin: "skin-1",
|
||||
pants: pickRandomVariant("pants"),
|
||||
shoes: pickRandomVariant("shoes"),
|
||||
torso: pickRandomVariant("torso"),
|
||||
eyes: pickRandomVariant("eyes"),
|
||||
head: pickRandomVariant("head", Math.random() < 0.15),
|
||||
skin: pickRandomLayerVariant("skin"),
|
||||
pants: pickRandomLayerVariant("pants"),
|
||||
shoes: pickRandomLayerVariant("shoes"),
|
||||
torso: pickRandomLayerVariant("torso"),
|
||||
eyes: pickRandomLayerVariant("eyes"),
|
||||
head: pickRandomLayerVariant("head"),
|
||||
},
|
||||
customized: true,
|
||||
equippedItems: [],
|
||||
roomBackground: pickRandom(freeRooms),
|
||||
roomBackground: pickRandom(ALL_ROOM_IDS) as RoomBackgroundId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,7 +120,14 @@ export function getGuestLandingPetCustomization(): AvatarCustomization {
|
||||
try {
|
||||
const cached = sessionStorage.getItem(GUEST_PET_STORAGE_KEY);
|
||||
if (cached) {
|
||||
return JSON.parse(cached) as AvatarCustomization;
|
||||
const parsed = JSON.parse(cached) as AvatarCustomization;
|
||||
if (
|
||||
GUEST_LANDING_BIT_NAMES.includes(
|
||||
parsed.name as (typeof GUEST_LANDING_BIT_NAMES)[number],
|
||||
)
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
const next = buildRandomLandingPetCustomization();
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function generateAiTaskLabels(
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": siteUrl,
|
||||
"X-Title": "Habit Pet",
|
||||
"X-Title": "HaBit",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: DEFAULT_OPENROUTER_MODEL,
|
||||
|
||||
26
lib/pet-status.ts
Normal file
26
lib/pet-status.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { AvatarMood } from "@/lib/avatar-state";
|
||||
|
||||
export const PET_EMOTION_DISPLAY_MS = 3200;
|
||||
|
||||
export type PetStatusImageId = "status-1" | "status-2" | "status-3";
|
||||
|
||||
export const PET_STATUS_BY_MOOD: Record<AvatarMood, PetStatusImageId> = {
|
||||
happy: "status-1",
|
||||
neutral: "status-2",
|
||||
tired: "status-3",
|
||||
};
|
||||
|
||||
export function getPetStatusImagePath(mood: AvatarMood): string {
|
||||
return `/status/${PET_STATUS_BY_MOOD[mood]}.png`;
|
||||
}
|
||||
|
||||
export function getPetEmotionMessage(mood: AvatarMood): string {
|
||||
switch (mood) {
|
||||
case "happy":
|
||||
return "Your bit seems cheerful and in good spirits.";
|
||||
case "tired":
|
||||
return "Your bit seems worn out and could use some care.";
|
||||
default:
|
||||
return "Your bit seems steady and doing okay.";
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ export const reminderDeliveryOptions = [
|
||||
value: "in_app",
|
||||
label: "In-app",
|
||||
description:
|
||||
"Show reminders inside Habit Pet when you have the site open in a tab.",
|
||||
"Show reminders inside HaBit when you have the site open in a tab.",
|
||||
},
|
||||
{
|
||||
value: "system",
|
||||
|
||||
@@ -16,7 +16,8 @@ export const routes = {
|
||||
export type AppRoute = (typeof routes)[keyof typeof routes];
|
||||
|
||||
export const appNavItems = [
|
||||
{ href: routes.avatar, label: "Pet", shortLabel: "Pet" },
|
||||
{ href: routes.avatar, label: "Bit", shortLabel: "Bit" },
|
||||
{ href: routes.customize, label: "Customize", shortLabel: "Style" },
|
||||
{ href: routes.habits, label: "Habits", shortLabel: "Habits" },
|
||||
{ href: routes.dailyQuiz, label: "Daily Quiz", shortLabel: "Quiz" },
|
||||
{ href: routes.shop, label: "Shop", shortLabel: "Shop" },
|
||||
|
||||
Reference in New Issue
Block a user