@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.
- package/dist/index.js +428 -0
- package/package.json +31 -0
- package/payload/maxy/.env.example +12 -0
- package/payload/maxy/app/admin/components/ActivityTimeline.tsx +348 -0
- package/payload/maxy/app/admin/components/MarkdownMessage.tsx +40 -0
- package/payload/maxy/app/api/admin/chat/route.ts +72 -0
- package/payload/maxy/app/api/admin/logs/route.ts +40 -0
- package/payload/maxy/app/api/admin/session/route.ts +74 -0
- package/payload/maxy/app/api/chat/route.ts +72 -0
- package/payload/maxy/app/api/health/route.ts +26 -0
- package/payload/maxy/app/api/onboarding/claude-auth/route.ts +216 -0
- package/payload/maxy/app/api/onboarding/set-pin/route.ts +44 -0
- package/payload/maxy/app/api/session/route.ts +51 -0
- package/payload/maxy/app/api/telegram/webhook/route.ts +107 -0
- package/payload/maxy/app/apple-icon.png +0 -0
- package/payload/maxy/app/bot/page.tsx +373 -0
- package/payload/maxy/app/favicon.ico +0 -0
- package/payload/maxy/app/globals.css +1681 -0
- package/payload/maxy/app/layout.tsx +58 -0
- package/payload/maxy/app/lib/claude-agent.ts +503 -0
- package/payload/maxy/app/og/layout.tsx +15 -0
- package/payload/maxy/app/og/page.tsx +252 -0
- package/payload/maxy/app/page.tsx +594 -0
- package/payload/maxy/app/privacy/page.tsx +72 -0
- package/payload/maxy/app/public/page.tsx +266 -0
- package/payload/maxy/next.config.mjs +26 -0
- package/payload/maxy/package-lock.json +2198 -0
- package/payload/maxy/package.json +25 -0
- package/payload/maxy/proxy.ts +41 -0
- package/payload/maxy/public/brand/claude.png +0 -0
- package/payload/maxy/public/brand/maxy-black.png +0 -0
- package/payload/maxy/public/brand/maxy.png +0 -0
- package/payload/maxy/public/favicon.ico +0 -0
- package/payload/maxy/public/og-landscape.png +0 -0
- package/payload/maxy/public/og-portrait.png +0 -0
- package/payload/maxy/public/og-square.png +0 -0
- package/payload/maxy/public/pi-5.jpg +0 -0
- package/payload/maxy/public/robots.txt +5 -0
- package/payload/maxy/tsconfig.json +41 -0
- package/payload/maxy/tsconfig.tsbuildinfo +1 -0
- package/payload/maxy/ui.md +28 -0
- package/payload/platform/config/cloudflared.yml +17 -0
- package/payload/platform/knowledge/maxy.md +161 -0
- package/payload/platform/neo4j/schema.cypher +108 -0
- package/payload/platform/package-lock.json +1835 -0
- package/payload/platform/package.json +17 -0
- package/payload/platform/plugins/admin/PLUGIN.md +24 -0
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +56 -0
- package/payload/platform/plugins/admin/hooks/session-start.sh +20 -0
- package/payload/platform/plugins/admin/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +149 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/package.json +18 -0
- package/payload/platform/plugins/anthropic/PLUGIN.md +30 -0
- package/payload/platform/plugins/anthropic/references/setup-guide.md +146 -0
- package/payload/platform/plugins/business-assistant/PLUGIN.md +46 -0
- package/payload/platform/plugins/business-assistant/references/crm.md +112 -0
- package/payload/platform/plugins/business-assistant/references/document-management.md +96 -0
- package/payload/platform/plugins/business-assistant/references/escalation.md +126 -0
- package/payload/platform/plugins/business-assistant/references/invoicing.md +163 -0
- package/payload/platform/plugins/business-assistant/references/quoting.md +56 -0
- package/payload/platform/plugins/business-assistant/references/scheduling.md +127 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +31 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +174 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +45 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +256 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +18 -0
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +110 -0
- package/payload/platform/plugins/contacts/PLUGIN.md +18 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +182 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js +34 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +19 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +68 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts +22 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js +46 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +20 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +56 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts +13 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js +54 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/package.json +19 -0
- package/payload/platform/plugins/documents/PLUGIN.md +12 -0
- package/payload/platform/plugins/documents/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/documents/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/documents/mcp/dist/index.js +82 -0
- package/payload/platform/plugins/documents/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/documents/mcp/package.json +20 -0
- package/payload/platform/plugins/memory/PLUGIN.md +17 -0
- package/payload/platform/plugins/memory/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +164 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js +29 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +34 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +71 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +24 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +125 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +56 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/package.json +19 -0
- package/payload/platform/plugins/sales/PLUGIN.md +65 -0
- package/payload/platform/plugins/sales/references/close-tracking.md +76 -0
- package/payload/platform/plugins/sales/references/closing-framework.md +108 -0
- package/payload/platform/plugins/sales/references/comparisons.md +99 -0
- package/payload/platform/plugins/sales/references/competitive-positioning.md +51 -0
- package/payload/platform/plugins/sales/references/faq.md +62 -0
- package/payload/platform/plugins/sales/references/objection-handling.md +157 -0
- package/payload/platform/plugins/sales/references/pricing.md +71 -0
- package/payload/platform/plugins/sales/references/waitlist.md +23 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +13 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/package.json +18 -0
- package/payload/platform/plugins/telegram/PLUGIN.md +31 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +101 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +27 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +41 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts +16 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js +62 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts +20 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +34 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/package.json +19 -0
- package/payload/platform/plugins/telegram/references/setup-guide.md +50 -0
- package/payload/platform/plugins/web/PLUGIN.md +12 -0
- package/payload/platform/plugins/web/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/web/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/web/mcp/dist/index.js +12 -0
- package/payload/platform/plugins/web/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/web/mcp/package.json +18 -0
- package/payload/platform/scripts/seed-neo4j.sh +73 -0
- package/payload/platform/scripts/setup.sh +177 -0
- package/payload/platform/scripts/start.sh +62 -0
- package/payload/platform/templates/account.json +4 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +28 -0
- package/payload/platform/templates/agents/admin/SOUL.md +1 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +21 -0
- package/payload/platform/templates/agents/public/SOUL.md +1 -0
- 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’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’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 — conversations, memory,
|
|
35
|
+
files, customer records — 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’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’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 — 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
|
+
}
|