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.
- package/.astro/content-assets.mjs +1 -0
- package/.astro/content-modules.mjs +1 -0
- package/.astro/content.d.ts +199 -0
- package/.astro/data-store.json +1 -0
- package/.astro/settings.json +8 -0
- package/.astro/types.d.ts +1 -0
- package/.devcontainer/devcontainer.json +23 -0
- package/.env.firebase.example +8 -0
- package/.firebaserc +5 -0
- package/.gitattributes +2 -0
- package/.github/copilot-instructions.md +131 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/deploy-admin.yml +48 -0
- package/.github/workflows/static.yml +43 -0
- package/.gitmodules +5 -0
- package/FIREBASE_SETUP.md +69 -0
- package/README.md +63 -0
- package/SECURITY.md +11 -0
- package/admin/Admin.csproj +7 -0
- package/admin/Dockerfile +14 -0
- package/admin/Program.cs +8 -0
- package/deploy-admin-cloud-run.md +229 -0
- package/eslint.config.js +28 -0
- package/firebase.json +5 -0
- package/firestore.rules +29 -0
- package/index.html +52 -0
- package/package.json +48 -0
- package/pagerts_output.json +1 -0
- package/public/5.html +967 -0
- package/public/BAHNSCHRIFT.TTF +0 -0
- package/public/Beep.ogg +0 -0
- package/public/Clippy.png +0 -0
- package/public/Layered Network Security Model for Home Networks (slides).pdf +0 -0
- package/public/Layered Network Security Model for Home Networks.pdf +0 -0
- package/public/TODO.pdf +0 -0
- package/public/WoW_Config.zip +3 -0
- package/public/addons/energy-swing.txt +1 -0
- package/public/addons/lego-yoda-death-readme.txt +11 -0
- package/public/addons/lego-yoda-death.mp3 +0 -0
- package/public/addons/mana-blast.txt +1 -0
- package/public/addons/rage-volley.txt +1 -0
- package/public/addons/rueg-cell.txt +1 -0
- package/public/addons/rueg-elvui-profile.txt +1 -0
- package/public/addons/rueg-grid2.txt +214 -0
- package/public/addons/rueg-plater-smol.txt +1 -0
- package/public/addons/rueg-plater.txt +1 -0
- package/public/addons/rueg-wa-druid.txt +1 -0
- package/public/addons/rueg-wa-priest.txt +1 -0
- package/public/addons/rueg-wa-rogue.txt +1 -0
- package/public/addons/rueg-wa-shaman.txt +1 -0
- package/public/addons/rueg-wa-warrior.txt +1 -0
- package/public/addons/spirit-smash.txt +1 -0
- package/public/avatar.jpg +0 -0
- package/public/avatar.png +0 -0
- package/public/crunchy_kick.ogg +0 -0
- package/public/documents/resume.html +312 -0
- package/public/favicon.ico +0 -0
- package/public/images/Ateric1.png +0 -0
- package/public/images/Ateric2.png +0 -0
- package/public/images/equal1.png +0 -0
- package/public/images/hyperawareofwhatacatis.png +0 -0
- package/public/images/kogg1.png +0 -0
- package/public/images/kogg2.png +0 -0
- package/public/images/rueg1.png +0 -0
- package/public/images/rueg2.png +0 -0
- package/public/incorrect_responses.txt +126 -0
- package/public/loading.css +51 -0
- package/public/resume.pdf +0 -0
- package/public/robots.txt +9 -0
- package/public/soundcloud.json +57 -0
- package/public/spinner.svg +12 -0
- package/public/tada.wav +0 -0
- package/public/yooh.mp3 +0 -0
- package/render.yaml +5 -0
- package/scripts/ensure-blog-worktree.mjs +24 -0
- package/scripts/generate-soundcloud-json.mjs +198 -0
- package/scripts/git-worktree-helper.mjs +122 -0
- package/scripts/hoist-dev-blog-local.mjs +149 -0
- package/scripts/music-schema.mjs +56 -0
- package/scripts/publish-soundcloud-json.mjs +32 -0
- package/scripts/sync-music-links-from-worktree.mjs +32 -0
- package/src/App.tsx +1500 -0
- package/src/addons.json +76 -0
- package/src/components/Addon.tsx +223 -0
- package/src/components/BlogContent.tsx +103 -0
- package/src/components/CopyToClipboardButton.tsx +21 -0
- package/src/components/MenuBar.tsx +151 -0
- package/src/components/MenuBarWithContext.tsx +6 -0
- package/src/components/Modal.tsx +17 -0
- package/src/components/MusicContent.tsx +309 -0
- package/src/components/NavBarController.tsx +55 -0
- package/src/components/NavBarControllerWrapper.tsx +13 -0
- package/src/components/Page.tsx +56 -0
- package/src/components/SitemapContent.tsx +125 -0
- package/src/contacts.json +32 -0
- package/src/env.d.ts +13 -0
- package/src/lib/assistantStateMachine.ts +80 -0
- package/src/lib/audioOverlap.ts +99 -0
- package/src/lib/keyboardInputUtils.ts +182 -0
- package/src/lib/musicSchema.ts +85 -0
- package/src/lib/naggingAssistantClient.ts +241 -0
- package/src/lib/resumeAnalytics.ts +163 -0
- package/src/main.tsx +35 -0
- package/src/pages.json +50 -0
- package/src/sections.json +243 -0
- package/src/src+addons.zip +3 -0
- package/src/styles/main.css +465 -0
- package/src/utils/blogSecurity.ts +87 -0
- package/src/utils/menuItems.ts +33 -0
- package/src/windowing/MinimizedSections.tsx +86 -0
- package/src/windowing/Section.tsx +586 -0
- package/src/windowing/context.tsx +13 -0
- package/src/windowing/hooks.ts +10 -0
- package/src/windowing/index.ts +7 -0
- package/src/windowing/provider.tsx +74 -0
- package/src/windowing/server.ts +3 -0
- package/src/windowing/types.ts +33 -0
- package/src/windowing/utils.ts +135 -0
- package/tests/generate-soundcloud-json.test.mjs +63 -0
- package/tests/music-schema.test.mjs +53 -0
- package/tsconfig.json +26 -0
- 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'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
|
+
}
|