Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9498aa82b4 | |||
| f724bb7951 | |||
| 47f085b0ce | |||
| 024307a329 | |||
| d25abace00 | |||
| 84bc3cf9a9 | |||
| 900da199cc | |||
| 7ce8778c7d | |||
| 46abdcc528 | |||
| af75cf7116 | |||
| 46b3d86dbb | |||
| 0b61819b2d | |||
| 21eede5c27 | |||
| 5d284ddb55 | |||
| 9b3ea0bf7c | |||
| f89656b582 | |||
| 7691c30bd5 | |||
| 622b02bb80 | |||
| 9156cece88 | |||
| 08e85a3827 | |||
| 85969a537b | |||
| 0e8a742dd9 | |||
| e9b8d6c6a5 | |||
| c103d1d708 | |||
| 279890f962 | |||
| f4b1be8330 | |||
| f0f05357ca | |||
| 2d6a628391 | |||
| cfef0bfc4b | |||
| 9943bbe4b7 | |||
| 4f26d270ed | |||
| 6c66be57a6 | |||
| 7bc643c889 | |||
| b77c8ed13d | |||
| 7e6d9ff4a2 | |||
| d237a6a7f3 | |||
| ec63c8e3ff | |||
| bc1960f1a5 | |||
| b3b2a4a09c | |||
| 7f02c0fa8a | |||
| 6d0b72ebd4 | |||
| 9972e8b2b4 | |||
| 22cef958c6 | |||
| c234ed3f38 | |||
| 1c810f3e28 | |||
| 6043696ca7 | |||
| e7f81e24dc | |||
| 28a2806d4b | |||
| 9683bef22e | |||
| 3e53eebcf4 | |||
| b406431755 | |||
| caf297283c | |||
| 9f7c200e69 | |||
| 4f26d0e120 | |||
| 039a4028a3 | |||
| e11a167a55 | |||
| 4639de7157 | |||
| 1462653763 | |||
| ebdabfefb4 | |||
| 560dc17883 | |||
| ea715bc2b8 | |||
| 1189277f46 | |||
| 73e017bb6f | |||
| 11e6c3f440 | |||
| 50a97c0729 | |||
| b991170aec | |||
| 6d1fbd3152 | |||
| 07005ec619 | |||
| 818ac2f32e | |||
| 246418367f | |||
| 2b5ff2c672 | |||
| 1e032ed819 | |||
| a7112fa127 | |||
| b529c1b275 | |||
| 345989463f | |||
| fbf2a69203 | |||
| b45918c4c3 | |||
| 60410ea046 | |||
| 9db18e74b7 | |||
| 954fbea6ba | |||
| 402016da36 | |||
| 2fac69e1f7 | |||
| 05c32f93ab | |||
| 0e8d8e2449 | |||
| d55748d291 | |||
| 612114beaf | |||
| 6a1d232347 | |||
| 437be3d61d | |||
| 008698943a | |||
| 62b5767bc9 | |||
| 8496265084 | |||
| a3ee86b65b | |||
| 2379887c93 | |||
| 1e029580c6 | |||
| e52826f731 | |||
| b428739a19 | |||
| 867147829b | |||
| 42c6125e94 | |||
| 63b0b66739 | |||
| cfdb8726d8 | |||
| b04295fa90 | |||
| 77799aadcc | |||
| f51e399dcb | |||
| 63b371b678 | |||
| d26692fbf2 | |||
| 0034dc39b5 | |||
| 36e73e4ad2 | |||
| 05ed853084 | |||
| 59bf6f13fe | |||
| dcc5c5cbbf | |||
| 1803c8e297 | |||
| 14f7cfadf8 | |||
| 1d93108df9 | |||
| cde0e0724c | |||
| ee712a0c8c | |||
| 721da5e226 | |||
| e3c658bd79 | |||
| 3b49211aab | |||
| b991f8fe10 | |||
| 9fbeddf8f4 | |||
| 252bcb5de3 | |||
| 4ef6246284 | |||
| a253e6524a | |||
| f34113302f | |||
| 1c287d3a0f | |||
| 29064d6ada | |||
| c95f80dd75 | |||
| 0e9258c300 | |||
| 08a9c26ed2 | |||
| 1dbdb88717 | |||
| cd1715e703 | |||
| d9fe66299a | |||
| e1741b528e | |||
| 68bf2e9be8 | |||
| 266ffe9b9e | |||
| a46ef57a60 | |||
| 6505f7601f | |||
| 9684a58dbc | |||
| f50c843cb7 | |||
| 7f34e89107 | |||
| 4315ccba98 | |||
| 6759e17f1c | |||
| b1d591c946 | |||
| fad9ea6a5d | |||
| 5e5d20b705 | |||
| fa5e180371 | |||
| ba3a120f6d | |||
| 8c54e612ac | |||
| 239119a12e | |||
| 34e529cc70 | |||
| e2a494e39d | |||
| cee1e2d521 |
@@ -27,6 +27,11 @@ dist
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
docker/
|
||||
|
||||
# Cloudflare
|
||||
cloudflare/
|
||||
wrangler.jsonc
|
||||
|
||||
# Development files
|
||||
.github
|
||||
|
||||
+1
-1
@@ -66,5 +66,5 @@ CRON_SECRET="your-secret-key-here"
|
||||
# SENTRY_AUTH_TOKEN=sentry-auth-token
|
||||
|
||||
# GOOGLE_ADSENSE_ACCOUNT=ca-pub-xxxxxxxxxxxxxxxx
|
||||
# EZOIC_ENABLED=true
|
||||
# NEXT_PUBLIC_EZOIC_ENABLED=true
|
||||
# EZOIC_SITE_DOMAIN=prompts.chat
|
||||
@@ -48,3 +48,4 @@ next-env.d.ts
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
.env.production.cloudflare
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Production Dockerfile for Cloudflare Containers
|
||||
# Multi-stage build producing a lean standalone Next.js image
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 1: Install dependencies
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:24-alpine AS deps
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY prisma ./prisma/
|
||||
|
||||
RUN npm ci
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 2: Build the application
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:24-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
# Dummy URL for prisma generate (only needs schema, not a real connection)
|
||||
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
|
||||
# Skip Sentry source map upload during container build
|
||||
ENV SENTRY_AUTH_TOKEN=""
|
||||
|
||||
RUN npx prisma generate
|
||||
# Use webpack instead of Turbopack to stay within Docker memory limits
|
||||
RUN npx next build --webpack
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 3: Production runner
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:24-alpine AS runner
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
|
||||
# YAML prompt files read at runtime via readFileSync(process.cwd() + path)
|
||||
COPY --from=builder /app/src/lib/ai/*.prompt.yml ./src/lib/ai/
|
||||
COPY --from=builder /app/src/app/api/prompt-builder/chat/*.prompt.yml ./src/app/api/prompt-builder/chat/
|
||||
|
||||
# Next.js standalone output includes a minimal server and all dependencies
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
+14392
-1111
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
# Deploying prompts.chat to Cloudflare Containers
|
||||
|
||||
This guide covers deploying the Next.js application to [Cloudflare Containers](https://developers.cloudflare.com/containers/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Docker](https://docs.docker.com/desktop/) running locally
|
||||
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) (`npm i -g wrangler`)
|
||||
- A Cloudflare account on the [Workers Paid plan](https://workers.cloudflare.com/pricing/)
|
||||
- An external PostgreSQL database (e.g. Neon, Supabase, or self-hosted)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install @cloudflare/containers
|
||||
```
|
||||
|
||||
### 2. Configure secrets
|
||||
|
||||
Set required environment variables as Wrangler secrets. These are injected into the container at runtime:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
wrangler secret put DATABASE_URL
|
||||
wrangler secret put AUTH_SECRET
|
||||
|
||||
# OAuth (if using these providers)
|
||||
wrangler secret put AUTH_GITHUB_ID
|
||||
wrangler secret put AUTH_GITHUB_SECRET
|
||||
wrangler secret put AUTH_GOOGLE_ID
|
||||
wrangler secret put AUTH_GOOGLE_SECRET
|
||||
|
||||
# Optional
|
||||
wrangler secret put OPENAI_API_KEY
|
||||
wrangler secret put SENTRY_DSN
|
||||
```
|
||||
|
||||
### 3. Deploy
|
||||
|
||||
```bash
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
On the first deploy, Wrangler will:
|
||||
1. Build the Docker image locally using `Dockerfile.cloudflare`
|
||||
2. Push the image to Cloudflare's managed registry
|
||||
3. Deploy the Worker that routes traffic to the container
|
||||
|
||||
The first deploy takes a few minutes for image push and container provisioning. Subsequent deploys are faster due to layer caching.
|
||||
|
||||
### 4. Check status
|
||||
|
||||
```bash
|
||||
npx wrangler containers list
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User Request → Cloudflare Network → Worker (router) → Container (Next.js on Node.js)
|
||||
↓
|
||||
PostgreSQL DB
|
||||
```
|
||||
|
||||
- **Worker** (`cloudflare/worker.js`): Thin routing layer that forwards all requests to the container
|
||||
- **Container** (`Dockerfile.cloudflare`): Multi-stage Node.js image running the Next.js standalone server
|
||||
- The container runs the full Node.js runtime, so all dependencies (Prisma, next-auth, next-intl, sharp) work without modification
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings in `wrangler.jsonc`:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `max_instances` | 3 | Maximum concurrent container instances |
|
||||
| `instance_type` | standard-1 | 2 vCPU, 2 GiB memory |
|
||||
| `sleepAfter` | 5m | Container sleeps after 5 minutes of inactivity |
|
||||
|
||||
Adjust `max_instances` and `instance_type` based on your traffic. See [instance types](https://developers.cloudflare.com/containers/platform-details/#instance-types) for all options.
|
||||
|
||||
## Local development
|
||||
|
||||
Local development remains unchanged:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
To test the Docker image locally:
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.cloudflare -t prompts-chat .
|
||||
docker run -p 3000:3000 --env-file .env prompts-chat
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Cold starts**: Containers take 2-3 seconds to wake from sleep. The `sleepAfter` setting in `cloudflare/worker.js` controls when idle containers go to sleep.
|
||||
- **Database**: Use an external PostgreSQL provider. Cloudflare Hyperdrive can be added for edge connection pooling if needed.
|
||||
- **Containers Beta**: Cloudflare Containers is currently in beta. See [beta info](https://developers.cloudflare.com/containers/beta-info/) for limitations.
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Container } from "@cloudflare/containers";
|
||||
import { env } from "cloudflare:workers";
|
||||
|
||||
const SECRET_KEYS = [
|
||||
"NEXTAUTH_SECRET",
|
||||
"GITHUB_CLIENT_ID",
|
||||
"GITHUB_CLIENT_SECRET",
|
||||
"GOOGLE_CLIENT_ID",
|
||||
"GOOGLE_CLIENT_SECRET",
|
||||
"AUTH_APPLE_ID",
|
||||
"AUTH_APPLE_SECRET",
|
||||
"OPENAI_API_KEY",
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"DO_SPACES_ACCESS_KEY_ID",
|
||||
"DO_SPACES_SECRET_ACCESS_KEY",
|
||||
"HF_API_TOKEN",
|
||||
"FAL_API_KEY",
|
||||
"WIRO_API_KEY",
|
||||
"CRON_SECRET",
|
||||
];
|
||||
|
||||
function getSecretEnvVars() {
|
||||
const vars = {};
|
||||
for (const key of SECRET_KEYS) {
|
||||
if (env[key]) vars[key] = env[key];
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
export class PromptsChatContainer extends Container {
|
||||
defaultPort = 3000;
|
||||
sleepAfter = "5m";
|
||||
envVars = {
|
||||
NODE_ENV: "production",
|
||||
DATABASE_URL: env.DATABASE_URL || "",
|
||||
NEXTAUTH_URL: env.NEXTAUTH_URL,
|
||||
OPENAI_BASE_URL: env.OPENAI_BASE_URL,
|
||||
OPENAI_EMBEDDING_MODEL: env.OPENAI_EMBEDDING_MODEL,
|
||||
OPENAI_GENERATIVE_MODEL: env.OPENAI_GENERATIVE_MODEL,
|
||||
GOOGLE_ANALYTICS_ID: env.GOOGLE_ANALYTICS_ID,
|
||||
ENABLED_STORAGE: env.ENABLED_STORAGE,
|
||||
DO_SPACES_BUCKET: env.DO_SPACES_BUCKET,
|
||||
DO_SPACES_REGION: env.DO_SPACES_REGION,
|
||||
FAL_IMAGE_MODELS: env.FAL_IMAGE_MODELS,
|
||||
FAL_VIDEO_MODELS: env.FAL_VIDEO_MODELS,
|
||||
FAL_AUDIO_MODELS: env.FAL_AUDIO_MODELS,
|
||||
WIRO_IMAGE_MODELS: env.WIRO_IMAGE_MODELS,
|
||||
WIRO_VIDEO_MODELS: env.WIRO_VIDEO_MODELS,
|
||||
EZOIC_SITE_DOMAIN: env.EZOIC_SITE_DOMAIN,
|
||||
NEXT_PUBLIC_EZOIC_ENABLED: env.NEXT_PUBLIC_EZOIC_ENABLED,
|
||||
GOOGLE_ADSENSE_ACCOUNT: env.GOOGLE_ADSENSE_ACCOUNT,
|
||||
...getSecretEnvVars(),
|
||||
};
|
||||
|
||||
onStart() {
|
||||
console.log("prompts.chat container started");
|
||||
}
|
||||
|
||||
onStop() {
|
||||
console.log("prompts.chat container stopped");
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
console.error("prompts.chat container error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const container = env.PROMPTS_CHAT.getByName("main");
|
||||
return container.fetch(request);
|
||||
},
|
||||
};
|
||||
+9
-5
@@ -146,23 +146,27 @@ if [ ! -f "$SEED_MARKER" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Wait for supervisord socket to be ready
|
||||
# Wait for supervisord to be ready
|
||||
echo "▶ Waiting for supervisord..."
|
||||
for i in $(seq 1 30); do
|
||||
if supervisorctl -c /etc/supervisor/conf.d/supervisord.conf status >/dev/null 2>&1; then
|
||||
if kill -0 $SUPERVISOR_PID 2>/dev/null && [ -S /var/run/supervisor.sock ]; then
|
||||
echo "✓ Supervisord is ready"
|
||||
break
|
||||
fi
|
||||
if ! kill -0 $SUPERVISOR_PID 2>/dev/null; then
|
||||
echo "✗ Supervisord process died"
|
||||
exit 1
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "✗ Supervisord failed to start"
|
||||
echo "✗ Supervisord socket not available after 30s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Start Next.js
|
||||
# Start Next.js via supervisorctl
|
||||
echo "▶ Starting Next.js..."
|
||||
supervisorctl -c /etc/supervisor/conf.d/supervisord.conf start nextjs
|
||||
supervisorctl start nextjs
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
[unix_http_server]
|
||||
file=/var/run/supervisor.sock
|
||||
chmod=0700
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///var/run/supervisor.sock
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
+5
-13
@@ -55,7 +55,7 @@ const nextConfig: NextConfig = {
|
||||
destination: "/embed",
|
||||
permanent: true,
|
||||
},
|
||||
// Redirect book PDF downloads to GitHub raw to save Vercel edge bandwidth
|
||||
// Redirect book PDF downloads to GitHub raw to save bandwidth
|
||||
{
|
||||
source: "/book-pdf/:filename",
|
||||
destination: "https://raw.githubusercontent.com/f/prompts.chat/refs/heads/main/public/book-pdf/:filename",
|
||||
@@ -82,22 +82,14 @@ export default withSentryConfig(withMDX(withNextIntl(nextConfig)), {
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
// tunnelRoute removed — was proxying all browser Sentry events through Vercel edge,
|
||||
// generating unnecessary edge requests ($2.45/M) and function invocations ($0.60/M).
|
||||
// Estimated savings: $100-400/month. Trade-off: some ad-blockers may block direct Sentry calls.
|
||||
// See: https://github.com/f/prompts.chat/issues/1085
|
||||
|
||||
webpack: {
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true,
|
||||
|
||||
// Tree-shaking options for reducing bundle size
|
||||
treeshake: {
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
removeDebugLogging: true,
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+944
-109
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -32,6 +32,7 @@
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.11.1",
|
||||
"@aws-sdk/client-s3": "^3.948.0",
|
||||
"@cloudflare/containers": "^0.1.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
@@ -111,7 +112,8 @@
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^2.1.8",
|
||||
"wrangler": "^4.76.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "24.x"
|
||||
|
||||
@@ -139,6 +139,8 @@ model Prompt {
|
||||
@@index([isPrivate])
|
||||
@@index([isFeatured])
|
||||
@@index([isUnlisted])
|
||||
@@index([slug])
|
||||
@@index([isPrivate, isUnlisted, deletedAt, createdAt(sort: Desc)])
|
||||
@@map("prompts")
|
||||
}
|
||||
|
||||
|
||||
+13528
-979
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
<svg width="240" height="24" viewBox="0 0 240 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.4283 0.610352L26.8565 23.8046H0L13.4283 0.610352Z" fill="#fff"/>
|
||||
<path d="M236.436 22.7547L235.081 17.1148V23.8045H233.849V15.1372H235.557L236.924 20.7405L238.291 15.1372H240V23.8045H238.767V17.1148L237.412 22.7547H236.436Z" fill="#fff"/>
|
||||
<path d="M226.237 23.8045L228.776 15.1372H230.436L232.974 23.8045H231.644L231.022 21.5462H228.19L227.568 23.8045H226.237ZM229.606 16.4678L228.52 20.362H230.692L229.606 16.4678Z" fill="#fff"/>
|
||||
<path d="M219.444 23.8045V15.1372H222.142C223.972 15.1372 225.095 16.126 225.095 17.7252C225.095 18.7628 224.436 19.5197 223.631 19.7761C224.485 19.9348 224.9 20.4109 224.973 21.3386L225.193 23.8045H223.911L223.716 21.5828C223.643 20.716 223.301 20.4231 222.239 20.4231H220.726V23.8045H219.444ZM220.726 19.2267H222.117C223.179 19.2267 223.777 18.7018 223.777 17.7862C223.777 16.8585 223.179 16.3458 222.117 16.3458H220.726V19.2267Z" fill="#fff"/>
|
||||
<path d="M214.957 23.9999C213.09 23.9999 211.748 22.4251 211.748 19.4831C211.748 16.6876 212.956 14.9419 214.994 14.9419C216.63 14.9419 217.704 16.0772 218.082 17.8107L216.752 17.8961C216.508 16.8341 215.934 16.1504 215.006 16.1504C213.774 16.1504 213.078 17.3468 213.078 19.4831C213.078 21.6072 213.798 22.7913 215.067 22.7913C216.178 22.7913 216.861 21.8758 216.91 20.4963H215.006V19.3854H218.094V23.8045H217.057L217.02 22.6937C216.642 23.4749 215.983 23.9999 214.957 23.9999Z" fill="#fff"/>
|
||||
<path d="M207.651 23.9999C205.589 23.9999 204.429 22.3763 204.429 19.4831C204.429 16.5655 205.589 14.9419 207.651 14.9419C209.714 14.9419 210.873 16.5655 210.873 19.4831C210.873 22.3763 209.714 23.9999 207.651 23.9999ZM207.651 22.8035C208.872 22.8035 209.543 21.6804 209.543 19.4831C209.543 17.2857 208.872 16.1382 207.651 16.1382C206.431 16.1382 205.76 17.2857 205.76 19.4831C205.76 21.6804 206.431 22.8035 207.651 22.8035Z" fill="#fff"/>
|
||||
<path d="M197.489 23.8045V15.1372H200.187C202.017 15.1372 203.14 16.126 203.14 17.7252C203.14 18.7628 202.481 19.5197 201.676 19.7761C202.53 19.9348 202.945 20.4109 203.018 21.3386L203.238 23.8045H201.956L201.761 21.5828C201.688 20.716 201.346 20.4231 200.284 20.4231H198.771V23.8045H197.489ZM198.771 19.2267H200.162C201.224 19.2267 201.822 18.7018 201.822 17.7862C201.822 16.8585 201.224 16.3458 200.162 16.3458H198.771V19.2267Z" fill="#fff"/>
|
||||
<path d="M190.403 23.8045V15.1372H193.088C194.894 15.1372 196.09 16.1626 196.09 17.8351C196.09 19.4953 194.894 20.5207 193.088 20.5207H191.684V23.8045H190.403ZM191.684 19.3122H192.954C194.186 19.3122 194.76 18.7995 194.76 17.8351C194.76 16.8707 194.186 16.3458 192.954 16.3458H191.684V19.3122Z" fill="#fff"/>
|
||||
<path d="M175.791 23.8045V15.1372H181.112V16.3458H177.073V18.8727H180.966V20.0446H177.073V22.596H181.21V23.8045H175.791Z" fill="#fff"/>
|
||||
<path d="M168.216 23.8045V15.1372H170.914C172.744 15.1372 173.867 16.126 173.867 17.7252C173.867 18.7628 173.208 19.5197 172.403 19.7761C173.257 19.9348 173.672 20.4109 173.745 21.3386L173.965 23.8045H172.683L172.488 21.5828C172.415 20.716 172.073 20.4231 171.011 20.4231H169.498V23.8045H168.216ZM169.498 19.2267H170.889C171.951 19.2267 172.549 18.7018 172.549 17.7862C172.549 16.8585 171.951 16.3458 170.889 16.3458H169.498V19.2267Z" fill="#fff"/>
|
||||
<path d="M160.374 23.8045L162.912 15.1372H164.572L167.111 23.8045H165.78L165.158 21.5462H162.326L161.704 23.8045H160.374ZM163.742 16.4678L162.656 20.362H164.828L163.742 16.4678Z" fill="#fff"/>
|
||||
<path d="M154.032 23.8045L153.104 15.1372H154.386L155.045 22.1565L155.862 16.1138H156.985L157.803 22.1565L158.462 15.1372H159.744L158.816 23.8045H157.144L156.424 18.5309L155.704 23.8045H154.032Z" fill="#fff"/>
|
||||
<path d="M148.471 23.8045V16.3458H145.932V15.1372H152.279V16.3458H149.74V23.8045H148.471Z" fill="#fff"/>
|
||||
<path d="M139.322 23.8045V15.1372H144.741V16.3213H140.604V18.9826H144.522V20.1545H140.604V23.8045H139.322Z" fill="#fff"/>
|
||||
<path d="M134.469 23.9999C132.407 23.9999 131.247 22.3763 131.247 19.4831C131.247 16.5655 132.407 14.9419 134.469 14.9419C136.532 14.9419 137.691 16.5655 137.691 19.4831C137.691 22.3763 136.532 23.9999 134.469 23.9999ZM134.469 22.8035C135.69 22.8035 136.361 21.6804 136.361 19.4831C136.361 17.2857 135.69 16.1382 134.469 16.1382C133.249 16.1382 132.577 17.2857 132.577 19.4831C132.577 21.6804 133.249 22.8035 134.469 22.8035Z" fill="#fff"/>
|
||||
<path d="M127.261 23.9999C125.381 23.9999 124.161 22.8035 124.014 20.9846L125.332 20.8992C125.503 22.1443 126.211 22.8035 127.31 22.8035C128.371 22.8035 128.969 22.3763 128.969 21.6072C128.969 20.8381 128.554 20.3987 126.736 19.8859C124.954 19.3976 124.209 18.7995 124.209 17.4566C124.209 15.9673 125.308 14.9419 127.126 14.9419C128.933 14.9419 129.97 16.0772 130.141 17.7496L128.823 17.8229C128.689 16.8219 128.127 16.1382 127.09 16.1382C126.138 16.1382 125.528 16.6387 125.528 17.3956C125.528 18.1525 125.955 18.4332 127.493 18.8605C129.567 19.4221 130.288 20.1911 130.288 21.6072C130.288 23.0355 129.079 23.9999 127.261 23.9999Z" fill="#fff"/>
|
||||
<path d="M109.927 23.8045V15.1372H115.248V16.3458H111.208V18.8727H115.102V20.0446H111.208V22.596H115.346V23.8045H109.927Z" fill="#fff"/>
|
||||
<path d="M105.232 23.9999C103.145 23.9999 101.974 22.2664 101.974 19.4831C101.974 16.6876 103.145 14.9419 105.232 14.9419C106.807 14.9419 107.942 16.0284 108.296 17.9083L106.953 17.9816C106.709 16.773 106.05 16.1382 105.232 16.1382C103.975 16.1382 103.304 17.3468 103.304 19.4831C103.304 21.6072 103.975 22.8035 105.232 22.8035C106.123 22.8035 106.795 22.1077 107.014 20.7893L108.345 20.8503C108.04 22.8279 106.892 23.9999 105.232 23.9999Z" fill="#fff"/>
|
||||
<path d="M95.0342 23.8045V15.1372H97.7315C99.5622 15.1372 100.685 16.126 100.685 17.7252C100.685 18.7628 100.026 19.5197 99.2205 19.7761C100.075 19.9348 100.49 20.4109 100.563 21.3386L100.783 23.8045H99.5012L99.3059 21.5828C99.2327 20.716 98.891 20.4231 97.8291 20.4231H96.3157V23.8045H95.0342ZM96.3157 19.2267H97.7071C98.7689 19.2267 99.367 18.7018 99.367 17.7862C99.367 16.8585 98.7689 16.3458 97.7071 16.3458H96.3157V19.2267Z" fill="#fff"/>
|
||||
<path d="M90.5596 23.9999C88.7166 23.9999 87.606 22.8035 87.606 20.7893V15.1372H88.8875V20.8381C88.8875 22.1077 89.4733 22.8035 90.5596 22.8035C91.6458 22.8035 92.2317 22.1077 92.2317 20.8381V15.1372H93.5132V20.7893C93.5132 22.8035 92.4025 23.9999 90.5596 23.9999Z" fill="#fff"/>
|
||||
<path d="M83.2417 23.9999C81.179 23.9999 80.0195 22.3763 80.0195 19.4831C80.0195 16.5655 81.179 14.9419 83.2417 14.9419C85.3043 14.9419 86.4638 16.5655 86.4638 19.4831C86.4638 22.3763 85.3043 23.9999 83.2417 23.9999ZM83.2417 22.8035C84.4622 22.8035 85.1334 21.6804 85.1334 19.4831C85.1334 17.2857 84.4622 16.1382 83.2417 16.1382C82.0212 16.1382 81.3499 17.2857 81.3499 19.4831C81.3499 21.6804 82.0212 22.8035 83.2417 22.8035Z" fill="#fff"/>
|
||||
<path d="M76.0332 23.9999C74.1536 23.9999 72.9331 22.8035 72.7866 20.9846L74.1048 20.8992C74.2756 22.1443 74.9835 22.8035 76.082 22.8035C77.1438 22.8035 77.7419 22.3763 77.7419 21.6072C77.7419 20.8381 77.3269 20.3987 75.5083 19.8859C73.7264 19.3976 72.9819 18.7995 72.9819 17.4566C72.9819 15.9673 74.0804 14.9419 75.8989 14.9419C77.7052 14.9419 78.7427 16.0772 78.9135 17.7496L77.5954 17.8229C77.4611 16.8219 76.8997 16.1382 75.8623 16.1382C74.9103 16.1382 74.3 16.6387 74.3 17.3956C74.3 18.1525 74.7272 18.4332 76.265 18.8605C78.3399 19.4221 79.06 20.1911 79.06 21.6072C79.06 23.0355 77.8517 23.9999 76.0332 23.9999Z" fill="#fff"/>
|
||||
<path d="M58.4556 23.8045V15.1372H60.0178L62.8616 21.9856V15.1372H64.1187V23.8045H62.5564L59.7127 16.9561V23.8045H58.4556Z" fill="#fff"/>
|
||||
<path d="M51.3813 23.8045V15.1372H56.7027V16.3458H52.6629V18.8727H56.5563V20.0446H52.6629V22.596H56.8004V23.8045H51.3813Z" fill="#fff"/>
|
||||
<path d="M44.0386 23.8045V15.1372H46.7237C48.53 15.1372 49.7261 16.1626 49.7261 17.8351C49.7261 19.4953 48.53 20.5207 46.7237 20.5207H45.3201V23.8045H44.0386ZM45.3201 19.3122H46.5894C47.8221 19.3122 48.3958 18.7995 48.3958 17.8351C48.3958 16.8707 47.8221 16.3458 46.5894 16.3458H45.3201V19.3122Z" fill="#fff"/>
|
||||
<path d="M39.332 23.9999C37.2693 23.9999 36.1099 22.3763 36.1099 19.4831C36.1099 16.5655 37.2693 14.9419 39.332 14.9419C41.3946 14.9419 42.5541 16.5655 42.5541 19.4831C42.5541 22.3763 41.3946 23.9999 39.332 23.9999ZM39.332 22.8035C40.5525 22.8035 41.2238 21.6804 41.2238 19.4831C41.2238 17.2857 40.5525 16.1382 39.332 16.1382C38.1115 16.1382 37.4402 17.2857 37.4402 19.4831C37.4402 21.6804 38.1115 22.8035 39.332 22.8035Z" fill="#fff"/>
|
||||
<path d="M171.085 9.35094C169.083 9.35094 167.96 7.95929 167.96 5.46895C167.96 2.60019 168.864 0.292969 171.414 0.292969C173.013 0.292969 173.819 1.1597 174.148 2.38045L172.83 2.49032C172.659 1.89215 172.269 1.48931 171.414 1.48931C170.243 1.48931 169.474 2.36825 169.303 4.2482C169.718 3.71107 170.426 3.32043 171.329 3.32043C173.05 3.32043 174.161 4.48015 174.161 6.25024C174.161 8.16681 172.916 9.35094 171.085 9.35094ZM171.085 8.1424C172.147 8.1424 172.83 7.45878 172.83 6.27465C172.83 5.17597 172.196 4.48015 171.183 4.48015C170.096 4.48015 169.315 5.17597 169.315 6.27465C169.315 7.34891 169.999 8.1424 171.085 8.1424Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M160.667 9.15562C160.667 7.22684 161.387 6.01829 163.925 4.67547C165.011 4.10171 165.475 3.72328 165.475 2.88096C165.475 2.05085 164.95 1.48931 163.937 1.48931C162.827 1.48931 162.217 2.09968 162.046 3.18615L160.715 3.1007C160.898 1.37944 162.021 0.292969 163.937 0.292969C165.78 0.292969 166.806 1.3184 166.806 2.83213C166.806 4.17496 166.22 4.83416 164.45 5.78635C162.949 6.59205 162.241 7.36112 162.192 7.94708H166.818V9.15562H160.667Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M156.424 9.35094C154.508 9.35094 153.348 7.61748 153.348 4.83416C153.348 2.03864 154.508 0.292969 156.424 0.292969C158.34 0.292969 159.499 2.03864 159.499 4.83416C159.499 7.61748 158.34 9.35094 156.424 9.35094ZM154.678 4.83416C154.678 5.55441 154.752 6.1892 154.91 6.68971L157.449 1.98981C157.181 1.66021 156.827 1.48931 156.424 1.48931C155.338 1.48931 154.678 2.72226 154.678 4.83416ZM156.424 8.15461C157.51 8.15461 158.169 6.92165 158.169 4.83416C158.169 4.10171 158.096 3.46692 157.937 2.96641L155.386 7.6541C155.667 7.9837 156.021 8.15461 156.424 8.15461Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M146.03 9.15562C146.03 7.22684 146.75 6.01829 149.289 4.67547C150.375 4.10171 150.839 3.72328 150.839 2.88096C150.839 2.05085 150.314 1.48931 149.301 1.48931C148.19 1.48931 147.58 2.09968 147.409 3.18615L146.079 3.1007C146.262 1.37944 147.385 0.292969 149.301 0.292969C151.144 0.292969 152.169 1.3184 152.169 2.83213C152.169 4.17496 151.583 4.83416 149.813 5.78635C148.312 6.59205 147.604 7.36112 147.555 7.94708H152.181V9.15562H146.03Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M132.041 10.4985L135.727 0H136.899L133.212 10.4985H132.041Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M124.722 10.4985L128.409 0H129.581L125.894 10.4985H124.722Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M111.684 9.15582V7.54443H113.344V9.15582H111.684Z" fill="#fff"/>
|
||||
<path d="M105.232 9.35094C103.145 9.35094 101.974 7.61748 101.974 4.83416C101.974 2.03864 103.145 0.292969 105.232 0.292969C106.807 0.292969 107.942 1.37944 108.296 3.25939L106.953 3.33264C106.709 2.1241 106.05 1.48931 105.232 1.48931C103.975 1.48931 103.304 2.69785 103.304 4.83416C103.304 6.95827 103.975 8.15461 105.232 8.15461C106.123 8.15461 106.795 7.45878 107.014 6.14037L108.345 6.20141C108.04 8.17902 106.892 9.35094 105.232 9.35094Z" fill="#fff"/>
|
||||
<path d="M95.0464 9.15562V0.488281H96.6086L99.4524 7.3367V0.488281H100.71V9.15562H99.1473L96.3035 2.3072V9.15562H95.0464Z" fill="#fff"/>
|
||||
<path d="M87.8501 9.15562V7.94707H89.9249V1.69683H87.8501V0.488281H93.2691V1.69683H91.1943V7.94707H93.2691V9.15562H87.8501Z" fill="#fff"/>
|
||||
<path d="M73.4575 9.15562V0.488281H74.739V7.94707H78.8765V9.15562H73.4575Z" fill="#fff"/>
|
||||
<path d="M66.0171 9.15562V0.488281H71.3385V1.69683H67.2986V4.22378H71.192V5.3957H67.2986V7.94707H71.4361V9.15562H66.0171Z" fill="#fff"/>
|
||||
<path d="M61.3232 9.35094C59.2361 9.35094 58.0645 7.61748 58.0645 4.83416C58.0645 2.03864 59.2361 0.292969 61.3232 0.292969C62.8976 0.292969 64.0327 1.37944 64.3866 3.25939L63.0441 3.33264C62.8 2.1241 62.1409 1.48931 61.3232 1.48931C60.0661 1.48931 59.3948 2.69785 59.3948 4.83416C59.3948 6.95827 60.0661 8.15461 61.3232 8.15461C62.2142 8.15461 62.8854 7.45878 63.1051 6.14037L64.4355 6.20141C64.1303 8.17902 62.9831 9.35094 61.3232 9.35094Z" fill="#fff"/>
|
||||
<path d="M51.125 9.15562V0.488281H53.8223C55.6531 0.488281 56.7759 1.47709 56.7759 3.07627C56.7759 4.11391 56.1168 4.87078 55.3113 5.12714C56.1657 5.28583 56.5806 5.76193 56.6539 6.6897L56.8736 9.15562H55.592L55.3968 6.93385C55.3235 6.06711 54.9818 5.77413 53.9199 5.77413H52.4065V9.15562H51.125ZM52.4065 4.5778H53.7979C54.8597 4.5778 55.4578 4.05288 55.4578 3.13731C55.4578 2.20954 54.8597 1.69683 53.7979 1.69683H52.4065V4.5778Z" fill="#fff"/>
|
||||
<path d="M44.063 9.15562V0.488281H49.3844V1.69683H45.3445V4.22378H49.2379V5.3957H45.3445V7.94707H49.482V9.15562H44.063Z" fill="#fff"/>
|
||||
<path d="M38.5753 9.15562L36 0.488281H37.3548L39.332 7.44656L41.3092 0.488281H42.6639L40.0887 9.15562H38.5753Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,44 @@
|
||||
<svg width="240" height="24" viewBox="0 0 240 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.4283 0.610352L26.8565 23.8046H0L13.4283 0.610352Z" fill="#000"/>
|
||||
<path d="M236.436 22.7547L235.081 17.1148V23.8045H233.849V15.1372H235.557L236.924 20.7405L238.291 15.1372H240V23.8045H238.767V17.1148L237.412 22.7547H236.436Z" fill="#000"/>
|
||||
<path d="M226.237 23.8045L228.776 15.1372H230.436L232.974 23.8045H231.644L231.022 21.5462H228.19L227.568 23.8045H226.237ZM229.606 16.4678L228.52 20.362H230.692L229.606 16.4678Z" fill="#000"/>
|
||||
<path d="M219.444 23.8045V15.1372H222.142C223.972 15.1372 225.095 16.126 225.095 17.7252C225.095 18.7628 224.436 19.5197 223.631 19.7761C224.485 19.9348 224.9 20.4109 224.973 21.3386L225.193 23.8045H223.911L223.716 21.5828C223.643 20.716 223.301 20.4231 222.239 20.4231H220.726V23.8045H219.444ZM220.726 19.2267H222.117C223.179 19.2267 223.777 18.7018 223.777 17.7862C223.777 16.8585 223.179 16.3458 222.117 16.3458H220.726V19.2267Z" fill="#000"/>
|
||||
<path d="M214.957 23.9999C213.09 23.9999 211.748 22.4251 211.748 19.4831C211.748 16.6876 212.956 14.9419 214.994 14.9419C216.63 14.9419 217.704 16.0772 218.082 17.8107L216.752 17.8961C216.508 16.8341 215.934 16.1504 215.006 16.1504C213.774 16.1504 213.078 17.3468 213.078 19.4831C213.078 21.6072 213.798 22.7913 215.067 22.7913C216.178 22.7913 216.861 21.8758 216.91 20.4963H215.006V19.3854H218.094V23.8045H217.057L217.02 22.6937C216.642 23.4749 215.983 23.9999 214.957 23.9999Z" fill="#000"/>
|
||||
<path d="M207.651 23.9999C205.589 23.9999 204.429 22.3763 204.429 19.4831C204.429 16.5655 205.589 14.9419 207.651 14.9419C209.714 14.9419 210.873 16.5655 210.873 19.4831C210.873 22.3763 209.714 23.9999 207.651 23.9999ZM207.651 22.8035C208.872 22.8035 209.543 21.6804 209.543 19.4831C209.543 17.2857 208.872 16.1382 207.651 16.1382C206.431 16.1382 205.76 17.2857 205.76 19.4831C205.76 21.6804 206.431 22.8035 207.651 22.8035Z" fill="#000"/>
|
||||
<path d="M197.489 23.8045V15.1372H200.187C202.017 15.1372 203.14 16.126 203.14 17.7252C203.14 18.7628 202.481 19.5197 201.676 19.7761C202.53 19.9348 202.945 20.4109 203.018 21.3386L203.238 23.8045H201.956L201.761 21.5828C201.688 20.716 201.346 20.4231 200.284 20.4231H198.771V23.8045H197.489ZM198.771 19.2267H200.162C201.224 19.2267 201.822 18.7018 201.822 17.7862C201.822 16.8585 201.224 16.3458 200.162 16.3458H198.771V19.2267Z" fill="#000"/>
|
||||
<path d="M190.403 23.8045V15.1372H193.088C194.894 15.1372 196.09 16.1626 196.09 17.8351C196.09 19.4953 194.894 20.5207 193.088 20.5207H191.684V23.8045H190.403ZM191.684 19.3122H192.954C194.186 19.3122 194.76 18.7995 194.76 17.8351C194.76 16.8707 194.186 16.3458 192.954 16.3458H191.684V19.3122Z" fill="#000"/>
|
||||
<path d="M175.791 23.8045V15.1372H181.112V16.3458H177.073V18.8727H180.966V20.0446H177.073V22.596H181.21V23.8045H175.791Z" fill="#000"/>
|
||||
<path d="M168.216 23.8045V15.1372H170.914C172.744 15.1372 173.867 16.126 173.867 17.7252C173.867 18.7628 173.208 19.5197 172.403 19.7761C173.257 19.9348 173.672 20.4109 173.745 21.3386L173.965 23.8045H172.683L172.488 21.5828C172.415 20.716 172.073 20.4231 171.011 20.4231H169.498V23.8045H168.216ZM169.498 19.2267H170.889C171.951 19.2267 172.549 18.7018 172.549 17.7862C172.549 16.8585 171.951 16.3458 170.889 16.3458H169.498V19.2267Z" fill="#000"/>
|
||||
<path d="M160.374 23.8045L162.912 15.1372H164.572L167.111 23.8045H165.78L165.158 21.5462H162.326L161.704 23.8045H160.374ZM163.742 16.4678L162.656 20.362H164.828L163.742 16.4678Z" fill="#000"/>
|
||||
<path d="M154.032 23.8045L153.104 15.1372H154.386L155.045 22.1565L155.862 16.1138H156.985L157.803 22.1565L158.462 15.1372H159.744L158.816 23.8045H157.144L156.424 18.5309L155.704 23.8045H154.032Z" fill="#000"/>
|
||||
<path d="M148.471 23.8045V16.3458H145.932V15.1372H152.279V16.3458H149.74V23.8045H148.471Z" fill="#000"/>
|
||||
<path d="M139.322 23.8045V15.1372H144.741V16.3213H140.604V18.9826H144.522V20.1545H140.604V23.8045H139.322Z" fill="#000"/>
|
||||
<path d="M134.469 23.9999C132.407 23.9999 131.247 22.3763 131.247 19.4831C131.247 16.5655 132.407 14.9419 134.469 14.9419C136.532 14.9419 137.691 16.5655 137.691 19.4831C137.691 22.3763 136.532 23.9999 134.469 23.9999ZM134.469 22.8035C135.69 22.8035 136.361 21.6804 136.361 19.4831C136.361 17.2857 135.69 16.1382 134.469 16.1382C133.249 16.1382 132.577 17.2857 132.577 19.4831C132.577 21.6804 133.249 22.8035 134.469 22.8035Z" fill="#000"/>
|
||||
<path d="M127.261 23.9999C125.381 23.9999 124.161 22.8035 124.014 20.9846L125.332 20.8992C125.503 22.1443 126.211 22.8035 127.31 22.8035C128.371 22.8035 128.969 22.3763 128.969 21.6072C128.969 20.8381 128.554 20.3987 126.736 19.8859C124.954 19.3976 124.209 18.7995 124.209 17.4566C124.209 15.9673 125.308 14.9419 127.126 14.9419C128.933 14.9419 129.97 16.0772 130.141 17.7496L128.823 17.8229C128.689 16.8219 128.127 16.1382 127.09 16.1382C126.138 16.1382 125.528 16.6387 125.528 17.3956C125.528 18.1525 125.955 18.4332 127.493 18.8605C129.567 19.4221 130.288 20.1911 130.288 21.6072C130.288 23.0355 129.079 23.9999 127.261 23.9999Z" fill="#000"/>
|
||||
<path d="M109.927 23.8045V15.1372H115.248V16.3458H111.208V18.8727H115.102V20.0446H111.208V22.596H115.346V23.8045H109.927Z" fill="#000"/>
|
||||
<path d="M105.232 23.9999C103.145 23.9999 101.974 22.2664 101.974 19.4831C101.974 16.6876 103.145 14.9419 105.232 14.9419C106.807 14.9419 107.942 16.0284 108.296 17.9083L106.953 17.9816C106.709 16.773 106.05 16.1382 105.232 16.1382C103.975 16.1382 103.304 17.3468 103.304 19.4831C103.304 21.6072 103.975 22.8035 105.232 22.8035C106.123 22.8035 106.795 22.1077 107.014 20.7893L108.345 20.8503C108.04 22.8279 106.892 23.9999 105.232 23.9999Z" fill="#000"/>
|
||||
<path d="M95.0342 23.8045V15.1372H97.7315C99.5622 15.1372 100.685 16.126 100.685 17.7252C100.685 18.7628 100.026 19.5197 99.2205 19.7761C100.075 19.9348 100.49 20.4109 100.563 21.3386L100.783 23.8045H99.5012L99.3059 21.5828C99.2327 20.716 98.891 20.4231 97.8291 20.4231H96.3157V23.8045H95.0342ZM96.3157 19.2267H97.7071C98.7689 19.2267 99.367 18.7018 99.367 17.7862C99.367 16.8585 98.7689 16.3458 97.7071 16.3458H96.3157V19.2267Z" fill="#000"/>
|
||||
<path d="M90.5596 23.9999C88.7166 23.9999 87.606 22.8035 87.606 20.7893V15.1372H88.8875V20.8381C88.8875 22.1077 89.4733 22.8035 90.5596 22.8035C91.6458 22.8035 92.2317 22.1077 92.2317 20.8381V15.1372H93.5132V20.7893C93.5132 22.8035 92.4025 23.9999 90.5596 23.9999Z" fill="#000"/>
|
||||
<path d="M83.2417 23.9999C81.179 23.9999 80.0195 22.3763 80.0195 19.4831C80.0195 16.5655 81.179 14.9419 83.2417 14.9419C85.3043 14.9419 86.4638 16.5655 86.4638 19.4831C86.4638 22.3763 85.3043 23.9999 83.2417 23.9999ZM83.2417 22.8035C84.4622 22.8035 85.1334 21.6804 85.1334 19.4831C85.1334 17.2857 84.4622 16.1382 83.2417 16.1382C82.0212 16.1382 81.3499 17.2857 81.3499 19.4831C81.3499 21.6804 82.0212 22.8035 83.2417 22.8035Z" fill="#000"/>
|
||||
<path d="M76.0332 23.9999C74.1536 23.9999 72.9331 22.8035 72.7866 20.9846L74.1048 20.8992C74.2756 22.1443 74.9835 22.8035 76.082 22.8035C77.1438 22.8035 77.7419 22.3763 77.7419 21.6072C77.7419 20.8381 77.3269 20.3987 75.5083 19.8859C73.7264 19.3976 72.9819 18.7995 72.9819 17.4566C72.9819 15.9673 74.0804 14.9419 75.8989 14.9419C77.7052 14.9419 78.7427 16.0772 78.9135 17.7496L77.5954 17.8229C77.4611 16.8219 76.8997 16.1382 75.8623 16.1382C74.9103 16.1382 74.3 16.6387 74.3 17.3956C74.3 18.1525 74.7272 18.4332 76.265 18.8605C78.3399 19.4221 79.06 20.1911 79.06 21.6072C79.06 23.0355 77.8517 23.9999 76.0332 23.9999Z" fill="#000"/>
|
||||
<path d="M58.4556 23.8045V15.1372H60.0178L62.8616 21.9856V15.1372H64.1187V23.8045H62.5564L59.7127 16.9561V23.8045H58.4556Z" fill="#000"/>
|
||||
<path d="M51.3813 23.8045V15.1372H56.7027V16.3458H52.6629V18.8727H56.5563V20.0446H52.6629V22.596H56.8004V23.8045H51.3813Z" fill="#000"/>
|
||||
<path d="M44.0386 23.8045V15.1372H46.7237C48.53 15.1372 49.7261 16.1626 49.7261 17.8351C49.7261 19.4953 48.53 20.5207 46.7237 20.5207H45.3201V23.8045H44.0386ZM45.3201 19.3122H46.5894C47.8221 19.3122 48.3958 18.7995 48.3958 17.8351C48.3958 16.8707 47.8221 16.3458 46.5894 16.3458H45.3201V19.3122Z" fill="#000"/>
|
||||
<path d="M39.332 23.9999C37.2693 23.9999 36.1099 22.3763 36.1099 19.4831C36.1099 16.5655 37.2693 14.9419 39.332 14.9419C41.3946 14.9419 42.5541 16.5655 42.5541 19.4831C42.5541 22.3763 41.3946 23.9999 39.332 23.9999ZM39.332 22.8035C40.5525 22.8035 41.2238 21.6804 41.2238 19.4831C41.2238 17.2857 40.5525 16.1382 39.332 16.1382C38.1115 16.1382 37.4402 17.2857 37.4402 19.4831C37.4402 21.6804 38.1115 22.8035 39.332 22.8035Z" fill="#000"/>
|
||||
<path d="M171.085 9.35094C169.083 9.35094 167.96 7.95929 167.96 5.46895C167.96 2.60019 168.864 0.292969 171.414 0.292969C173.013 0.292969 173.819 1.1597 174.148 2.38045L172.83 2.49032C172.659 1.89215 172.269 1.48931 171.414 1.48931C170.243 1.48931 169.474 2.36825 169.303 4.2482C169.718 3.71107 170.426 3.32043 171.329 3.32043C173.05 3.32043 174.161 4.48015 174.161 6.25024C174.161 8.16681 172.916 9.35094 171.085 9.35094ZM171.085 8.1424C172.147 8.1424 172.83 7.45878 172.83 6.27465C172.83 5.17597 172.196 4.48015 171.183 4.48015C170.096 4.48015 169.315 5.17597 169.315 6.27465C169.315 7.34891 169.999 8.1424 171.085 8.1424Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M160.667 9.15562C160.667 7.22684 161.387 6.01829 163.925 4.67547C165.011 4.10171 165.475 3.72328 165.475 2.88096C165.475 2.05085 164.95 1.48931 163.937 1.48931C162.827 1.48931 162.217 2.09968 162.046 3.18615L160.715 3.1007C160.898 1.37944 162.021 0.292969 163.937 0.292969C165.78 0.292969 166.806 1.3184 166.806 2.83213C166.806 4.17496 166.22 4.83416 164.45 5.78635C162.949 6.59205 162.241 7.36112 162.192 7.94708H166.818V9.15562H160.667Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M156.424 9.35094C154.508 9.35094 153.348 7.61748 153.348 4.83416C153.348 2.03864 154.508 0.292969 156.424 0.292969C158.34 0.292969 159.499 2.03864 159.499 4.83416C159.499 7.61748 158.34 9.35094 156.424 9.35094ZM154.678 4.83416C154.678 5.55441 154.752 6.1892 154.91 6.68971L157.449 1.98981C157.181 1.66021 156.827 1.48931 156.424 1.48931C155.338 1.48931 154.678 2.72226 154.678 4.83416ZM156.424 8.15461C157.51 8.15461 158.169 6.92165 158.169 4.83416C158.169 4.10171 158.096 3.46692 157.937 2.96641L155.386 7.6541C155.667 7.9837 156.021 8.15461 156.424 8.15461Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M146.03 9.15562C146.03 7.22684 146.75 6.01829 149.289 4.67547C150.375 4.10171 150.839 3.72328 150.839 2.88096C150.839 2.05085 150.314 1.48931 149.301 1.48931C148.19 1.48931 147.58 2.09968 147.409 3.18615L146.079 3.1007C146.262 1.37944 147.385 0.292969 149.301 0.292969C151.144 0.292969 152.169 1.3184 152.169 2.83213C152.169 4.17496 151.583 4.83416 149.813 5.78635C148.312 6.59205 147.604 7.36112 147.555 7.94708H152.181V9.15562H146.03Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M132.041 10.4985L135.727 0H136.899L133.212 10.4985H132.041Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M124.722 10.4985L128.409 0H129.581L125.894 10.4985H124.722Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M111.684 9.15582V7.54443H113.344V9.15582H111.684Z" fill="#000"/>
|
||||
<path d="M105.232 9.35094C103.145 9.35094 101.974 7.61748 101.974 4.83416C101.974 2.03864 103.145 0.292969 105.232 0.292969C106.807 0.292969 107.942 1.37944 108.296 3.25939L106.953 3.33264C106.709 2.1241 106.05 1.48931 105.232 1.48931C103.975 1.48931 103.304 2.69785 103.304 4.83416C103.304 6.95827 103.975 8.15461 105.232 8.15461C106.123 8.15461 106.795 7.45878 107.014 6.14037L108.345 6.20141C108.04 8.17902 106.892 9.35094 105.232 9.35094Z" fill="#000"/>
|
||||
<path d="M95.0464 9.15562V0.488281H96.6086L99.4524 7.3367V0.488281H100.71V9.15562H99.1473L96.3035 2.3072V9.15562H95.0464Z" fill="#000"/>
|
||||
<path d="M87.8501 9.15562V7.94707H89.9249V1.69683H87.8501V0.488281H93.2691V1.69683H91.1943V7.94707H93.2691V9.15562H87.8501Z" fill="#000"/>
|
||||
<path d="M73.4575 9.15562V0.488281H74.739V7.94707H78.8765V9.15562H73.4575Z" fill="#000"/>
|
||||
<path d="M66.0171 9.15562V0.488281H71.3385V1.69683H67.2986V4.22378H71.192V5.3957H67.2986V7.94707H71.4361V9.15562H66.0171Z" fill="#000"/>
|
||||
<path d="M61.3232 9.35094C59.2361 9.35094 58.0645 7.61748 58.0645 4.83416C58.0645 2.03864 59.2361 0.292969 61.3232 0.292969C62.8976 0.292969 64.0327 1.37944 64.3866 3.25939L63.0441 3.33264C62.8 2.1241 62.1409 1.48931 61.3232 1.48931C60.0661 1.48931 59.3948 2.69785 59.3948 4.83416C59.3948 6.95827 60.0661 8.15461 61.3232 8.15461C62.2142 8.15461 62.8854 7.45878 63.1051 6.14037L64.4355 6.20141C64.1303 8.17902 62.9831 9.35094 61.3232 9.35094Z" fill="#000"/>
|
||||
<path d="M51.125 9.15562V0.488281H53.8223C55.6531 0.488281 56.7759 1.47709 56.7759 3.07627C56.7759 4.11391 56.1168 4.87078 55.3113 5.12714C56.1657 5.28583 56.5806 5.76193 56.6539 6.6897L56.8736 9.15562H55.592L55.3968 6.93385C55.3235 6.06711 54.9818 5.77413 53.9199 5.77413H52.4065V9.15562H51.125ZM52.4065 4.5778H53.7979C54.8597 4.5778 55.4578 4.05288 55.4578 3.13731C55.4578 2.20954 54.8597 1.69683 53.7979 1.69683H52.4065V4.5778Z" fill="#000"/>
|
||||
<path d="M44.063 9.15562V0.488281H49.3844V1.69683H45.3445V4.22378H49.2379V5.3957H45.3445V7.94707H49.482V9.15562H44.063Z" fill="#000"/>
|
||||
<path d="M38.5753 9.15562L36 0.488281H37.3548L39.332 7.44656L41.3092 0.488281H42.6639L40.0887 9.15562H38.5753Z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -12,7 +12,7 @@ Sentry.init({
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
tracesSampleRate: 0.1,
|
||||
|
||||
// Enable logs to be sent to Sentry
|
||||
enableLogs: true,
|
||||
|
||||
@@ -11,7 +11,7 @@ Sentry.init({
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
tracesSampleRate: 0.1,
|
||||
|
||||
// Enable logs to be sent to Sentry
|
||||
enableLogs: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const size = {
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
export const revalidate = 3600;
|
||||
|
||||
const radiusMap: Record<string, number> = {
|
||||
none: 0,
|
||||
|
||||
@@ -130,7 +130,7 @@ export default async function BookHomePage() {
|
||||
{/* JSON-LD structured data */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
|
||||
/>
|
||||
<div className="max-w-2xl">
|
||||
{/* Continue Reading Button */}
|
||||
|
||||
+3
-2
@@ -7,7 +7,7 @@ import { Header } from "@/components/layout/header";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CookieConsentBanner } from "@/components/layout/cookie-consent";
|
||||
import { Analytics } from "@/components/layout/analytics";
|
||||
import { EzoicAds } from "@/components/layout/ezoic-ads";
|
||||
import { EzoicScripts, EzoicRouteHandler } from "@/components/layout/ezoic-ads";
|
||||
import { WebsiteStructuredData } from "@/components/seo/structured-data";
|
||||
import { AppBanner } from "@/components/layout/app-banner";
|
||||
import { LocaleDetector } from "@/components/providers/locale-detector";
|
||||
@@ -185,12 +185,13 @@ export default async function RootLayout({
|
||||
<meta name="google-adsense-account" content={process.env.GOOGLE_ADSENSE_ACCOUNT} />
|
||||
)}
|
||||
<WebsiteStructuredData />
|
||||
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && <EzoicScripts />}
|
||||
</head>
|
||||
<body className={`${fontClasses} antialiased`}>
|
||||
{process.env.GOOGLE_ANALYTICS_ID && (
|
||||
<Analytics gaId={process.env.GOOGLE_ANALYTICS_ID} />
|
||||
)}
|
||||
{process.env.EZOIC_ENABLED === "true" && <EzoicAds />}
|
||||
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && <EzoicRouteHandler />}
|
||||
<Providers locale={locale} messages={messages} theme={config.theme} branding={{ ...config.branding, useCloneBranding: config.homepage?.useCloneBranding }}>
|
||||
{isEmbedRoute || isKidsRoute ? (
|
||||
children
|
||||
|
||||
@@ -313,6 +313,24 @@ export default async function HomePage() {
|
||||
/>
|
||||
</BuiltWithLink>
|
||||
<span>by Cognition</span>
|
||||
<span className="mx-0.5">·</span>
|
||||
<span>hosted on</span>
|
||||
<Link href="https://vercel.com/oss" target="_blank" rel="noopener noreferrer" className="inline-flex items-center opacity-60 hover:opacity-100 transition-opacity">
|
||||
<Image
|
||||
src="/sponsors/vercel-oss.svg"
|
||||
alt="Vercel OSS Program"
|
||||
width={240}
|
||||
height={24}
|
||||
className="h-4.5 w-auto dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/sponsors/vercel-oss-dark.svg"
|
||||
alt="Vercel OSS Program"
|
||||
width={240}
|
||||
height={24}
|
||||
className="h-4.5 w-auto hidden dark:block"
|
||||
/>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function PrivacyPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{process.env.EZOIC_ENABLED === "true" && (
|
||||
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && (
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold">Advertising & Ezoic</h2>
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
@@ -23,6 +23,8 @@ function getUserIdentifier(user: { email: string; username: string; githubUserna
|
||||
return user.githubUsername || (isUnclaimedAccount ? user.username : user.email);
|
||||
}
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const prompts = await db.prompt.findMany({
|
||||
|
||||
+210
-121
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import crypto from "crypto";
|
||||
|
||||
function getUserIdentifier(user: {
|
||||
username: string;
|
||||
@@ -8,139 +9,227 @@ function getUserIdentifier(user: {
|
||||
return user.githubUsername || user.username;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const CONTENT_PREVIEW_LENGTH = 500;
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 3600;
|
||||
|
||||
function generateETag(count: number, latestUpdatedAt: Date | null): string {
|
||||
const raw = `${count}-${latestUpdatedAt?.toISOString() ?? "none"}`;
|
||||
const hash = crypto.createHash("md5").update(raw).digest("hex");
|
||||
return `"${hash}"`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const prompts = await db.prompt.findMany({
|
||||
where: {
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const fullContent = searchParams.get("full_content") === "true";
|
||||
const pageParam = searchParams.get("page");
|
||||
const limitParam = searchParams.get("limit");
|
||||
|
||||
const isPaginated = pageParam !== null || limitParam !== null;
|
||||
|
||||
const page = Math.max(1, parseInt(pageParam ?? "1", 10) || 1);
|
||||
const limit = Math.min(
|
||||
MAX_LIMIT,
|
||||
Math.max(1, parseInt(limitParam ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT)
|
||||
);
|
||||
|
||||
const whereClause = {
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
// Fetch total count and latest updatedAt for ETag generation
|
||||
const [totalCount, latestPrompt] = await Promise.all([
|
||||
db.prompt.count({ where: whereClause }),
|
||||
db.prompt.findFirst({
|
||||
where: whereClause,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
select: { updatedAt: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Generate and check ETag for conditional requests
|
||||
const etag = generateETag(totalCount, latestPrompt?.updatedAt ?? null);
|
||||
const ifNoneMatch = request.headers.get("If-None-Match");
|
||||
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
return new NextResponse(null, {
|
||||
status: 304,
|
||||
headers: {
|
||||
ETag: etag,
|
||||
"Cache-Control":
|
||||
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const selectClause = {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
content: true,
|
||||
type: true,
|
||||
mediaUrl: true,
|
||||
viewCount: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
structuredFormat: true,
|
||||
isFeatured: true,
|
||||
featuredAt: true,
|
||||
requiresMediaUpload: true,
|
||||
requiredMediaType: true,
|
||||
requiredMediaCount: true,
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
content: true,
|
||||
type: true,
|
||||
mediaUrl: true,
|
||||
viewCount: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
structuredFormat: true,
|
||||
isFeatured: true,
|
||||
featuredAt: true,
|
||||
requiresMediaUpload: true,
|
||||
requiredMediaType: true,
|
||||
requiredMediaCount: true,
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
icon: true,
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
contributors: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
contributors: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
votes: true,
|
||||
comments: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
votes: true,
|
||||
comments: true,
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const queryOptions: any = {
|
||||
where: whereClause,
|
||||
select: selectClause,
|
||||
orderBy: { createdAt: "desc" as const },
|
||||
};
|
||||
|
||||
if (isPaginated) {
|
||||
queryOptions.skip = (page - 1) * limit;
|
||||
queryOptions.take = limit;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const prompts: any[] = await db.prompt.findMany(queryOptions);
|
||||
|
||||
const includeFullContent = fullContent;
|
||||
|
||||
const formattedPrompts = prompts.map((prompt) => {
|
||||
const content = prompt.content ?? "";
|
||||
const contentPreview =
|
||||
content.length > CONTENT_PREVIEW_LENGTH
|
||||
? content.slice(0, CONTENT_PREVIEW_LENGTH) + "..."
|
||||
: content;
|
||||
|
||||
return {
|
||||
id: prompt.id,
|
||||
title: prompt.title,
|
||||
slug: prompt.slug,
|
||||
description: prompt.description,
|
||||
...(includeFullContent
|
||||
? { content, contentPreview }
|
||||
: { contentPreview }),
|
||||
type: prompt.type,
|
||||
structuredFormat: prompt.structuredFormat,
|
||||
mediaUrl: prompt.mediaUrl,
|
||||
viewCount: prompt.viewCount,
|
||||
voteCount: prompt._count.votes,
|
||||
commentCount: prompt._count.comments,
|
||||
isFeatured: prompt.isFeatured,
|
||||
featuredAt: prompt.featuredAt,
|
||||
requiresMediaUpload: prompt.requiresMediaUpload,
|
||||
requiredMediaType: prompt.requiredMediaType,
|
||||
requiredMediaCount: prompt.requiredMediaCount,
|
||||
createdAt: prompt.createdAt,
|
||||
updatedAt: prompt.updatedAt,
|
||||
category: prompt.category
|
||||
? {
|
||||
id: prompt.category.id,
|
||||
name: prompt.category.name,
|
||||
slug: prompt.category.slug,
|
||||
icon: prompt.category.icon,
|
||||
}
|
||||
: null,
|
||||
author: {
|
||||
username: prompt.author.username,
|
||||
name: prompt.author.name,
|
||||
avatar: prompt.author.avatar,
|
||||
identifier: getUserIdentifier(prompt.author),
|
||||
verified: prompt.author.verified,
|
||||
},
|
||||
contributors: prompt.contributors.map((c: any) => ({
|
||||
username: c.username,
|
||||
name: c.name,
|
||||
avatar: c.avatar,
|
||||
identifier: getUserIdentifier(c),
|
||||
verified: c.verified,
|
||||
})),
|
||||
tags: prompt.tags.map((pt: any) => ({
|
||||
id: pt.tag.id,
|
||||
name: pt.tag.name,
|
||||
slug: pt.tag.slug,
|
||||
color: pt.tag.color,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const formattedPrompts = prompts.map((prompt) => ({
|
||||
id: prompt.id,
|
||||
title: prompt.title,
|
||||
slug: prompt.slug,
|
||||
description: prompt.description,
|
||||
content: prompt.content,
|
||||
type: prompt.type,
|
||||
structuredFormat: prompt.structuredFormat,
|
||||
mediaUrl: prompt.mediaUrl,
|
||||
viewCount: prompt.viewCount,
|
||||
voteCount: prompt._count.votes,
|
||||
commentCount: prompt._count.comments,
|
||||
isFeatured: prompt.isFeatured,
|
||||
featuredAt: prompt.featuredAt,
|
||||
requiresMediaUpload: prompt.requiresMediaUpload,
|
||||
requiredMediaType: prompt.requiredMediaType,
|
||||
requiredMediaCount: prompt.requiredMediaCount,
|
||||
createdAt: prompt.createdAt,
|
||||
updatedAt: prompt.updatedAt,
|
||||
category: prompt.category
|
||||
? {
|
||||
id: prompt.category.id,
|
||||
name: prompt.category.name,
|
||||
slug: prompt.category.slug,
|
||||
icon: prompt.category.icon,
|
||||
}
|
||||
: null,
|
||||
author: {
|
||||
username: prompt.author.username,
|
||||
name: prompt.author.name,
|
||||
avatar: prompt.author.avatar,
|
||||
identifier: getUserIdentifier(prompt.author),
|
||||
verified: prompt.author.verified,
|
||||
},
|
||||
contributors: prompt.contributors.map((c) => ({
|
||||
username: c.username,
|
||||
name: c.name,
|
||||
avatar: c.avatar,
|
||||
identifier: getUserIdentifier(c),
|
||||
verified: c.verified,
|
||||
})),
|
||||
tags: prompt.tags.map((pt) => ({
|
||||
id: pt.tag.id,
|
||||
name: pt.tag.name,
|
||||
slug: pt.tag.slug,
|
||||
color: pt.tag.color,
|
||||
})),
|
||||
}));
|
||||
const totalPages = isPaginated ? Math.ceil(totalCount / limit) : 1;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
count: formattedPrompts.length,
|
||||
prompts: formattedPrompts,
|
||||
const responseBody = isPaginated
|
||||
? {
|
||||
count: totalCount,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
hasMore: page < totalPages,
|
||||
prompts: formattedPrompts,
|
||||
}
|
||||
: {
|
||||
count: formattedPrompts.length,
|
||||
prompts: formattedPrompts,
|
||||
};
|
||||
|
||||
return NextResponse.json(responseBody, {
|
||||
headers: {
|
||||
"Cache-Control":
|
||||
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||
ETag: etag,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("prompts.json error:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -8,6 +8,7 @@ export const size = {
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
export const revalidate = 3600;
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
TEXT: "Text Prompt",
|
||||
|
||||
@@ -32,6 +32,7 @@ import { AddToCollectionButton } from "@/components/prompts/add-to-collection-bu
|
||||
import { getConfig } from "@/lib/config";
|
||||
import { StructuredData } from "@/components/seo/structured-data";
|
||||
import { AI_MODELS } from "@/lib/works-best-with";
|
||||
import { EzoicAd } from "@/components/ads/ezoic-ad";
|
||||
|
||||
interface PromptPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -701,6 +702,9 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ad Placement */}
|
||||
{process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && <EzoicAd id={201} />}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="versions" className="mt-0">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { EzoicPlaceholder } from "./ezoic-placeholder";
|
||||
|
||||
interface EzoicAdProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-only wrapper for EzoicPlaceholder.
|
||||
* Renders the placeholder div after mount so it's in the DOM
|
||||
* when Ezoic's showAds() runs.
|
||||
*
|
||||
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
|
||||
*/
|
||||
export function EzoicAd({ id }: EzoicAdProps) {
|
||||
return <EzoicPlaceholder id={id} />;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { runEzoic } from "@/lib/ezoic";
|
||||
|
||||
@@ -8,74 +8,36 @@ interface EzoicPlaceholderProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of a single Ezoic ad slot.
|
||||
* The placeholder div is always in the DOM so Ezoic can find it
|
||||
* when showAds() runs. Destroys the placeholder on unmount.
|
||||
*
|
||||
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
|
||||
*/
|
||||
export function EzoicPlaceholder({ id }: EzoicPlaceholderProps) {
|
||||
const t = useTranslations("common");
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const [hasAdContent, setHasAdContent] = useState(true);
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Render the placeholder div first, then tell Ezoic to fill it.
|
||||
// This ensures the DOM element exists before showAds is called.
|
||||
setIsRendered(true);
|
||||
|
||||
// Use requestAnimationFrame to guarantee the placeholder div
|
||||
// is in the DOM before Ezoic tries to find it.
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.showAds(id);
|
||||
});
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.showAds(id);
|
||||
});
|
||||
|
||||
// Observe the placeholder for ad content injection by Ezoic.
|
||||
// If nothing is injected after a timeout, hide the container
|
||||
// to avoid showing an empty white card.
|
||||
const el = document.getElementById(`ezoic-pub-ad-placeholder-${id}`);
|
||||
let observer: MutationObserver | undefined;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
if (el) {
|
||||
let filled = false;
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
if (el.childNodes.length > 0 || el.innerHTML.trim().length > 0) {
|
||||
filled = true;
|
||||
setHasAdContent(true);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(el, { childList: true, subtree: true });
|
||||
|
||||
// If Ezoic doesn't fill the placeholder within 5s, hide it
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!filled && el.childNodes.length === 0) {
|
||||
setHasAdContent(false);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
observer?.disconnect();
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.destroyPlaceholders(id);
|
||||
});
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (!hasAdContent) return null;
|
||||
|
||||
return (
|
||||
<div className="ezoic-ad-container group border rounded-[var(--radius)] overflow-hidden hover:border-foreground/20 transition-colors flex flex-col">
|
||||
<div className="px-3 py-1.5 border-b bg-muted/50">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
<div className="ezoic-ad-container my-2 overflow-hidden">
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50 select-none">
|
||||
{t("ad")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ezoic-ad-content overflow-hidden flex items-center justify-center">
|
||||
{isRendered && <div id={`ezoic-pub-ad-placeholder-${id}`} ref={placeholderRef} />}
|
||||
</div>
|
||||
<div id={`ezoic-pub-ad-placeholder-${id}`} className="overflow-hidden" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Script from "next/script";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { runEzoic } from "@/lib/ezoic";
|
||||
@@ -12,34 +12,21 @@ declare global {
|
||||
showAds: (...ids: number[]) => void;
|
||||
destroyAll: () => void;
|
||||
destroyPlaceholders: (...ids: number[]) => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function EzoicAds() {
|
||||
const pathname = usePathname();
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
// Re-trigger ads on SPA route changes
|
||||
// Per Ezoic docs: destroyPlaceholders first, then showAds in next frame.
|
||||
// Skip the first render to avoid racing with individual EzoicPlaceholder
|
||||
// components that call showAds(id) on mount.
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.destroyPlaceholders();
|
||||
requestAnimationFrame(() => {
|
||||
window.ezstandalone?.showAds();
|
||||
});
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
/**
|
||||
* Ezoic script loader — renders all required <Script> tags.
|
||||
* Place inside <head> in layout.tsx per official docs.
|
||||
*
|
||||
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
|
||||
*/
|
||||
export function EzoicScripts() {
|
||||
return (
|
||||
<>
|
||||
{/* Ezoic Privacy / Consent Management - must load first */}
|
||||
{/* Ezoic Privacy / Consent Management */}
|
||||
<Script
|
||||
id="ezoic-cmp"
|
||||
src="https://cmp.gatekeeperconsent.com/min.js"
|
||||
@@ -53,12 +40,14 @@ export function EzoicAds() {
|
||||
data-cfasync="false"
|
||||
/>
|
||||
|
||||
{/* Ezoic Header Script */}
|
||||
{/* Ezoic standalone script */}
|
||||
<Script
|
||||
id="ezoic-sa"
|
||||
src="//www.ezojs.com/ezoic/sa.min.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
|
||||
{/* Ezoic init — cmd queue must exist before any runEzoic() calls */}
|
||||
<Script id="ezoic-init" strategy="afterInteractive">
|
||||
{`
|
||||
window.ezstandalone = window.ezstandalone || {};
|
||||
@@ -75,3 +64,25 @@ export function EzoicAds() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SPA route changes for Ezoic.
|
||||
* Destroys stale placeholders and re-scans the DOM on each navigation.
|
||||
*
|
||||
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
|
||||
*/
|
||||
export function EzoicRouteHandler() {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.destroyPlaceholders();
|
||||
requestAnimationFrame(() => {
|
||||
window.ezstandalone?.showAds();
|
||||
});
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { db } from "@/lib/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Masonry } from "@/components/ui/masonry";
|
||||
import { PromptCard } from "@/components/prompts/prompt-card";
|
||||
import { EzoicAd } from "@/components/ads/ezoic-ad";
|
||||
|
||||
interface DiscoveryPromptsProps {
|
||||
isHomepage?: boolean;
|
||||
@@ -157,6 +158,15 @@ export async function DiscoveryPrompts({ isHomepage = false }: DiscoveryPromptsP
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Ad Placement - after featured */}
|
||||
{isHomepage && process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && (
|
||||
<section className="py-8 border-b">
|
||||
<div className="container max-w-2xl">
|
||||
<EzoicAd id={202} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Today's Most Upvoted Section */}
|
||||
{todaysMostUpvoted.length > 0 && (
|
||||
<section className={isHomepage ? "py-12 border-b" : "pb-8 mb-8 border-b"}>
|
||||
@@ -207,6 +217,15 @@ export async function DiscoveryPrompts({ isHomepage = false }: DiscoveryPromptsP
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Ad Placement - after latest */}
|
||||
{isHomepage && process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true" && (
|
||||
<section className="py-8 border-b">
|
||||
<div className="container max-w-2xl">
|
||||
<EzoicAd id={203} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Recently Updated Section */}
|
||||
{recentlyUpdated.length > 0 && (
|
||||
<section className={isHomepage ? "py-12 border-b" : "pb-8 mb-8 border-b"}>
|
||||
|
||||
@@ -199,7 +199,7 @@ export async function StructuredData({ type, data }: StructuredDataProps) {
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/</g, '\\u003c') }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ Sentry.init({
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
tracesSampleRate: 0.1,
|
||||
// Enable logs to be sent to Sentry
|
||||
enableLogs: true,
|
||||
|
||||
|
||||
+5
-8
@@ -2,15 +2,12 @@
|
||||
* Safely push a function to the Ezoic command queue.
|
||||
* Initializes window.ezstandalone and cmd array if they don't exist yet,
|
||||
* preventing errors when components mount before the Ezoic script loads.
|
||||
*
|
||||
* @see https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
|
||||
*/
|
||||
export function runEzoic(fn: () => void) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!window.ezstandalone) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).ezstandalone = { cmd: [] };
|
||||
}
|
||||
if (!window.ezstandalone!.cmd) {
|
||||
window.ezstandalone!.cmd = [];
|
||||
}
|
||||
window.ezstandalone!.cmd.push(fn);
|
||||
const ez = (window.ezstandalone ??= {} as NonNullable<typeof window.ezstandalone>);
|
||||
ez!.cmd = ez!.cmd || [];
|
||||
ez!.cmd.push(fn);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ export const ezoicWidget: WidgetPlugin = {
|
||||
position: 4,
|
||||
mode: "repeat",
|
||||
repeatEvery: 12,
|
||||
maxCount: 5,
|
||||
maxCount: 20,
|
||||
},
|
||||
shouldInject: () => true,
|
||||
shouldInject: () => process.env.NEXT_PUBLIC_EZOIC_ENABLED === "true",
|
||||
render: (instanceIndex: number) => (
|
||||
<EzoicPlaceholder id={EZOIC_FEED_BASE_ID + instanceIndex} />
|
||||
),
|
||||
|
||||
+161
-139
@@ -10,7 +10,6 @@ import {
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { isValidApiKeyFormat } from "@/lib/api-key";
|
||||
import { improvePrompt } from "@/lib/ai/improve-prompt";
|
||||
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
|
||||
import appConfig from "@/../prompts.config";
|
||||
|
||||
@@ -103,7 +102,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
{
|
||||
capabilities: {
|
||||
prompts: { listChanged: false },
|
||||
tools: {},
|
||||
tools: { listChanged: false },
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -206,24 +205,30 @@ function createServer(options: ServerOptions = {}) {
|
||||
const promptSlug = request.params.name;
|
||||
const args = request.params.arguments || {};
|
||||
|
||||
// Fetch all matching prompts and find by slug
|
||||
const prompts = await db.prompt.findMany({
|
||||
where: promptFilter,
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
content: true,
|
||||
},
|
||||
});
|
||||
const promptSelect = { id: true, slug: true, title: true, description: true, content: true };
|
||||
|
||||
// Find by slug field first, then by slugified title, then by id
|
||||
const prompt = prompts.find((p) =>
|
||||
p.slug === promptSlug ||
|
||||
slugify(p.title) === promptSlug ||
|
||||
p.id === promptSlug
|
||||
);
|
||||
// Try direct lookup by slug first
|
||||
let prompt = await db.prompt.findFirst({
|
||||
where: { ...promptFilter, slug: promptSlug },
|
||||
select: promptSelect,
|
||||
});
|
||||
// Fallback: lookup by id
|
||||
if (!prompt) {
|
||||
prompt = await db.prompt.findFirst({
|
||||
where: { ...promptFilter, id: promptSlug },
|
||||
select: promptSelect,
|
||||
});
|
||||
}
|
||||
// Fallback: lookup by slugified title (for prompts without stored slug)
|
||||
// TODO: Backfill slug column for all existing prompts so this fallback can be removed
|
||||
if (!prompt) {
|
||||
const unslugged = await db.prompt.findMany({
|
||||
where: { ...promptFilter, slug: null },
|
||||
select: promptSelect,
|
||||
take: 500,
|
||||
});
|
||||
prompt = unslugged.find((p) => slugify(p.title) === promptSlug) || null;
|
||||
}
|
||||
|
||||
if (!prompt) {
|
||||
throw new Error(`Prompt not found: ${promptSlug}`);
|
||||
@@ -318,7 +323,6 @@ function createServer(options: ServerOptions = {}) {
|
||||
description: true,
|
||||
content: true,
|
||||
type: true,
|
||||
structuredFormat: true,
|
||||
createdAt: true,
|
||||
author: { select: { username: true, name: true } },
|
||||
category: { select: { name: true, slug: true } },
|
||||
@@ -332,9 +336,8 @@ function createServer(options: ServerOptions = {}) {
|
||||
slug: getPromptName(p),
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
content: p.content,
|
||||
contentPreview: p.content.substring(0, 1000) + (p.content.length > 1000 ? '...' : ''),
|
||||
type: p.type,
|
||||
structuredFormat: p.structuredFormat,
|
||||
author: p.author.name || p.author.username,
|
||||
category: p.category?.name || null,
|
||||
tags: p.tags.map((t) => t.tag.name),
|
||||
@@ -346,7 +349,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({ query, count: results.length, prompts: results }, null, 2),
|
||||
text: JSON.stringify({ query, count: results.length, prompts: results }),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -368,9 +371,12 @@ function createServer(options: ServerOptions = {}) {
|
||||
"Get a prompt by ID and optionally fill in its variables. If the prompt contains template variables (like {{variable}}), you will be asked to provide values for them.",
|
||||
inputSchema: {
|
||||
id: z.string().describe("The ID of the prompt to retrieve"),
|
||||
fill_variables: z.boolean().default(false).describe(
|
||||
"If true and the prompt has template variables, triggers interactive variable filling. Default false — returns raw prompt with variable metadata."
|
||||
),
|
||||
},
|
||||
},
|
||||
async ({ id }, extra) => {
|
||||
async ({ id, fill_variables }, extra) => {
|
||||
try {
|
||||
const prompt = await db.prompt.findFirst({
|
||||
where: {
|
||||
@@ -402,7 +408,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
|
||||
const variables = extractVariables(prompt.content);
|
||||
|
||||
if (variables.length > 0) {
|
||||
if (fill_variables && variables.length > 0) {
|
||||
const properties: Record<string, PrimitiveSchemaDefinition> = {};
|
||||
const requiredFields: string[] = [];
|
||||
for (const variable of variables) {
|
||||
@@ -436,73 +442,75 @@ function createServer(options: ServerOptions = {}) {
|
||||
},
|
||||
ElicitResultSchema
|
||||
);
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs)
|
||||
);
|
||||
|
||||
const result = await Promise.race([elicitationPromise, timeoutPromise]);
|
||||
|
||||
if (result.action === "accept" && result.content) {
|
||||
let filledContent = prompt.content;
|
||||
for (const [key, value] of Object.entries(result.content)) {
|
||||
// Replace ${key} or ${key:default} patterns
|
||||
filledContent = filledContent.replace(
|
||||
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
|
||||
String(value)
|
||||
);
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
const elicitResult = await Promise.race([elicitationPromise, timeoutPromise]);
|
||||
clearTimeout(timeoutId!);
|
||||
|
||||
if (elicitResult.action === "accept" && elicitResult.content) {
|
||||
let filledContent = prompt.content;
|
||||
for (const [key, value] of Object.entries(elicitResult.content)) {
|
||||
// Skip keys that don't match valid variable name format (ReDoS prevention)
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_\s]*$/.test(key)) {
|
||||
continue;
|
||||
}
|
||||
// Replace ${key} or ${key:default} patterns
|
||||
filledContent = filledContent.replace(
|
||||
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
|
||||
String(value)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
...prompt,
|
||||
content: filledContent,
|
||||
originalContent: prompt.content,
|
||||
variables: elicitResult.content,
|
||||
author: prompt.author.name || prompt.author.username,
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
...prompt,
|
||||
variablesRequired: variables,
|
||||
message: "User declined to provide variable values. Returning original prompt.",
|
||||
author: prompt.author.name || prompt.author.username,
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
...prompt,
|
||||
content: filledContent,
|
||||
originalContent: prompt.content,
|
||||
variables: result.content,
|
||||
author: prompt.author.name || prompt.author.username,
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
...prompt,
|
||||
variablesRequired: variables,
|
||||
message: "User declined to provide variable values. Returning original prompt.",
|
||||
author: prompt.author.name || prompt.author.username,
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (e) {
|
||||
clearTimeout(timeoutId!);
|
||||
throw e;
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
...prompt,
|
||||
variablesRequired: variables,
|
||||
message: "Elicitation not supported. Variables need to be filled manually.",
|
||||
@@ -510,31 +518,41 @@ function createServer(options: ServerOptions = {}) {
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} else if (variables.length > 0) {
|
||||
// Return prompt with variable metadata, no timeout
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify({
|
||||
id: prompt.id,
|
||||
slug: getPromptName(prompt),
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
content: prompt.content,
|
||||
type: prompt.type,
|
||||
author: prompt.author.name || prompt.author.username,
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
variables: variables.map(v => ({ name: v.name, defaultValue: v.defaultValue })),
|
||||
hint: "This prompt has template variables. Call get_prompt with fill_variables=true to fill them interactively.",
|
||||
}) }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
...prompt,
|
||||
author: prompt.author.name || prompt.author.username,
|
||||
category: prompt.category?.name || null,
|
||||
tags: prompt.tags.map((t) => t.tag.name),
|
||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -639,8 +657,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
prompt: {
|
||||
...prompt,
|
||||
@@ -648,10 +665,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
category: prompt.category?.name || null,
|
||||
link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -693,13 +707,14 @@ function createServer(options: ServerOptions = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { improvePrompt } = await import("@/lib/ai/improve-prompt");
|
||||
const result = await improvePrompt({ prompt, outputType, outputFormat });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(result, null, 2),
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -810,8 +825,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
skill: {
|
||||
...skill,
|
||||
@@ -820,10 +834,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
category: skill.category?.name || null,
|
||||
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -910,17 +921,13 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `File '${filename}' added to skill`,
|
||||
skillId,
|
||||
files: files.map(f => f.filename),
|
||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1000,17 +1007,13 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `File '${filename}' updated in skill`,
|
||||
skillId,
|
||||
files: files.map(f => f.filename),
|
||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1096,17 +1099,13 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: `File '${filename}' removed from skill`,
|
||||
skillId,
|
||||
files: updatedFiles.map(f => f.filename),
|
||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1181,8 +1180,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
text: JSON.stringify({
|
||||
id: skill.id,
|
||||
slug: getPromptName(skill),
|
||||
title: skill.title,
|
||||
@@ -1199,10 +1197,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: f.content,
|
||||
})),
|
||||
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1294,7 +1289,8 @@ function createServer(options: ServerOptions = {}) {
|
||||
category: s.category?.name || null,
|
||||
tags: s.tags.map((t) => t.tag.name),
|
||||
votes: s._count.votes,
|
||||
files: files.map(f => f.filename),
|
||||
fileNames: files.map(f => f.filename),
|
||||
fileCount: files.length,
|
||||
createdAt: s.createdAt.toISOString(),
|
||||
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
|
||||
};
|
||||
@@ -1304,7 +1300,7 @@ function createServer(options: ServerOptions = {}) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({ query, count: results.length, skills: results }, null, 2),
|
||||
text: JSON.stringify({ query, count: results.length, skills: results }),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1321,10 +1317,27 @@ function createServer(options: ServerOptions = {}) {
|
||||
return server;
|
||||
}
|
||||
|
||||
class PayloadTooLargeError extends Error {
|
||||
constructor() {
|
||||
super("Body too large");
|
||||
this.name = "PayloadTooLargeError";
|
||||
}
|
||||
}
|
||||
|
||||
async function parseBody(req: NextApiRequest): Promise<unknown> {
|
||||
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = "";
|
||||
req.on("data", (chunk) => (body += chunk));
|
||||
let bytesReceived = 0;
|
||||
req.on("data", (chunk: Buffer | string) => {
|
||||
bytesReceived += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
||||
body += chunk;
|
||||
if (bytesReceived > MAX_BODY_SIZE) {
|
||||
req.destroy();
|
||||
reject(new PayloadTooLargeError());
|
||||
return;
|
||||
}
|
||||
});
|
||||
req.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
@@ -1342,6 +1355,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
|
||||
return res.status(200).json({
|
||||
name: "prompts-chat",
|
||||
version: "1.0.0",
|
||||
@@ -1458,11 +1472,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
if (error instanceof PayloadTooLargeError) {
|
||||
res.status(413).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Payload too large. Maximum body size is 1MB." },
|
||||
id: null,
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "prompts-chat",
|
||||
"main": "cloudflare/worker.js",
|
||||
"compatibility_date": "2026-03-22",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
"hyperdrive": [
|
||||
{
|
||||
"binding": "HYPERDRIVE",
|
||||
"id": "2c6ef99cbde84baaac5edacc418eee72"
|
||||
}
|
||||
],
|
||||
|
||||
"vars": {
|
||||
"NEXTAUTH_URL": "https://prompts.chat",
|
||||
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
||||
"OPENAI_EMBEDDING_MODEL": "text-embedding-3-large",
|
||||
"OPENAI_GENERATIVE_MODEL": "gpt-4o",
|
||||
"GOOGLE_ANALYTICS_ID": "G-MSNHFWTE77",
|
||||
"ENABLED_STORAGE": "do-spaces",
|
||||
"DO_SPACES_BUCKET": "prompts-chat-space",
|
||||
"DO_SPACES_REGION": "fra1",
|
||||
"FAL_IMAGE_MODELS": "fal-ai/flux-pro/v1.1-ultra,fal-ai/nano-banana-pro",
|
||||
"FAL_VIDEO_MODELS": "fal-ai/veo3.1/fast",
|
||||
"FAL_AUDIO_MODELS": "sonauto/v2/text-to-music,cassetteai/music-generator",
|
||||
"WIRO_IMAGE_MODELS": "google/nano-banana-pro,google/nano-banana",
|
||||
"WIRO_VIDEO_MODELS": "google/veo3.1-fast",
|
||||
"EZOIC_SITE_DOMAIN": "prompts.chat",
|
||||
"NEXT_PUBLIC_EZOIC_ENABLED": "true",
|
||||
"GOOGLE_ADSENSE_ACCOUNT": "ca-pub-6945602608405209"
|
||||
},
|
||||
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "PromptsChatContainer",
|
||||
"image": "./Dockerfile.cloudflare",
|
||||
"max_instances": 3,
|
||||
// standard-1: 2 vCPU, 2 GiB memory
|
||||
"instance_type": "standard-1"
|
||||
}
|
||||
],
|
||||
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "PROMPTS_CHAT",
|
||||
"class_name": "PromptsChatContainer"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["PromptsChatContainer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user