diff --git a/app/manifest.ts b/app/manifest.ts index 47162004..411107c3 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -1,24 +1,24 @@ import type { MetadataRoute } from "next" - +import { getAssetUrl } from "@/lib/base-path" export default function manifest(): MetadataRoute.Manifest { return { name: "Next AI Draw.io", short_name: "AIDraw.io", description: "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", background_color: "#f9fafb", theme_color: "#171d26", icons: [ { - src: "/favicon-192x192.png", + src: getAssetUrl("/favicon-192x192.png"), sizes: "192x192", type: "image/png", purpose: "any", }, { - src: "/favicon-512x512.png", + src: getAssetUrl("/favicon-512x512.png"), sizes: "512x512", type: "image/png", purpose: "any", diff --git a/components/chat-example-panel.tsx b/components/chat-example-panel.tsx index 1b547cf7..dfb1bdcd 100644 --- a/components/chat-example-panel.tsx +++ b/components/chat-example-panel.tsx @@ -8,6 +8,7 @@ import { Terminal, Zap, } from "lucide-react" +import { getAssetUrl } from "@/lib/base-path" interface ExampleCardProps { icon: React.ReactNode @@ -74,7 +75,7 @@ export default function ExamplePanel({ setInput("Replicate this flowchart.") try { - const response = await fetch("/example.png") + const response = await fetch(getAssetUrl("/example.png")) const blob = await response.blob() const file = new File([blob], "example.png", { type: "image/png" }) setFiles([file]) @@ -87,7 +88,7 @@ export default function ExamplePanel({ setInput("Replicate this in aws style") try { - const response = await fetch("/architecture.png") + const response = await fetch(getAssetUrl("/architecture.png")) const blob = await response.blob() const file = new File([blob], "architecture.png", { type: "image/png", @@ -102,7 +103,7 @@ export default function ExamplePanel({ setInput("Summarize this paper as a diagram") try { - const response = await fetch("/chain-of-thought.txt") + const response = await fetch(getAssetUrl("/chain-of-thought.txt")) const blob = await response.blob() const file = new File([blob], "chain-of-thought.txt", { type: "text/plain", diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index b2dafadb..8009ae23 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -27,6 +27,7 @@ import { ReasoningTrigger, } from "@/components/ai-elements/reasoning" import { ScrollArea } from "@/components/ui/scroll-area" +import { getApiEndpoint } from "@/lib/base-path" import { applyDiagramOperations, convertToLegalXml, @@ -291,7 +292,7 @@ export function ChatMessageDisplay({ setFeedback((prev) => ({ ...prev, [messageId]: value })) try { - await fetch("/api/log-feedback", { + await fetch(getApiEndpoint("/api/log-feedback"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index ea383924..16c8867f 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -22,6 +22,7 @@ import { ResetWarningModal } from "@/components/reset-warning-modal" import { SettingsDialog } from "@/components/settings-dialog" import { useDiagram } from "@/contexts/diagram-context" import { getAIConfig } from "@/lib/ai-config" +import { getApiEndpoint } from "@/lib/base-path" import { findCachedResponse } from "@/lib/cached-responses" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { type FileData, useFileProcessor } from "@/lib/use-file-processor" @@ -160,7 +161,7 @@ export default function ChatPanel({ // Check config on mount useEffect(() => { - fetch("/api/config") + fetch(getApiEndpoint("/api/config")) .then((res) => res.json()) .then((data) => { setAccessCodeRequired(data.accessCodeRequired) @@ -232,7 +233,7 @@ export default function ChatPanel({ setMessages, } = useChat({ transport: new DefaultChatTransport({ - api: "/api/chat", + api: getApiEndpoint("/api/chat"), }), async onToolCall({ toolCall }) { if (DEBUG) { diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 30381f59..7b624e23 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -20,6 +20,7 @@ import { SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" +import { getApiEndpoint } from "@/lib/base-path" interface SettingsDialogProps { open: boolean @@ -71,7 +72,7 @@ export function SettingsDialog({ // Only fetch if not cached in localStorage if (getStoredAccessCodeRequired() !== null) return - fetch("/api/config") + fetch(getApiEndpoint("/api/config")) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() @@ -119,12 +120,15 @@ export function SettingsDialog({ setIsVerifying(true) try { - const response = await fetch("/api/verify-access-code", { - method: "POST", - headers: { - "x-access-code": accessCode.trim(), + const response = await fetch( + getApiEndpoint("/api/verify-access-code"), + { + method: "POST", + headers: { + "x-access-code": accessCode.trim(), + }, }, - }) + ) const data = await response.json() diff --git a/contexts/diagram-context.tsx b/contexts/diagram-context.tsx index 5563e8bd..a43aeafa 100644 --- a/contexts/diagram-context.tsx +++ b/contexts/diagram-context.tsx @@ -5,6 +5,7 @@ import { createContext, useContext, useEffect, useRef, useState } from "react" import type { DrawIoEmbedRef } from "react-drawio" import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel" import type { ExportFormat } from "@/components/save-dialog" +import { getApiEndpoint } from "@/lib/base-path" import { extractDiagramXML, validateAndFixXml } from "../lib/utils" interface DiagramContextType { @@ -329,7 +330,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { sessionId?: string, ) => { try { - await fetch("/api/log-save", { + await fetch(getApiEndpoint("/api/log-save"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename, format, sessionId }), diff --git a/docker-compose.yml b/docker-compose.yml index 84970bca..89e3a860 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,11 @@ services: context: . args: - NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080 + # Uncomment below for subdirectory deployment + # - NEXT_PUBLIC_BASE_PATH=/nextaidrawio ports: ["3000:3000"] env_file: .env + environment: + # For subdirectory deployment, uncomment and set your path: + # NEXT_PUBLIC_BASE_PATH: /nextaidrawio depends_on: [drawio] diff --git a/env.example b/env.example index 36c3d35b..1adbc4a1 100644 --- a/env.example +++ b/env.example @@ -93,6 +93,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 # 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) # Enable PDF file upload to extract text and generate diagrams # Enabled by default. Set to "false" to disable. diff --git a/lib/base-path.ts b/lib/base-path.ts new file mode 100644 index 00000000..b11d2db5 --- /dev/null +++ b/lib/base-path.ts @@ -0,0 +1,33 @@ +/** + * 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) + return process.env.NEXT_PUBLIC_BASE_PATH || "" +} + +/** + * 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}` +} diff --git a/next.config.ts b/next.config.ts index 8c0356a6..76bcf1f8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,9 @@ import type { NextConfig } from "next" const nextConfig: NextConfig = { /* config options here */ 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 || "", } export default nextConfig