claudmax 2.0.0 → 2.0.1
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/claudmax-1.0.16.tgz +0 -0
- package/{packages/cli/index.js → index.js} +2 -0
- package/package.json +27 -55
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -24
- package/.github/workflows/publish.yml +0 -31
- package/README.md +0 -178
- package/claudmax-mcp-1.0.2.tgz +0 -0
- package/help +0 -0
- package/help-wal +0 -0
- package/next-env.d.ts +0 -6
- package/next.config.mjs +0 -43
- package/packages/cli/claudmax-1.0.16.tgz +0 -0
- package/packages/cli/package.json +0 -33
- package/packages/mcp/claudmax-mcp-1.0.0.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.1.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.2.tgz +0 -0
- package/packages/mcp/claudmax-mcp-1.0.3.tgz +0 -0
- package/packages/mcp/index.js +0 -129
- package/packages/mcp/package-lock.json +0 -1146
- package/packages/mcp/package.json +0 -32
- package/postcss.config.mjs +0 -6
- package/prisma/schema.prisma +0 -130
- package/prisma/seed.ts +0 -27
- package/public/favicon.svg +0 -10
- package/public/robots.txt +0 -10
- package/run_build.sh +0 -4
- package/scripts/migrate-plans.js +0 -98
- package/scripts/seed-blog.ts +0 -1014
- package/src/app/admin/dashboard/AdminDashboardClient.tsx +0 -1546
- package/src/app/admin/dashboard/page.tsx +0 -13
- package/src/app/admin/page.tsx +0 -132
- package/src/app/api/admin/auth/me/route.ts +0 -34
- package/src/app/api/admin/health/route.ts +0 -110
- package/src/app/api/admin/keys/[id]/route.ts +0 -116
- package/src/app/api/admin/keys/route.ts +0 -192
- package/src/app/api/admin/keys-list/route.ts +0 -81
- package/src/app/api/admin/login/route.ts +0 -72
- package/src/app/api/admin/logout/route.ts +0 -8
- package/src/app/api/admin/migrate/route.ts +0 -133
- package/src/app/api/admin/plans/[id]/route.ts +0 -65
- package/src/app/api/admin/plans/route.ts +0 -66
- package/src/app/api/admin/posts/[id]/route.ts +0 -81
- package/src/app/api/admin/posts/route.ts +0 -83
- package/src/app/api/admin/seed/route.ts +0 -145
- package/src/app/api/admin/settings/route.ts +0 -44
- package/src/app/api/admin/stats/route.ts +0 -74
- package/src/app/api/admin/users/[id]/route.ts +0 -166
- package/src/app/api/admin/users/plans/route.ts +0 -45
- package/src/app/api/admin/users/route.ts +0 -202
- package/src/app/api/blog/[slug]/route.ts +0 -22
- package/src/app/api/blog/route.ts +0 -40
- package/src/app/api/cron/daily-status/route.ts +0 -208
- package/src/app/api/support/chat/route.ts +0 -55
- package/src/app/api/support/chat/session/route.ts +0 -62
- package/src/app/api/support/chat/stream/route.ts +0 -44
- package/src/app/api/support/email/route.ts +0 -63
- package/src/app/api/tools/understand_image/route.ts +0 -113
- package/src/app/api/tools/upload/route.ts +0 -179
- package/src/app/api/tools/web_search/route.ts +0 -99
- package/src/app/api/v1/audio/route.ts +0 -67
- package/src/app/api/v1/audio/speech/route.ts +0 -73
- package/src/app/api/v1/chat/completions/route.ts +0 -3
- package/src/app/api/v1/chat/route.ts +0 -1079
- package/src/app/api/v1/images/generations/route.ts +0 -93
- package/src/app/api/v1/info/route.ts +0 -30
- package/src/app/api/v1/key-status/route.ts +0 -109
- package/src/app/api/v1/key-status/stream/route.ts +0 -135
- package/src/app/api/v1/messages/count_tokens/route.ts +0 -22
- package/src/app/api/v1/messages/route.ts +0 -807
- package/src/app/api/v1/models/route.ts +0 -14
- package/src/app/api/v1/route.ts +0 -18
- package/src/app/blog/BlogClient.tsx +0 -193
- package/src/app/blog/[slug]/page.tsx +0 -117
- package/src/app/blog/page.tsx +0 -20
- package/src/app/check-usage/CheckUsageClient.tsx +0 -186
- package/src/app/check-usage/layout.tsx +0 -11
- package/src/app/check-usage/page.tsx +0 -15
- package/src/app/docs/layout.tsx +0 -16
- package/src/app/docs/page.tsx +0 -1055
- package/src/app/faq/FAQClient.tsx +0 -227
- package/src/app/faq/page.tsx +0 -21
- package/src/app/globals.css +0 -75
- package/src/app/layout.tsx +0 -80
- package/src/app/page.tsx +0 -256
- package/src/app/reseller/ResellerClient.tsx +0 -435
- package/src/app/reseller/page.tsx +0 -15
- package/src/app/setup.ps1/route.ts +0 -79
- package/src/app/setup.sh/route.ts +0 -113
- package/src/app/sitemap.ts +0 -50
- package/src/app/status/StatusClient.tsx +0 -103
- package/src/app/status/layout.tsx +0 -11
- package/src/app/status/page.tsx +0 -15
- package/src/app/support/SupportClient.tsx +0 -411
- package/src/app/support/page.tsx +0 -25
- package/src/app/v1/chat/completions/route.ts +0 -3
- package/src/app/v1/chat/route.ts +0 -4
- package/src/app/v1/messages/route.ts +0 -3
- package/src/components/Footer.tsx +0 -120
- package/src/components/Header.tsx +0 -131
- package/src/components/landing/features.tsx +0 -99
- package/src/components/ui/badge.tsx +0 -32
- package/src/components/ui/button.tsx +0 -46
- package/src/components/ui/card.tsx +0 -50
- package/src/components/ui/dialog.tsx +0 -97
- package/src/components/ui/dropdown-menu.tsx +0 -156
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -15
- package/src/components/ui/separator.tsx +0 -22
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/tabs.tsx +0 -51
- package/src/components/ui/toast.tsx +0 -103
- package/src/lib/auth.ts +0 -45
- package/src/lib/prisma.ts +0 -20
- package/src/lib/providers.ts +0 -158
- package/src/lib/security.ts +0 -165
- package/src/lib/utils.ts +0 -14
- package/src/middleware.ts +0 -30
- package/tailwind.config.ts +0 -53
- package/tsconfig.json +0 -41
- package/tsconfig.tsbuildinfo +0 -1
- package/vercel.json +0 -8
- /package/{packages/cli/bin → bin}/claudmax.js +0 -0
- /package/{packages/cli/claudmax-1.0.17.tgz → claudmax-1.0.17.tgz} +0 -0
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { Resend } from 'resend';
|
|
3
|
-
import { prisma } from '@/lib/prisma';
|
|
4
|
-
|
|
5
|
-
const TO_EMAIL = 'codewpremium@gmail.com';
|
|
6
|
-
const FROM_EMAIL = 'ClaudMax Status <onboarding@resend.dev>';
|
|
7
|
-
|
|
8
|
-
function getResendClient() {
|
|
9
|
-
if (!process.env.RESEND_API_KEY) return null;
|
|
10
|
-
return new Resend(process.env.RESEND_API_KEY);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function GET() {
|
|
14
|
-
try {
|
|
15
|
-
const client = getResendClient();
|
|
16
|
-
if (!client) {
|
|
17
|
-
return NextResponse.json({ error: 'RESEND_API_KEY not configured' }, { status: 503 });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const baseUrl = process.env.NEXT_PUBLIC_API_BASE || 'https://api.claudmax.pro';
|
|
21
|
-
const now = new Date();
|
|
22
|
-
|
|
23
|
-
// Check OpenRouter API health
|
|
24
|
-
const apiStart = Date.now();
|
|
25
|
-
let apiStatus = 'Operational';
|
|
26
|
-
let apiLatency = 0;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const r = await fetch('https://openrouter.ai/api/v1/models', {
|
|
30
|
-
headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` },
|
|
31
|
-
signal: AbortSignal.timeout(10000),
|
|
32
|
-
});
|
|
33
|
-
apiLatency = Date.now() - apiStart;
|
|
34
|
-
if (!r.ok) apiStatus = 'Degraded';
|
|
35
|
-
} catch {
|
|
36
|
-
apiStatus = 'Down';
|
|
37
|
-
apiLatency = Date.now() - apiStart;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Get stats from database
|
|
41
|
-
let totalKeys = 0, activeKeys = 0, totalTokens = 0, totalUsers = 0, newKeysToday = 0;
|
|
42
|
-
let topKeys: any[] = [];
|
|
43
|
-
try {
|
|
44
|
-
const [keys, users, todayStart] = await Promise.all([
|
|
45
|
-
prisma.apiKey.count(),
|
|
46
|
-
prisma.admin.count(),
|
|
47
|
-
Promise.resolve(new Date(now.getFullYear(), now.getMonth(), now.getDate())),
|
|
48
|
-
]);
|
|
49
|
-
totalKeys = keys;
|
|
50
|
-
activeKeys = await prisma.apiKey.count({ where: { isActive: true } });
|
|
51
|
-
totalUsers = users;
|
|
52
|
-
newKeysToday = await prisma.apiKey.count({ where: { createdAt: { gte: todayStart } } });
|
|
53
|
-
|
|
54
|
-
const agg = await prisma.apiKey.aggregate({ _sum: { totalTokensUsed: true } });
|
|
55
|
-
totalTokens = Number(agg._sum.totalTokensUsed ?? 0);
|
|
56
|
-
|
|
57
|
-
topKeys = await prisma.apiKey.findMany({
|
|
58
|
-
orderBy: { totalTokensUsed: 'desc' },
|
|
59
|
-
take: 5,
|
|
60
|
-
select: {
|
|
61
|
-
name: true, tier: true, windowTokensUsed: true,
|
|
62
|
-
windowRequestsUsed: true, totalTokensUsed: true, lastUsedAt: true,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
} catch {
|
|
66
|
-
// DB unavailable — continue without stats
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check endpoints
|
|
70
|
-
const endpoints = [
|
|
71
|
-
{ name: 'Chat Completions', path: '/api/v1/chat/completions' },
|
|
72
|
-
{ name: 'Messages', path: '/api/v1/messages' },
|
|
73
|
-
{ name: 'Key Status', path: '/api/v1/key-status' },
|
|
74
|
-
{ name: 'Models', path: '/api/v1/models' },
|
|
75
|
-
{ name: 'Image Generation', path: '/api/v1/images/generations' },
|
|
76
|
-
{ name: 'Audio TTS', path: '/api/v1/audio/speech' },
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
const endpointResults = await Promise.all(
|
|
80
|
-
endpoints.map(async (ep) => {
|
|
81
|
-
try {
|
|
82
|
-
const res = await fetch(`${baseUrl}${ep.path}`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: { 'Content-Type': 'application/json' },
|
|
85
|
-
body: JSON.stringify({ apiKey: 'health-check' }),
|
|
86
|
-
signal: AbortSignal.timeout(8000),
|
|
87
|
-
});
|
|
88
|
-
return { name: ep.name, status: !(res.status >= 500) ? 'Operational' : 'Down' };
|
|
89
|
-
} catch {
|
|
90
|
-
return { name: ep.name, status: 'Unknown' };
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const statusColor = apiStatus === 'Operational' ? '#22c55e' : apiStatus === 'Degraded' ? '#f59e0b' : '#ef4444';
|
|
96
|
-
|
|
97
|
-
const topKeysRows = topKeys.map(k => `
|
|
98
|
-
<tr>
|
|
99
|
-
<td style="padding:8px;border:1px solid #e5e7eb;">${k.name}</td>
|
|
100
|
-
<td style="padding:8px;border:1px solid #e5e7eb;text-transform:capitalize;">${k.tier}</td>
|
|
101
|
-
<td style="padding:8px;border:1px solid #e5e7eb;font-family:monospace;">${(Number(k.totalTokensUsed) / 1_000_000).toFixed(2)}M</td>
|
|
102
|
-
<td style="padding:8px;border:1px solid #e5e7eb;">${k.windowRequestsUsed}</td>
|
|
103
|
-
</tr>
|
|
104
|
-
`).join('');
|
|
105
|
-
|
|
106
|
-
const endpointRows = endpointResults.map(ep => `
|
|
107
|
-
<tr>
|
|
108
|
-
<td style="padding:8px;border:1px solid #e5e7eb;">${ep.name}</td>
|
|
109
|
-
<td style="padding:8px;border:1px solid #e5e7eb;color:${ep.status === 'Operational' ? '#22c55e' : '#ef4444'};font-weight:600;">${ep.status}</td>
|
|
110
|
-
</tr>
|
|
111
|
-
`).join('');
|
|
112
|
-
|
|
113
|
-
const istDate = now.toLocaleDateString('en-US', {
|
|
114
|
-
timeZone: 'Asia/Kolkata',
|
|
115
|
-
weekday: 'long', month: 'long', day: 'numeric', year: 'numeric',
|
|
116
|
-
});
|
|
117
|
-
const istTime = now.toLocaleTimeString('en-US', {
|
|
118
|
-
timeZone: 'Asia/Kolkata', hour: '2-digit', minute: '2-digit',
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const html = `
|
|
122
|
-
<!DOCTYPE html>
|
|
123
|
-
<html>
|
|
124
|
-
<head><meta charset="utf-8"></head>
|
|
125
|
-
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;background:#f9fafb;">
|
|
126
|
-
<div style="background:white;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.1);">
|
|
127
|
-
<div style="background:linear-gradient(135deg,#5244F3,#7c3aed);padding:24px;text-align:center;">
|
|
128
|
-
<h1 style="color:white;margin:0;font-size:24px;">⚡ ClaudMax Daily Status</h1>
|
|
129
|
-
<p style="color:#c4b5fd;margin:8px 0 0;font-size:14px;">${istDate} · ${istTime} IST</p>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div style="padding:20px 24px;border-bottom:1px solid #e5e7eb;">
|
|
133
|
-
<h2 style="font-size:16px;color:#374151;margin:0 0 12px;">OpenRouter API</h2>
|
|
134
|
-
<div style="display:flex;align-items:center;gap:12px;">
|
|
135
|
-
<div style="width:12px;height:12px;border-radius:50%;background:${statusColor};"></div>
|
|
136
|
-
<span style="font-size:18px;font-weight:700;color:#111827;">${apiStatus}</span>
|
|
137
|
-
${apiLatency > 0 ? `<span style="color:#6b7280;font-size:13px;">· ${apiLatency}ms</span>` : ''}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div style="padding:20px 24px;display:grid;grid-template-columns:1fr 1fr 1fr;border-bottom:1px solid #e5e7eb;">
|
|
142
|
-
<div style="text-align:center;">
|
|
143
|
-
<div style="font-size:28px;font-weight:800;color:#5244F3;">${activeKeys}/${totalKeys}</div>
|
|
144
|
-
<div style="font-size:12px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.05em;">Active Keys</div>
|
|
145
|
-
</div>
|
|
146
|
-
<div style="text-align:center;">
|
|
147
|
-
<div style="font-size:28px;font-weight:800;color:#111827;">${totalUsers}</div>
|
|
148
|
-
<div style="font-size:12px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.05em;">Total Users</div>
|
|
149
|
-
</div>
|
|
150
|
-
<div style="text-align:center;">
|
|
151
|
-
<div style="font-size:28px;font-weight:800;color:#111827;">${newKeysToday}</div>
|
|
152
|
-
<div style="font-size:12px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.05em;">New Today</div>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
<div style="padding:16px 24px;border-bottom:1px solid #e5e7eb;text-align:center;">
|
|
157
|
-
<span style="font-size:13px;color:#6b7280;">Total Lifetime Tokens: </span>
|
|
158
|
-
<span style="font-size:18px;font-weight:800;color:#111827;font-family:monospace;">${(totalTokens / 1_000_000_000).toFixed(2)}B</span>
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
<div style="padding:20px 24px;border-bottom:1px solid #e5e7eb;">
|
|
162
|
-
<h2 style="font-size:16px;color:#374151;margin:0 0 12px;">API Endpoints</h2>
|
|
163
|
-
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
|
164
|
-
<tr style="background:#f9fafb;">
|
|
165
|
-
<th style="padding:8px;border:1px solid #e5e7eb;text-align:left;font-size:11px;color:#9ca3af;text-transform:uppercase;">Endpoint</th>
|
|
166
|
-
<th style="padding:8px;border:1px solid #e5e7eb;text-align:left;font-size:11px;color:#9ca3af;text-transform:uppercase;">Status</th>
|
|
167
|
-
</tr>
|
|
168
|
-
${endpointRows}
|
|
169
|
-
</table>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
<div style="padding:20px 24px;">
|
|
173
|
-
<h2 style="font-size:16px;color:#374151;margin:0 0 12px;">Top Keys by Usage</h2>
|
|
174
|
-
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
|
175
|
-
<tr style="background:#f9fafb;">
|
|
176
|
-
<th style="padding:8px;border:1px solid #e5e7eb;text-align:left;font-size:11px;color:#9ca3af;text-transform:uppercase;">Name</th>
|
|
177
|
-
<th style="padding:8px;border:1px solid #e5e7eb;text-align:left;font-size:11px;color:#9ca3af;text-transform:uppercase;">Tier</th>
|
|
178
|
-
<th style="padding:8px;border:1px solid #e5e7eb;text-align:left;font-size:11px;color:#9ca3af;text-transform:uppercase;">Tokens</th>
|
|
179
|
-
<th style="padding:8px;border:1px solid #e5e7eb;text-align:left;font-size:11px;color:#9ca3af;text-transform:uppercase;">Requests</th>
|
|
180
|
-
</tr>
|
|
181
|
-
${topKeysRows || '<tr><td colspan="4" style="padding:16px;text-align:center;color:#9ca3af;">No data available</td></tr>'}
|
|
182
|
-
</table>
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
<div style="padding:16px 24px;background:#f9fafb;text-align:center;">
|
|
186
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
|
187
|
-
ClaudMax · <a href="https://claudmax.pro" style="color:#5244F3;text-decoration:none;">claudmax.pro</a> ·
|
|
188
|
-
<a href="https://claudmax.pro/admin" style="color:#5244F3;text-decoration:none;">Admin Panel</a>
|
|
189
|
-
</p>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</body>
|
|
193
|
-
</html>
|
|
194
|
-
`.trim();
|
|
195
|
-
|
|
196
|
-
await client.emails.send({
|
|
197
|
-
from: FROM_EMAIL,
|
|
198
|
-
to: TO_EMAIL,
|
|
199
|
-
subject: `⚡ ClaudMax Daily Status · ${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} · ${apiStatus}`,
|
|
200
|
-
html,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return NextResponse.json({ success: true, sentAt: new Date().toISOString() });
|
|
204
|
-
} catch (err) {
|
|
205
|
-
console.error('[ClaudMax] Daily status email error:', err);
|
|
206
|
-
return NextResponse.json({ error: 'Failed to send status email' }, { status: 500 });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
export async function GET(req: Request) {
|
|
5
|
-
const { searchParams } = new URL(req.url);
|
|
6
|
-
const sessionId = searchParams.get('sessionId');
|
|
7
|
-
|
|
8
|
-
if (!sessionId) {
|
|
9
|
-
return NextResponse.json({ error: 'sessionId required' }, { status: 400 });
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const messages = await prisma.chatMessage.findMany({
|
|
13
|
-
where: { sessionId },
|
|
14
|
-
orderBy: { createdAt: 'asc' },
|
|
15
|
-
select: { id: true, sender: true, content: true, isRead: true, createdAt: true },
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// Mark messages from visitor as read
|
|
19
|
-
await prisma.chatMessage.updateMany({
|
|
20
|
-
where: { sessionId, sender: 'visitor', isRead: false },
|
|
21
|
-
data: { isRead: true },
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
return NextResponse.json({ messages });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function POST(req: Request) {
|
|
28
|
-
try {
|
|
29
|
-
const body = await req.json();
|
|
30
|
-
const { sessionId, content, sender = 'visitor' } = body;
|
|
31
|
-
|
|
32
|
-
if (!sessionId || !content?.trim()) {
|
|
33
|
-
return NextResponse.json({ error: 'sessionId and content required' }, { status: 400 });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const message = await prisma.chatMessage.create({
|
|
37
|
-
data: { sessionId, content: content.trim(), sender },
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Update session last message
|
|
41
|
-
await prisma.chatSession.update({
|
|
42
|
-
where: { id: sessionId },
|
|
43
|
-
data: {
|
|
44
|
-
lastMessage: content.trim().slice(0, 200),
|
|
45
|
-
lastMessageAt: new Date(),
|
|
46
|
-
status: 'open',
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return NextResponse.json({ message });
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.error('[ClaudMax] Chat POST error:', err);
|
|
53
|
-
return NextResponse.json({ error: 'Unable to send message. Please try again.' }, { status: 500 });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
export async function POST(req: Request) {
|
|
5
|
-
try {
|
|
6
|
-
const body = await req.json();
|
|
7
|
-
const { name, whatsapp, email, content } = body;
|
|
8
|
-
|
|
9
|
-
if (!name?.trim() || !email?.trim()) {
|
|
10
|
-
return NextResponse.json({ error: 'Name and email are required' }, { status: 400 });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
14
|
-
if (!emailRegex.test(email)) {
|
|
15
|
-
return NextResponse.json({ error: 'Invalid email address' }, { status: 400 });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Create session
|
|
19
|
-
const session = await prisma.chatSession.create({
|
|
20
|
-
data: {
|
|
21
|
-
visitorName: name.trim(),
|
|
22
|
-
whatsapp: whatsapp?.trim() || null,
|
|
23
|
-
email: email.trim().toLowerCase(),
|
|
24
|
-
status: 'open',
|
|
25
|
-
lastMessage: content?.trim() || null,
|
|
26
|
-
lastMessageAt: content?.trim() ? new Date() : null,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Create first message if provided
|
|
31
|
-
if (content?.trim()) {
|
|
32
|
-
await prisma.chatMessage.create({
|
|
33
|
-
data: { sessionId: session.id, content: content.trim(), sender: 'visitor' },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return NextResponse.json({ session });
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.error('[ClaudMax] Chat session error:', err);
|
|
40
|
-
return NextResponse.json({ error: 'Unable to create session. Please try again.' }, { status: 500 });
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function GET() {
|
|
45
|
-
// List all open sessions for admin
|
|
46
|
-
const sessions = await prisma.chatSession.findMany({
|
|
47
|
-
orderBy: { updatedAt: 'desc' },
|
|
48
|
-
select: {
|
|
49
|
-
id: true,
|
|
50
|
-
visitorName: true,
|
|
51
|
-
whatsapp: true,
|
|
52
|
-
email: true,
|
|
53
|
-
status: true,
|
|
54
|
-
unreadAdmin: true,
|
|
55
|
-
lastMessage: true,
|
|
56
|
-
lastMessageAt: true,
|
|
57
|
-
createdAt: true,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return NextResponse.json({ sessions });
|
|
62
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
export async function GET(req: Request) {
|
|
5
|
-
const { searchParams } = new URL(req.url);
|
|
6
|
-
const sessionId = searchParams.get('sessionId');
|
|
7
|
-
|
|
8
|
-
if (sessionId) {
|
|
9
|
-
const messages = await prisma.chatMessage.findMany({
|
|
10
|
-
where: { sessionId },
|
|
11
|
-
orderBy: { createdAt: 'asc' },
|
|
12
|
-
});
|
|
13
|
-
return NextResponse.json({ messages });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Admin only — list all sessions
|
|
17
|
-
const sessions = await prisma.chatSession.findMany({
|
|
18
|
-
orderBy: { updatedAt: 'desc' },
|
|
19
|
-
});
|
|
20
|
-
return NextResponse.json({ sessions });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function POST(req: Request) {
|
|
24
|
-
const body = await req.json();
|
|
25
|
-
const { sessionId, content, sender = 'visitor' } = body;
|
|
26
|
-
|
|
27
|
-
if (!sessionId || !content?.trim()) {
|
|
28
|
-
return NextResponse.json({ error: 'sessionId and content required' }, { status: 400 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const message = await prisma.chatMessage.create({
|
|
32
|
-
data: { sessionId, content: content.trim(), sender },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
await prisma.chatSession.update({
|
|
36
|
-
where: { id: sessionId },
|
|
37
|
-
data: {
|
|
38
|
-
lastMessage: content.trim().slice(0, 200),
|
|
39
|
-
lastMessageAt: new Date(),
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return NextResponse.json({ message });
|
|
44
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { Resend } from 'resend';
|
|
3
|
-
|
|
4
|
-
const TO_EMAIL = 'codewpremium@gmail.com';
|
|
5
|
-
const FROM_EMAIL = 'ClaudMax Support <onboarding@resend.dev>';
|
|
6
|
-
|
|
7
|
-
function getResendClient() {
|
|
8
|
-
if (!process.env.RESEND_API_KEY) return null;
|
|
9
|
-
return new Resend(process.env.RESEND_API_KEY);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function POST(req: Request) {
|
|
13
|
-
try {
|
|
14
|
-
const body = await req.json();
|
|
15
|
-
const { name, whatsapp, email, subject, content } = body;
|
|
16
|
-
|
|
17
|
-
if (!name?.trim() || !email?.trim()) {
|
|
18
|
-
return NextResponse.json({ error: 'Name and email are required' }, { status: 400 });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
22
|
-
if (!emailRegex.test(email)) {
|
|
23
|
-
return NextResponse.json({ error: 'Invalid email address' }, { status: 400 });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const client = getResendClient();
|
|
27
|
-
if (client) {
|
|
28
|
-
const emailBody = `
|
|
29
|
-
New support message received!
|
|
30
|
-
|
|
31
|
-
Name: ${name.trim()}
|
|
32
|
-
Email: ${email.trim()}
|
|
33
|
-
WhatsApp: ${whatsapp?.trim() || 'Not provided'}
|
|
34
|
-
Subject: ${subject?.trim() || 'No subject'}
|
|
35
|
-
|
|
36
|
-
Message:
|
|
37
|
-
${content?.trim() || 'No message content'}
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
Sent from ClaudMax Support Form
|
|
41
|
-
`.trim();
|
|
42
|
-
|
|
43
|
-
const emailSubject = subject?.trim()
|
|
44
|
-
? `[ClaudMax Support] ${subject.trim()}`
|
|
45
|
-
: `[ClaudMax Support] New message from ${name.trim()}`;
|
|
46
|
-
|
|
47
|
-
await client.emails.send({
|
|
48
|
-
from: FROM_EMAIL,
|
|
49
|
-
to: TO_EMAIL,
|
|
50
|
-
subject: emailSubject,
|
|
51
|
-
text: emailBody,
|
|
52
|
-
replyTo: email.trim(),
|
|
53
|
-
});
|
|
54
|
-
} else {
|
|
55
|
-
console.log('[ClaudMax] Email would be sent (RESEND_API_KEY not configured):', { name, email, subject, content });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return NextResponse.json({ success: true, message: 'Email sent successfully' });
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.error('[ClaudMax] Email send error:', err);
|
|
61
|
-
return NextResponse.json({ error: 'Failed to send email. Please try again.' }, { status: 500 });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
-
|
|
4
|
-
// Browser/Edge compatible base64 encoding (avoids Node.js Buffer)
|
|
5
|
-
function toBase64(arrayBuffer: ArrayBuffer, mimeType: string): string {
|
|
6
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
7
|
-
let binary = '';
|
|
8
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
9
|
-
binary += String.fromCharCode(bytes[i]);
|
|
10
|
-
}
|
|
11
|
-
return `data:${mimeType};base64,${btoa(binary)}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
15
|
-
|
|
16
|
-
async function validateKey(apiKey: string) {
|
|
17
|
-
if (!apiKey?.startsWith('sk-cmx_')) return { error: 'Invalid API key', status: 401 };
|
|
18
|
-
const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
|
|
19
|
-
if (!key) return { error: 'Invalid API key', status: 401 };
|
|
20
|
-
if (!key.isActive) return { error: 'API key revoked', status: 401 };
|
|
21
|
-
return { key };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function POST(req: Request) {
|
|
25
|
-
try {
|
|
26
|
-
const apiKey = req.headers.get('x-api-key') ?? req.headers.get('authorization')?.replace('Bearer ', '');
|
|
27
|
-
if (!apiKey) {
|
|
28
|
-
return NextResponse.json({ error: 'Missing API key' }, { status: 401 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const val = await validateKey(apiKey);
|
|
32
|
-
if ('error' in val) {
|
|
33
|
-
return NextResponse.json({ error: val.error }, { status: val.status });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Support both JSON and multipart/form-data
|
|
37
|
-
const contentType = req.headers.get('content-type') ?? '';
|
|
38
|
-
let imageData: string | Blob | null = null;
|
|
39
|
-
let prompt = 'Describe this image in detail.';
|
|
40
|
-
|
|
41
|
-
if (contentType.includes('application/json')) {
|
|
42
|
-
const body = await req.json();
|
|
43
|
-
imageData = body.image_url ?? body.image ?? body.image_source ?? null;
|
|
44
|
-
prompt = body.prompt ?? prompt;
|
|
45
|
-
} else {
|
|
46
|
-
const formData = await req.formData();
|
|
47
|
-
imageData = formData.get('image') ?? formData.get('image_url') ?? null;
|
|
48
|
-
prompt = (formData.get('prompt') as string | null) ?? prompt;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!imageData) {
|
|
52
|
-
return NextResponse.json({ error: 'Image is required' }, { status: 400 });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Build image content block
|
|
56
|
-
let imageContent: Record<string, unknown>;
|
|
57
|
-
|
|
58
|
-
if (typeof imageData === 'string') {
|
|
59
|
-
// Base64 data URL or HTTP URL
|
|
60
|
-
imageContent = { type: 'image_url', image_url: { url: imageData } };
|
|
61
|
-
} else if (imageData instanceof Blob) {
|
|
62
|
-
// File upload — convert to base64
|
|
63
|
-
const mimeType = imageData.type || 'image/jpeg';
|
|
64
|
-
imageContent = { type: 'image_url', image_url: { url: toBase64(await imageData.arrayBuffer(), mimeType) } };
|
|
65
|
-
} else {
|
|
66
|
-
return NextResponse.json({ error: 'Invalid image format' }, { status: 400 });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const visionRes = await fetch(OPENROUTER_API_URL, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
73
|
-
'Content-Type': 'application/json',
|
|
74
|
-
'HTTP-Referer': process.env.OPENROUTER_SITE_URL ?? 'https://claudmax.pro',
|
|
75
|
-
'X-Title': 'ClaudMax',
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify({
|
|
78
|
-
model: 'qwen/qwen3.6-plus:free',
|
|
79
|
-
messages: [
|
|
80
|
-
{
|
|
81
|
-
role: 'user',
|
|
82
|
-
content: [
|
|
83
|
-
imageContent,
|
|
84
|
-
{ type: 'text', text: prompt },
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
max_tokens: 2048,
|
|
89
|
-
temperature: 0.3,
|
|
90
|
-
}),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (!visionRes.ok) {
|
|
94
|
-
const err = await visionRes.json().catch(() => ({}));
|
|
95
|
-
return NextResponse.json(
|
|
96
|
-
{ error: { type: 'upstream_error', message: (err as any)?.error?.message ?? 'Image analysis failed' } },
|
|
97
|
-
{ status: visionRes.status }
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const data = await visionRes.json();
|
|
102
|
-
const description = data.choices?.[0]?.message?.content ?? 'Unable to analyze image.';
|
|
103
|
-
|
|
104
|
-
return NextResponse.json({
|
|
105
|
-
description,
|
|
106
|
-
type: 'image_understood',
|
|
107
|
-
model: 'claude-sonnet-4-vision',
|
|
108
|
-
});
|
|
109
|
-
} catch (err) {
|
|
110
|
-
console.error('[ClaudMax] Image analysis error:', err);
|
|
111
|
-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
112
|
-
}
|
|
113
|
-
}
|