| @@ -53,10 +53,17 @@ const LocaleLayout = async ({ | |||
| return ( | |||
| <html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning> | |||
| <head> | |||
| <meta name="theme-color" content="#FFFFFF" /> | |||
| <link rel="manifest" href="/manifest.json" /> | |||
| <meta name="theme-color" content="#1C64F2" /> | |||
| <meta name="mobile-web-app-capable" content="yes" /> | |||
| <meta name="apple-mobile-web-app-capable" content="yes" /> | |||
| <meta name="apple-mobile-web-app-status-bar-style" content="default" /> | |||
| <meta name="apple-mobile-web-app-title" content="Dify" /> | |||
| <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> | |||
| <link rel="icon" type="image/png" sizes="32x32" href="/icon-192x192.png" /> | |||
| <link rel="icon" type="image/png" sizes="16x16" href="/icon-192x192.png" /> | |||
| <meta name="msapplication-TileColor" content="#1C64F2" /> | |||
| <meta name="msapplication-config" content="/browserconfig.xml" /> | |||
| </head> | |||
| <body | |||
| className="color-scheme h-full select-auto" | |||
| @@ -1,4 +1,71 @@ | |||
| const { codeInspectorPlugin } = require('code-inspector-plugin') | |||
| const withPWA = require('next-pwa')({ | |||
| dest: 'public', | |||
| register: true, | |||
| skipWaiting: true, | |||
| disable: process.env.NODE_ENV === 'development', | |||
| fallbacks: { | |||
| document: '/_offline.html', | |||
| }, | |||
| runtimeCaching: [ | |||
| { | |||
| urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, | |||
| handler: 'CacheFirst', | |||
| options: { | |||
| cacheName: 'google-fonts', | |||
| expiration: { | |||
| maxEntries: 4, | |||
| maxAgeSeconds: 365 * 24 * 60 * 60 // 1 year | |||
| } | |||
| } | |||
| }, | |||
| { | |||
| urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, | |||
| handler: 'CacheFirst', | |||
| options: { | |||
| cacheName: 'google-fonts-webfonts', | |||
| expiration: { | |||
| maxEntries: 4, | |||
| maxAgeSeconds: 365 * 24 * 60 * 60 // 1 year | |||
| } | |||
| } | |||
| }, | |||
| { | |||
| urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/i, | |||
| handler: 'CacheFirst', | |||
| options: { | |||
| cacheName: 'images', | |||
| expiration: { | |||
| maxEntries: 64, | |||
| maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days | |||
| } | |||
| } | |||
| }, | |||
| { | |||
| urlPattern: /\.(?:js|css)$/i, | |||
| handler: 'StaleWhileRevalidate', | |||
| options: { | |||
| cacheName: 'static-resources', | |||
| expiration: { | |||
| maxEntries: 32, | |||
| maxAgeSeconds: 24 * 60 * 60 // 1 day | |||
| } | |||
| } | |||
| }, | |||
| { | |||
| urlPattern: /^\/api\/.*/i, | |||
| handler: 'NetworkFirst', | |||
| options: { | |||
| cacheName: 'api-cache', | |||
| networkTimeoutSeconds: 10, | |||
| expiration: { | |||
| maxEntries: 16, | |||
| maxAgeSeconds: 60 * 60 // 1 hour | |||
| } | |||
| } | |||
| } | |||
| ] | |||
| }) | |||
| const withMDX = require('@next/mdx')({ | |||
| extension: /\.mdx?$/, | |||
| options: { | |||
| @@ -70,4 +137,4 @@ const nextConfig = { | |||
| output: 'standalone', | |||
| } | |||
| module.exports = withBundleAnalyzer(withMDX(nextConfig)) | |||
| module.exports = withPWA(withBundleAnalyzer(withMDX(nextConfig))) | |||
| @@ -106,6 +106,7 @@ | |||
| "mitt": "^3.0.1", | |||
| "negotiator": "^0.6.3", | |||
| "next": "15.5.0", | |||
| "next-pwa": "^5.6.0", | |||
| "next-themes": "^0.4.3", | |||
| "pinyin-pro": "^3.25.0", | |||
| "qrcode.react": "^4.2.0", | |||
| @@ -155,6 +156,8 @@ | |||
| }, | |||
| "devDependencies": { | |||
| "@antfu/eslint-config": "^5.0.0", | |||
| "@babel/core": "^7.28.3", | |||
| "@babel/preset-env": "^7.28.3", | |||
| "@chromatic-com/storybook": "^3.1.0", | |||
| "@eslint-react/eslint-plugin": "^1.15.0", | |||
| "@eslint/eslintrc": "^3.1.0", | |||
| @@ -198,6 +201,7 @@ | |||
| "@types/sortablejs": "^1.15.1", | |||
| "@types/uuid": "^10.0.0", | |||
| "autoprefixer": "^10.4.20", | |||
| "babel-loader": "^10.0.0", | |||
| "bing-translate-api": "^4.0.2", | |||
| "code-inspector-plugin": "^0.18.1", | |||
| "cross-env": "^7.0.3", | |||
| @@ -0,0 +1,129 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
| <title>Dify - Offline</title> | |||
| <style> | |||
| * { | |||
| margin: 0; | |||
| padding: 0; | |||
| box-sizing: border-box; | |||
| } | |||
| body { | |||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |||
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |||
| min-height: 100vh; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| color: white; | |||
| text-align: center; | |||
| padding: 20px; | |||
| } | |||
| .container { | |||
| max-width: 600px; | |||
| background: rgba(255, 255, 255, 0.1); | |||
| backdrop-filter: blur(10px); | |||
| border-radius: 20px; | |||
| padding: 40px; | |||
| box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2); | |||
| } | |||
| .icon { | |||
| width: 100px; | |||
| height: 100px; | |||
| margin: 0 auto 30px; | |||
| background: rgba(255, 255, 255, 0.2); | |||
| border-radius: 20px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| font-size: 48px; | |||
| } | |||
| h1 { | |||
| font-size: 32px; | |||
| font-weight: 600; | |||
| margin-bottom: 15px; | |||
| } | |||
| p { | |||
| font-size: 18px; | |||
| line-height: 1.6; | |||
| opacity: 0.9; | |||
| margin-bottom: 30px; | |||
| } | |||
| button { | |||
| background: white; | |||
| color: #764ba2; | |||
| border: none; | |||
| padding: 15px 30px; | |||
| font-size: 16px; | |||
| font-weight: 600; | |||
| border-radius: 50px; | |||
| cursor: pointer; | |||
| transition: transform 0.2s, box-shadow 0.2s; | |||
| } | |||
| button:hover { | |||
| transform: translateY(-2px); | |||
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); | |||
| } | |||
| button:active { | |||
| transform: translateY(0); | |||
| } | |||
| @media (max-width: 640px) { | |||
| .container { | |||
| padding: 30px; | |||
| } | |||
| h1 { | |||
| font-size: 24px; | |||
| } | |||
| p { | |||
| font-size: 16px; | |||
| } | |||
| } | |||
| </style> | |||
| </head> | |||
| <body> | |||
| <div class="container"> | |||
| <div class="icon"> | |||
| ⚡ | |||
| </div> | |||
| <h1>You're Offline</h1> | |||
| <p> | |||
| It looks like you've lost your internet connection. | |||
| Some features may not be available until you're back online. | |||
| </p> | |||
| <button onclick="window.location.reload()"> | |||
| Try Again | |||
| </button> | |||
| </div> | |||
| <script> | |||
| // Check for connection status changes | |||
| window.addEventListener('online', function() { | |||
| window.location.reload(); | |||
| }); | |||
| // Periodically check if online | |||
| setInterval(function() { | |||
| fetch(window.location.origin, { method: 'HEAD' }) | |||
| .then(function() { | |||
| window.location.reload(); | |||
| }) | |||
| .catch(function() { | |||
| // Still offline | |||
| }); | |||
| }, 5000); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,11 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <browserconfig> | |||
| <msapplication> | |||
| <tile> | |||
| <square70x70logo src="/icon-72x72.png"/> | |||
| <square150x150logo src="/icon-152x152.png"/> | |||
| <square310x310logo src="/icon-512x512.png"/> | |||
| <TileColor>#1C64F2</TileColor> | |||
| </tile> | |||
| </msapplication> | |||
| </browserconfig> | |||
| @@ -0,0 +1 @@ | |||
| (()=>{"use strict";self.fallback=async e=>"document"===e.destination?caches.match("/_offline.html",{ignoreSearch:!0}):Response.error()})(); | |||
| @@ -0,0 +1,58 @@ | |||
| { | |||
| "name": "Dify", | |||
| "short_name": "Dify", | |||
| "description": "Build Production Ready Agentic AI Solutions", | |||
| "icons": [ | |||
| { | |||
| "src": "/icon-192x192.png", | |||
| "sizes": "192x192", | |||
| "type": "image/png", | |||
| "purpose": "any" | |||
| }, | |||
| { | |||
| "src": "/icon-192x192.png", | |||
| "sizes": "192x192", | |||
| "type": "image/png", | |||
| "purpose": "maskable" | |||
| }, | |||
| { | |||
| "src": "/icon-256x256.png", | |||
| "sizes": "256x256", | |||
| "type": "image/png" | |||
| }, | |||
| { | |||
| "src": "/icon-384x384.png", | |||
| "sizes": "384x384", | |||
| "type": "image/png" | |||
| }, | |||
| { | |||
| "src": "/icon-512x512.png", | |||
| "sizes": "512x512", | |||
| "type": "image/png" | |||
| } | |||
| ], | |||
| "theme_color": "#1C64F2", | |||
| "background_color": "#ffffff", | |||
| "display": "standalone", | |||
| "scope": "/", | |||
| "start_url": "/", | |||
| "orientation": "portrait-primary", | |||
| "categories": ["productivity", "utilities", "developer"], | |||
| "lang": "en-US", | |||
| "dir": "ltr", | |||
| "prefer_related_applications": false, | |||
| "shortcuts": [ | |||
| { | |||
| "name": "Apps", | |||
| "short_name": "Apps", | |||
| "url": "/apps", | |||
| "icons": [{ "src": "/icon-96x96.png", "sizes": "96x96" }] | |||
| }, | |||
| { | |||
| "name": "Datasets", | |||
| "short_name": "Datasets", | |||
| "url": "/datasets", | |||
| "icons": [{ "src": "/icon-96x96.png", "sizes": "96x96" }] | |||
| } | |||
| ] | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| const sharp = require('sharp'); | |||
| const fs = require('fs'); | |||
| const path = require('path'); | |||
| const sizes = [ | |||
| { size: 192, name: 'icon-192x192.png' }, | |||
| { size: 256, name: 'icon-256x256.png' }, | |||
| { size: 384, name: 'icon-384x384.png' }, | |||
| { size: 512, name: 'icon-512x512.png' }, | |||
| { size: 96, name: 'icon-96x96.png' }, | |||
| { size: 72, name: 'icon-72x72.png' }, | |||
| { size: 128, name: 'icon-128x128.png' }, | |||
| { size: 144, name: 'icon-144x144.png' }, | |||
| { size: 152, name: 'icon-152x152.png' }, | |||
| ]; | |||
| const inputPath = path.join(__dirname, '../public/icon.svg'); | |||
| const outputDir = path.join(__dirname, '../public'); | |||
| // Generate icons | |||
| async function generateIcons() { | |||
| try { | |||
| console.log('Generating PWA icons...'); | |||
| for (const { size, name } of sizes) { | |||
| const outputPath = path.join(outputDir, name); | |||
| await sharp(inputPath) | |||
| .resize(size, size) | |||
| .png() | |||
| .toFile(outputPath); | |||
| console.log(`✓ Generated ${name} (${size}x${size})`); | |||
| } | |||
| // Generate apple-touch-icon | |||
| await sharp(inputPath) | |||
| .resize(180, 180) | |||
| .png() | |||
| .toFile(path.join(outputDir, 'apple-touch-icon.png')); | |||
| console.log('✓ Generated apple-touch-icon.png (180x180)'); | |||
| console.log('\n✅ All icons generated successfully!'); | |||
| } catch (error) { | |||
| console.error('Error generating icons:', error); | |||
| process.exit(1); | |||
| } | |||
| } | |||
| generateIcons(); | |||