@rubytech/create-maxy 0.3.4 → 0.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Install Maxy — your personal AI assistant",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -1,176 +1,132 @@
1
1
  'use client'
2
2
 
3
- import { useState, useRef, useEffect, useCallback, FormEvent } from 'react'
3
+ import { useState, useRef, useEffect, FormEvent } from 'react'
4
+ import { ActivityEvent, type AdminEvent } from './admin/components/ActivityEvent'
4
5
 
5
6
  interface Message {
6
- role: 'maxy' | 'visitor'
7
- content: string
7
+ role: 'admin' | 'maxy'
8
+ content?: string
9
+ events?: AdminEvent[]
8
10
  }
9
11
 
10
- const suggestions = [
11
- 'What is Maxy and how is it different from just using ChatGPT?',
12
- 'What do I need to buy and how much will it all cost me each month?',
13
- 'How do I actually set it up and talk to it — at home and when I\u2019m out?',
14
- 'Is my family\u2019s data private, and can you see what we\u2019re talking about?',
15
- 'What can Maxy actually do for me day-to-day?',
16
- ]
17
-
18
- /** Generate a random session ID (Taskmaster prepends its own anon- prefix). */
19
- function generateSessionId(): string {
20
- return crypto.randomUUID()
21
- }
22
-
23
- export default function ChatPage() {
12
+ export default function AdminPage() {
13
+ const [authenticated, setAuthenticated] = useState(false)
14
+ const [pin, setPin] = useState('')
15
+ const [pinError, setPinError] = useState('')
16
+ const [sessionKey, setSessionKey] = useState<string | null>(null)
24
17
  const [messages, setMessages] = useState<Message[]>([])
25
- const [remainingSuggestions, setRemainingSuggestions] = useState(suggestions)
26
18
  const [input, setInput] = useState('')
27
19
  const [isStreaming, setIsStreaming] = useState(false)
28
20
  const messagesEndRef = useRef<HTMLDivElement>(null)
29
21
  const inputRef = useRef<HTMLInputElement>(null)
30
- const nudgeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
31
- const sessionKeyRef = useRef<string | null>(null)
32
- const sessionPendingRef = useRef<Promise<string | null> | null>(null)
22
+ const pinInputRef = useRef<HTMLInputElement>(null)
33
23
 
34
- // Scroll to bottom when messages change
35
24
  useEffect(() => {
36
25
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
37
26
  }, [messages])
38
27
 
39
- // Nudge timer — if no input after 15s, show gentle prompt
40
28
  useEffect(() => {
41
- nudgeTimerRef.current = setTimeout(() => {
42
- setMessages(prev => {
43
- if (prev.length > 0) return prev // user already started chatting
44
- return [{ role: 'maxy', content: 'No wrong answers. Just tell me what\'s on your mind.' }]
45
- })
46
- }, 15000)
47
- return () => {
48
- if (nudgeTimerRef.current) clearTimeout(nudgeTimerRef.current)
49
- }
50
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
51
-
52
- // Focus input on mount
53
- useEffect(() => {
54
- inputRef.current?.focus()
55
- }, [])
29
+ if (authenticated) inputRef.current?.focus()
30
+ else pinInputRef.current?.focus()
31
+ }, [authenticated])
56
32
 
57
- /** Ensure we have a Taskmaster session, creating one if needed. */
58
- const ensureSession = useCallback(async (): Promise<string | null> => {
59
- if (sessionKeyRef.current) return sessionKeyRef.current
33
+ async function handlePinSubmit(e: FormEvent) {
34
+ e.preventDefault()
35
+ setPinError('')
60
36
 
61
- // Deduplicate concurrent calls
62
- if (sessionPendingRef.current) return sessionPendingRef.current
37
+ try {
38
+ const res = await fetch('/api/admin/session', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ pin }),
42
+ })
63
43
 
64
- const promise = (async () => {
65
- try {
66
- const res = await fetch('/api/session', {
67
- method: 'POST',
68
- headers: { 'Content-Type': 'application/json' },
69
- body: JSON.stringify({ session_id: generateSessionId() }),
70
- })
71
- if (!res.ok) return null
72
- const data = await res.json()
73
- sessionKeyRef.current = data.session_key
74
- return data.session_key as string
75
- } catch {
76
- return null
77
- } finally {
78
- sessionPendingRef.current = null
44
+ if (!res.ok) {
45
+ const data = await res.json().catch(() => ({}))
46
+ setPinError(data.error || 'Invalid PIN')
47
+ return
79
48
  }
80
- })()
81
49
 
82
- sessionPendingRef.current = promise
83
- return promise
84
- }, [])
85
-
86
- async function sendMessage(text: string) {
87
- if (!text || isStreaming) return
88
-
89
- // Clear nudge timer
90
- if (nudgeTimerRef.current) {
91
- clearTimeout(nudgeTimerRef.current)
92
- nudgeTimerRef.current = null
50
+ const data = await res.json()
51
+ setSessionKey(data.session_key)
52
+ setAuthenticated(true)
53
+ } catch {
54
+ setPinError('Could not connect.')
93
55
  }
56
+ }
94
57
 
95
- // Remove this suggestion if it was from the list
96
- setRemainingSuggestions(prev => prev.filter(s => s !== text))
58
+ async function sendMessage(text: string) {
59
+ if (!text || isStreaming || !sessionKey) return
97
60
 
98
- // Add visitor message
99
- const visitorMessage: Message = { role: 'visitor', content: text }
100
- const updatedMessages = [...messages, visitorMessage]
101
- setMessages(updatedMessages)
61
+ const adminMessage: Message = { role: 'admin', content: text }
62
+ const currentLength = messages.length
63
+ setMessages(prev => [...prev, adminMessage])
102
64
  setInput('')
103
65
  setIsStreaming(true)
104
66
 
105
- // Add placeholder for Maxy's response
106
- const placeholderIndex = updatedMessages.length
107
- setMessages(prev => [...prev, { role: 'maxy', content: '' }])
67
+ const maxyMessage: Message = { role: 'maxy', events: [] }
68
+ setMessages(prev => [...prev, maxyMessage])
69
+ const messageIndex = currentLength + 1
108
70
 
109
71
  try {
110
- // Ensure we have a session before sending
111
- const sessionKey = await ensureSession()
112
- if (!sessionKey) {
113
- throw new Error('session')
114
- }
115
-
116
- const res = await fetch('/api/chat', {
72
+ const res = await fetch('/api/admin/chat', {
117
73
  method: 'POST',
118
74
  headers: { 'Content-Type': 'application/json' },
119
75
  body: JSON.stringify({ message: text, session_key: sessionKey }),
120
76
  })
121
77
 
122
- if (!res.ok) {
123
- const errorData = await res.json().catch(() => ({}))
124
- throw new Error(errorData.error || 'Something went wrong')
125
- }
78
+ if (!res.ok) throw new Error('Chat request failed')
126
79
 
127
80
  const reader = res.body?.getReader()
128
- const decoder = new TextDecoder()
129
-
130
81
  if (!reader) throw new Error('No response stream')
131
82
 
132
- let accumulated = ''
133
- let sseBuffer = ''
83
+ const decoder = new TextDecoder()
84
+ let buffer = ''
134
85
 
135
86
  while (true) {
136
87
  const { done, value } = await reader.read()
137
88
  if (done) break
138
89
 
139
- sseBuffer += decoder.decode(value, { stream: true })
140
- const parts = sseBuffer.split('\n')
141
- sseBuffer = parts.pop()! // keep incomplete trailing segment
90
+ buffer += decoder.decode(value, { stream: true })
91
+ const parts = buffer.split('\n')
92
+ buffer = parts.pop()!
142
93
 
143
94
  for (const line of parts) {
144
95
  if (!line.startsWith('data: ')) continue
145
- const data = line.slice(6)
146
- if (data === '[DONE]') continue
96
+ const payload = line.slice(6)
97
+ if (payload === '[DONE]') continue
98
+
99
+ let parsed: { type: string; [key: string]: unknown }
100
+ try {
101
+ parsed = JSON.parse(payload)
102
+ } catch {
103
+ continue
104
+ }
147
105
 
148
- let parsed
149
- try { parsed = JSON.parse(data) } catch { continue }
106
+ if (parsed.type === 'done' || parsed.type === 'session_init') continue
150
107
 
151
- if (parsed.error) throw new Error(parsed.error)
152
- if (parsed.text) {
153
- accumulated += parsed.text
154
- setMessages(prev => {
155
- const updated = [...prev]
156
- updated[placeholderIndex] = {
157
- role: 'maxy',
158
- content: accumulated,
108
+ const event = parsed as AdminEvent
109
+
110
+ setMessages(prev => {
111
+ const updated = [...prev]
112
+ const msg = updated[messageIndex]
113
+ if (msg && msg.role === 'maxy') {
114
+ updated[messageIndex] = {
115
+ ...msg,
116
+ events: [...(msg.events ?? []), event],
159
117
  }
160
- return updated
161
- })
162
- }
118
+ }
119
+ return updated
120
+ })
163
121
  }
164
122
  }
165
123
  } catch (err) {
166
124
  const errorMessage = err instanceof Error ? err.message : 'Something went wrong'
167
125
  setMessages(prev => {
168
126
  const updated = [...prev]
169
- updated[placeholderIndex] = {
127
+ updated[messageIndex] = {
170
128
  role: 'maxy',
171
- content: errorMessage === 'session'
172
- ? "I'm having trouble connecting right now. Try refreshing the page."
173
- : "Sorry, I hit a snag. Try again in a moment.",
129
+ events: [{ type: 'text', content: `Error: ${errorMessage}` }],
174
130
  }
175
131
  return updated
176
132
  })
@@ -185,56 +141,67 @@ export default function ChatPage() {
185
141
  sendMessage(input.trim())
186
142
  }
187
143
 
144
+ if (!authenticated) {
145
+ return (
146
+ <div className="chat-page admin-page">
147
+ <header className="chat-header">
148
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
149
+ <h1 className="chat-tagline">Maxy</h1>
150
+ <p className="chat-intro">Convenience as standard.</p>
151
+ </header>
152
+ <div className="admin-pin-form">
153
+ <form onSubmit={handlePinSubmit}>
154
+ <input
155
+ ref={pinInputRef}
156
+ type="password"
157
+ value={pin}
158
+ onChange={e => setPin(e.target.value)}
159
+ placeholder="Enter PIN"
160
+ className="chat-input"
161
+ autoFocus
162
+ />
163
+ <button type="submit" className="chat-send" disabled={!pin}>
164
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
165
+ <line x1="5" y1="12" x2="19" y2="12" />
166
+ <polyline points="12 5 19 12 12 19" />
167
+ </svg>
168
+ </button>
169
+ </form>
170
+ {pinError && <p className="admin-pin-error">{pinError}</p>}
171
+ </div>
172
+ </div>
173
+ )
174
+ }
175
+
188
176
  return (
189
- <div className="chat-page">
177
+ <div className="chat-page admin-page">
190
178
  <header className="chat-header">
191
179
  <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
192
- <h1 className="chat-tagline">No Stress. Quiet Life.</h1>
193
- <p className="chat-intro">
194
- Hey, I{'\u2019'}m Maxy. I{'\u2019'}m a personal AI that handles the
195
- stuff you don{'\u2019'}t want to think about. I{'\u2019'}m not a product
196
- yet{'\u2009'}{'\u2014'}{'\u2009'}I{'\u2019'}m learning what stresses people
197
- most so I can be built right. Tell me what{'\u2019'}s on your mind.
198
- No advice, no judgment, just a conversation.
199
- </p>
200
- <div className="chat-trust">
201
- <span className="chat-trust-item">Anonymous</span>
202
- <span className="chat-trust-sep">{'\u00B7'}</span>
203
- <span className="chat-trust-item">Private</span>
204
- <span className="chat-trust-sep">{'\u00B7'}</span>
205
- <span className="chat-trust-item">No sign-up</span>
206
- <span className="chat-trust-sep">{'\u00B7'}</span>
207
- <span className="chat-trust-item">Free</span>
208
- </div>
180
+ <h1 className="chat-tagline">Maxy</h1>
181
+ <p className="chat-intro">Convenience as standard.</p>
209
182
  </header>
210
183
 
211
184
  <div className="chat-messages">
212
185
  {messages.map((msg, i) => (
213
- <div key={i} className={`message ${msg.role}`}>
214
- <div className="bubble">
215
- {msg.content || (
216
- <span className="typing-indicator">
217
- <span className="typing-dot" />
218
- <span className="typing-dot" />
219
- <span className="typing-dot" />
220
- </span>
221
- )}
222
- </div>
186
+ <div key={i} className={`message ${msg.role === 'admin' ? 'visitor' : 'maxy'}`}>
187
+ {msg.role === 'admin' ? (
188
+ <div className="bubble">{msg.content}</div>
189
+ ) : (
190
+ <div className="admin-activity">
191
+ {msg.events?.map((event, j) => (
192
+ <ActivityEvent key={j} event={event} />
193
+ ))}
194
+ {isStreaming && i === messages.length - 1 && (
195
+ <span className="typing-indicator">
196
+ <span className="typing-dot" />
197
+ <span className="typing-dot" />
198
+ <span className="typing-dot" />
199
+ </span>
200
+ )}
201
+ </div>
202
+ )}
223
203
  </div>
224
204
  ))}
225
- {!isStreaming && remainingSuggestions.length > 0 && (
226
- <div className="chat-suggestions">
227
- {remainingSuggestions.map(s => (
228
- <button
229
- key={s}
230
- className="chat-suggestion"
231
- onClick={() => sendMessage(s)}
232
- >
233
- {s}
234
- </button>
235
- ))}
236
- </div>
237
- )}
238
205
  <div ref={messagesEndRef} />
239
206
  </div>
240
207
 
@@ -246,7 +213,7 @@ export default function ChatPage() {
246
213
  type="text"
247
214
  value={input}
248
215
  onChange={e => setInput(e.target.value)}
249
- placeholder="Tell me what you need taking care of in your life!"
216
+ placeholder="Tell Maxy what you need..."
250
217
  disabled={isStreaming}
251
218
  aria-label="Type your message"
252
219
  />
@@ -263,10 +230,6 @@ export default function ChatPage() {
263
230
  </button>
264
231
  </form>
265
232
  </div>
266
-
267
- <footer className="chat-footer">
268
- <a href="https://maxy.bot">maxy.bot {'\u2197'}</a>
269
- </footer>
270
233
  </div>
271
234
  )
272
235
  }
@@ -0,0 +1,266 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, useCallback, FormEvent } from 'react'
4
+
5
+ interface Message {
6
+ role: 'maxy' | 'visitor'
7
+ content: string
8
+ }
9
+
10
+ const suggestions: string[] = []
11
+
12
+ /** Generate a random session ID (Taskmaster prepends its own anon- prefix). */
13
+ function generateSessionId(): string {
14
+ return crypto.randomUUID()
15
+ }
16
+
17
+ export default function ChatPage() {
18
+ const [messages, setMessages] = useState<Message[]>([])
19
+ const [remainingSuggestions, setRemainingSuggestions] = useState(suggestions)
20
+ const [input, setInput] = useState('')
21
+ const [isStreaming, setIsStreaming] = useState(false)
22
+ const messagesEndRef = useRef<HTMLDivElement>(null)
23
+ const inputRef = useRef<HTMLInputElement>(null)
24
+ const nudgeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
25
+ const sessionKeyRef = useRef<string | null>(null)
26
+ const sessionPendingRef = useRef<Promise<string | null> | null>(null)
27
+
28
+ // Scroll to bottom when messages change
29
+ useEffect(() => {
30
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
31
+ }, [messages])
32
+
33
+ // Nudge timer — if no input after 15s, show gentle prompt
34
+ useEffect(() => {
35
+ nudgeTimerRef.current = setTimeout(() => {
36
+ setMessages(prev => {
37
+ if (prev.length > 0) return prev // user already started chatting
38
+ return [{ role: 'maxy', content: 'No wrong answers. Just tell me what\'s on your mind.' }]
39
+ })
40
+ }, 15000)
41
+ return () => {
42
+ if (nudgeTimerRef.current) clearTimeout(nudgeTimerRef.current)
43
+ }
44
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
45
+
46
+ // Focus input on mount
47
+ useEffect(() => {
48
+ inputRef.current?.focus()
49
+ }, [])
50
+
51
+ /** Ensure we have a Taskmaster session, creating one if needed. */
52
+ const ensureSession = useCallback(async (): Promise<string | null> => {
53
+ if (sessionKeyRef.current) return sessionKeyRef.current
54
+
55
+ // Deduplicate concurrent calls
56
+ if (sessionPendingRef.current) return sessionPendingRef.current
57
+
58
+ const promise = (async () => {
59
+ try {
60
+ const res = await fetch('/api/session', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ session_id: generateSessionId() }),
64
+ })
65
+ if (!res.ok) return null
66
+ const data = await res.json()
67
+ sessionKeyRef.current = data.session_key
68
+ return data.session_key as string
69
+ } catch {
70
+ return null
71
+ } finally {
72
+ sessionPendingRef.current = null
73
+ }
74
+ })()
75
+
76
+ sessionPendingRef.current = promise
77
+ return promise
78
+ }, [])
79
+
80
+ async function sendMessage(text: string) {
81
+ if (!text || isStreaming) return
82
+
83
+ // Clear nudge timer
84
+ if (nudgeTimerRef.current) {
85
+ clearTimeout(nudgeTimerRef.current)
86
+ nudgeTimerRef.current = null
87
+ }
88
+
89
+ // Remove this suggestion if it was from the list
90
+ setRemainingSuggestions(prev => prev.filter(s => s !== text))
91
+
92
+ // Add visitor message
93
+ const visitorMessage: Message = { role: 'visitor', content: text }
94
+ const updatedMessages = [...messages, visitorMessage]
95
+ setMessages(updatedMessages)
96
+ setInput('')
97
+ setIsStreaming(true)
98
+
99
+ // Add placeholder for Maxy's response
100
+ const placeholderIndex = updatedMessages.length
101
+ setMessages(prev => [...prev, { role: 'maxy', content: '' }])
102
+
103
+ try {
104
+ // Ensure we have a session before sending
105
+ const sessionKey = await ensureSession()
106
+ if (!sessionKey) {
107
+ throw new Error('session')
108
+ }
109
+
110
+ const res = await fetch('/api/chat', {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify({ message: text, session_key: sessionKey }),
114
+ })
115
+
116
+ if (!res.ok) {
117
+ const errorData = await res.json().catch(() => ({}))
118
+ throw new Error(errorData.error || 'Something went wrong')
119
+ }
120
+
121
+ const reader = res.body?.getReader()
122
+ const decoder = new TextDecoder()
123
+
124
+ if (!reader) throw new Error('No response stream')
125
+
126
+ let accumulated = ''
127
+ let sseBuffer = ''
128
+
129
+ while (true) {
130
+ const { done, value } = await reader.read()
131
+ if (done) break
132
+
133
+ sseBuffer += decoder.decode(value, { stream: true })
134
+ const parts = sseBuffer.split('\n')
135
+ sseBuffer = parts.pop()! // keep incomplete trailing segment
136
+
137
+ for (const line of parts) {
138
+ if (!line.startsWith('data: ')) continue
139
+ const data = line.slice(6)
140
+ if (data === '[DONE]') continue
141
+
142
+ let parsed
143
+ try { parsed = JSON.parse(data) } catch { continue }
144
+
145
+ if (parsed.error) throw new Error(parsed.error)
146
+ if (parsed.text) {
147
+ accumulated += parsed.text
148
+ setMessages(prev => {
149
+ const updated = [...prev]
150
+ updated[placeholderIndex] = {
151
+ role: 'maxy',
152
+ content: accumulated,
153
+ }
154
+ return updated
155
+ })
156
+ }
157
+ }
158
+ }
159
+ } catch (err) {
160
+ const errorMessage = err instanceof Error ? err.message : 'Something went wrong'
161
+ setMessages(prev => {
162
+ const updated = [...prev]
163
+ updated[placeholderIndex] = {
164
+ role: 'maxy',
165
+ content: errorMessage === 'session'
166
+ ? "I'm having trouble connecting right now. Try refreshing the page."
167
+ : "Sorry, I hit a snag. Try again in a moment.",
168
+ }
169
+ return updated
170
+ })
171
+ } finally {
172
+ setIsStreaming(false)
173
+ inputRef.current?.focus()
174
+ }
175
+ }
176
+
177
+ function handleSubmit(e: FormEvent) {
178
+ e.preventDefault()
179
+ sendMessage(input.trim())
180
+ }
181
+
182
+ return (
183
+ <div className="chat-page">
184
+ <header className="chat-header">
185
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
186
+ <h1 className="chat-tagline">No Stress. Quiet Life.</h1>
187
+ <p className="chat-intro">
188
+ Hey, I{'\u2019'}m Maxy. I{'\u2019'}m a personal AI that handles the
189
+ stuff you don{'\u2019'}t want to think about. I{'\u2019'}m not a product
190
+ yet{'\u2009'}{'\u2014'}{'\u2009'}I{'\u2019'}m learning what stresses people
191
+ most so I can be built right. Tell me what{'\u2019'}s on your mind.
192
+ No advice, no judgment, just a conversation.
193
+ </p>
194
+ <div className="chat-trust">
195
+ <span className="chat-trust-item">Anonymous</span>
196
+ <span className="chat-trust-sep">{'\u00B7'}</span>
197
+ <span className="chat-trust-item">Private</span>
198
+ <span className="chat-trust-sep">{'\u00B7'}</span>
199
+ <span className="chat-trust-item">No sign-up</span>
200
+ <span className="chat-trust-sep">{'\u00B7'}</span>
201
+ <span className="chat-trust-item">Free</span>
202
+ </div>
203
+ </header>
204
+
205
+ <div className="chat-messages">
206
+ {messages.map((msg, i) => (
207
+ <div key={i} className={`message ${msg.role}`}>
208
+ <div className="bubble">
209
+ {msg.content || (
210
+ <span className="typing-indicator">
211
+ <span className="typing-dot" />
212
+ <span className="typing-dot" />
213
+ <span className="typing-dot" />
214
+ </span>
215
+ )}
216
+ </div>
217
+ </div>
218
+ ))}
219
+ {!isStreaming && remainingSuggestions.length > 0 && (
220
+ <div className="chat-suggestions">
221
+ {remainingSuggestions.map(s => (
222
+ <button
223
+ key={s}
224
+ className="chat-suggestion"
225
+ onClick={() => sendMessage(s)}
226
+ >
227
+ {s}
228
+ </button>
229
+ ))}
230
+ </div>
231
+ )}
232
+ <div ref={messagesEndRef} />
233
+ </div>
234
+
235
+ <div className="chat-input-area">
236
+ <form className="chat-form" onSubmit={handleSubmit}>
237
+ <input
238
+ ref={inputRef}
239
+ className="chat-input"
240
+ type="text"
241
+ value={input}
242
+ onChange={e => setInput(e.target.value)}
243
+ placeholder="Tell me what you need taking care of in your life!"
244
+ disabled={isStreaming}
245
+ aria-label="Type your message"
246
+ />
247
+ <button
248
+ className="chat-send"
249
+ type="submit"
250
+ disabled={isStreaming || !input.trim()}
251
+ aria-label="Send message"
252
+ >
253
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
254
+ <line x1="5" y1="12" x2="19" y2="12" />
255
+ <polyline points="12 5 19 12 12 19" />
256
+ </svg>
257
+ </button>
258
+ </form>
259
+ </div>
260
+
261
+ <footer className="chat-footer">
262
+ <a href="https://maxy.bot">maxy.bot {'\u2197'}</a>
263
+ </footer>
264
+ </div>
265
+ )
266
+ }
@@ -1,42 +1,20 @@
1
1
  import { NextRequest, NextResponse } from 'next/server'
2
2
 
3
- function isLocalAccess(host: string): boolean {
4
- return host.includes('.local') ||
5
- host.includes('localhost') ||
6
- host.includes('127.0.0.1') ||
7
- host.includes('192.168.') ||
8
- host.includes('10.') ||
9
- host.startsWith('172.')
10
- }
11
-
12
3
  export function proxy(request: NextRequest) {
13
4
  const host = request.headers.get('host') || ''
14
5
  const { pathname } = request.nextUrl
15
6
 
16
- // Local network access (maxy.local:19200, 192.168.x.x, etc.) admin interface
17
- // On the local network, the user IS the admin. No public chat on local.
18
- if (isLocalAccess(host) && pathname === '/') {
19
- return NextResponse.rewrite(new URL('/admin', request.url))
20
- }
21
-
22
- // Allow admin API from local network and admin.maxy.bot
23
- if (pathname.startsWith('/api/admin/')) {
24
- if (!isLocalAccess(host) && !host.includes('admin.maxy.bot')) {
25
- return new NextResponse(JSON.stringify({ error: 'Admin API not available on this domain' }), {
26
- status: 403,
27
- headers: { 'Content-Type': 'application/json' },
28
- })
29
- }
30
- }
31
-
32
- // public.maxy.bot → serve public chat
33
- if (host.includes('public.maxy.bot')) {
34
- return NextResponse.next()
7
+ // public.maxy.bot serve public chat (not admin)
8
+ if (host.includes('public.maxy.bot') && pathname === '/') {
9
+ return NextResponse.rewrite(new URL('/public', request.url))
35
10
  }
36
11
 
37
- // admin.maxy.bot serve admin page
38
- if (host.includes('admin.maxy.bot') && pathname === '/') {
39
- return NextResponse.rewrite(new URL('/admin', request.url))
12
+ // Block admin API from public subdomain
13
+ if (host.includes('public.maxy.bot') && pathname.startsWith('/api/admin/')) {
14
+ return new NextResponse(JSON.stringify({ error: 'Not available' }), {
15
+ status: 403,
16
+ headers: { 'Content-Type': 'application/json' },
17
+ })
40
18
  }
41
19
 
42
20
  // getmaxy.com → serve landing page
@@ -45,7 +23,7 @@ export function proxy(request: NextRequest) {
45
23
  }
46
24
 
47
25
  // maxy.bot (bare) → redirect to public.maxy.bot
48
- if ((host === 'maxy.bot' || host === 'www.maxy.bot') && !host.includes('public.') && !host.includes('admin.')) {
26
+ if (host === 'maxy.bot' || host === 'www.maxy.bot') {
49
27
  return NextResponse.redirect(`https://public.maxy.bot${pathname}`, 301)
50
28
  }
51
29
 
@@ -54,6 +32,7 @@ export function proxy(request: NextRequest) {
54
32
  return NextResponse.redirect(`https://public.maxy.bot${pathname}`, 301)
55
33
  }
56
34
 
35
+ // Everything else (local access, admin.maxy.bot) → root page (admin)
57
36
  return NextResponse.next()
58
37
  }
59
38