mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
Support subdirectory deployment and fix API path handling (#311)
* feat: support subdirectory deployment (NEXT_PUBLIC_BASE_PATH) * removed unwanted check and fix favicon issue * Use getAssetUrl for manifest assets to avoid undefined NEXT_PUBLIC_BASE_PATH * Add validation warning for NEXT_PUBLIC_BASE_PATH format --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
This commit is contained in:
@@ -1,24 +1,24 @@
|
|||||||
import type { MetadataRoute } from "next"
|
import type { MetadataRoute } from "next"
|
||||||
|
import { getAssetUrl } from "@/lib/base-path"
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
return {
|
return {
|
||||||
name: "Next AI Draw.io",
|
name: "Next AI Draw.io",
|
||||||
short_name: "AIDraw.io",
|
short_name: "AIDraw.io",
|
||||||
description:
|
description:
|
||||||
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
||||||
start_url: "/",
|
start_url: getAssetUrl("/"),
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
background_color: "#f9fafb",
|
background_color: "#f9fafb",
|
||||||
theme_color: "#171d26",
|
theme_color: "#171d26",
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: "/favicon-192x192.png",
|
src: getAssetUrl("/favicon-192x192.png"),
|
||||||
sizes: "192x192",
|
sizes: "192x192",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "any",
|
purpose: "any",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/favicon-512x512.png",
|
src: getAssetUrl("/favicon-512x512.png"),
|
||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "any",
|
purpose: "any",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { getAssetUrl } from "@/lib/base-path"
|
||||||
|
|
||||||
interface ExampleCardProps {
|
interface ExampleCardProps {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
@@ -79,7 +80,7 @@ export default function ExamplePanel({
|
|||||||
setInput("Replicate this flowchart.")
|
setInput("Replicate this flowchart.")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/example.png")
|
const response = await fetch(getAssetUrl("/example.png"))
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
const file = new File([blob], "example.png", { type: "image/png" })
|
const file = new File([blob], "example.png", { type: "image/png" })
|
||||||
setFiles([file])
|
setFiles([file])
|
||||||
@@ -92,7 +93,7 @@ export default function ExamplePanel({
|
|||||||
setInput("Replicate this in aws style")
|
setInput("Replicate this in aws style")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/architecture.png")
|
const response = await fetch(getAssetUrl("/architecture.png"))
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
const file = new File([blob], "architecture.png", {
|
const file = new File([blob], "architecture.png", {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
@@ -107,7 +108,7 @@ export default function ExamplePanel({
|
|||||||
setInput("Summarize this paper as a diagram")
|
setInput("Summarize this paper as a diagram")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/chain-of-thought.txt")
|
const response = await fetch(getAssetUrl("/chain-of-thought.txt"))
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
const file = new File([blob], "chain-of-thought.txt", {
|
const file = new File([blob], "chain-of-thought.txt", {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
ReasoningTrigger,
|
ReasoningTrigger,
|
||||||
} from "@/components/ai-elements/reasoning"
|
} from "@/components/ai-elements/reasoning"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import {
|
import {
|
||||||
applyDiagramOperations,
|
applyDiagramOperations,
|
||||||
convertToLegalXml,
|
convertToLegalXml,
|
||||||
@@ -291,7 +292,7 @@ export function ChatMessageDisplay({
|
|||||||
setFeedback((prev) => ({ ...prev, [messageId]: value }))
|
setFeedback((prev) => ({ ...prev, [messageId]: value }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch("/api/log-feedback", {
|
await fetch(getApiEndpoint("/api/log-feedback"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
||||||
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
||||||
@@ -172,7 +173,7 @@ export default function ChatPanel({
|
|||||||
|
|
||||||
// Check config on mount
|
// Check config on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/config")
|
fetch(getApiEndpoint("/api/config"))
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setDailyRequestLimit(data.dailyRequestLimit || 0)
|
setDailyRequestLimit(data.dailyRequestLimit || 0)
|
||||||
@@ -243,7 +244,7 @@ export default function ChatPanel({
|
|||||||
setMessages,
|
setMessages,
|
||||||
} = useChat({
|
} = useChat({
|
||||||
transport: new DefaultChatTransport({
|
transport: new DefaultChatTransport({
|
||||||
api: "/api/chat",
|
api: getApiEndpoint("/api/chat"),
|
||||||
}),
|
}),
|
||||||
async onToolCall({ toolCall }) {
|
async onToolCall({ toolCall }) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
|
||||||
const LANGUAGE_LABELS: Record<Locale, string> = {
|
const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||||
@@ -77,7 +78,7 @@ function SettingsContent({
|
|||||||
// Only fetch if not cached in localStorage
|
// Only fetch if not cached in localStorage
|
||||||
if (getStoredAccessCodeRequired() !== null) return
|
if (getStoredAccessCodeRequired() !== null) return
|
||||||
|
|
||||||
fetch("/api/config")
|
fetch(getApiEndpoint("/api/config"))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
return res.json()
|
return res.json()
|
||||||
@@ -142,12 +143,15 @@ function SettingsContent({
|
|||||||
setIsVerifying(true)
|
setIsVerifying(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/verify-access-code", {
|
const response = await fetch(
|
||||||
method: "POST",
|
getApiEndpoint("/api/verify-access-code"),
|
||||||
headers: {
|
{
|
||||||
"x-access-code": accessCode.trim(),
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode.trim(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createContext, useContext, useEffect, useRef, useState } from "react"
|
|||||||
import type { DrawIoEmbedRef } from "react-drawio"
|
import type { DrawIoEmbedRef } from "react-drawio"
|
||||||
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
||||||
import type { ExportFormat } from "@/components/save-dialog"
|
import type { ExportFormat } from "@/components/save-dialog"
|
||||||
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { extractDiagramXML, validateAndFixXml } from "../lib/utils"
|
import { extractDiagramXML, validateAndFixXml } from "../lib/utils"
|
||||||
|
|
||||||
interface DiagramContextType {
|
interface DiagramContextType {
|
||||||
@@ -329,7 +330,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await fetch("/api/log-save", {
|
await fetch(getApiEndpoint("/api/log-save"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ filename, format, sessionId }),
|
body: JSON.stringify({ filename, format, sessionId }),
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
||||||
|
# Uncomment below for subdirectory deployment
|
||||||
|
# - NEXT_PUBLIC_BASE_PATH=/nextaidrawio
|
||||||
ports: ["3000:3000"]
|
ports: ["3000:3000"]
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
# For subdirectory deployment, uncomment and set your path:
|
||||||
|
# NEXT_PUBLIC_BASE_PATH: /nextaidrawio
|
||||||
depends_on: [drawio]
|
depends_on: [drawio]
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net # Default: https://embed.diagrams.net
|
# NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net # Default: https://embed.diagrams.net
|
||||||
# Use this to point to a self-hosted draw.io instance
|
# Use this to point to a self-hosted draw.io instance
|
||||||
|
|
||||||
|
# Subdirectory Deployment (Optional)
|
||||||
|
# For deploying to a subdirectory (e.g., https://example.com/nextaidrawio)
|
||||||
|
# Set this to your subdirectory path with leading slash (e.g., /nextaidrawio)
|
||||||
|
# Leave empty for root deployment (default)
|
||||||
|
# NEXT_PUBLIC_BASE_PATH=/nextaidrawio
|
||||||
|
|
||||||
# PDF Input Feature (Optional)
|
# PDF Input Feature (Optional)
|
||||||
# Enable PDF file upload to extract text and generate diagrams
|
# Enable PDF file upload to extract text and generate diagrams
|
||||||
# Enabled by default. Set to "false" to disable.
|
# Enabled by default. Set to "false" to disable.
|
||||||
|
|||||||
37
lib/base-path.ts
Normal file
37
lib/base-path.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Get the base path for API calls and static assets
|
||||||
|
* This is used for subdirectory deployment support
|
||||||
|
*
|
||||||
|
* Example: If deployed at https://example.com/nextaidrawio, this returns "/nextaidrawio"
|
||||||
|
* For root deployment, this returns ""
|
||||||
|
*
|
||||||
|
* Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
|
||||||
|
*/
|
||||||
|
export function getBasePath(): string {
|
||||||
|
// Read from environment variable (must start with NEXT_PUBLIC_ to be available on client)
|
||||||
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""
|
||||||
|
if (basePath && !basePath.startsWith("/")) {
|
||||||
|
console.warn("NEXT_PUBLIC_BASE_PATH should start with /")
|
||||||
|
}
|
||||||
|
return basePath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full API endpoint URL
|
||||||
|
* @param endpoint - API endpoint path (e.g., "/api/chat", "/api/config")
|
||||||
|
* @returns Full API path with base path prefix
|
||||||
|
*/
|
||||||
|
export function getApiEndpoint(endpoint: string): string {
|
||||||
|
const basePath = getBasePath()
|
||||||
|
return `${basePath}${endpoint}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full static asset URL
|
||||||
|
* @param assetPath - Asset path (e.g., "/example.png", "/chain-of-thought.txt")
|
||||||
|
* @returns Full asset path with base path prefix
|
||||||
|
*/
|
||||||
|
export function getAssetUrl(assetPath: string): string {
|
||||||
|
const basePath = getBasePath()
|
||||||
|
return `${basePath}${assetPath}`
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import packageJson from "./package.json"
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
// Support for subdirectory deployment (e.g., https://example.com/nextaidrawio)
|
||||||
|
// Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
|
||||||
|
basePath: process.env.NEXT_PUBLIC_BASE_PATH || "",
|
||||||
env: {
|
env: {
|
||||||
APP_VERSION: packageJson.version,
|
APP_VERSION: packageJson.version,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user