Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ NEXT_PUBLIC_API_BASE_EXTERNAL=
# [Optional] Direct API base URL (alternative to above)
NEXT_PUBLIC_API_BASE=

# [Optional] Application Base Path (if deployed under a subpath)
# Example: /deep-tutor
NEXT_PUBLIC_APP_BASE_PATH=

# ==============================================================================
# Debug & Development
# ==============================================================================
Expand Down
4 changes: 3 additions & 1 deletion .env.example_CN
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ NEXT_PUBLIC_API_BASE_EXTERNAL=

# [可选] 直接 API 基础地址(上述配置的替代方案)
NEXT_PUBLIC_API_BASE=

# [可选] 应用基础路径(如果部署在子路径下)
# 示例:/deep-tutor
NEXT_PUBLIC_APP_BASE_PATH=
# ==============================================================================
# 调试与开发配置
# ==============================================================================
Expand Down
38 changes: 36 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ COPY web/ ./
# Create .env.local with placeholder that will be replaced at runtime
# Use a unique placeholder that can be safely replaced
RUN echo "NEXT_PUBLIC_API_BASE=__NEXT_PUBLIC_API_BASE_PLACEHOLDER__" > .env.local
RUN echo "NEXT_PUBLIC_APP_BASE_PATH=/__APP_BASE_PATH_PLACEHOLDER__" >> .env.local

# Build Next.js for production with standalone output
# This allows runtime environment variable injection
Expand Down Expand Up @@ -236,15 +237,48 @@ else
echo "[Frontend] Example: -e NEXT_PUBLIC_API_BASE_EXTERNAL=https://your-server.com:${BACKEND_PORT}"
fi

# Get App Base Path
APP_BASE_PATH=${NEXT_PUBLIC_APP_BASE_PATH:-}
# Remove trailing slash if present
APP_BASE_PATH=${APP_BASE_PATH%/}
if [ -n "$APP_BASE_PATH" ]; then
echo "[Frontend] 📌 Using App Base Path: ${APP_BASE_PATH}"
fi

echo "[Frontend] 🚀 Starting Next.js frontend on port ${FRONTEND_PORT}..."

# Replace placeholder in built Next.js files
# This is necessary because NEXT_PUBLIC_* vars are inlined at build time
find /app/web/.next -type f \( -name "*.js" -o -name "*.json" \) -exec \
sed -i "s|__NEXT_PUBLIC_API_BASE_PLACEHOLDER__|${API_BASE}|g" {} \; 2>/dev/null || true
# We must include html and css files as they may contain hardcoded paths
# Use grep first to speed up and ensure we only touch files with the placeholder
echo "[Frontend] 🔍 Scanning and replacing placeholders..."

# 1. Replace API Base
grep -lR "__NEXT_PUBLIC_API_BASE_PLACEHOLDER__" /app/web/.next | while read file; do
sed -i "s|__NEXT_PUBLIC_API_BASE_PLACEHOLDER__|${API_BASE}|g" "$file"
done

# 2. Replace App Base Path
# We need to handle the leading slash in the placeholder /__APP_BASE_PATH_PLACEHOLDER__
if [ -z "$APP_BASE_PATH" ]; then
# If empty, replace placeholder with empty string
echo "[Frontend] 🧹 Removing base path placeholder..."
grep -lR "/__APP_BASE_PATH_PLACEHOLDER__" /app/web/.next | while read file; do
sed -i "s|/__APP_BASE_PATH_PLACEHOLDER__||g" "$file"
done
else
# If set, replace placeholder with actual path
echo "[Frontend] 🔄 Replacing base path placeholder with ${APP_BASE_PATH}..."
grep -lR "/__APP_BASE_PATH_PLACEHOLDER__" /app/web/.next | while read file; do
sed -i "s|/__APP_BASE_PATH_PLACEHOLDER__|${APP_BASE_PATH}|g" "$file"
done
fi

# Also update .env.local for any runtime reads
echo "NEXT_PUBLIC_API_BASE=${API_BASE}" > /app/web/.env.local
if [ -n "$APP_BASE_PATH" ]; then
echo "NEXT_PUBLIC_APP_BASE_PATH=${APP_BASE_PATH}" >> /app/web/.env.local
fi

# Start Next.js
cd /app/web && exec node node_modules/next/dist/bin/next start -H 0.0.0.0 -p ${FRONTEND_PORT}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ services:
- NEXT_PUBLIC_API_BASE_EXTERNAL=${NEXT_PUBLIC_API_BASE_EXTERNAL:-}
# Alternative: Direct API base URL (same priority as NEXT_PUBLIC_API_BASE_EXTERNAL)
- NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE:-}
# Application base path (if deployed in a subdirectory)
- NEXT_PUBLIC_APP_BASE_PATH=${NEXT_PUBLIC_APP_BASE_PATH:-}

volumes:
# Mount local config directory (read-only)
Expand Down
9 changes: 9 additions & 0 deletions scripts/start_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ def start_frontend():
print_flush(f"📌 Using default API URL: {api_base_url}")
print_flush(" 💡 For remote access, set NEXT_PUBLIC_API_BASE in .env file")

# Get App Base Path
app_base_path = os.environ.get("NEXT_PUBLIC_APP_BASE_PATH", "")
if app_base_path:
print_flush(f"📌 Using App Base Path: {app_base_path}")

# Generate/update .env.local file with port configuration
# This ensures Next.js can read the backend port even if environment variables are not passed
env_local_path = os.path.join(web_dir, ".env.local")
Expand All @@ -346,6 +351,7 @@ def start_frontend():
f.write("# NEXT_PUBLIC_API_BASE=http://your-server-ip:8001\n")
f.write("# ============================================\n\n")
f.write(f"NEXT_PUBLIC_API_BASE={api_base_url}\n")
f.write(f"NEXT_PUBLIC_APP_BASE_PATH={app_base_path}\n")
print_flush(f"✅ Updated .env.local with API base: {api_base_url}")
except Exception as e:
print_flush(f"⚠️ Warning: Failed to update .env.local: {e}")
Expand All @@ -355,6 +361,9 @@ def start_frontend():
env = os.environ.copy()
env["PORT"] = str(frontend_port)
env["NEXT_PUBLIC_API_BASE"] = api_base_url
if app_base_path:
env["NEXT_PUBLIC_APP_BASE_PATH"] = app_base_path

# Set encoding environment variables for Windows
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONUTF8"] = "1"
Expand Down
1 change: 1 addition & 0 deletions web/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
# ============================================

NEXT_PUBLIC_API_BASE=http://localhost:8001
NEXT_PUBLIC_APP_BASE_PATH=
3 changes: 2 additions & 1 deletion web/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
LucideIcon,
} from "lucide-react";
import { useGlobal } from "@/context/GlobalContext";
import { appPath } from "@/lib/path";

const SIDEBAR_EXPANDED_WIDTH = 256;
const SIDEBAR_COLLAPSED_WIDTH = 64;
Expand Down Expand Up @@ -228,7 +229,7 @@ export default function Sidebar() {
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg flex items-center justify-center overflow-hidden flex-shrink-0">
<Image
src="/logo.png"
src={appPath("/logo.png")}
alt={t("DeepTutor Logo")}
width={32}
height={32}
Expand Down
22 changes: 22 additions & 0 deletions web/lib/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const getBasePath = () => {
if (typeof window !== 'undefined') {
return process.env.NEXT_PUBLIC_APP_BASE_PATH?.replace(/\/$/, '') || '';
}
return process.env.NEXT_PUBLIC_APP_BASE_PATH?.replace(/\/$/, '') || '';
};

export const appPath = (path: string) => {
const basePath = getBasePath();
if (!path) return basePath;

// If path is absolute URL, return as is
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}

const cleanPath = path.startsWith('/') ? path : `/${path}`;

if (!basePath) return cleanPath;

return `${basePath}${cleanPath}`;
};
31 changes: 18 additions & 13 deletions web/next.config.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
/** @type {import('next').NextConfig} */

// Ensure basePath doesn't have a trailing slash, as Next.js doesn't like it
const basePath = process.env.NEXT_PUBLIC_APP_BASE_PATH?.replace(/\/$/, '')

if (basePath) {
console.log(`> [DeepTutor] Applying basePath: ${basePath}`)
}

const nextConfig = {
...(basePath ? { basePath } : {}),
// Move dev indicator to bottom-right corner
devIndicators: {
position: "bottom-right",
position: 'bottom-right',
},

// Transpile mermaid and related packages for proper ESM handling
transpilePackages: ["mermaid"],
transpilePackages: ['mermaid'],

// Turbopack configuration (Next.js 16+ uses Turbopack by default for dev)
turbopack: {
resolveAlias: {
// Fix for mermaid's cytoscape dependency - use CJS version
cytoscape: "cytoscape/dist/cytoscape.cjs.js",
cytoscape: 'cytoscape/dist/cytoscape.cjs.js',
},
},

// Webpack configuration (used for production builds - next build)
webpack: (config) => {
const path = require("path");
webpack: config => {
const path = require('path')
config.resolve.alias = {
...config.resolve.alias,
cytoscape: path.resolve(
__dirname,
"node_modules/cytoscape/dist/cytoscape.cjs.js",
),
};
return config;
cytoscape: path.resolve(__dirname, 'node_modules/cytoscape/dist/cytoscape.cjs.js'),
}
return config
},
};
}

module.exports = nextConfig;
module.exports = nextConfig
Loading