Skip to content

Commit e386085

Browse files
committed
Add demo reel assets and autoplay route
1 parent 412fc3d commit e386085

9 files changed

Lines changed: 808 additions & 50 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
InboxAnchor is a safety-first inbox triage system for overloaded email accounts. It classifies, prioritizes, summarizes, drafts, and recommends cleanup actions while keeping a human in the loop for anything risky. The project is intentionally opinionated: it is not an auto-delete bot, and it treats approval, auditability, and rollback-friendly behavior as product features, not afterthoughts.
44

5+
## Demo
6+
7+
![InboxAnchor demo reel](docs/assets/inboxanchor-demo.gif)
8+
9+
The demo reel shows the command center, live unread-scan progress, the stickman loading game, inbox triage, mailbox upgrade actions, and managed privacy aliases in one short product loop.
10+
511
## What It Does
612

713
- Connects to demo, Gmail, and IMAP-family provider paths through a shared provider abstraction
512 KB
Loading

docs/assets/inboxanchor-demo.gif

1.22 MB
Loading

docs/assets/inboxanchor-demo.mp4

397 KB
Binary file not shown.

frontend/src/components/StickmanInboxRunner.tsx

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ type RunnerState = {
1818
obstacles: Obstacle[];
1919
};
2020

21+
export type StickmanInboxRunnerProps = {
22+
autoplay?: boolean;
23+
className?: string;
24+
showControlsHint?: boolean;
25+
};
26+
2127
const WIDTH = 320;
2228
const HEIGHT = 152;
2329
const GROUND_Y = 116;
@@ -64,19 +70,31 @@ function intersects(a: { left: number; right: number; top: number; bottom: numbe
6470
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
6571
}
6672

67-
export function StickmanInboxRunner({ className = "" }: { className?: string }) {
73+
export function StickmanInboxRunner({
74+
autoplay = false,
75+
className = "",
76+
showControlsHint = true,
77+
}: StickmanInboxRunnerProps) {
6878
const [game, setGame] = useState<RunnerState>(() => initialState());
6979
const containerRef = useRef<HTMLDivElement | null>(null);
7080
const jumpQueuedRef = useRef(0);
7181
const crouchActiveRef = useRef(false);
7282
const obstacleIdRef = useRef(0);
73-
const spawnCooldownRef = useRef(900);
83+
const autoplayPatternRef = useRef<ObstacleKind[]>(["envelope", "thread", "envelope", "thread"]);
84+
const autoplayPatternIndexRef = useRef(0);
85+
const spawnCooldownRef = useRef(autoplay ? 460 : 900);
7486
const spawnElapsedRef = useRef(0);
7587

7688
useEffect(() => {
7789
containerRef.current?.focus();
7890
}, []);
7991

92+
useEffect(() => {
93+
spawnCooldownRef.current = autoplay ? 460 : 900;
94+
spawnElapsedRef.current = 0;
95+
autoplayPatternIndexRef.current = 0;
96+
}, [autoplay]);
97+
8098
useEffect(() => {
8199
const handleKeyDown = (event: KeyboardEvent | ReactKeyboardEvent<HTMLDivElement>) => {
82100
if (["ArrowUp", "ArrowDown", "Space", "KeyW", "KeyS"].includes(event.code)) {
@@ -144,7 +162,51 @@ export function StickmanInboxRunner({ className = "" }: { className?: string })
144162
setGame((previous) => {
145163
let y = previous.y;
146164
let velocityY = previous.velocityY;
165+
const speed = 3.25 * timeScale;
166+
let obstacles = previous.obstacles
167+
.map((obstacle) => ({ ...obstacle, x: obstacle.x - speed }))
168+
.filter((obstacle) => obstacle.x > -60);
169+
170+
spawnElapsedRef.current += dt;
171+
if (spawnElapsedRef.current >= spawnCooldownRef.current) {
172+
obstacleIdRef.current += 1;
173+
const kind: ObstacleKind = autoplay
174+
? autoplayPatternRef.current[
175+
autoplayPatternIndexRef.current++ % autoplayPatternRef.current.length
176+
]
177+
: Math.random() > 0.32
178+
? "envelope"
179+
: "thread";
180+
obstacles = [...obstacles, { id: obstacleIdRef.current, kind, x: WIDTH + 16 }];
181+
spawnElapsedRef.current = 0;
182+
spawnCooldownRef.current = autoplay ? 640 : 900 + Math.random() * 700;
183+
}
184+
147185
const grounded = y <= 0.001;
186+
const nextObstacle = obstacles.find((obstacle) => {
187+
const box = obstacleBox(obstacle);
188+
return box.right >= PLAYER_X - 6;
189+
});
190+
const nextObstacleBox = nextObstacle ? obstacleBox(nextObstacle) : null;
191+
const autoplayCrouch =
192+
autoplay &&
193+
grounded &&
194+
nextObstacle?.kind === "thread" &&
195+
nextObstacleBox !== null &&
196+
nextObstacleBox.left <= PLAYER_X + PLAYER_WIDTH + 26 &&
197+
nextObstacleBox.right >= PLAYER_X - 4;
198+
199+
if (
200+
autoplay &&
201+
grounded &&
202+
nextObstacle?.kind === "envelope" &&
203+
nextObstacleBox !== null &&
204+
nextObstacleBox.left <= PLAYER_X + PLAYER_WIDTH + 34
205+
) {
206+
jumpQueuedRef.current = Math.max(jumpQueuedRef.current, 1);
207+
velocityY = Math.max(velocityY, 11.2);
208+
jumpQueuedRef.current = 0;
209+
}
148210

149211
if (grounded && jumpQueuedRef.current > 0) {
150212
velocityY = Math.max(velocityY, 11.2);
@@ -157,21 +219,7 @@ export function StickmanInboxRunner({ className = "" }: { className?: string })
157219
velocityY = 0;
158220
}
159221

160-
const crouching = crouchActiveRef.current && y === 0;
161-
const speed = 3.25 * timeScale;
162-
let obstacles = previous.obstacles
163-
.map((obstacle) => ({ ...obstacle, x: obstacle.x - speed }))
164-
.filter((obstacle) => obstacle.x > -60);
165-
166-
spawnElapsedRef.current += dt;
167-
if (spawnElapsedRef.current >= spawnCooldownRef.current) {
168-
obstacleIdRef.current += 1;
169-
const kind: ObstacleKind = Math.random() > 0.32 ? "envelope" : "thread";
170-
obstacles = [...obstacles, { id: obstacleIdRef.current, kind, x: WIDTH + 16 }];
171-
spawnElapsedRef.current = 0;
172-
spawnCooldownRef.current = 900 + Math.random() * 700;
173-
}
174-
222+
const crouching = (crouchActiveRef.current || autoplayCrouch) && y === 0;
175223
const playerHeight = crouching ? CROUCH_HEIGHT : STANDING_HEIGHT;
176224
const playerBox = {
177225
left: PLAYER_X,
@@ -291,9 +339,13 @@ export function StickmanInboxRunner({ className = "" }: { className?: string })
291339
<span>Best {Math.max(game.best, score)}</span>
292340
<span>Resets {game.crashes}</span>
293341
</div>
294-
<p className="mt-1 text-center text-[10px] uppercase tracking-[0.18em] text-muted-foreground/80">
295-
Click or hover here, then press W, S, arrows, or space.
296-
</p>
342+
{showControlsHint ? (
343+
<p className="mt-1 text-center text-[10px] uppercase tracking-[0.18em] text-muted-foreground/80">
344+
{autoplay
345+
? "Autoplay is on. Press W, S, arrows, or space to take over."
346+
: "Click or hover here, then press W, S, arrows, or space."}
347+
</p>
348+
) : null}
297349
</div>
298350
);
299351
}

frontend/src/components/StickmenAnimations.tsx

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { StickmanInboxRunner } from "@/components/StickmanInboxRunner";
1+
import {
2+
StickmanInboxRunner,
3+
type StickmanInboxRunnerProps,
4+
} from "@/components/StickmanInboxRunner";
25
import { useLoaderMode } from "@/hooks/use-loader-mode";
36

47
type LoaderStat = {
@@ -127,44 +130,54 @@ export function StickmanLoader({
127130
stage,
128131
activity,
129132
stats = [],
133+
modeOverride,
134+
runnerProps,
135+
showModeToggle = true,
130136
}: {
131137
message?: string;
132138
playful?: boolean;
133139
stage?: string;
134140
activity?: string;
135141
stats?: LoaderStat[];
142+
modeOverride?: "fun" | "serious";
143+
runnerProps?: StickmanInboxRunnerProps;
144+
showModeToggle?: boolean;
136145
}) {
137146
const { mode, setMode, isFunMode } = useLoaderMode();
147+
const resolvedMode = modeOverride ?? mode;
148+
const resolvedFunMode = modeOverride ? modeOverride === "fun" : isFunMode;
138149

139150
if (playful) {
140151
return (
141152
<div className="flex flex-col items-center justify-center gap-3 py-6 text-muted-foreground">
142-
<div className="flex items-center gap-2 rounded-full border border-border bg-card/70 p-1">
143-
<button
144-
type="button"
145-
onClick={() => setMode("fun")}
146-
className={`rounded-full px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] transition-colors ${
147-
mode === "fun"
148-
? "bg-primary text-primary-foreground"
149-
: "text-muted-foreground hover:bg-secondary hover:text-foreground"
150-
}`}
151-
>
152-
Fun Mode
153-
</button>
154-
<button
155-
type="button"
156-
onClick={() => setMode("serious")}
157-
className={`rounded-full px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] transition-colors ${
158-
mode === "serious"
159-
? "bg-primary text-primary-foreground"
160-
: "text-muted-foreground hover:bg-secondary hover:text-foreground"
161-
}`}
162-
>
163-
Serious Mode
164-
</button>
165-
</div>
166-
{isFunMode ? (
167-
<StickmanInboxRunner />
153+
{showModeToggle ? (
154+
<div className="flex items-center gap-2 rounded-full border border-border bg-card/70 p-1">
155+
<button
156+
type="button"
157+
onClick={() => setMode("fun")}
158+
className={`rounded-full px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] transition-colors ${
159+
resolvedMode === "fun"
160+
? "bg-primary text-primary-foreground"
161+
: "text-muted-foreground hover:bg-secondary hover:text-foreground"
162+
}`}
163+
>
164+
Fun Mode
165+
</button>
166+
<button
167+
type="button"
168+
onClick={() => setMode("serious")}
169+
className={`rounded-full px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] transition-colors ${
170+
resolvedMode === "serious"
171+
? "bg-primary text-primary-foreground"
172+
: "text-muted-foreground hover:bg-secondary hover:text-foreground"
173+
}`}
174+
>
175+
Serious Mode
176+
</button>
177+
</div>
178+
) : null}
179+
{resolvedFunMode ? (
180+
<StickmanInboxRunner autoplay {...runnerProps} />
168181
) : (
169182
<div className="flex w-full max-w-[360px] flex-col items-center gap-4 rounded-2xl border border-border bg-background/80 p-6 shadow-sm">
170183
<div className="h-12 w-12 animate-spin rounded-full border-4 border-border border-t-primary" />

frontend/src/routeTree.gen.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as SettingsRouteImport } from './routes/settings'
1313
import { Route as LoginRouteImport } from './routes/login'
1414
import { Route as InboxRouteImport } from './routes/inbox'
15+
import { Route as DemoRouteImport } from './routes/demo'
1516
import { Route as IndexRouteImport } from './routes/index'
1617

1718
const SettingsRoute = SettingsRouteImport.update({
@@ -29,6 +30,11 @@ const InboxRoute = InboxRouteImport.update({
2930
path: '/inbox',
3031
getParentRoute: () => rootRouteImport,
3132
} as any)
33+
const DemoRoute = DemoRouteImport.update({
34+
id: '/demo',
35+
path: '/demo',
36+
getParentRoute: () => rootRouteImport,
37+
} as any)
3238
const IndexRoute = IndexRouteImport.update({
3339
id: '/',
3440
path: '/',
@@ -37,33 +43,37 @@ const IndexRoute = IndexRouteImport.update({
3743

3844
export interface FileRoutesByFullPath {
3945
'/': typeof IndexRoute
46+
'/demo': typeof DemoRoute
4047
'/inbox': typeof InboxRoute
4148
'/login': typeof LoginRoute
4249
'/settings': typeof SettingsRoute
4350
}
4451
export interface FileRoutesByTo {
4552
'/': typeof IndexRoute
53+
'/demo': typeof DemoRoute
4654
'/inbox': typeof InboxRoute
4755
'/login': typeof LoginRoute
4856
'/settings': typeof SettingsRoute
4957
}
5058
export interface FileRoutesById {
5159
__root__: typeof rootRouteImport
5260
'/': typeof IndexRoute
61+
'/demo': typeof DemoRoute
5362
'/inbox': typeof InboxRoute
5463
'/login': typeof LoginRoute
5564
'/settings': typeof SettingsRoute
5665
}
5766
export interface FileRouteTypes {
5867
fileRoutesByFullPath: FileRoutesByFullPath
59-
fullPaths: '/' | '/inbox' | '/login' | '/settings'
68+
fullPaths: '/' | '/demo' | '/inbox' | '/login' | '/settings'
6069
fileRoutesByTo: FileRoutesByTo
61-
to: '/' | '/inbox' | '/login' | '/settings'
62-
id: '__root__' | '/' | '/inbox' | '/login' | '/settings'
70+
to: '/' | '/demo' | '/inbox' | '/login' | '/settings'
71+
id: '__root__' | '/' | '/demo' | '/inbox' | '/login' | '/settings'
6372
fileRoutesById: FileRoutesById
6473
}
6574
export interface RootRouteChildren {
6675
IndexRoute: typeof IndexRoute
76+
DemoRoute: typeof DemoRoute
6777
InboxRoute: typeof InboxRoute
6878
LoginRoute: typeof LoginRoute
6979
SettingsRoute: typeof SettingsRoute
@@ -92,6 +102,13 @@ declare module '@tanstack/react-router' {
92102
preLoaderRoute: typeof InboxRouteImport
93103
parentRoute: typeof rootRouteImport
94104
}
105+
'/demo': {
106+
id: '/demo'
107+
path: '/demo'
108+
fullPath: '/demo'
109+
preLoaderRoute: typeof DemoRouteImport
110+
parentRoute: typeof rootRouteImport
111+
}
95112
'/': {
96113
id: '/'
97114
path: '/'
@@ -104,6 +121,7 @@ declare module '@tanstack/react-router' {
104121

105122
const rootRouteChildren: RootRouteChildren = {
106123
IndexRoute: IndexRoute,
124+
DemoRoute: DemoRoute,
107125
InboxRoute: InboxRoute,
108126
LoginRoute: LoginRoute,
109127
SettingsRoute: SettingsRoute,

0 commit comments

Comments
 (0)