@hadoku/resume-bot 0.0.0-stage → 3.14.16

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 (70) hide show
  1. package/README.md +135 -3
  2. package/dist/App.d.ts +9 -0
  3. package/dist/App.d.ts.map +1 -0
  4. package/dist/ChatInterface-mHtxUGpp.js +128 -0
  5. package/dist/ChatInterface-mHtxUGpp.js.map +1 -0
  6. package/dist/ResumeViewer-C-uxZ-7E.js +296 -0
  7. package/dist/ResumeViewer-C-uxZ-7E.js.map +1 -0
  8. package/dist/builder/BlockCard.d.ts +15 -0
  9. package/dist/builder/BlockCard.d.ts.map +1 -0
  10. package/dist/builder/BuilderApp.d.ts +3 -0
  11. package/dist/builder/BuilderApp.d.ts.map +1 -0
  12. package/dist/builder/api.d.ts +37 -0
  13. package/dist/builder/api.d.ts.map +1 -0
  14. package/dist/builder/entry.d.ts +8 -0
  15. package/dist/builder/entry.d.ts.map +1 -0
  16. package/dist/builder/useBuilderAuth.d.ts +10 -0
  17. package/dist/builder/useBuilderAuth.d.ts.map +1 -0
  18. package/dist/builder.js +537 -0
  19. package/dist/builder.js.map +1 -0
  20. package/dist/components/ChatInterface.d.ts +10 -0
  21. package/dist/components/ChatInterface.d.ts.map +1 -0
  22. package/dist/components/ResumeDocument.d.ts +30 -0
  23. package/dist/components/ResumeDocument.d.ts.map +1 -0
  24. package/dist/components/ResumeViewer.d.ts +12 -0
  25. package/dist/components/ResumeViewer.d.ts.map +1 -0
  26. package/dist/entry-BTJjyohQ.js +154 -0
  27. package/dist/entry-BTJjyohQ.js.map +1 -0
  28. package/dist/entry.d.ts +22 -0
  29. package/dist/entry.d.ts.map +1 -0
  30. package/dist/index.js +9 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/markdown-vendor-61sRGACd.js +10510 -0
  33. package/dist/markdown-vendor-61sRGACd.js.map +1 -0
  34. package/dist/rehype-vendor-C_Q-vwLE.js +5647 -0
  35. package/dist/rehype-vendor-C_Q-vwLE.js.map +1 -0
  36. package/dist/services/api.d.ts +62 -0
  37. package/dist/services/api.d.ts.map +1 -0
  38. package/dist/style.css +1 -0
  39. package/dist/worker/application-extras.d.ts +56 -0
  40. package/dist/worker/application-extras.d.ts.map +1 -0
  41. package/dist/worker/blocks.d.ts +12 -0
  42. package/dist/worker/blocks.d.ts.map +1 -0
  43. package/dist/worker/builder.d.ts +33 -0
  44. package/dist/worker/builder.d.ts.map +1 -0
  45. package/dist/worker/constants.d.ts +39 -0
  46. package/dist/worker/constants.d.ts.map +1 -0
  47. package/dist/worker/contact.d.ts +34 -0
  48. package/dist/worker/contact.d.ts.map +1 -0
  49. package/dist/worker/cover-letter.d.ts +16 -0
  50. package/dist/worker/cover-letter.d.ts.map +1 -0
  51. package/dist/worker/index.d.ts +38 -0
  52. package/dist/worker/index.d.ts.map +1 -0
  53. package/dist/worker/llm.d.ts +77 -0
  54. package/dist/worker/llm.d.ts.map +1 -0
  55. package/dist/worker/pdf.d.ts +46 -0
  56. package/dist/worker/pdf.d.ts.map +1 -0
  57. package/dist/worker/rate-limit.d.ts +10 -0
  58. package/dist/worker/rate-limit.d.ts.map +1 -0
  59. package/dist/worker/resume.d.ts +33 -0
  60. package/dist/worker/resume.d.ts.map +1 -0
  61. package/dist/worker/skeleton.d.ts +73 -0
  62. package/dist/worker/skeleton.d.ts.map +1 -0
  63. package/dist/worker/tailored-resume.d.ts +31 -0
  64. package/dist/worker/tailored-resume.d.ts.map +1 -0
  65. package/dist/worker/typography.d.ts +12 -0
  66. package/dist/worker/typography.d.ts.map +1 -0
  67. package/dist/worker/variants.d.ts +51 -0
  68. package/dist/worker/variants.d.ts.map +1 -0
  69. package/dist/worker.js +42508 -0
  70. package/package.json +43 -4
package/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @wolffm/resume-bot
2
+
3
+ AI-powered conversational interface for interactive resume exploration — chat with an AI about my background, skills, and experience while browsing the resume itself.
4
+
5
+ **See it live:** [hadoku.me/resume](https://hadoku.me/resume)
6
+
7
+ This repo is the npm package that powers that widget. The rest of this README is for contributors working on the package itself.
8
+
9
+ ## Overview
10
+
11
+ Two-panel UI: a markdown resume viewer on the left, an AI chat interface on the right. The chat uses Groq's LLM to answer questions about the resume. Also exposes a worker API for generating tailored resumes and cover letters.
12
+
13
+ ## Development
14
+
15
+ ### Local Development Setup
16
+
17
+ This package includes a development environment for testing the component locally:
18
+
19
+ ```bash
20
+ # Install dependencies
21
+ pnpm install
22
+
23
+ # Start dev server (opens http://localhost:5173)
24
+ pnpm dev
25
+
26
+ # Build for production
27
+ pnpm build
28
+
29
+ # Lint and format
30
+ pnpm lint:fix
31
+ pnpm format
32
+ ```
33
+
34
+ The dev server uses `index.html` which imports and mounts the component directly. You can pass different configurations via URL parameters:
35
+
36
+ **Example URLs:**
37
+
38
+ ```bash
39
+ # Default configuration
40
+ http://localhost:5173
41
+
42
+ # Custom theme
43
+ http://localhost:5173?theme=ocean
44
+
45
+ # Admin user
46
+ http://localhost:5173?userType=admin&sessionId=test-123
47
+
48
+ # Full configuration
49
+ http://localhost:5173?theme=ocean&userType=admin&sessionId=dev-session
50
+ ```
51
+
52
+ ## Integration
53
+
54
+ This app is a child component of the [hadoku_site](https://github.com/WolffM/hadoku_site) parent application.
55
+
56
+ ### Props
57
+
58
+ ```typescript
59
+ interface ResumeBotAppProps {
60
+ theme?: string // Optional: 'default', 'ocean', 'forest', etc.
61
+ apiBaseUrl: string // Required: API root path or URL (e.g., '/resume/api' or 'https://api.yourapp.com/api')
62
+ ownerName?: string // Optional: name shown in the chat welcome message (default: 'the candidate')
63
+ }
64
+ ```
65
+
66
+ ### Mounting
67
+
68
+ ```typescript
69
+ import { mount, unmount } from '@wolffm/resume-bot'
70
+
71
+ // Mount the app
72
+ const element = document.getElementById('app-root')
73
+ mount(element, {
74
+ theme: 'ocean', // optional
75
+ apiBaseUrl: '/resume/api' // required - path to the API root
76
+ })
77
+
78
+ // Unmount when done
79
+ unmount(element)
80
+ ```
81
+
82
+ **Important:** `apiBaseUrl` is the **API root**, matching the rest of the hadoku
83
+ ecosystem (`/oss/api`, `/jobplatform/api`). Endpoints are appended to it directly.
84
+ It can be either:
85
+
86
+ - A **path** (e.g., `/resume/api`) if the backend is hosted on the same domain
87
+ - A **full URL** (e.g., `https://api.yourapp.com/api`) if the backend is on a different domain
88
+
89
+ > **Breaking change in 2.0.0.** Through 1.x this prop was the _app_ root (`/resume`)
90
+ > and the package appended `/api/...` itself; 1.3.14 accepted both spellings. 2.0.0
91
+ > removes that shim — pass the API root. Passing `/resume` now requests `/resume/chat`
92
+ > and 404s. Migration is a one-line change at the mount site: `/resume` -> `/resume/api`.
93
+ > This does not affect the `./api` worker export — `createResumeHandler(basePath)` is
94
+ > unchanged.
95
+
96
+ The backend handles these endpoints (public unless noted; non-public routes are
97
+ gated in-worker via `@wolffm/worker-utils` edge-auth — see `worker/src/index.ts`):
98
+
99
+ - `${apiBaseUrl}/chat` - POST - Chat with the bot (public, rate-limited)
100
+ - `${apiBaseUrl}/resume` - GET - Fetch resume content, incl. `?v={slug}` variants (public)
101
+ - `${apiBaseUrl}/system-prompt` - GET - Fetch system prompt (admin/friend)
102
+ - `${apiBaseUrl}/tailored-resume` - POST - Per-job tailored resume (admin/friend/service)
103
+ - `${apiBaseUrl}/cover-letter` - POST - Per-job cover letter (admin/friend/service)
104
+ - `${apiBaseUrl}/variants` - POST/GET/DELETE - Variant management (admin/friend)
105
+
106
+ ## Deployment
107
+
108
+ Pushes to `main` automatically:
109
+
110
+ 1. Build and publish to GitHub Packages
111
+ 2. Notify parent site to update
112
+ 3. Parent pulls new version and redeploys
113
+
114
+ ### Versioning
115
+
116
+ Version bumps are handled automatically through two mechanisms:
117
+
118
+ 1. **Pre-commit hook** (primary): The `.husky/pre-commit` hook automatically bumps the version for every commit
119
+ 2. **Workflow fallback**: The publish workflow checks if the current version already exists in the registry and bumps it if needed
120
+
121
+ This dual approach ensures versions are always incremented, even if commits bypass the pre-commit hook (e.g., web UI edits, `--no-verify` commits).
122
+
123
+ Version bumping follows this pattern:
124
+
125
+ - Patch version increments on each commit (1.1.8 → 1.1.9)
126
+ - At patch 20, rolls over to next minor (1.1.20 → 1.2.0)
127
+
128
+ ## Architecture
129
+
130
+ This package has two exports:
131
+
132
+ - **UI component** (`.` export): `mount(el, props)` / `unmount(el)` — React app from `src/entry.tsx`
133
+ - **Worker API** (`./api` export): `createResumeHandler(basePath, options)` — Hono router from `worker/src/index.ts` (`options.ownerName` sets the chat owner)
134
+
135
+ Both are built and published together as `@wolffm/resume-bot`.
package/dist/App.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { ResumeBotAppProps } from './entry';
2
+ /**
3
+ * Provider boundary. Theme state belongs to the platform (@wolffm/themes),
4
+ * not to this app — the local hooks/useTheme.ts, prefs/themePrefs.ts and
5
+ * app/themeConfig.tsx copies are gone. AppHeader renders the shared picker
6
+ * from this context, so nothing below passes one.
7
+ */
8
+ export default function App(props: ResumeBotAppProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAMhD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,KAAK,EAAE,iBAAiB,2CAOnD"}
@@ -0,0 +1,128 @@
1
+ import { jsxs as o, jsx as a } from "react/jsx-runtime";
2
+ import { forwardRef as E, useState as u, useRef as b, useEffect as C, useImperativeHandle as x } from "react";
3
+ import { logger as M } from "@wolffm/logger/client";
4
+ import { s as y } from "./entry-BTJjyohQ.js";
5
+ import { M as T, a as j } from "./markdown-vendor-61sRGACd.js";
6
+ import { r as B } from "./rehype-vendor-C_Q-vwLE.js";
7
+ const V = E(
8
+ ({ ownerName: k = "the candidate" }, w) => {
9
+ const [r, c] = u([]), [m, g] = u(""), [i, h] = u(!1), [_, l] = u(null), v = b(null), N = b(null);
10
+ C(() => {
11
+ r.length > 0 && v.current?.scrollIntoView({ behavior: "smooth" });
12
+ }, [r]), C(() => {
13
+ i || N.current?.focus();
14
+ }, [i]), x(w, () => ({
15
+ askAbout: (e) => {
16
+ const n = {
17
+ role: "user",
18
+ content: `Tell me about: ${e}`
19
+ };
20
+ c((s) => [...s, n]), l(null), h(!0), (async () => {
21
+ try {
22
+ const s = [...r, n], p = {
23
+ role: "assistant",
24
+ content: (await y(s)).message
25
+ };
26
+ c((S) => [...S, p]);
27
+ } catch (s) {
28
+ const d = s instanceof Error ? s.message : "Failed to send message";
29
+ l(d), c((p) => p.slice(0, -1));
30
+ } finally {
31
+ h(!1);
32
+ }
33
+ })().catch(
34
+ (s) => M.error("[ChatInterface] Error sending message", {
35
+ error: s?.message ?? String(s)
36
+ })
37
+ );
38
+ }
39
+ }));
40
+ async function I(e) {
41
+ if (e.preventDefault(), !m.trim() || i) return;
42
+ const t = {
43
+ role: "user",
44
+ content: m.trim()
45
+ };
46
+ c((n) => [...n, t]), g(""), l(null), h(!0);
47
+ try {
48
+ const n = [...r, t], s = {
49
+ role: "assistant",
50
+ content: (await y(n)).message
51
+ };
52
+ c((d) => [...d, s]);
53
+ } catch (n) {
54
+ const f = n instanceof Error ? n.message : "Failed to send message";
55
+ l(f), c((s) => s.slice(0, -1)), g(t.content);
56
+ } finally {
57
+ h(!1);
58
+ }
59
+ }
60
+ function R() {
61
+ c([]), l(null);
62
+ }
63
+ return /* @__PURE__ */ o("div", { className: "chat-interface", children: [
64
+ /* @__PURE__ */ o("div", { className: "chat-interface__header", children: [
65
+ /* @__PURE__ */ a("h3", { children: "Chat with Resume Bot" }),
66
+ r.length > 0 && /* @__PURE__ */ a("button", { onClick: R, className: "chat-interface__clear-button", children: "Clear Chat" })
67
+ ] }),
68
+ /* @__PURE__ */ o("div", { className: "chat-interface__messages", children: [
69
+ r.length === 0 && /* @__PURE__ */ a("div", { className: "chat-interface__welcome", children: /* @__PURE__ */ o("p", { children: [
70
+ "Hi! I can answer questions about ",
71
+ k,
72
+ "'s resume. Try asking about their experience, skills, or projects!"
73
+ ] }) }),
74
+ r.map((e, t) => /* @__PURE__ */ o("div", { className: `chat-message chat-message--${e.role}`, children: [
75
+ /* @__PURE__ */ a("div", { className: "chat-message__role", children: e.role === "user" ? "You" : "Resume Bot" }),
76
+ /* @__PURE__ */ a("div", { className: "chat-message__content", children: e.role === "assistant" ? /* @__PURE__ */ a(T, { remarkPlugins: [j], rehypePlugins: [B], children: e.content }) : e.content })
77
+ ] }, t)),
78
+ i && /* @__PURE__ */ o("div", { className: "chat-message chat-message--assistant chat-message--loading", children: [
79
+ /* @__PURE__ */ a("div", { className: "chat-message__role", children: "Resume Bot" }),
80
+ /* @__PURE__ */ a("div", { className: "chat-message__content", children: "Thinking..." })
81
+ ] }),
82
+ /* @__PURE__ */ a("div", { ref: v })
83
+ ] }),
84
+ _ && /* @__PURE__ */ a("div", { className: "chat-interface__error", children: /* @__PURE__ */ a("p", { children: _ }) }),
85
+ /* @__PURE__ */ o(
86
+ "form",
87
+ {
88
+ onSubmit: (e) => {
89
+ I(e).catch(
90
+ (t) => M.error("[ChatInterface] Error sending message", {
91
+ error: t?.message ?? String(t)
92
+ })
93
+ );
94
+ },
95
+ className: "chat-interface__input-form",
96
+ children: [
97
+ /* @__PURE__ */ a(
98
+ "input",
99
+ {
100
+ ref: N,
101
+ type: "text",
102
+ value: m,
103
+ onChange: (e) => g(e.target.value),
104
+ placeholder: "Ask me about the resume...",
105
+ disabled: i,
106
+ className: "chat-interface__input"
107
+ }
108
+ ),
109
+ /* @__PURE__ */ a(
110
+ "button",
111
+ {
112
+ type: "submit",
113
+ disabled: i || !m.trim(),
114
+ className: "chat-interface__send-button",
115
+ children: "Send"
116
+ }
117
+ )
118
+ ]
119
+ }
120
+ )
121
+ ] });
122
+ }
123
+ );
124
+ V.displayName = "ChatInterface";
125
+ export {
126
+ V as default
127
+ };
128
+ //# sourceMappingURL=ChatInterface-mHtxUGpp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatInterface-mHtxUGpp.js","sources":["../src/components/ChatInterface.tsx"],"sourcesContent":["import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef } from 'react'\r\nimport ReactMarkdown from 'react-markdown'\r\nimport remarkGfm from 'remark-gfm'\r\nimport rehypeRaw from 'rehype-raw'\r\nimport { logger } from '@wolffm/logger/client'\r\nimport { sendChatMessage, type ChatMessage } from '../services/api'\r\n\r\nexport interface ChatInterfaceRef {\r\n askAbout: (text: string) => void\r\n}\r\n\r\ninterface ChatInterfaceProps {\r\n ownerName?: string\r\n}\r\n\r\nconst ChatInterface = forwardRef<ChatInterfaceRef, ChatInterfaceProps>(\r\n ({ ownerName = 'the candidate' }, ref) => {\r\n const [messages, setMessages] = useState<ChatMessage[]>([])\r\n const [inputValue, setInputValue] = useState('')\r\n const [isLoading, setIsLoading] = useState(false)\r\n const [error, setError] = useState<string | null>(null)\r\n const messagesEndRef = useRef<HTMLDivElement>(null)\r\n const inputRef = useRef<HTMLInputElement>(null)\r\n\r\n // Scroll to bottom when messages change (but not on initial render)\r\n useEffect(() => {\r\n if (messages.length > 0) {\r\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })\r\n }\r\n }, [messages])\r\n\r\n // Re-focus input after loading completes\r\n useEffect(() => {\r\n if (!isLoading) {\r\n inputRef.current?.focus()\r\n }\r\n }, [isLoading])\r\n\r\n // Expose method to parent component\r\n useImperativeHandle(ref, () => ({\r\n askAbout: (text: string) => {\r\n // Create the message content\r\n const messageContent = `Tell me about: ${text}`\r\n\r\n // Create user message\r\n const userMessage: ChatMessage = {\r\n role: 'user',\r\n content: messageContent\r\n }\r\n\r\n // Add user message to chat\r\n setMessages(prev => [...prev, userMessage])\r\n setError(null)\r\n setIsLoading(true)\r\n\r\n // Send the message automatically\r\n const sendMessage = async () => {\r\n try {\r\n const messagesToSend: ChatMessage[] = [...messages, userMessage]\r\n\r\n const response = await sendChatMessage(messagesToSend)\r\n\r\n const assistantMessage: ChatMessage = {\r\n role: 'assistant',\r\n content: response.message\r\n }\r\n\r\n setMessages(prev => [...prev, assistantMessage])\r\n } catch (err) {\r\n const errorMessage = err instanceof Error ? err.message : 'Failed to send message'\r\n setError(errorMessage)\r\n setMessages(prev => prev.slice(0, -1))\r\n } finally {\r\n setIsLoading(false)\r\n }\r\n }\r\n\r\n sendMessage().catch(err =>\r\n logger.error('[ChatInterface] Error sending message', {\r\n error: (err as Error)?.message ?? String(err)\r\n })\r\n )\r\n }\r\n }))\r\n\r\n async function handleSendMessage(e: React.FormEvent) {\r\n e.preventDefault()\r\n\r\n if (!inputValue.trim() || isLoading) return\r\n\r\n const userMessage: ChatMessage = {\r\n role: 'user',\r\n content: inputValue.trim()\r\n }\r\n\r\n // Add user message to chat\r\n setMessages(prev => [...prev, userMessage])\r\n setInputValue('')\r\n setError(null)\r\n setIsLoading(true)\r\n\r\n try {\r\n const messagesToSend: ChatMessage[] = [...messages, userMessage]\r\n\r\n // Send all messages to backend\r\n const response = await sendChatMessage(messagesToSend)\r\n\r\n // Add assistant response to chat\r\n const assistantMessage: ChatMessage = {\r\n role: 'assistant',\r\n content: response.message\r\n }\r\n\r\n setMessages(prev => [...prev, assistantMessage])\r\n } catch (err) {\r\n const errorMessage = err instanceof Error ? err.message : 'Failed to send message'\r\n setError(errorMessage)\r\n\r\n // Remove the failed user message\r\n setMessages(prev => prev.slice(0, -1))\r\n // Restore the input\r\n setInputValue(userMessage.content)\r\n } finally {\r\n setIsLoading(false)\r\n }\r\n }\r\n\r\n function handleClearChat() {\r\n setMessages([])\r\n setError(null)\r\n }\r\n\r\n return (\r\n <div className=\"chat-interface\">\r\n <div className=\"chat-interface__header\">\r\n <h3>Chat with Resume Bot</h3>\r\n {messages.length > 0 && (\r\n <button onClick={handleClearChat} className=\"chat-interface__clear-button\">\r\n Clear Chat\r\n </button>\r\n )}\r\n </div>\r\n\r\n <div className=\"chat-interface__messages\">\r\n {messages.length === 0 && (\r\n <div className=\"chat-interface__welcome\">\r\n <p>\r\n Hi! I can answer questions about {ownerName}&apos;s resume. Try asking about their\r\n experience, skills, or projects!\r\n </p>\r\n </div>\r\n )}\r\n\r\n {messages.map((message, index) => (\r\n <div key={index} className={`chat-message chat-message--${message.role}`}>\r\n <div className=\"chat-message__role\">\r\n {message.role === 'user' ? 'You' : 'Resume Bot'}\r\n </div>\r\n <div className=\"chat-message__content\">\r\n {message.role === 'assistant' ? (\r\n <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>\r\n {message.content}\r\n </ReactMarkdown>\r\n ) : (\r\n message.content\r\n )}\r\n </div>\r\n </div>\r\n ))}\r\n\r\n {isLoading && (\r\n <div className=\"chat-message chat-message--assistant chat-message--loading\">\r\n <div className=\"chat-message__role\">Resume Bot</div>\r\n <div className=\"chat-message__content\">Thinking...</div>\r\n </div>\r\n )}\r\n\r\n <div ref={messagesEndRef} />\r\n </div>\r\n\r\n {error && (\r\n <div className=\"chat-interface__error\">\r\n <p>{error}</p>\r\n </div>\r\n )}\r\n\r\n <form\r\n onSubmit={e => {\r\n handleSendMessage(e).catch(err =>\r\n logger.error('[ChatInterface] Error sending message', {\r\n error: (err as Error)?.message ?? String(err)\r\n })\r\n )\r\n }}\r\n className=\"chat-interface__input-form\"\r\n >\r\n <input\r\n ref={inputRef}\r\n type=\"text\"\r\n value={inputValue}\r\n onChange={e => setInputValue(e.target.value)}\r\n placeholder=\"Ask me about the resume...\"\r\n disabled={isLoading}\r\n className=\"chat-interface__input\"\r\n />\r\n <button\r\n type=\"submit\"\r\n disabled={isLoading || !inputValue.trim()}\r\n className=\"chat-interface__send-button\"\r\n >\r\n Send\r\n </button>\r\n </form>\r\n </div>\r\n )\r\n }\r\n)\r\n\r\nChatInterface.displayName = 'ChatInterface'\r\n\r\nexport default ChatInterface\r\n"],"names":["ChatInterface","forwardRef","ownerName","ref","messages","setMessages","useState","inputValue","setInputValue","isLoading","setIsLoading","error","setError","messagesEndRef","useRef","inputRef","useEffect","useImperativeHandle","text","userMessage","prev","messagesToSend","assistantMessage","sendChatMessage","err","errorMessage","logger","handleSendMessage","handleClearChat","jsxs","jsx","message","index","ReactMarkdown","remarkGfm","rehypeRaw"],"mappings":"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"}
@@ -0,0 +1,296 @@
1
+ import { jsx as n, jsxs as i, Fragment as N } from "react/jsx-runtime";
2
+ import z, { Children as $, isValidElement as U, useState as f, useEffect as j } from "react";
3
+ import { logger as A } from "@wolffm/logger/client";
4
+ import { f as G, r as I } from "./entry-BTJjyohQ.js";
5
+ import { M as H, a as J } from "./markdown-vendor-61sRGACd.js";
6
+ const K = "<!-- page-break -->", X = /^#[^#]\s*(.+?)\s*$/;
7
+ function Y(e) {
8
+ return e.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9
+ }
10
+ function q(e) {
11
+ const t = [];
12
+ let r = {
13
+ id: "masthead",
14
+ heading: null,
15
+ body: []
16
+ };
17
+ const s = () => {
18
+ (r.heading !== null || r.body.join("").trim() !== "") && t.push({ id: r.id, heading: r.heading, body: r.body.join(`
19
+ `) });
20
+ };
21
+ for (const o of e.split(`
22
+ `)) {
23
+ const c = X.exec(o);
24
+ c ? (s(), r = { id: Y(c[1]), heading: c[1], body: [] }) : r.body.push(o);
25
+ }
26
+ return s(), t;
27
+ }
28
+ function Q(e) {
29
+ return e.replace(/^(\*\*[^*]+\*\*)\s*[-–—]\s+/gm, "$1 ").replace(/\s*·\s*(?=\[)/g, " ");
30
+ }
31
+ function W(e) {
32
+ return e.replace(/^(\*\*[^*]+?)\s*:\*\*/gm, "$1**");
33
+ }
34
+ const M = (e) => e.type !== "text" || (e.value ?? "").trim() !== "";
35
+ function Z(e) {
36
+ const t = e?.children?.find(M);
37
+ return t?.type === "element" ? t : void 0;
38
+ }
39
+ function ee(e) {
40
+ return (e?.children ?? []).filter(M).length;
41
+ }
42
+ function te(e) {
43
+ const t = $.toArray(e);
44
+ let r = t.length;
45
+ for (let s = t.length - 1; s >= 0; s--) {
46
+ const o = t[s];
47
+ if (!(typeof o == "string" && o.trim() === "")) {
48
+ if (U(o) && o.type === "a") {
49
+ r = s;
50
+ continue;
51
+ }
52
+ break;
53
+ }
54
+ }
55
+ return [t.slice(0, r), t.slice(r)];
56
+ }
57
+ function ne(e) {
58
+ const t = [[]];
59
+ for (const r of $.toArray(e))
60
+ U(r) && r.type === "br" ? t.push([]) : t[t.length - 1].push(r);
61
+ return t;
62
+ }
63
+ function re() {
64
+ let e = !1;
65
+ return {
66
+ // `node` is destructured off so it never reaches the DOM — react-markdown
67
+ // hands it to every override, and React warns on an unknown `node` attr.
68
+ p({ node: t, children: r, ...s }) {
69
+ const [o, ...c] = ne(r);
70
+ return e || c.length === 0 ? /* @__PURE__ */ n("p", { ...s, children: r }) : (e = !0, /* @__PURE__ */ i(N, { children: [
71
+ /* @__PURE__ */ n("h1", { className: "resume-doc__name", children: o }),
72
+ c.map((a, m) => /* @__PURE__ */ n("p", { className: "resume-doc__contact", children: a }, m))
73
+ ] }));
74
+ }
75
+ };
76
+ }
77
+ function se(e) {
78
+ let t = !1;
79
+ return {
80
+ p({ node: r, children: s, ...o }) {
81
+ const c = Z(r);
82
+ if (c?.tagName === "strong") {
83
+ const [a, m] = te(s);
84
+ return /* @__PURE__ */ i("p", { className: "resume-doc__entry", ...o, children: [
85
+ a,
86
+ m.length > 0 && /* @__PURE__ */ n("span", { className: "resume-doc__links", children: m })
87
+ ] });
88
+ }
89
+ return c?.tagName === "em" && ee(r) === 1 ? /* @__PURE__ */ n("p", { className: "resume-doc__meta", ...o, children: s }) : e === "projects" && !t ? (t = !0, /* @__PURE__ */ n("p", { className: "resume-doc__lede", ...o, children: s })) : /* @__PURE__ */ n("p", { ...o, children: s });
90
+ }
91
+ };
92
+ }
93
+ const oe = {
94
+ projects: Q,
95
+ "technical-skills": W
96
+ }, ce = (e) => e;
97
+ function le({
98
+ id: e,
99
+ heading: t,
100
+ body: r,
101
+ masthead: s
102
+ }) {
103
+ const o = (oe[e] ?? ce)(r);
104
+ return /* @__PURE__ */ i("section", { className: `resume-doc__section resume-doc__section--${e}`, children: [
105
+ t !== null && /* @__PURE__ */ n("h2", { className: "resume-doc__section-title", children: t }),
106
+ /* @__PURE__ */ n(
107
+ H,
108
+ {
109
+ remarkPlugins: [J],
110
+ components: s ? re() : se(e),
111
+ children: o
112
+ }
113
+ )
114
+ ] });
115
+ }
116
+ function b({
117
+ content: e,
118
+ variant: t = "resume"
119
+ }) {
120
+ const r = e.split(K);
121
+ return /* @__PURE__ */ n("div", { className: `resume-doc resume-doc--${t}`, children: r.map((s, o) => /* @__PURE__ */ i(z.Fragment, { children: [
122
+ o > 0 && /* @__PURE__ */ n("div", { className: "resume-viewer__pagebreak" }),
123
+ /* @__PURE__ */ n("article", { className: "resume-doc__sheet", children: q(s).map((c, a) => /* @__PURE__ */ n(
124
+ le,
125
+ {
126
+ ...c,
127
+ masthead: t === "resume" && c.heading === null
128
+ },
129
+ a
130
+ )) })
131
+ ] }, o)) });
132
+ }
133
+ const ie = "/contact/";
134
+ function ae(e) {
135
+ const t = (e ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
136
+ return t ? `${t}-resume` : "resume";
137
+ }
138
+ function ge({
139
+ onAskAbout: e,
140
+ ownerName: t,
141
+ contactUrl: r = ie
142
+ }) {
143
+ const [s, o] = f(null), [c, a] = f("resume"), [m, k] = f(!0), [y, C] = f(null), [g, w] = f(
144
+ null
145
+ );
146
+ j(() => {
147
+ R().catch(
148
+ (l) => A.error("[ResumeViewer] Error loading resume", {
149
+ error: l?.message ?? String(l)
150
+ })
151
+ );
152
+ }, []);
153
+ async function R() {
154
+ try {
155
+ k(!0), C(null);
156
+ const l = new URLSearchParams(window.location.search).get("v") ?? void 0, p = await G(l);
157
+ o(p);
158
+ } catch (l) {
159
+ C(l instanceof Error ? l.message : "Failed to load resume");
160
+ } finally {
161
+ k(!1);
162
+ }
163
+ }
164
+ j(() => {
165
+ const l = () => w(null);
166
+ return document.addEventListener("click", l), () => document.removeEventListener("click", l);
167
+ }, []);
168
+ const P = (l) => {
169
+ l.preventDefault();
170
+ const _ = window.getSelection()?.toString().trim();
171
+ _ && _.length > 0 && w({
172
+ x: l.clientX,
173
+ y: l.clientY,
174
+ text: _
175
+ });
176
+ }, T = () => {
177
+ g && (e(g.text), w(null));
178
+ }, x = (l, p, _) => {
179
+ const S = URL.createObjectURL(new Blob([l], { type: p })), u = document.createElement("a");
180
+ u.href = S, u.download = _, u.rel = "noopener", u.style.display = "none", document.body.appendChild(u), u.click(), setTimeout(() => {
181
+ u.remove(), URL.revokeObjectURL(S);
182
+ }, 0);
183
+ }, d = s?.content ?? "", h = s?.coverLetter ?? null, v = h !== null && h.length > 0, D = c === "cover" && v ? h : d, E = s?.variant ?? new URLSearchParams(window.location.search).get("v"), L = ae(t), F = v ? `${d}
184
+
185
+ ---
186
+
187
+ # Cover Letter
188
+
189
+ ${h}` : d, B = () => x(v ? F : d, "text/markdown", `${L}.md`), O = () => x(
190
+ JSON.stringify(
191
+ {
192
+ content: d,
193
+ cover_letter: h ?? void 0,
194
+ variant: E ?? void 0,
195
+ label: s?.label ?? void 0,
196
+ company: s?.company ?? void 0,
197
+ job_title: s?.jobTitle ?? void 0,
198
+ format: "markdown"
199
+ },
200
+ null,
201
+ 2
202
+ ),
203
+ "application/json",
204
+ `${L}.json`
205
+ ), V = I(E ?? void 0);
206
+ return m ? /* @__PURE__ */ n("div", { className: "resume-viewer resume-viewer--loading", children: /* @__PURE__ */ n("p", { children: "Loading resume..." }) }) : y ? /* @__PURE__ */ i("div", { className: "resume-viewer resume-viewer--error", children: [
207
+ /* @__PURE__ */ i("p", { className: "resume-viewer__error-message", children: [
208
+ "Error: ",
209
+ y
210
+ ] }),
211
+ /* @__PURE__ */ n(
212
+ "button",
213
+ {
214
+ onClick: () => {
215
+ R().catch(
216
+ (l) => A.error("[ResumeViewer] Error loading resume", {
217
+ error: l?.message ?? String(l)
218
+ })
219
+ );
220
+ },
221
+ className: "resume-viewer__retry-button",
222
+ children: "Retry"
223
+ }
224
+ )
225
+ ] }) : /* @__PURE__ */ i(N, { children: [
226
+ /* @__PURE__ */ i("div", { className: "resume-viewer", onContextMenu: P, children: [
227
+ /* @__PURE__ */ i("div", { className: "resume-viewer__toolbar", children: [
228
+ v && /* @__PURE__ */ i("div", { className: "resume-viewer__toggle", role: "tablist", "aria-label": "Packet document", children: [
229
+ /* @__PURE__ */ n(
230
+ "button",
231
+ {
232
+ type: "button",
233
+ role: "tab",
234
+ "aria-selected": c === "resume",
235
+ className: c === "resume" ? "resume-viewer__toggle-btn resume-viewer__toggle-btn--active" : "resume-viewer__toggle-btn",
236
+ onClick: () => a("resume"),
237
+ children: "Résumé"
238
+ }
239
+ ),
240
+ /* @__PURE__ */ n(
241
+ "button",
242
+ {
243
+ type: "button",
244
+ role: "tab",
245
+ "aria-selected": c === "cover",
246
+ className: c === "cover" ? "resume-viewer__toggle-btn resume-viewer__toggle-btn--active" : "resume-viewer__toggle-btn",
247
+ onClick: () => a("cover"),
248
+ children: "Cover letter"
249
+ }
250
+ )
251
+ ] }),
252
+ /* @__PURE__ */ i("div", { className: "resume-viewer__downloads", children: [
253
+ /* @__PURE__ */ n("span", { className: "resume-viewer__downloads-label", children: "Download" }),
254
+ /* @__PURE__ */ n("a", { className: "resume-viewer__download-button", href: V, download: !0, children: ".pdf" }),
255
+ /* @__PURE__ */ n("button", { className: "resume-viewer__download-button", onClick: B, children: ".md" }),
256
+ /* @__PURE__ */ n("button", { className: "resume-viewer__download-button", onClick: O, children: ".json" })
257
+ ] }),
258
+ /* @__PURE__ */ n("a", { className: "resume-viewer__schedule-button", href: r, children: "Schedule a Meeting" })
259
+ ] }),
260
+ /* @__PURE__ */ i("div", { className: "resume-viewer__scroll", children: [
261
+ /* @__PURE__ */ n("div", { className: "resume-viewer__screen", children: /* @__PURE__ */ n(
262
+ b,
263
+ {
264
+ content: D,
265
+ variant: c === "cover" && v ? "letter" : "resume"
266
+ }
267
+ ) }),
268
+ /* @__PURE__ */ i("div", { className: "resume-viewer__print", children: [
269
+ /* @__PURE__ */ n(b, { content: d }),
270
+ v && /* @__PURE__ */ i(N, { children: [
271
+ /* @__PURE__ */ n("div", { className: "resume-viewer__pagebreak" }),
272
+ /* @__PURE__ */ n(b, { content: h, variant: "letter" })
273
+ ] })
274
+ ] })
275
+ ] })
276
+ ] }),
277
+ g && /* @__PURE__ */ n(
278
+ "div",
279
+ {
280
+ className: "context-menu",
281
+ style: {
282
+ position: "fixed",
283
+ top: g.y,
284
+ left: g.x,
285
+ zIndex: 1e3
286
+ },
287
+ children: /* @__PURE__ */ n("button", { className: "context-menu__item", onClick: T, children: "Ask about this" })
288
+ }
289
+ )
290
+ ] });
291
+ }
292
+ export {
293
+ ge as default,
294
+ ae as resumeFilenameBase
295
+ };
296
+ //# sourceMappingURL=ResumeViewer-C-uxZ-7E.js.map