@rubytech/create-maxy 1.0.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 (181) hide show
  1. package/dist/index.js +428 -0
  2. package/package.json +31 -0
  3. package/payload/maxy/.env.example +12 -0
  4. package/payload/maxy/app/admin/components/ActivityTimeline.tsx +348 -0
  5. package/payload/maxy/app/admin/components/MarkdownMessage.tsx +40 -0
  6. package/payload/maxy/app/api/admin/chat/route.ts +72 -0
  7. package/payload/maxy/app/api/admin/logs/route.ts +40 -0
  8. package/payload/maxy/app/api/admin/session/route.ts +74 -0
  9. package/payload/maxy/app/api/chat/route.ts +72 -0
  10. package/payload/maxy/app/api/health/route.ts +26 -0
  11. package/payload/maxy/app/api/onboarding/claude-auth/route.ts +216 -0
  12. package/payload/maxy/app/api/onboarding/set-pin/route.ts +44 -0
  13. package/payload/maxy/app/api/session/route.ts +51 -0
  14. package/payload/maxy/app/api/telegram/webhook/route.ts +107 -0
  15. package/payload/maxy/app/apple-icon.png +0 -0
  16. package/payload/maxy/app/bot/page.tsx +373 -0
  17. package/payload/maxy/app/favicon.ico +0 -0
  18. package/payload/maxy/app/globals.css +1681 -0
  19. package/payload/maxy/app/layout.tsx +58 -0
  20. package/payload/maxy/app/lib/claude-agent.ts +503 -0
  21. package/payload/maxy/app/og/layout.tsx +15 -0
  22. package/payload/maxy/app/og/page.tsx +252 -0
  23. package/payload/maxy/app/page.tsx +594 -0
  24. package/payload/maxy/app/privacy/page.tsx +72 -0
  25. package/payload/maxy/app/public/page.tsx +266 -0
  26. package/payload/maxy/next.config.mjs +26 -0
  27. package/payload/maxy/package-lock.json +2198 -0
  28. package/payload/maxy/package.json +25 -0
  29. package/payload/maxy/proxy.ts +41 -0
  30. package/payload/maxy/public/brand/claude.png +0 -0
  31. package/payload/maxy/public/brand/maxy-black.png +0 -0
  32. package/payload/maxy/public/brand/maxy.png +0 -0
  33. package/payload/maxy/public/favicon.ico +0 -0
  34. package/payload/maxy/public/og-landscape.png +0 -0
  35. package/payload/maxy/public/og-portrait.png +0 -0
  36. package/payload/maxy/public/og-square.png +0 -0
  37. package/payload/maxy/public/pi-5.jpg +0 -0
  38. package/payload/maxy/public/robots.txt +5 -0
  39. package/payload/maxy/tsconfig.json +41 -0
  40. package/payload/maxy/tsconfig.tsbuildinfo +1 -0
  41. package/payload/maxy/ui.md +28 -0
  42. package/payload/platform/config/cloudflared.yml +17 -0
  43. package/payload/platform/knowledge/maxy.md +161 -0
  44. package/payload/platform/neo4j/schema.cypher +108 -0
  45. package/payload/platform/package-lock.json +1835 -0
  46. package/payload/platform/package.json +17 -0
  47. package/payload/platform/plugins/admin/PLUGIN.md +24 -0
  48. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +56 -0
  49. package/payload/platform/plugins/admin/hooks/session-start.sh +20 -0
  50. package/payload/platform/plugins/admin/mcp/dist/index.d.ts +2 -0
  51. package/payload/platform/plugins/admin/mcp/dist/index.d.ts.map +1 -0
  52. package/payload/platform/plugins/admin/mcp/dist/index.js +149 -0
  53. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -0
  54. package/payload/platform/plugins/admin/mcp/package.json +18 -0
  55. package/payload/platform/plugins/anthropic/PLUGIN.md +30 -0
  56. package/payload/platform/plugins/anthropic/references/setup-guide.md +146 -0
  57. package/payload/platform/plugins/business-assistant/PLUGIN.md +46 -0
  58. package/payload/platform/plugins/business-assistant/references/crm.md +112 -0
  59. package/payload/platform/plugins/business-assistant/references/document-management.md +96 -0
  60. package/payload/platform/plugins/business-assistant/references/escalation.md +126 -0
  61. package/payload/platform/plugins/business-assistant/references/invoicing.md +163 -0
  62. package/payload/platform/plugins/business-assistant/references/quoting.md +56 -0
  63. package/payload/platform/plugins/business-assistant/references/scheduling.md +127 -0
  64. package/payload/platform/plugins/cloudflare/PLUGIN.md +31 -0
  65. package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts +2 -0
  66. package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts.map +1 -0
  67. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +174 -0
  68. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -0
  69. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +45 -0
  70. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -0
  71. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +256 -0
  72. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -0
  73. package/payload/platform/plugins/cloudflare/mcp/package.json +18 -0
  74. package/payload/platform/plugins/cloudflare/references/setup-guide.md +110 -0
  75. package/payload/platform/plugins/contacts/PLUGIN.md +18 -0
  76. package/payload/platform/plugins/contacts/mcp/dist/index.d.ts +2 -0
  77. package/payload/platform/plugins/contacts/mcp/dist/index.d.ts.map +1 -0
  78. package/payload/platform/plugins/contacts/mcp/dist/index.js +182 -0
  79. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -0
  80. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts +5 -0
  81. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts.map +1 -0
  82. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js +34 -0
  83. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js.map +1 -0
  84. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +19 -0
  85. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -0
  86. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +68 -0
  87. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -0
  88. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts +22 -0
  89. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts.map +1 -0
  90. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js +46 -0
  91. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js.map +1 -0
  92. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +20 -0
  93. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -0
  94. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +56 -0
  95. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -0
  96. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts +13 -0
  97. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts.map +1 -0
  98. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js +54 -0
  99. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js.map +1 -0
  100. package/payload/platform/plugins/contacts/mcp/package.json +19 -0
  101. package/payload/platform/plugins/documents/PLUGIN.md +12 -0
  102. package/payload/platform/plugins/documents/mcp/dist/index.d.ts +2 -0
  103. package/payload/platform/plugins/documents/mcp/dist/index.d.ts.map +1 -0
  104. package/payload/platform/plugins/documents/mcp/dist/index.js +82 -0
  105. package/payload/platform/plugins/documents/mcp/dist/index.js.map +1 -0
  106. package/payload/platform/plugins/documents/mcp/package.json +20 -0
  107. package/payload/platform/plugins/memory/PLUGIN.md +17 -0
  108. package/payload/platform/plugins/memory/mcp/dist/index.d.ts +2 -0
  109. package/payload/platform/plugins/memory/mcp/dist/index.d.ts.map +1 -0
  110. package/payload/platform/plugins/memory/mcp/dist/index.js +164 -0
  111. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -0
  112. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts +3 -0
  113. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts.map +1 -0
  114. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js +29 -0
  115. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js.map +1 -0
  116. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +5 -0
  117. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -0
  118. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +34 -0
  119. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -0
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +8 -0
  121. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -0
  122. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +71 -0
  123. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -0
  124. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +24 -0
  125. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -0
  126. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +125 -0
  127. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -0
  128. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +18 -0
  129. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -0
  130. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +56 -0
  131. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -0
  132. package/payload/platform/plugins/memory/mcp/package.json +19 -0
  133. package/payload/platform/plugins/sales/PLUGIN.md +65 -0
  134. package/payload/platform/plugins/sales/references/close-tracking.md +76 -0
  135. package/payload/platform/plugins/sales/references/closing-framework.md +108 -0
  136. package/payload/platform/plugins/sales/references/comparisons.md +99 -0
  137. package/payload/platform/plugins/sales/references/competitive-positioning.md +51 -0
  138. package/payload/platform/plugins/sales/references/faq.md +62 -0
  139. package/payload/platform/plugins/sales/references/objection-handling.md +157 -0
  140. package/payload/platform/plugins/sales/references/pricing.md +71 -0
  141. package/payload/platform/plugins/sales/references/waitlist.md +23 -0
  142. package/payload/platform/plugins/scheduling/PLUGIN.md +12 -0
  143. package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts +2 -0
  144. package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts.map +1 -0
  145. package/payload/platform/plugins/scheduling/mcp/dist/index.js +13 -0
  146. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -0
  147. package/payload/platform/plugins/scheduling/mcp/package.json +18 -0
  148. package/payload/platform/plugins/telegram/PLUGIN.md +31 -0
  149. package/payload/platform/plugins/telegram/mcp/dist/index.d.ts +2 -0
  150. package/payload/platform/plugins/telegram/mcp/dist/index.d.ts.map +1 -0
  151. package/payload/platform/plugins/telegram/mcp/dist/index.js +101 -0
  152. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -0
  153. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +27 -0
  154. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -0
  155. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +41 -0
  156. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -0
  157. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts +16 -0
  158. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts.map +1 -0
  159. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js +62 -0
  160. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js.map +1 -0
  161. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts +20 -0
  162. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -0
  163. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +34 -0
  164. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -0
  165. package/payload/platform/plugins/telegram/mcp/package.json +19 -0
  166. package/payload/platform/plugins/telegram/references/setup-guide.md +50 -0
  167. package/payload/platform/plugins/web/PLUGIN.md +12 -0
  168. package/payload/platform/plugins/web/mcp/dist/index.d.ts +2 -0
  169. package/payload/platform/plugins/web/mcp/dist/index.d.ts.map +1 -0
  170. package/payload/platform/plugins/web/mcp/dist/index.js +12 -0
  171. package/payload/platform/plugins/web/mcp/dist/index.js.map +1 -0
  172. package/payload/platform/plugins/web/mcp/package.json +18 -0
  173. package/payload/platform/scripts/seed-neo4j.sh +73 -0
  174. package/payload/platform/scripts/setup.sh +177 -0
  175. package/payload/platform/scripts/start.sh +62 -0
  176. package/payload/platform/templates/account.json +4 -0
  177. package/payload/platform/templates/agents/admin/IDENTITY.md +28 -0
  178. package/payload/platform/templates/agents/admin/SOUL.md +1 -0
  179. package/payload/platform/templates/agents/public/IDENTITY.md +21 -0
  180. package/payload/platform/templates/agents/public/SOUL.md +1 -0
  181. package/payload/platform/tsconfig.base.json +18 -0
@@ -0,0 +1,594 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, FormEvent } from 'react'
4
+ import { Eye, EyeOff, BotOff } from 'lucide-react'
5
+ import { ActivityTimeline, type AdminEvent } from './admin/components/ActivityTimeline'
6
+
7
+ type AppState = 'loading' | 'set-pin' | 'enter-pin' | 'connect-claude' | 'chat'
8
+
9
+ interface Message {
10
+ role: 'admin' | 'maxy'
11
+ content?: string
12
+ events?: AdminEvent[]
13
+ }
14
+
15
+ export default function AdminPage() {
16
+ const [appState, setAppState] = useState<AppState>('loading')
17
+ const [pin, setPin] = useState('')
18
+ const [confirmPin, setConfirmPin] = useState('')
19
+ const [pinError, setPinError] = useState('')
20
+ const [showPin, setShowPin] = useState(false)
21
+ const [authPolling, setAuthPolling] = useState(false)
22
+ const [authLoading, setAuthLoading] = useState(false)
23
+ const [sessionKey, setSessionKey] = useState<string | null>(null)
24
+ const [messages, setMessages] = useState<Message[]>([])
25
+ const [input, setInput] = useState('')
26
+ const [isStreaming, setIsStreaming] = useState(false)
27
+ const [elapsedSeconds, setElapsedSeconds] = useState(0)
28
+ const elapsedRef = useRef<ReturnType<typeof setInterval> | null>(null)
29
+ const [sessionElapsed, setSessionElapsed] = useState(0)
30
+ const sessionStartRef = useRef<number | null>(null)
31
+ const sessionIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
32
+ const messagesEndRef = useRef<HTMLDivElement>(null)
33
+ const inputRef = useRef<HTMLInputElement>(null)
34
+ const pinInputRef = useRef<HTMLInputElement>(null)
35
+
36
+ function startElapsedTimer() {
37
+ if (!sessionStartRef.current) {
38
+ sessionStartRef.current = Date.now()
39
+ sessionIntervalRef.current = setInterval(() => {
40
+ setSessionElapsed(Math.floor((Date.now() - sessionStartRef.current!) / 1000))
41
+ }, 1000)
42
+ }
43
+ setElapsedSeconds(0)
44
+ elapsedRef.current = setInterval(() => setElapsedSeconds(s => s + 1), 1000)
45
+ }
46
+
47
+ function stopElapsedTimer() {
48
+ if (elapsedRef.current) {
49
+ clearInterval(elapsedRef.current)
50
+ elapsedRef.current = null
51
+ }
52
+ }
53
+
54
+ function formatSessionTime(s: number): string {
55
+ if (s < 60) return `${s}s`
56
+ const m = Math.floor(s / 60)
57
+ const rem = s % 60
58
+ return rem > 0 ? `${m}m ${rem}s` : `${m}m`
59
+ }
60
+
61
+ function fmtTokens(n: number): string {
62
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
63
+ }
64
+
65
+ useEffect(() => {
66
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
67
+ }, [messages])
68
+
69
+ // Check platform state on mount
70
+ useEffect(() => {
71
+ async function checkHealth() {
72
+ try {
73
+ const res = await fetch('/api/health')
74
+ if (!res.ok) {
75
+ setAppState('set-pin')
76
+ return
77
+ }
78
+ const health = await res.json()
79
+ if (!health.pin_configured) {
80
+ setAppState('set-pin')
81
+ } else if (!health.claude_authenticated) {
82
+ setAppState('connect-claude')
83
+ } else {
84
+ setAppState('enter-pin')
85
+ }
86
+ } catch {
87
+ setAppState('set-pin')
88
+ }
89
+ }
90
+ checkHealth()
91
+ }, [])
92
+
93
+ useEffect(() => {
94
+ if (appState === 'set-pin' || appState === 'enter-pin') {
95
+ setTimeout(() => pinInputRef.current?.focus(), 100)
96
+ }
97
+ if (appState === 'chat' && messages.length === 0 && sessionKey) {
98
+ const greetingTimer = setTimeout(() => sendSystemPrompt('[New session. Assess the current state and introduce yourself. Be proactive.]'), 300)
99
+ return () => clearTimeout(greetingTimer)
100
+ } else if (appState === 'chat') {
101
+ setTimeout(() => inputRef.current?.focus(), 100)
102
+ }
103
+ }, [appState])
104
+
105
+ async function handleSetPin(e: FormEvent) {
106
+ e.preventDefault()
107
+ setPinError('')
108
+
109
+ if (pin.length < 4) {
110
+ setPinError('PIN must be at least 4 characters.')
111
+ return
112
+ }
113
+ if (pin !== confirmPin) {
114
+ setPinError('PINs do not match.')
115
+ return
116
+ }
117
+
118
+ try {
119
+ const res = await fetch('/api/onboarding/set-pin', {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({ pin }),
123
+ })
124
+
125
+ if (!res.ok) {
126
+ const data = await res.json().catch(() => ({}))
127
+ setPinError(data.error || 'Failed to set PIN.')
128
+ return
129
+ }
130
+
131
+ // PIN set — move to Claude auth
132
+ setPin('')
133
+ setConfirmPin('')
134
+ setAppState('connect-claude')
135
+ } catch {
136
+ setPinError('Could not connect.')
137
+ }
138
+ }
139
+
140
+ async function handleLogin(e: FormEvent) {
141
+ e.preventDefault()
142
+ setPinError('')
143
+ await doLogin(pin)
144
+ }
145
+
146
+ async function doLogin(pinValue: string) {
147
+ try {
148
+ const res = await fetch('/api/admin/session', {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({ pin: pinValue }),
152
+ })
153
+
154
+ if (!res.ok) {
155
+ const data = await res.json().catch(() => ({}))
156
+ setPinError(data.error || 'Invalid PIN')
157
+ return
158
+ }
159
+
160
+ const data = await res.json()
161
+ setSessionKey(data.session_key)
162
+ setPin('')
163
+ setConfirmPin('')
164
+ setAppState('chat')
165
+ } catch {
166
+ setPinError('Could not connect.')
167
+ }
168
+ }
169
+
170
+ // Agent-initiated message — no user message shown in chat
171
+ async function sendSystemPrompt(directive: string) {
172
+ if (isStreaming || !sessionKey) return
173
+ startElapsedTimer()
174
+ setIsStreaming(true)
175
+
176
+ const maxyMessage: Message = { role: 'maxy', events: [] }
177
+ setMessages([maxyMessage])
178
+
179
+ try {
180
+ const res = await fetch('/api/admin/chat', {
181
+ method: 'POST',
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify({ message: directive, session_key: sessionKey }),
184
+ })
185
+
186
+ if (!res.ok) throw new Error('Chat request failed')
187
+
188
+ const reader = res.body?.getReader()
189
+ if (!reader) throw new Error('No response stream')
190
+
191
+ const decoder = new TextDecoder()
192
+ let buffer = ''
193
+
194
+ while (true) {
195
+ const { done, value } = await reader.read()
196
+ if (done) break
197
+
198
+ buffer += decoder.decode(value, { stream: true })
199
+ const parts = buffer.split('\n')
200
+ buffer = parts.pop()!
201
+
202
+ for (const line of parts) {
203
+ if (!line.startsWith('data: ')) continue
204
+ const payload = line.slice(6)
205
+ if (payload === '[DONE]') continue
206
+
207
+ let parsed: { type: string; [key: string]: unknown }
208
+ try { parsed = JSON.parse(payload) } catch { continue }
209
+ if (parsed.type === 'done' || parsed.type === 'session_init') continue
210
+
211
+ const event = parsed as AdminEvent
212
+ setMessages(prev => {
213
+ const updated = [...prev]
214
+ if (updated[0]?.role === 'maxy') {
215
+ updated[0] = { ...updated[0], events: [...(updated[0].events ?? []), event] }
216
+ }
217
+ return updated
218
+ })
219
+ }
220
+ }
221
+ } catch (err) {
222
+ const errorMessage = err instanceof Error ? err.message : 'Something went wrong'
223
+ setMessages([{ role: 'maxy', events: [{ type: 'text', content: `Error: ${errorMessage}` }] }])
224
+ } finally {
225
+ stopElapsedTimer()
226
+ setIsStreaming(false)
227
+ inputRef.current?.focus()
228
+ }
229
+ }
230
+
231
+ async function sendMessage(text: string) {
232
+ if (!text || isStreaming || !sessionKey) return
233
+
234
+ const adminMessage: Message = { role: 'admin', content: text }
235
+ const currentLength = messages.length
236
+ setMessages(prev => [...prev, adminMessage])
237
+ setInput('')
238
+ startElapsedTimer()
239
+ setIsStreaming(true)
240
+
241
+ const maxyMessage: Message = { role: 'maxy', events: [] }
242
+ setMessages(prev => [...prev, maxyMessage])
243
+ const messageIndex = currentLength + 1
244
+
245
+ try {
246
+ const res = await fetch('/api/admin/chat', {
247
+ method: 'POST',
248
+ headers: { 'Content-Type': 'application/json' },
249
+ body: JSON.stringify({ message: text, session_key: sessionKey }),
250
+ })
251
+
252
+ if (!res.ok) throw new Error('Chat request failed')
253
+
254
+ const reader = res.body?.getReader()
255
+ if (!reader) throw new Error('No response stream')
256
+
257
+ const decoder = new TextDecoder()
258
+ let buffer = ''
259
+
260
+ while (true) {
261
+ const { done, value } = await reader.read()
262
+ if (done) break
263
+
264
+ buffer += decoder.decode(value, { stream: true })
265
+ const parts = buffer.split('\n')
266
+ buffer = parts.pop()!
267
+
268
+ for (const line of parts) {
269
+ if (!line.startsWith('data: ')) continue
270
+ const payload = line.slice(6)
271
+ if (payload === '[DONE]') continue
272
+
273
+ let parsed: { type: string; [key: string]: unknown }
274
+ try { parsed = JSON.parse(payload) } catch { continue }
275
+ if (parsed.type === 'done' || parsed.type === 'session_init') continue
276
+
277
+ const event = parsed as AdminEvent
278
+ setMessages(prev => {
279
+ const updated = [...prev]
280
+ const msg = updated[messageIndex]
281
+ if (msg && msg.role === 'maxy') {
282
+ updated[messageIndex] = {
283
+ ...msg,
284
+ events: [...(msg.events ?? []), event],
285
+ }
286
+ }
287
+ return updated
288
+ })
289
+ }
290
+ }
291
+ } catch (err) {
292
+ const errorMessage = err instanceof Error ? err.message : 'Something went wrong'
293
+ setMessages(prev => {
294
+ const updated = [...prev]
295
+ updated[messageIndex] = {
296
+ role: 'maxy',
297
+ events: [{ type: 'text', content: `Error: ${errorMessage}` }],
298
+ }
299
+ return updated
300
+ })
301
+ } finally {
302
+ stopElapsedTimer()
303
+ setIsStreaming(false)
304
+ inputRef.current?.focus()
305
+ }
306
+ }
307
+
308
+ // --- Loading ---
309
+ if (appState === 'loading') {
310
+ return (
311
+ <div className="chat-page admin-page">
312
+ <header className="chat-header">
313
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
314
+ </header>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ // --- Set PIN (first boot) ---
320
+ if (appState === 'set-pin') {
321
+ return (
322
+ <div className="chat-page admin-page">
323
+ <header className="chat-header">
324
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
325
+ <h1 className="chat-tagline">Welcome to Maxy</h1>
326
+ <p className="chat-intro">Choose a PIN to secure your admin access.</p>
327
+ </header>
328
+ <div className="admin-pin-form">
329
+ <form onSubmit={handleSetPin}>
330
+ <div className="pin-input-row">
331
+ <input
332
+ ref={pinInputRef}
333
+ type={showPin ? 'text' : 'password'}
334
+ value={pin}
335
+ onChange={e => setPin(e.target.value)}
336
+ placeholder="Choose a PIN"
337
+ className="chat-input"
338
+ autoFocus
339
+ />
340
+ <button type="button" className="pin-toggle" onClick={() => setShowPin(!showPin)} aria-label={showPin ? 'Hide' : 'Show'}>
341
+ {showPin ? <EyeOff size={18} /> : <Eye size={18} />}
342
+ </button>
343
+ </div>
344
+ <div className="pin-input-row">
345
+ <input
346
+ type={showPin ? 'text' : 'password'}
347
+ value={confirmPin}
348
+ onChange={e => setConfirmPin(e.target.value)}
349
+ placeholder="Confirm PIN"
350
+ className="chat-input"
351
+ />
352
+ <button type="submit" className="chat-send" disabled={!pin || !confirmPin} aria-label="Set PIN">
353
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
354
+ <line x1="5" y1="12" x2="19" y2="12" />
355
+ <polyline points="12 5 19 12 12 19" />
356
+ </svg>
357
+ </button>
358
+ </div>
359
+ </form>
360
+ {pinError && <p className="admin-pin-error">{pinError}</p>}
361
+ </div>
362
+ </div>
363
+ )
364
+ }
365
+
366
+ // --- Connect Claude ---
367
+ if (appState === 'connect-claude') {
368
+ async function startAuth() {
369
+ setAuthLoading(true)
370
+ setPinError('')
371
+ try {
372
+ const res = await fetch('/api/onboarding/claude-auth', { method: 'POST' })
373
+ const data = await res.json()
374
+ if (data.started) {
375
+ setAuthPolling(true)
376
+ setAuthLoading(false)
377
+ for (let i = 0; i < 60; i++) {
378
+ await new Promise(r => setTimeout(r, 2000))
379
+ const pollRes = await fetch('/api/onboarding/claude-auth', {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json' },
382
+ body: JSON.stringify({ action: 'wait' }),
383
+ })
384
+ const pollData = await pollRes.json()
385
+ if (pollData.authenticated) {
386
+ await fetch('/api/onboarding/claude-auth', {
387
+ method: 'POST',
388
+ headers: { 'Content-Type': 'application/json' },
389
+ body: JSON.stringify({ action: 'stop' }),
390
+ })
391
+ setAppState('enter-pin')
392
+ return
393
+ }
394
+ }
395
+ setPinError('Timed out waiting for sign-in. Try again.')
396
+ setAuthPolling(false)
397
+ } else if (data.error) {
398
+ setPinError(data.error)
399
+ }
400
+ } catch {
401
+ setPinError('Could not start auth flow.')
402
+ }
403
+ setAuthLoading(false)
404
+ }
405
+
406
+ if (authPolling) {
407
+ async function cancelAuth() {
408
+ await fetch('/api/onboarding/claude-auth', {
409
+ method: 'POST',
410
+ headers: { 'Content-Type': 'application/json' },
411
+ body: JSON.stringify({ action: 'stop' }),
412
+ })
413
+ setAuthPolling(false)
414
+ setPinError('')
415
+ }
416
+
417
+ return (
418
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100dvh', overflow: 'auto' }}>
419
+ <header className="chat-header" style={{ paddingBottom: '12px', flexShrink: 0, position: 'relative', maxWidth: '680px', width: '100%', margin: '0 auto', padding: '24px 20px 12px' }}>
420
+ <button
421
+ onClick={cancelAuth}
422
+ style={{ position: 'absolute', top: '12px', right: '12px', background: 'none', border: 'none', color: '#999', fontSize: '13px', cursor: 'pointer', padding: '4px 8px' }}
423
+ aria-label="Cancel"
424
+ >
425
+
426
+ </button>
427
+ <img src="/brand/claude.png" alt="Claude" className="chat-logo" />
428
+ <h1 className="chat-tagline">Connect Claude</h1>
429
+ <p className="chat-intro">Sign in and authorize in the browser below.</p>
430
+ </header>
431
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, gap: '10px', padding: '0 0 16px' }}>
432
+ <iframe
433
+ src={`/vnc-viewer.html?host=${window.location.hostname}&port=6080`}
434
+ style={{ flex: 1, width: '100%', minHeight: 0, border: 'none', background: '#111', display: 'block' }}
435
+ title="Claude Sign-in"
436
+ />
437
+ </div>
438
+ </div>
439
+ )
440
+ }
441
+
442
+ return (
443
+ <div className="chat-page admin-page">
444
+ <header className="chat-header">
445
+ <img src="/brand/claude.png" alt="Claude" className="chat-logo" />
446
+ <h1 className="chat-tagline">Connect Claude</h1>
447
+ <p className="chat-intro">to power Maxy</p>
448
+ </header>
449
+ <div className="admin-pin-form">
450
+ <button
451
+ className="btn-primary"
452
+ onClick={startAuth}
453
+ disabled={authLoading}
454
+ >
455
+ {authLoading ? 'Starting...' : 'Sign in'}
456
+ </button>
457
+ {pinError && <p className="admin-pin-error">{pinError}</p>}
458
+ </div>
459
+ </div>
460
+ )
461
+ }
462
+
463
+ // --- Enter PIN (returning user) ---
464
+ if (appState === 'enter-pin') {
465
+ return (
466
+ <div className="chat-page admin-page">
467
+ <header className="chat-header">
468
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
469
+ <h1 className="chat-tagline">Maxy</h1>
470
+ <p className="chat-intro">AI for Productive People.</p>
471
+ </header>
472
+ <div className="admin-pin-form">
473
+ <form onSubmit={handleLogin}>
474
+ <div className="pin-input-row">
475
+ <input
476
+ ref={pinInputRef}
477
+ type={showPin ? 'text' : 'password'}
478
+ value={pin}
479
+ onChange={e => setPin(e.target.value)}
480
+ placeholder="Enter PIN"
481
+ className="chat-input"
482
+ autoFocus
483
+ />
484
+ <button type="button" className="pin-toggle" onClick={() => setShowPin(!showPin)} aria-label={showPin ? 'Hide' : 'Show'}>
485
+ {showPin ? <EyeOff size={18} /> : <Eye size={18} />}
486
+ </button>
487
+ <button type="submit" className="chat-send" disabled={!pin}>
488
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
489
+ <line x1="5" y1="12" x2="19" y2="12" />
490
+ <polyline points="12 5 19 12 12 19" />
491
+ </svg>
492
+ </button>
493
+ </div>
494
+ </form>
495
+ {pinError && <p className="admin-pin-error">{pinError}</p>}
496
+ </div>
497
+ </div>
498
+ )
499
+ }
500
+
501
+ // --- Chat ---
502
+ return (
503
+ <div className="chat-page admin-page">
504
+ <header className="chat-header">
505
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
506
+ <h1 className="chat-tagline">Maxy</h1>
507
+ <p className="chat-intro">AI for Productive People.</p>
508
+ </header>
509
+
510
+ <div className="chat-messages">
511
+ {messages.map((msg, i) => (
512
+ <div key={i} className={`message ${msg.role === 'admin' ? 'visitor' : 'maxy'}`}>
513
+ {msg.role === 'admin' ? (
514
+ <div className="bubble">{msg.content}</div>
515
+ ) : (
516
+ <ActivityTimeline
517
+ events={msg.events ?? []}
518
+ isStreaming={isStreaming && i === messages.length - 1}
519
+ elapsedSeconds={i === messages.length - 1 ? elapsedSeconds : 0}
520
+ />
521
+ )}
522
+ </div>
523
+ ))}
524
+ <div ref={messagesEndRef} />
525
+ </div>
526
+
527
+ <div className="chat-input-area">
528
+ {(() => {
529
+ const sessionTokens = messages.reduce((sum, msg) => {
530
+ const usage = msg.events?.find(e => e.type === 'usage') as { type: 'usage'; input_tokens: number; output_tokens: number } | undefined
531
+ return usage ? sum + usage.input_tokens + usage.output_tokens : sum
532
+ }, 0)
533
+ const latestInputTokens = [...messages].reverse().reduce<number>((found, msg) => {
534
+ if (found > 0) return found
535
+ const usage = msg.events?.find(e => e.type === 'usage') as { type: 'usage'; input_tokens: number; output_tokens: number } | undefined
536
+ return usage?.input_tokens ?? 0
537
+ }, 0)
538
+ const contextPct = latestInputTokens > 0 ? Math.round(latestInputTokens / 200000 * 100) : 0
539
+ return messages.some(m => m.role === 'maxy') ? (
540
+ <div className="session-stats">
541
+ <span className="session-stat">Effort <b>Standard</b></span>
542
+ <span className="session-stat">Style <b>Standard</b></span>
543
+ <span className="session-stat">Context <b>{contextPct > 0 ? `${contextPct}%` : '—'}</b></span>
544
+ <span className="session-stat">Tokens <b>{fmtTokens(sessionTokens)}</b></span>
545
+ <span className="session-stat">Session <b>{formatSessionTime(sessionElapsed)}</b></span>
546
+ </div>
547
+ ) : null
548
+ })()}
549
+ <form className="chat-form" onSubmit={(e) => { e.preventDefault(); sendMessage(input.trim()) }}>
550
+ <input
551
+ ref={inputRef}
552
+ className="chat-input"
553
+ type="text"
554
+ value={input}
555
+ onChange={e => setInput(e.target.value)}
556
+ placeholder=""
557
+ disabled={isStreaming}
558
+ aria-label="Type a message..."
559
+ />
560
+ <button
561
+ className="chat-send"
562
+ type="submit"
563
+ disabled={isStreaming || !input.trim()}
564
+ aria-label="Send message"
565
+ >
566
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
567
+ <line x1="5" y1="12" x2="19" y2="12" />
568
+ <polyline points="12 5 19 12 12 19" />
569
+ </svg>
570
+ </button>
571
+ </form>
572
+ <div className="chat-actions">
573
+ <button
574
+ className="chat-action"
575
+ onClick={async () => {
576
+ await fetch('/api/onboarding/claude-auth', {
577
+ method: 'POST',
578
+ headers: { 'Content-Type': 'application/json' },
579
+ body: JSON.stringify({ action: 'logout' }),
580
+ })
581
+ setSessionKey(null)
582
+ setMessages([])
583
+ setAppState('connect-claude')
584
+ }}
585
+ >
586
+ <BotOff size={14} />
587
+ <span className="action-label">Disconnect Claude</span>
588
+ </button>
589
+ </div>
590
+ </div>
591
+
592
+ </div>
593
+ )
594
+ }
@@ -0,0 +1,72 @@
1
+ import type { Metadata } from 'next'
2
+
3
+ export const metadata: Metadata = {
4
+ title: 'Privacy — Maxy',
5
+ description: 'How Maxy handles your data.',
6
+ }
7
+
8
+ export default function PrivacyPage() {
9
+ return (
10
+ <div className="legal-page">
11
+ <h1>Privacy</h1>
12
+
13
+ <h2>The short version</h2>
14
+ <p>
15
+ Maxy runs on a device in your home. Your data stays on that device.
16
+ We don&rsquo;t collect it, store it, or have access to it.
17
+ </p>
18
+
19
+ <h2>maxy.chat conversations</h2>
20
+ <p>
21
+ When you chat with Maxy on this website, your conversation is processed
22
+ by Claude (Anthropic&rsquo;s AI) to generate responses. We do not store
23
+ conversation content beyond the current session. No personal data is
24
+ retained unless you voluntarily provide contact information.
25
+ </p>
26
+ <p>
27
+ We may collect anonymous, aggregated data about conversation topics
28
+ (not content) to improve the product. This data cannot be linked back
29
+ to any individual.
30
+ </p>
31
+
32
+ <h2>The Maxy device</h2>
33
+ <p>
34
+ Once you have your own Maxy device, all data &mdash; conversations, memory,
35
+ files, customer records &mdash; is stored locally on the device. We have no
36
+ access to this data. It never leaves your home network.
37
+ </p>
38
+ <p>
39
+ AI processing requires a connection to Anthropic&rsquo;s Claude service.
40
+ Your conversations with Claude are subject to{' '}
41
+ <a href="https://www.anthropic.com/privacy" target="_blank" rel="noopener noreferrer">
42
+ Anthropic&rsquo;s privacy policy
43
+ </a>.
44
+ </p>
45
+
46
+ <h2>What we collect</h2>
47
+ <p>
48
+ From the marketing sites (maxy.chat and maxy.bot), we may collect
49
+ standard web analytics: page visits, referral source, and device type.
50
+ No cookies are used for tracking. No personal data is sold or shared
51
+ with third parties.
52
+ </p>
53
+
54
+ <h2>Your rights</h2>
55
+ <p>
56
+ You can request deletion of any data associated with you by contacting
57
+ us. Since Maxy device data is stored locally, you have full control
58
+ over it at all times &mdash; including the ability to delete everything.
59
+ </p>
60
+
61
+ <h2>Contact</h2>
62
+ <p>
63
+ Questions about privacy? Message us on WhatsApp or email{' '}
64
+ <a href="mailto:privacy@maxy.bot">privacy@maxy.bot</a>.
65
+ </p>
66
+
67
+ <p style={{ marginTop: '48px', fontSize: '13px', color: '#9A9A9A' }}>
68
+ Last updated: February 2026
69
+ </p>
70
+ </div>
71
+ )
72
+ }