website-xp-phone 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.astro/content-assets.mjs +1 -0
  2. package/.astro/content-modules.mjs +1 -0
  3. package/.astro/content.d.ts +199 -0
  4. package/.astro/data-store.json +1 -0
  5. package/.astro/settings.json +8 -0
  6. package/.astro/types.d.ts +1 -0
  7. package/.devcontainer/devcontainer.json +23 -0
  8. package/.env.firebase.example +8 -0
  9. package/.firebaserc +5 -0
  10. package/.gitattributes +2 -0
  11. package/.github/copilot-instructions.md +131 -0
  12. package/.github/dependabot.yml +11 -0
  13. package/.github/workflows/ci.yml +45 -0
  14. package/.github/workflows/deploy-admin.yml +48 -0
  15. package/.github/workflows/static.yml +43 -0
  16. package/.gitmodules +5 -0
  17. package/FIREBASE_SETUP.md +69 -0
  18. package/README.md +63 -0
  19. package/SECURITY.md +11 -0
  20. package/admin/Admin.csproj +7 -0
  21. package/admin/Dockerfile +14 -0
  22. package/admin/Program.cs +8 -0
  23. package/deploy-admin-cloud-run.md +229 -0
  24. package/eslint.config.js +28 -0
  25. package/firebase.json +5 -0
  26. package/firestore.rules +29 -0
  27. package/index.html +52 -0
  28. package/package.json +48 -0
  29. package/pagerts_output.json +1 -0
  30. package/public/5.html +967 -0
  31. package/public/BAHNSCHRIFT.TTF +0 -0
  32. package/public/Beep.ogg +0 -0
  33. package/public/Clippy.png +0 -0
  34. package/public/Layered Network Security Model for Home Networks (slides).pdf +0 -0
  35. package/public/Layered Network Security Model for Home Networks.pdf +0 -0
  36. package/public/TODO.pdf +0 -0
  37. package/public/WoW_Config.zip +3 -0
  38. package/public/addons/energy-swing.txt +1 -0
  39. package/public/addons/lego-yoda-death-readme.txt +11 -0
  40. package/public/addons/lego-yoda-death.mp3 +0 -0
  41. package/public/addons/mana-blast.txt +1 -0
  42. package/public/addons/rage-volley.txt +1 -0
  43. package/public/addons/rueg-cell.txt +1 -0
  44. package/public/addons/rueg-elvui-profile.txt +1 -0
  45. package/public/addons/rueg-grid2.txt +214 -0
  46. package/public/addons/rueg-plater-smol.txt +1 -0
  47. package/public/addons/rueg-plater.txt +1 -0
  48. package/public/addons/rueg-wa-druid.txt +1 -0
  49. package/public/addons/rueg-wa-priest.txt +1 -0
  50. package/public/addons/rueg-wa-rogue.txt +1 -0
  51. package/public/addons/rueg-wa-shaman.txt +1 -0
  52. package/public/addons/rueg-wa-warrior.txt +1 -0
  53. package/public/addons/spirit-smash.txt +1 -0
  54. package/public/avatar.jpg +0 -0
  55. package/public/avatar.png +0 -0
  56. package/public/crunchy_kick.ogg +0 -0
  57. package/public/documents/resume.html +312 -0
  58. package/public/favicon.ico +0 -0
  59. package/public/images/Ateric1.png +0 -0
  60. package/public/images/Ateric2.png +0 -0
  61. package/public/images/equal1.png +0 -0
  62. package/public/images/hyperawareofwhatacatis.png +0 -0
  63. package/public/images/kogg1.png +0 -0
  64. package/public/images/kogg2.png +0 -0
  65. package/public/images/rueg1.png +0 -0
  66. package/public/images/rueg2.png +0 -0
  67. package/public/incorrect_responses.txt +126 -0
  68. package/public/loading.css +51 -0
  69. package/public/resume.pdf +0 -0
  70. package/public/robots.txt +9 -0
  71. package/public/soundcloud.json +57 -0
  72. package/public/spinner.svg +12 -0
  73. package/public/tada.wav +0 -0
  74. package/public/yooh.mp3 +0 -0
  75. package/render.yaml +5 -0
  76. package/scripts/ensure-blog-worktree.mjs +24 -0
  77. package/scripts/generate-soundcloud-json.mjs +198 -0
  78. package/scripts/git-worktree-helper.mjs +122 -0
  79. package/scripts/hoist-dev-blog-local.mjs +149 -0
  80. package/scripts/music-schema.mjs +56 -0
  81. package/scripts/publish-soundcloud-json.mjs +32 -0
  82. package/scripts/sync-music-links-from-worktree.mjs +32 -0
  83. package/src/App.tsx +1500 -0
  84. package/src/addons.json +76 -0
  85. package/src/components/Addon.tsx +223 -0
  86. package/src/components/BlogContent.tsx +103 -0
  87. package/src/components/CopyToClipboardButton.tsx +21 -0
  88. package/src/components/MenuBar.tsx +151 -0
  89. package/src/components/MenuBarWithContext.tsx +6 -0
  90. package/src/components/Modal.tsx +17 -0
  91. package/src/components/MusicContent.tsx +309 -0
  92. package/src/components/NavBarController.tsx +55 -0
  93. package/src/components/NavBarControllerWrapper.tsx +13 -0
  94. package/src/components/Page.tsx +56 -0
  95. package/src/components/SitemapContent.tsx +125 -0
  96. package/src/contacts.json +32 -0
  97. package/src/env.d.ts +13 -0
  98. package/src/lib/assistantStateMachine.ts +80 -0
  99. package/src/lib/audioOverlap.ts +99 -0
  100. package/src/lib/keyboardInputUtils.ts +182 -0
  101. package/src/lib/musicSchema.ts +85 -0
  102. package/src/lib/naggingAssistantClient.ts +241 -0
  103. package/src/lib/resumeAnalytics.ts +163 -0
  104. package/src/main.tsx +35 -0
  105. package/src/pages.json +50 -0
  106. package/src/sections.json +243 -0
  107. package/src/src+addons.zip +3 -0
  108. package/src/styles/main.css +465 -0
  109. package/src/utils/blogSecurity.ts +87 -0
  110. package/src/utils/menuItems.ts +33 -0
  111. package/src/windowing/MinimizedSections.tsx +86 -0
  112. package/src/windowing/Section.tsx +586 -0
  113. package/src/windowing/context.tsx +13 -0
  114. package/src/windowing/hooks.ts +10 -0
  115. package/src/windowing/index.ts +7 -0
  116. package/src/windowing/provider.tsx +74 -0
  117. package/src/windowing/server.ts +3 -0
  118. package/src/windowing/types.ts +33 -0
  119. package/src/windowing/utils.ts +135 -0
  120. package/tests/generate-soundcloud-json.test.mjs +63 -0
  121. package/tests/music-schema.test.mjs +53 -0
  122. package/tsconfig.json +26 -0
  123. package/vite.config.ts +304 -0
package/src/App.tsx ADDED
@@ -0,0 +1,1500 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type ComponentProps,
8
+ type ReactElement,
9
+ type FormEvent,
10
+ } from "react";
11
+ import Markdown, { type Options as ReactMarkdownOptions } from "react-markdown";
12
+ import rehypeRaw from "rehype-raw";
13
+ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
14
+ import { ToastContainer } from "react-toastify";
15
+ import {
16
+ attachClippyListener,
17
+ detachClippyListener,
18
+ onClippyClick,
19
+ showClippyHint,
20
+ subscribeClippyBubble,
21
+ subscribeClippyVisibility,
22
+ } from "./lib/keyboardInputUtils";
23
+ import {
24
+ loadAssistantConfig,
25
+ requestAssistantCompletion,
26
+ type AssistantConfig,
27
+ } from "./lib/naggingAssistantClient";
28
+ import {
29
+ buildClippyShadowFilter,
30
+ hasConfiguredAssistant,
31
+ type AssistantPromptOptions,
32
+ } from "./lib/assistantStateMachine";
33
+
34
+ import MenuBar from "./components/MenuBar.tsx";
35
+ import BlogContent from "./components/BlogContent";
36
+ import MusicContent from "./components/MusicContent";
37
+ import SitemapContent from "./components/SitemapContent";
38
+ import { PageContent, PageWithAddons } from "./components/Page";
39
+ import sections from "./sections.json";
40
+ import contacts from "./contacts.json";
41
+ import addons from "./addons.json";
42
+ import pages from "./pages.json";
43
+ import type { AddonProps } from "./components/Addon";
44
+ import type { SectionProps } from "./windowing";
45
+ import { processContent } from "./windowing/utils";
46
+ import { buildMusicGroupSchema, serializeJsonLd } from "./lib/musicSchema.ts";
47
+ import { submitResumeInterest, trackResumeEvent } from "./lib/resumeAnalytics";
48
+
49
+ type RouteConfig = {
50
+ title: string;
51
+ description: string;
52
+ };
53
+
54
+ type SoundCloudTrack = {
55
+ title: string;
56
+ url: string;
57
+ };
58
+
59
+ type SoundCloudPayload = {
60
+ tracks: SoundCloudTrack[];
61
+ };
62
+
63
+ type PageRoute = RouteConfig & {
64
+ path: string;
65
+ };
66
+
67
+ const DEFAULT_ROUTE: RouteConfig = {
68
+ title: "home of kine",
69
+ description: "my cozy little personal website",
70
+ };
71
+
72
+ const PAGE_ROUTES = pages as PageRoute[];
73
+
74
+ const TOP_LEVEL_ROUTES = new Set(
75
+ PAGE_ROUTES.map(({ path }) => path.split("/").filter(Boolean)[0]).filter(
76
+ Boolean,
77
+ ),
78
+ );
79
+
80
+ const ROUTE_CONFIG = Object.fromEntries(
81
+ PAGE_ROUTES.map(({ path, title, description }) => [
82
+ path,
83
+ { title, description },
84
+ ]),
85
+ ) as Record<string, RouteConfig>;
86
+
87
+ const normalizePath = (path: string) => {
88
+ if (!path || path === "/") {
89
+ return "/";
90
+ }
91
+
92
+ const decodedPath = decodeURIComponent(path);
93
+ const withoutIndexHtml = decodedPath.replace(/\/index\.html$/i, "/");
94
+ const withoutHtml = withoutIndexHtml.replace(/\.html$/i, "");
95
+ const withoutTrailingSlash = withoutHtml.replace(/\/+$/, "");
96
+ const segments = withoutTrailingSlash.split("/").filter(Boolean);
97
+
98
+ if (segments.length === 0) {
99
+ return "/";
100
+ }
101
+
102
+ const [firstSegment] = segments;
103
+
104
+ if (firstSegment && TOP_LEVEL_ROUTES.has(firstSegment.toLowerCase())) {
105
+ return `/${firstSegment.toLowerCase()}`;
106
+ }
107
+
108
+ return withoutTrailingSlash || "/";
109
+ };
110
+
111
+ const isInternalPath = (href: string) => href.startsWith("/");
112
+
113
+ const STRUCTURED_DATA_SCRIPT_ID = "homepage-music-structured-data";
114
+ // const ASSISTANT_CONFIG_MENU_HREF = "#assistant-config";
115
+
116
+ const markdownSanitizeSchema: unknown = {
117
+ ...defaultSchema,
118
+ tagNames: [...(defaultSchema.tagNames || []), "iframe"],
119
+ attributes: {
120
+ ...defaultSchema.attributes,
121
+ a: [...(defaultSchema.attributes?.a || []), ["target"], ["rel"]],
122
+ img: [...(defaultSchema.attributes?.img || []), ["loading"], ["decoding"]],
123
+ iframe: [
124
+ ["title"],
125
+ ["src"],
126
+ ["width"],
127
+ ["height"],
128
+ ["style"],
129
+ ["scrolling"],
130
+ ["loading"],
131
+ ["allow"],
132
+ ["allowfullscreen"],
133
+ ["referrerpolicy"],
134
+ ["frameborder"],
135
+ ],
136
+ },
137
+ };
138
+
139
+ const markdownRehypePlugins = [
140
+ rehypeRaw,
141
+ [rehypeSanitize, markdownSanitizeSchema],
142
+ ] as ReactMarkdownOptions["rehypePlugins"];
143
+
144
+ const markdownComponents = {
145
+ img: (props: ComponentProps<"img">) => (
146
+ <img
147
+ {...props}
148
+ style={{ maxWidth: "100%", height: "auto", ...(props.style ?? {}) }}
149
+ />
150
+ ),
151
+ };
152
+
153
+ // const TOP_BAR_ADDITIONAL_LINKS: MenuItem[] = [
154
+ // { label: "admin", href: ASSISTANT_CONFIG_MENU_HREF },
155
+ // ];
156
+
157
+ const SHADOW_PULSE_MS = 700;
158
+
159
+ const isSoundCloudPayload = (value: unknown): value is SoundCloudPayload => {
160
+ if (!value || typeof value !== "object") return false;
161
+
162
+ const candidate = value as Partial<SoundCloudPayload>;
163
+ return (
164
+ Array.isArray(candidate.tracks) &&
165
+ candidate.tracks.every(
166
+ (track) =>
167
+ track && typeof track.title === "string" && typeof track.url === "string",
168
+ )
169
+ );
170
+ };
171
+
172
+ const upsertMeta = (selector: string, attributes: Record<string, string>) => {
173
+ let element = document.querySelector(selector) as HTMLMetaElement | null;
174
+
175
+ if (!element) {
176
+ element = document.createElement("meta");
177
+ Object.entries(attributes).forEach(([key, value]) => {
178
+ element?.setAttribute(key, value);
179
+ });
180
+ document.head.appendChild(element);
181
+ }
182
+
183
+ if ("content" in attributes) {
184
+ element.setAttribute("content", attributes.content);
185
+ }
186
+ };
187
+
188
+ const upsertCanonicalLink = (href: string) => {
189
+ let canonical = document.querySelector(
190
+ 'link[rel="canonical"]',
191
+ ) as HTMLLinkElement | null;
192
+ if (!canonical) {
193
+ canonical = document.createElement("link");
194
+ canonical.setAttribute("rel", "canonical");
195
+ document.head.appendChild(canonical);
196
+ }
197
+ canonical.setAttribute("href", href);
198
+ };
199
+
200
+ const removeStructuredDataScript = () => {
201
+ document.getElementById(STRUCTURED_DATA_SCRIPT_ID)?.remove();
202
+ };
203
+
204
+ const upsertStructuredDataScript = (schema: unknown) => {
205
+ let script = document.getElementById(
206
+ STRUCTURED_DATA_SCRIPT_ID,
207
+ ) as HTMLScriptElement | null;
208
+
209
+ if (!script) {
210
+ script = document.createElement("script");
211
+ script.id = STRUCTURED_DATA_SCRIPT_ID;
212
+ script.type = "application/ld+json";
213
+ document.head.appendChild(script);
214
+ }
215
+
216
+ script.textContent = serializeJsonLd(schema);
217
+ };
218
+
219
+ const HomePage = () => {
220
+ const { processed, metadata } = useMemo(
221
+ () => processContent(sections as SectionProps),
222
+ [],
223
+ );
224
+
225
+ return (
226
+ <main>
227
+ <PageContent
228
+ sections={processed}
229
+ pageMetadata={{ sections: metadata }}
230
+ />
231
+ </main>
232
+ );
233
+ };
234
+
235
+ const AddonsPage = () => {
236
+ const { processed, metadata } = useMemo(
237
+ () => processContent(addons as AddonProps),
238
+ [],
239
+ );
240
+
241
+ return (
242
+ <main>
243
+ <PageWithAddons
244
+ addons={processed as AddonProps}
245
+ pageMetadata={{ sections: metadata }}
246
+ />
247
+ </main>
248
+ );
249
+ };
250
+
251
+ const ContactPage = () => {
252
+ const { processed, metadata } = useMemo(
253
+ () => processContent(contacts as SectionProps),
254
+ [],
255
+ );
256
+
257
+ return (
258
+ <main>
259
+ <PageContent
260
+ sections={processed}
261
+ pageMetadata={{ sections: metadata }}
262
+ />
263
+ </main>
264
+ );
265
+ };
266
+
267
+ const ResumePage = () => {
268
+ const RESUME_ACCESS_MODE_KEY = "resumeAccessMode";
269
+ type ResumeAccessMode = "html" | "pdf";
270
+ const [showResumeModal, setShowResumeModal] = useState(false);
271
+ const [showEmailModal, setShowEmailModal] = useState(false);
272
+ const [interestEmail, setInterestEmail] = useState("");
273
+ const [interestMessage, setInterestMessage] = useState("");
274
+ const [isSubmittingInterest, setIsSubmittingInterest] = useState(false);
275
+ const [printShortcutStep, setPrintShortcutStep] = useState(0);
276
+ const [resumeAccessMode, setResumeAccessMode] =
277
+ useState<ResumeAccessMode | null>(() => {
278
+ const persistedMode = window.localStorage.getItem(RESUME_ACCESS_MODE_KEY);
279
+ return persistedMode === "html" || persistedMode === "pdf"
280
+ ? persistedMode
281
+ : null;
282
+ });
283
+ const resumeIframeRef = useRef<HTMLIFrameElement | null>(null);
284
+
285
+ const resumeDocumentPath =
286
+ resumeAccessMode === "pdf" ? "/resume.pdf" : "/documents/resume.html";
287
+ const hasResolvedInterestSubmission = resumeAccessMode !== null;
288
+ const shouldBlurResume = !hasResolvedInterestSubmission;
289
+
290
+ useEffect(() => {
291
+ void trackResumeEvent("resume_page_view");
292
+ }, []);
293
+
294
+ useEffect(() => {
295
+ if (!showResumeModal) {
296
+ return;
297
+ }
298
+
299
+ const handlePrintShortcut = (event: KeyboardEvent) => {
300
+ const isPrintShortcut =
301
+ (event.ctrlKey || event.metaKey) &&
302
+ event.key.toLowerCase() === "p";
303
+
304
+ if (!isPrintShortcut) {
305
+ return;
306
+ }
307
+
308
+ event.preventDefault();
309
+ event.stopPropagation();
310
+
311
+ if (printShortcutStep === 0) {
312
+ const iframeWindow = resumeIframeRef.current?.contentWindow;
313
+ if (iframeWindow) {
314
+ iframeWindow.focus();
315
+ iframeWindow.print();
316
+ }
317
+ setPrintShortcutStep(1);
318
+ return;
319
+ }
320
+
321
+ window.location.assign(resumeDocumentPath);
322
+ };
323
+
324
+ window.addEventListener("keydown", handlePrintShortcut, true);
325
+ return () => {
326
+ window.removeEventListener("keydown", handlePrintShortcut, true);
327
+ };
328
+ }, [printShortcutStep, resumeDocumentPath, showResumeModal]);
329
+
330
+ const handleInterestSubmit = async (event: FormEvent<HTMLFormElement>) => {
331
+ event.preventDefault();
332
+ const trimmedEmail = interestEmail.trim();
333
+
334
+ if (!trimmedEmail) {
335
+ setInterestMessage("Please provide an email address.");
336
+ return;
337
+ }
338
+
339
+ setIsSubmittingInterest(true);
340
+ setInterestMessage("");
341
+
342
+ try {
343
+ await submitResumeInterest(trimmedEmail);
344
+ setInterestMessage("Thanks. Your interest has been recorded.");
345
+ setInterestEmail("");
346
+ setResumeAccessMode("html");
347
+ window.localStorage.setItem(RESUME_ACCESS_MODE_KEY, "html");
348
+ } catch {
349
+ setInterestMessage(
350
+ "Could not submit interest right now. Please try again shortly.",
351
+ );
352
+ } finally {
353
+ setIsSubmittingInterest(false);
354
+ }
355
+ };
356
+
357
+ return (
358
+ <main>
359
+ <section className="page">
360
+ <div className="window">
361
+ <div className="title-bar">
362
+ <div className="title-bar-text">Resume</div>
363
+ <div className="title-bar-controls">
364
+ <button aria-label="Minimize"></button>
365
+ <button aria-label="Maximize"></button>
366
+ <button aria-label="Close"></button>
367
+ </div>
368
+ </div>
369
+ <div
370
+ className="window-body"
371
+ style={{
372
+ display: "flex",
373
+ flexDirection: "column",
374
+ gap: "1rem",
375
+ justifyContent: "center",
376
+ alignItems: "center",
377
+ padding: "2rem",
378
+ }}
379
+ >
380
+ <button
381
+ onClick={() => {
382
+ void trackResumeEvent("resume_open_click");
383
+ setPrintShortcutStep(0);
384
+ setShowResumeModal(true);
385
+ }}
386
+ >
387
+ View My Resume
388
+ </button>
389
+ <button onClick={() => setShowEmailModal(true)}>
390
+ Share Interest Email
391
+ </button>
392
+ </div>
393
+ </div>
394
+ </section>
395
+
396
+ {showResumeModal ? (
397
+ <div
398
+ style={{
399
+ position: "fixed",
400
+ top: 0,
401
+ left: 0,
402
+ right: 0,
403
+ bottom: 0,
404
+ background: "rgba(0, 0, 0, 0.5)",
405
+ zIndex: 9999,
406
+ display: "flex",
407
+ alignItems: "center",
408
+ justifyContent: "center",
409
+ }}
410
+ onClick={(event) => {
411
+ if (event.target === event.currentTarget) {
412
+ setShowResumeModal(false);
413
+ }
414
+ }}
415
+ >
416
+ <div
417
+ className="window"
418
+ style={{ width: "90vw", height: "90vh", maxWidth: "1200px" }}
419
+ >
420
+ <div className="title-bar">
421
+ <div className="title-bar-text">Resume</div>
422
+ <div className="title-bar-controls">
423
+ <button
424
+ aria-label="Close"
425
+ onClick={() => setShowResumeModal(false)}
426
+ ></button>
427
+ </div>
428
+ </div>
429
+ <div
430
+ className="window-body"
431
+ style={{
432
+ padding: 0,
433
+ height: "calc(100% - 2rem)",
434
+ overflow: "hidden",
435
+ position: "relative",
436
+ }}
437
+ >
438
+ <iframe
439
+ ref={resumeIframeRef}
440
+ src={resumeDocumentPath}
441
+ title="Resume"
442
+ style={{
443
+ width: "100%",
444
+ height: "100%",
445
+ border: "none",
446
+ filter: shouldBlurResume ? "blur(7px)" : "none",
447
+ transition: "filter 180ms ease",
448
+ }}
449
+ />
450
+ {shouldBlurResume ? (
451
+ <div
452
+ style={{
453
+ position: "absolute",
454
+ inset: 0,
455
+ display: "flex",
456
+ flexDirection: "column",
457
+ alignItems: "center",
458
+ justifyContent: "center",
459
+ gap: "0.75rem",
460
+ background: "rgba(255, 255, 255, 0.2)",
461
+ backdropFilter: "blur(1px)",
462
+ padding: "1rem",
463
+ textAlign: "center",
464
+ }}
465
+ >
466
+ <p style={{ margin: 0 }}>
467
+ Submit your interest email to unblur this preview.
468
+ </p>
469
+ <button onClick={() => setShowEmailModal(true)}>
470
+ Share Interest Email
471
+ </button>
472
+ </div>
473
+ ) : null}
474
+ </div>
475
+ </div>
476
+ </div>
477
+ ) : null}
478
+
479
+ {showEmailModal ? (
480
+ <div
481
+ style={{
482
+ position: "fixed",
483
+ top: 0,
484
+ left: 0,
485
+ right: 0,
486
+ bottom: 0,
487
+ background: "rgba(0, 0, 0, 0.5)",
488
+ zIndex: 9999,
489
+ display: "flex",
490
+ alignItems: "center",
491
+ justifyContent: "center",
492
+ }}
493
+ onClick={(event) => {
494
+ if (event.target === event.currentTarget) {
495
+ setShowEmailModal(false);
496
+ }
497
+ }}
498
+ >
499
+ <div className="window" style={{ maxWidth: "400px", margin: "auto" }}>
500
+ <div className="title-bar">
501
+ <div className="title-bar-text">Resume Interest</div>
502
+ <div className="title-bar-controls">
503
+ <button
504
+ aria-label="Close"
505
+ onClick={() => setShowEmailModal(false)}
506
+ ></button>
507
+ </div>
508
+ </div>
509
+ <div className="window-body">
510
+ <form
511
+ onSubmit={handleInterestSubmit}
512
+ style={{ display: "grid", gap: "0.5rem", margin: "0.5rem 0" }}
513
+ >
514
+ <label htmlFor="resume-interest-email">
515
+ Share your email if you are interested in this resume:
516
+ </label>
517
+ <input
518
+ id="resume-interest-email"
519
+ type="email"
520
+ value={interestEmail}
521
+ onChange={(event) => setInterestEmail(event.target.value)}
522
+ placeholder="you@example.com"
523
+ required
524
+ />
525
+ <button type="submit" disabled={isSubmittingInterest}>
526
+ {isSubmittingInterest ? "Submitting..." : "Submit Interest"}
527
+ </button>
528
+ </form>
529
+ {interestMessage ? (
530
+ <p style={{ margin: "0.5rem 0", fontSize: "0.9rem" }}>
531
+ {interestMessage}
532
+ </p>
533
+ ) : null}
534
+ <div
535
+ style={{
536
+ display: "flex",
537
+ justifyContent: "flex-end",
538
+ marginTop: "1rem",
539
+ }}
540
+ >
541
+ <button
542
+ onClick={() => {
543
+ if (!hasResolvedInterestSubmission) {
544
+ setResumeAccessMode("pdf");
545
+ window.localStorage.setItem(RESUME_ACCESS_MODE_KEY, "pdf");
546
+ }
547
+ setShowEmailModal(false);
548
+ }}
549
+ >
550
+ OK
551
+ </button>
552
+ </div>
553
+ </div>
554
+ </div>
555
+ </div>
556
+ ) : null}
557
+ </main>
558
+ );
559
+ };
560
+
561
+ const WowPage = () => {
562
+ const [username, setUsername] = useState("");
563
+ const [date, setDate] = useState("");
564
+ const [message, setMessage] = useState<string>("");
565
+ const [messageType, setMessageType] = useState<"error" | "success" | "">("");
566
+
567
+ const today = useMemo(() => new Date().toISOString().split("T")[0], []);
568
+
569
+ const handleSubmit = (event: FormEvent) => {
570
+ event.preventDefault();
571
+
572
+ if (!username.trim()) {
573
+ setMessageType("error");
574
+ setMessage("Please enter a username.");
575
+ return;
576
+ }
577
+
578
+ if (!date) {
579
+ setMessageType("error");
580
+ setMessage("Please enter a date.");
581
+ return;
582
+ }
583
+
584
+ if (username.trim().toLowerCase() !== "kine") {
585
+ setMessageType("error");
586
+ setMessage("Invalid username.");
587
+ return;
588
+ }
589
+
590
+ if (date !== today) {
591
+ setMessageType("error");
592
+ setMessage("Incorrect date. Please enter today's date.");
593
+ return;
594
+ }
595
+
596
+ setMessageType("success");
597
+ setMessage("Date verified! Starting download...");
598
+
599
+ window.setTimeout(() => {
600
+ const link = document.createElement("a");
601
+ link.href = "/WoW_Config.zip";
602
+ link.download = "WoW_Config.zip";
603
+ document.body.appendChild(link);
604
+ link.click();
605
+ document.body.removeChild(link);
606
+
607
+ setMessageType("success");
608
+ setMessage("Download started successfully!");
609
+ }, 500);
610
+ };
611
+
612
+ return (
613
+ <main>
614
+ <div style={{ maxWidth: "600px", margin: "2rem auto", padding: "1rem" }}>
615
+ <div
616
+ className="window"
617
+ style={{ background: "#ece9d8", border: "2px outset #dfdfdf" }}
618
+ >
619
+ <div className="title-bar">
620
+ <span className="title-bar-text">WoW Configuration Download</span>
621
+ </div>
622
+
623
+ <div style={{ marginBottom: "1rem", lineHeight: 1.5 }}>
624
+ <p>
625
+ To download the World of Warcraft configuration files, please
626
+ enter your username and verify today's date.
627
+ </p>
628
+ </div>
629
+
630
+ <form onSubmit={handleSubmit}>
631
+ <div style={{ marginBottom: "1rem" }}>
632
+ <label
633
+ htmlFor="usernameInput"
634
+ style={{ display: "block", marginBottom: "0.5rem" }}
635
+ >
636
+ Username:
637
+ </label>
638
+ <input
639
+ id="usernameInput"
640
+ type="text"
641
+ value={username}
642
+ onChange={(event) => setUsername(event.target.value)}
643
+ placeholder="Enter username"
644
+ required
645
+ style={{
646
+ width: "300px",
647
+ padding: "0.5rem",
648
+ border: "2px inset #808080",
649
+ }}
650
+ />
651
+ </div>
652
+
653
+ <div style={{ marginBottom: "1rem" }}>
654
+ <label
655
+ htmlFor="dateInput"
656
+ style={{ display: "block", marginBottom: "0.5rem" }}
657
+ >
658
+ Enter today's date:
659
+ </label>
660
+ <input
661
+ id="dateInput"
662
+ type="date"
663
+ value={date}
664
+ onChange={(event) => setDate(event.target.value)}
665
+ max={today}
666
+ required
667
+ style={{
668
+ width: "300px",
669
+ padding: "0.5rem",
670
+ border: "2px inset #808080",
671
+ }}
672
+ />
673
+ </div>
674
+
675
+ <button
676
+ type="submit"
677
+ style={{ padding: "0.5rem 1.5rem", marginRight: "0.5rem" }}
678
+ >
679
+ Verify & Download
680
+ </button>
681
+ <button
682
+ type="button"
683
+ style={{ padding: "0.5rem 1.5rem" }}
684
+ onClick={() => {
685
+ window.history.pushState({}, "", "/");
686
+ window.dispatchEvent(new PopStateEvent("popstate"));
687
+ }}
688
+ >
689
+ Cancel
690
+ </button>
691
+
692
+ {message ? (
693
+ <p
694
+ style={{
695
+ marginTop: "0.75rem",
696
+ fontWeight: "bold",
697
+ color: messageType === "error" ? "#c00" : "#080",
698
+ }}
699
+ >
700
+ {message}
701
+ </p>
702
+ ) : null}
703
+ </form>
704
+ </div>
705
+ </div>
706
+ </main>
707
+ );
708
+ };
709
+
710
+ const NotFoundPage = () => (
711
+ <main>
712
+ <section className="page">
713
+ <div className="window">
714
+ <div className="title-bar">
715
+ <div className="title-bar-text">Not Found</div>
716
+ </div>
717
+ <div className="window-body">
718
+ <p>That page does not exist.</p>
719
+ <a href="/">Go home</a>
720
+ </div>
721
+ </div>
722
+ </section>
723
+ </main>
724
+ );
725
+
726
+ export default function App() {
727
+ const [path, setPath] = useState(() =>
728
+ normalizePath(window.location.pathname),
729
+ );
730
+ const [showClippy, setShowClippy] = useState(false);
731
+ const [showClippyBubble, setShowClippyBubble] = useState(false);
732
+ const [clippyBubbleSaysNo, setClippyBubbleSaysNo] = useState(false);
733
+ // const [showAssistantConfigModal, setShowAssistantConfigModal] = useState(false);
734
+ const [assistantConfig] = useState<AssistantConfig>(() =>
735
+ loadAssistantConfig(),
736
+ );
737
+ // const [assistantModelOptions, setAssistantModelOptions] = useState<string[]>([]);
738
+ // const [assistantConfigError, setAssistantConfigError] = useState("");
739
+ // const [isDiscoveringAssistantModels, setIsDiscoveringAssistantModels] =
740
+ // useState(false);
741
+ const [showConversationModal, setShowConversationModal] = useState(false);
742
+ const [conversationInput, setConversationInput] = useState("");
743
+ const [conversationError, setConversationError] = useState("");
744
+ const [assistantWindowText, setAssistantWindowText] = useState("");
745
+ const [assistantWindowVisible, setAssistantWindowVisible] = useState(false);
746
+ const [assistantWindowFading, setAssistantWindowFading] = useState(false);
747
+ const [assistantWindowMinimized, setAssistantWindowMinimized] =
748
+ useState(false);
749
+ const [isAssistantRequestPending, setIsAssistantRequestPending] =
750
+ useState(false);
751
+ const [isSubmitPulseActive, setIsSubmitPulseActive] = useState(false);
752
+ const [isClippyHovered, setIsClippyHovered] = useState(false);
753
+ const [assistantConnectionInterrupted, setAssistantConnectionInterrupted] =
754
+ useState(false);
755
+ const [isConnectionFlashActive, setIsConnectionFlashActive] = useState(false);
756
+ const holdTimerRef = useRef<number | null>(null);
757
+ const holdTriggeredRef = useRef(false);
758
+ const connectionFlashTimerRef = useRef<number | null>(null);
759
+ const rightClickFlashArmedRef = useRef(true);
760
+ const wasClippyBubbleVisibleRef = useRef(false);
761
+ // const wisdomPulseClockRef = useRef(new WisdomPulseClock());
762
+
763
+ useEffect(() => {
764
+ attachClippyListener();
765
+ const unsubscribeVisibility = subscribeClippyVisibility(setShowClippy);
766
+ const unsubscribeBubble = subscribeClippyBubble(setShowClippyBubble);
767
+ return () => {
768
+ unsubscribeVisibility();
769
+ unsubscribeBubble();
770
+ detachClippyListener();
771
+ };
772
+ }, []);
773
+
774
+ useEffect(() => {
775
+ const markDisconnected = () => {
776
+ setAssistantConnectionInterrupted(true);
777
+ };
778
+
779
+ const markConnected = () => {
780
+ setAssistantConnectionInterrupted(false);
781
+ };
782
+
783
+ window.addEventListener("offline", markDisconnected);
784
+ window.addEventListener("online", markConnected);
785
+
786
+ return () => {
787
+ window.removeEventListener("offline", markDisconnected);
788
+ window.removeEventListener("online", markConnected);
789
+ };
790
+ }, []);
791
+
792
+ useEffect(() => {
793
+ if (!assistantConnectionInterrupted) {
794
+ return;
795
+ }
796
+
797
+ setIsConnectionFlashActive(true);
798
+ if (connectionFlashTimerRef.current !== null) {
799
+ window.clearTimeout(connectionFlashTimerRef.current);
800
+ }
801
+
802
+ connectionFlashTimerRef.current = window.setTimeout(() => {
803
+ setIsConnectionFlashActive(false);
804
+ setAssistantConnectionInterrupted(false);
805
+ connectionFlashTimerRef.current = null;
806
+ }, SHADOW_PULSE_MS);
807
+ }, [assistantConnectionInterrupted]);
808
+
809
+ useEffect(() => {
810
+ const wasVisible = wasClippyBubbleVisibleRef.current;
811
+ if (!wasVisible && showClippyBubble) {
812
+ setClippyBubbleSaysNo(false);
813
+ }
814
+ wasClippyBubbleVisibleRef.current = showClippyBubble;
815
+ }, [showClippyBubble]);
816
+
817
+ useEffect(() => {
818
+ const handleCrunchyKickPlayed = () => {
819
+ if (showClippyBubble) {
820
+ setClippyBubbleSaysNo(true);
821
+ }
822
+ };
823
+
824
+ window.addEventListener("crunchy-kick-played", handleCrunchyKickPlayed);
825
+ return () => {
826
+ window.removeEventListener("crunchy-kick-played", handleCrunchyKickPlayed);
827
+ };
828
+ }, [showClippyBubble]);
829
+
830
+ // Wisdom pulse animation is disabled in production due to unresolved CORS issues.
831
+ // useEffect(() => {
832
+ // if (!isWisdomRequestPending) {
833
+ // setWisdomPulsePhase(0);
834
+ // wisdomPulseClockRef.current.stop();
835
+ // return;
836
+ // }
837
+ //
838
+ // wisdomPulseClockRef.current.start(() => {
839
+ // setWisdomPulsePhase((previous) => previous + 0.22);
840
+ // }, 100);
841
+ //
842
+ // return () => wisdomPulseClockRef.current.stop();
843
+ // }, [isWisdomRequestPending]);
844
+
845
+ useEffect(() => {
846
+ return () => {
847
+ if (holdTimerRef.current !== null) {
848
+ window.clearTimeout(holdTimerRef.current);
849
+ }
850
+ if (connectionFlashTimerRef.current !== null) {
851
+ window.clearTimeout(connectionFlashTimerRef.current);
852
+ }
853
+ // wisdomPulseClockRef.current.stop();
854
+ };
855
+ }, []);
856
+
857
+ useEffect(() => {
858
+ if (!showConversationModal) {
859
+ return;
860
+ }
861
+
862
+ const handleModalEscape = (event: KeyboardEvent) => {
863
+ if (event.key !== "Escape") {
864
+ return;
865
+ }
866
+
867
+ if (showConversationModal) {
868
+ setShowConversationModal(false);
869
+ }
870
+
871
+ // if (showAssistantConfigModal) {
872
+ // setShowAssistantConfigModal(false);
873
+ // }
874
+ };
875
+
876
+ window.addEventListener("keydown", handleModalEscape);
877
+ return () => {
878
+ window.removeEventListener("keydown", handleModalEscape);
879
+ };
880
+ }, [showConversationModal]);
881
+
882
+ useEffect(() => {
883
+ const onPopState = () => {
884
+ setPath(normalizePath(window.location.pathname));
885
+ };
886
+
887
+ window.addEventListener("popstate", onPopState);
888
+ return () => {
889
+ window.removeEventListener("popstate", onPopState);
890
+ };
891
+ }, []);
892
+
893
+ const route = ROUTE_CONFIG[path] ?? DEFAULT_ROUTE;
894
+
895
+ useEffect(() => {
896
+ document.title = route.title;
897
+
898
+ const canonicalUrl = new URL(path, window.location.origin).toString();
899
+ const socialImageUrl = new URL(
900
+ "/avatar.png",
901
+ window.location.origin,
902
+ ).toString();
903
+
904
+ upsertMeta('meta[name="description"]', {
905
+ name: "description",
906
+ content: route.description,
907
+ });
908
+ upsertCanonicalLink(canonicalUrl);
909
+
910
+ upsertMeta('meta[property="og:title"]', {
911
+ property: "og:title",
912
+ content: route.title,
913
+ });
914
+ upsertMeta('meta[property="og:description"]', {
915
+ property: "og:description",
916
+ content: route.description,
917
+ });
918
+ upsertMeta('meta[property="og:url"]', {
919
+ property: "og:url",
920
+ content: canonicalUrl,
921
+ });
922
+ upsertMeta('meta[property="og:image"]', {
923
+ property: "og:image",
924
+ content: socialImageUrl,
925
+ });
926
+ upsertMeta('meta[name="twitter:card"]', {
927
+ name: "twitter:card",
928
+ content: "summary",
929
+ });
930
+ upsertMeta('meta[name="twitter:image"]', {
931
+ name: "twitter:image",
932
+ content: socialImageUrl,
933
+ });
934
+ upsertMeta('meta[name="twitter:title"]', {
935
+ name: "twitter:title",
936
+ content: route.title,
937
+ });
938
+ upsertMeta('meta[name="twitter:description"]', {
939
+ name: "twitter:description",
940
+ content: route.description,
941
+ });
942
+ }, [path, route.title, route.description]);
943
+
944
+ useEffect(() => {
945
+ if (path !== "/") {
946
+ removeStructuredDataScript();
947
+ return;
948
+ }
949
+
950
+ let cancelled = false;
951
+
952
+ const loadStructuredData = async () => {
953
+ try {
954
+ const response = await fetch("/soundcloud.json", {
955
+ method: "GET",
956
+ cache: "no-store",
957
+ headers: {
958
+ Accept: "application/json",
959
+ },
960
+ });
961
+
962
+ if (!response.ok) {
963
+ throw new Error(`HTTP ${response.status}`);
964
+ }
965
+
966
+ const payload: unknown = await response.json();
967
+ if (!isSoundCloudPayload(payload)) {
968
+ throw new Error("Invalid SoundCloud payload schema");
969
+ }
970
+
971
+ if (!cancelled) {
972
+ upsertStructuredDataScript(
973
+ buildMusicGroupSchema(payload.tracks),
974
+ );
975
+ }
976
+ } catch {
977
+ if (!cancelled) {
978
+ removeStructuredDataScript();
979
+ }
980
+ }
981
+ };
982
+
983
+ void loadStructuredData();
984
+
985
+ return () => {
986
+ cancelled = true;
987
+ };
988
+ }, [path]);
989
+
990
+ const navigate = (href: string) => {
991
+ if (!isInternalPath(href)) {
992
+ window.location.assign(href);
993
+ return;
994
+ }
995
+
996
+ const current = normalizePath(window.location.pathname);
997
+ const next = normalizePath(href);
998
+
999
+ if (current === next && window.location.search === "") {
1000
+ return;
1001
+ }
1002
+
1003
+ window.history.pushState({}, "", href);
1004
+ setPath(next);
1005
+ };
1006
+
1007
+ // const handleTopMenuAction = (href: string) => {
1008
+ // if (href !== ASSISTANT_CONFIG_MENU_HREF) {
1009
+ // return false;
1010
+ // }
1011
+ //
1012
+ // setAssistantConfigError("");
1013
+ // setShowAssistantConfigModal(true);
1014
+ // return true;
1015
+ // };
1016
+
1017
+ // const resolvedAssistantModels = useMemo(() => {
1018
+ // const options = new Set(assistantModelOptions);
1019
+ // if (assistantConfig.model.trim()) {
1020
+ // options.add(assistantConfig.model.trim());
1021
+ // }
1022
+ // return Array.from(options).sort((a, b) => a.localeCompare(b));
1023
+ // }, [assistantConfig.model, assistantModelOptions]);
1024
+
1025
+ // const handleDiscoverAssistantModels = async () => {
1026
+ // setAssistantConfigError("");
1027
+ // setIsDiscoveringAssistantModels(true);
1028
+ //
1029
+ // try {
1030
+ // const models = await discoverAssistantModels(
1031
+ // assistantConfig.endpoint,
1032
+ // assistantConfig.apiKey,
1033
+ // );
1034
+ // const ids = models.map((model) => model.id);
1035
+ // setAssistantModelOptions(ids);
1036
+ //
1037
+ // if (!assistantConfig.model.trim() && ids.length > 0) {
1038
+ // setAssistantConfig((previous) => ({
1039
+ // ...previous,
1040
+ // model: ids[0] ?? "",
1041
+ // }));
1042
+ // }
1043
+ // } catch (error) {
1044
+ // setAssistantConfigError(
1045
+ // error instanceof Error
1046
+ // ? error.message
1047
+ // : "Failed to discover models from endpoint.",
1048
+ // );
1049
+ // } finally {
1050
+ // setIsDiscoveringAssistantModels(false);
1051
+ // }
1052
+ // };
1053
+
1054
+ // const handleSaveAssistantConfig = () => {
1055
+ // saveAssistantConfig(assistantConfig);
1056
+ // setShowAssistantConfigModal(false);
1057
+ // };
1058
+
1059
+ const hasAssistantEndpointAndModel = useCallback(
1060
+ () => hasConfiguredAssistant(assistantConfig),
1061
+ [assistantConfig],
1062
+ );
1063
+
1064
+ const triggerSubmitPulse = () => {
1065
+ setIsSubmitPulseActive(true);
1066
+ window.setTimeout(() => setIsSubmitPulseActive(false), SHADOW_PULSE_MS);
1067
+ };
1068
+
1069
+ const submitAssistantPrompt = async (
1070
+ prompt: string,
1071
+ options?: AssistantPromptOptions,
1072
+ ) => {
1073
+ const trimmedPrompt = prompt.trim();
1074
+ if (!trimmedPrompt) {
1075
+ return;
1076
+ }
1077
+
1078
+ if (!hasAssistantEndpointAndModel()) {
1079
+ if (options?.closeModalOnSubmit) {
1080
+ setConversationError("Please configure endpoint and model first.");
1081
+ }
1082
+ return;
1083
+ }
1084
+
1085
+ if (assistantWindowVisible || assistantWindowText) {
1086
+ setAssistantWindowFading(true);
1087
+ await new Promise<void>((resolve) => {
1088
+ window.setTimeout(() => resolve(), 220);
1089
+ });
1090
+ setAssistantWindowVisible(false);
1091
+ setAssistantWindowText("");
1092
+ setAssistantWindowFading(false);
1093
+ setAssistantWindowMinimized(false);
1094
+ }
1095
+
1096
+ if (options?.closeModalOnSubmit) {
1097
+ setShowConversationModal(false);
1098
+ }
1099
+
1100
+ // const shouldPulseInTransit = shouldShowInTransitPulse(options);
1101
+
1102
+ setConversationError("");
1103
+ setIsAssistantRequestPending(true);
1104
+ // if (shouldPulseInTransit) {
1105
+ // setIsWisdomRequestPending(true);
1106
+ // }
1107
+ triggerSubmitPulse();
1108
+
1109
+ try {
1110
+ const result = await requestAssistantCompletion(
1111
+ assistantConfig,
1112
+ trimmedPrompt,
1113
+ {
1114
+ conversationPrompt: !!options?.closeModalOnSubmit,
1115
+ },
1116
+ );
1117
+ setAssistantWindowText(result);
1118
+ setAssistantWindowVisible(true);
1119
+ setAssistantWindowMinimized(false);
1120
+ setAssistantConnectionInterrupted(false);
1121
+ const readyBeep = new Audio("/Beep.ogg");
1122
+ void readyBeep.play().catch(() => {});
1123
+ } catch (error) {
1124
+ setConversationError(
1125
+ error instanceof Error
1126
+ ? error.message
1127
+ : "Failed to reach configured assistant endpoint.",
1128
+ );
1129
+ setAssistantConnectionInterrupted(true);
1130
+ } finally {
1131
+ setIsAssistantRequestPending(false);
1132
+ // if (shouldPulseInTransit) {
1133
+ // setIsWisdomRequestPending(false);
1134
+ // }
1135
+ }
1136
+ };
1137
+
1138
+ const openConversationModal = () => {
1139
+ if (!hasAssistantEndpointAndModel()) {
1140
+ handleUnavailableAssistantConfig();
1141
+ return;
1142
+ }
1143
+
1144
+ setShowConversationModal(true);
1145
+ setConversationError("");
1146
+ };
1147
+
1148
+ const handleClippyMouseDown = (event: React.MouseEvent<HTMLImageElement>) => {
1149
+ if (event.button !== 0) {
1150
+ return;
1151
+ }
1152
+
1153
+ holdTriggeredRef.current = false;
1154
+ if (holdTimerRef.current !== null) {
1155
+ window.clearTimeout(holdTimerRef.current);
1156
+ }
1157
+
1158
+ holdTimerRef.current = window.setTimeout(() => {
1159
+ holdTriggeredRef.current = true;
1160
+ openConversationModal();
1161
+ holdTimerRef.current = null;
1162
+ }, 450);
1163
+ };
1164
+
1165
+ const clearClippyHoldTimer = () => {
1166
+ if (holdTimerRef.current !== null) {
1167
+ window.clearTimeout(holdTimerRef.current);
1168
+ holdTimerRef.current = null;
1169
+ }
1170
+ };
1171
+
1172
+ const handleClippyClick = () => {
1173
+ if (holdTriggeredRef.current) {
1174
+ holdTriggeredRef.current = false;
1175
+ return;
1176
+ }
1177
+
1178
+ onClippyClick();
1179
+ };
1180
+
1181
+ const handleClippyDoubleClick = () => {
1182
+ // Wisdom-on-double-click is intentionally disabled.
1183
+ };
1184
+
1185
+ useEffect(() => {
1186
+ if (!showConversationModal) {
1187
+ return;
1188
+ }
1189
+
1190
+ if (!hasAssistantEndpointAndModel()) {
1191
+ setShowConversationModal(false);
1192
+ setConversationError("");
1193
+ }
1194
+ }, [hasAssistantEndpointAndModel, showConversationModal]);
1195
+
1196
+ const handleDismissAssistantWindow = () => {
1197
+ setAssistantWindowVisible(false);
1198
+ setAssistantWindowFading(false);
1199
+ setAssistantWindowText("");
1200
+ setAssistantWindowMinimized(false);
1201
+ };
1202
+
1203
+ const handleConversationSubmit = () => {
1204
+ if (isAssistantRequestPending || !conversationInput.trim()) {
1205
+ return;
1206
+ }
1207
+
1208
+ void submitAssistantPrompt(conversationInput, {
1209
+ closeModalOnSubmit: true,
1210
+ });
1211
+ };
1212
+
1213
+ const triggerConnectionFlashOnce = () => {
1214
+ if (!rightClickFlashArmedRef.current) {
1215
+ return;
1216
+ }
1217
+
1218
+ rightClickFlashArmedRef.current = false;
1219
+ setIsConnectionFlashActive(true);
1220
+
1221
+ if (connectionFlashTimerRef.current !== null) {
1222
+ window.clearTimeout(connectionFlashTimerRef.current);
1223
+ }
1224
+
1225
+ connectionFlashTimerRef.current = window.setTimeout(() => {
1226
+ setIsConnectionFlashActive(false);
1227
+ connectionFlashTimerRef.current = null;
1228
+ }, SHADOW_PULSE_MS);
1229
+ };
1230
+
1231
+ const handleUnavailableAssistantConfig = () => {
1232
+ triggerConnectionFlashOnce();
1233
+ onClippyClick();
1234
+ showClippyHint();
1235
+ };
1236
+
1237
+ const clippyFilter = useMemo(() => buildClippyShadowFilter({
1238
+ isSubmitPulseActive,
1239
+ isConnectionFlashActive,
1240
+ showConversationModal,
1241
+ isAssistantRequestPending,
1242
+ isClippyHovered,
1243
+ }), [
1244
+ isConnectionFlashActive,
1245
+ isAssistantRequestPending,
1246
+ isClippyHovered,
1247
+ isSubmitPulseActive,
1248
+ showConversationModal,
1249
+ ]);
1250
+
1251
+ let content: ReactElement;
1252
+ switch (path) {
1253
+ case "/":
1254
+ content = <HomePage />;
1255
+ break;
1256
+ case "/addons":
1257
+ content = <AddonsPage />;
1258
+ break;
1259
+ case "/blog":
1260
+ content = (
1261
+ <main>
1262
+ <BlogContent />
1263
+ </main>
1264
+ );
1265
+ break;
1266
+ case "/music":
1267
+ content = (
1268
+ <main>
1269
+ <MusicContent />
1270
+ </main>
1271
+ );
1272
+ break;
1273
+ case "/sitemap":
1274
+ content = (
1275
+ <main>
1276
+ <SitemapContent />
1277
+ </main>
1278
+ );
1279
+ break;
1280
+ case "/contact":
1281
+ content = <ContactPage />;
1282
+ break;
1283
+ case "/resume":
1284
+ content = <ResumePage />;
1285
+ break;
1286
+ case "/wow":
1287
+ content = <WowPage />;
1288
+ break;
1289
+ default:
1290
+ content = <NotFoundPage />;
1291
+ break;
1292
+ }
1293
+
1294
+ return (
1295
+ <>
1296
+ <MenuBar
1297
+ onNavigate={navigate}
1298
+ // additionalLinks={showClippy ? TOP_BAR_ADDITIONAL_LINKS : []}
1299
+ additionalLinks={[]}
1300
+ // onMenuAction={handleTopMenuAction}
1301
+ />
1302
+ {content}
1303
+ {/* Assistant config modal intentionally disabled. */}
1304
+ {/* {showAssistantConfigModal ? (
1305
+ ...
1306
+ ) : null} */}
1307
+ {showConversationModal ? (
1308
+ <div
1309
+ style={{
1310
+ position: "fixed",
1311
+ top: 0,
1312
+ left: 0,
1313
+ right: 0,
1314
+ bottom: 0,
1315
+ background: "rgba(0, 0, 0, 0.5)",
1316
+ zIndex: 11000,
1317
+ display: "flex",
1318
+ alignItems: "center",
1319
+ justifyContent: "center",
1320
+ }}
1321
+ onClick={(event) => {
1322
+ if (event.target === event.currentTarget) {
1323
+ setShowConversationModal(false);
1324
+ }
1325
+ }}
1326
+ >
1327
+ <div className="window" style={{ width: "min(560px, 92vw)" }}>
1328
+ <div className="title-bar">
1329
+ <div className="title-bar-text">Assistant Conversation</div>
1330
+ <div className="title-bar-controls">
1331
+ <button
1332
+ aria-label="Close"
1333
+ onClick={() => setShowConversationModal(false)}
1334
+ ></button>
1335
+ </div>
1336
+ </div>
1337
+ <div className="window-body" style={{ display: "grid", gap: "0.6rem" }}>
1338
+ <label htmlFor="assistant-prompt-input">Prompt (max 256 chars)</label>
1339
+ <textarea
1340
+ id="assistant-prompt-input"
1341
+ value={conversationInput}
1342
+ maxLength={256}
1343
+ onChange={(event) => setConversationInput(event.target.value)}
1344
+ onKeyDown={(event) => {
1345
+ if (event.key !== "Enter") {
1346
+ return;
1347
+ }
1348
+
1349
+ event.preventDefault();
1350
+ handleConversationSubmit();
1351
+ }}
1352
+ rows={4}
1353
+ placeholder="Ask for advice..."
1354
+ />
1355
+ <div style={{ fontSize: "0.85rem", textAlign: "right" }}>
1356
+ {conversationInput.length}/256
1357
+ </div>
1358
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: "0.5rem" }}>
1359
+ <button
1360
+ type="button"
1361
+ onClick={handleConversationSubmit}
1362
+ disabled={isAssistantRequestPending || !conversationInput.trim()}
1363
+ >
1364
+ {isAssistantRequestPending ? "Sending..." : "Send"}
1365
+ </button>
1366
+ </div>
1367
+ {conversationError ? (
1368
+ <p style={{ margin: 0, color: "#c00" }}>{conversationError}</p>
1369
+ ) : null}
1370
+ </div>
1371
+ </div>
1372
+ </div>
1373
+ ) : null}
1374
+ {assistantWindowVisible || assistantWindowFading ? (
1375
+ <div
1376
+ className="window"
1377
+ style={{
1378
+ position: "fixed",
1379
+ right: "1rem",
1380
+ bottom: "9.6rem",
1381
+ width: "min(420px, 92vw)",
1382
+ zIndex: 10950,
1383
+ opacity: assistantWindowFading ? 0 : 1,
1384
+ transition: "opacity 220ms ease",
1385
+ pointerEvents: "auto",
1386
+ }}
1387
+ >
1388
+ <div className="title-bar">
1389
+ <div className="title-bar-text">Assistant Response</div>
1390
+ <div className="title-bar-controls">
1391
+ <button
1392
+ aria-label={assistantWindowMinimized ? "Maximize" : "Minimize"}
1393
+ onClick={() =>
1394
+ setAssistantWindowMinimized((previous) => !previous)
1395
+ }
1396
+ ></button>
1397
+ <button
1398
+ aria-label="Close"
1399
+ onClick={handleDismissAssistantWindow}
1400
+ ></button>
1401
+ </div>
1402
+ </div>
1403
+ {!assistantWindowMinimized ? (
1404
+ <div
1405
+ className="window-body"
1406
+ style={{ whiteSpace: "pre-wrap" }}
1407
+ >
1408
+ <Markdown
1409
+ rehypePlugins={markdownRehypePlugins}
1410
+ components={markdownComponents}
1411
+ >
1412
+ {assistantWindowText}
1413
+ </Markdown>
1414
+ </div>
1415
+ ) : null}
1416
+ </div>
1417
+ ) : null}
1418
+ {showClippy ? (
1419
+ <div
1420
+ style={{
1421
+ position: "fixed",
1422
+ right: "1rem",
1423
+ bottom: "1rem",
1424
+ zIndex: 10000,
1425
+ display: "flex",
1426
+ flexDirection: "column",
1427
+ alignItems: "flex-end",
1428
+ gap: "0.4rem",
1429
+ }}
1430
+ >
1431
+ {showClippyBubble ? (
1432
+ <div
1433
+ style={{
1434
+ background: "#fffde7",
1435
+ border: "2px solid #aaa",
1436
+ borderRadius: "8px",
1437
+ padding: "0.5rem 0.75rem",
1438
+ maxWidth: "180px",
1439
+ fontSize: "0.8rem",
1440
+ lineHeight: 1.4,
1441
+ boxShadow: "2px 2px 6px rgba(0,0,0,0.25)",
1442
+ position: "relative",
1443
+ }}
1444
+ >
1445
+ {clippyBubbleSaysNo ? (
1446
+ "haha it said no"
1447
+ ) : (
1448
+ <>
1449
+ It looks like you&apos;re trying to close something. Try clicking
1450
+ one of the{" "}
1451
+ <strong>✕ close buttons</strong> on the page!
1452
+ </>
1453
+ )}
1454
+ <span
1455
+ style={{
1456
+ position: "absolute",
1457
+ bottom: "-8px",
1458
+ right: "20px",
1459
+ width: 0,
1460
+ height: 0,
1461
+ borderLeft: "8px solid transparent",
1462
+ borderRight: "8px solid transparent",
1463
+ borderTop: "8px solid #aaa",
1464
+ }}
1465
+ />
1466
+ </div>
1467
+ ) : null}
1468
+ <img
1469
+ src="/Clippy.png"
1470
+ alt=""
1471
+ onClick={handleClippyClick}
1472
+ onDoubleClick={handleClippyDoubleClick}
1473
+ onMouseDown={handleClippyMouseDown}
1474
+ onMouseUp={clearClippyHoldTimer}
1475
+ onMouseLeave={() => {
1476
+ clearClippyHoldTimer();
1477
+ setIsClippyHovered(false);
1478
+ }}
1479
+ onMouseEnter={() => setIsClippyHovered(true)}
1480
+ onContextMenu={(event) => {
1481
+ event.preventDefault();
1482
+ event.stopPropagation();
1483
+ rightClickFlashArmedRef.current = true;
1484
+ handleUnavailableAssistantConfig();
1485
+ }}
1486
+ style={{
1487
+ width: "120px",
1488
+ maxWidth: "28vw",
1489
+ height: "auto",
1490
+ filter: clippyFilter,
1491
+ cursor: "pointer",
1492
+ transition: "filter 160ms ease",
1493
+ }}
1494
+ />
1495
+ </div>
1496
+ ) : null}
1497
+ <ToastContainer />
1498
+ </>
1499
+ );
1500
+ }